Flutter Slick

A batteries-included application framework for Flutter. Slick gives you an opinionated MVVM layer, typed routing built on go_router, permission/feature-access control, authentication hooks, and a library of form, layout and grid widgets — so you can assemble production apps without re-inventing the plumbing for every screen.

Slick is the foundation behind a family of production apps (payments, POS, fleet management). The patterns documented here are the ones used in those apps daily.

Contents

Installing

dependencies:
  flutter_slick: ^5.4.0

Slick builds on provider, get_it, go_router, dropdown_search, connectivity_plus, flutter_staggered_grid_view, intl and validators. These come in transitively — you do not need to add them yourself unless you use them directly.

Importing

Slick has no barrel file — import the specific file you need. Every public type lives under package:flutter_slick/<path>.dart:

import 'package:flutter_slick/navigation/logic.dart';        // NavigationLogic
import 'package:flutter_slick/navigation/slick_route.dart';  // SlickRoute
import 'package:flutter_slick/navigation/view_model.dart';   // ViewModel
import 'package:flutter_slick/navigation/view_model_widget.dart';
import 'package:flutter_slick/components/layout/spaced_column.dart';
import 'package:flutter_slick/components/fields/text.dart';
import 'package:flutter_slick/utils/validators.dart';

The path mirrors the folder layout shown throughout this document.

Quick start

A Slick app comes together in three pieces: routes, app setup (providers + NavigationLogic), and the root widget (MaterialApp.router).

1. Define your routes

Routes are typed classes that extend SlickRoute and use go_router's @TypedGoRoute code generation. Create routes.dart:

import 'package:flutter/material.dart';
import 'package:flutter_slick/navigation/slick_route.dart';
import 'package:go_router/go_router.dart';

import 'screens/home.dart';
import 'screens/sample.dart';

part 'routes.g.dart';

@TypedGoRoute<HomeRoute>(path: '/')
class HomeRoute extends SlickRoute with $HomeRoute {
  const HomeRoute();

  @override
  Widget build(BuildContext context, GoRouterState state) => const HomeScreen();
}

@TypedGoRoute<SampleRoute>(path: '/sample')
class SampleRoute extends SlickRoute with $SampleRoute {
  const SampleRoute();

  @override
  Widget build(BuildContext context, GoRouterState state) => const SampleScreen();
}

Run the generator to produce routes.g.dart (which defines the $HomeRoute mixins and the $appRoutes list):

dart run build_runner build --delete-conflicting-outputs

Add go_router_builder and build_runner to your dev_dependencies for code generation. See the go_router_builder docs.

2. Set up providers and NavigationLogic

NavigationLogic is the heart of Slick — it owns the GoRouter, history, and auth/permission redirects. Provide it (and your auth service) at the app root:

import 'package:flutter/material.dart';
import 'package:flutter_slick/navigation/logic.dart';
import 'package:provider/provider.dart';

import 'application.dart';
import 'routes.dart';
import 'services/my_authentication.dart';

void main() {
  runApp(
    MultiProvider(
      providers: [
        Provider<MyAuthenticationService>(
          create: (_) => MyAuthenticationService(),
        ),
        ChangeNotifierProvider<NavigationLogic>(
          create: (context) => NavigationLogic(
            routes: $appRoutes,
            authState: context.read<MyAuthenticationService>().authState,
            signInPath: const SignInRoute().location,
            signOutPath: const LogOutRoute().location,
            defaultLoading: () => const Center(child: CircularProgressIndicator()),
          ),
        ),
      ],
      child: const Application(),
    ),
  );
}

3. Build the root widget

Consume NavigationLogic and feed its goRouter into MaterialApp.router:

import 'package:flutter/material.dart';
import 'package:flutter_slick/navigation/logic.dart';
import 'package:provider/provider.dart';

class Application extends StatelessWidget {
  const Application({super.key});

  @override
  Widget build(BuildContext context) {
    return Consumer<NavigationLogic>(
      builder: (context, logic, _) {
        return MaterialApp.router(
          title: 'My Slick App',
          routerConfig: logic.goRouter,
        );
      },
    );
  }
}

That's it. You now have type-safe routing, auth-aware redirects, history tracking and a loading state baked in.

Routing

Route parameters

Path and query parameters become typed constructor fields. The generator extracts them from the URL for you:

// Path parameter: /name/slick
@TypedGoRoute<UrlParamRoute>(path: '/name/:name')
class UrlParamRoute extends SlickRoute with $UrlParamRoute {
  const UrlParamRoute({required this.name});
  final String name;

  @override
  Widget build(BuildContext context, GoRouterState state) =>
      UserScreen(name: name);
}

// Query parameter: /name-query?name=slick
@TypedGoRoute<QueryParamRoute>(path: '/name-query')
class QueryParamRoute extends SlickRoute with $QueryParamRoute {
  const QueryParamRoute({this.name});
  final String? name;

  @override
  Widget build(BuildContext context, GoRouterState state) =>
      UserScreen(name: name ?? 'default');
}

Get NavigationLogic from GetIt (it registers itself as a singleton) or from Provider, then call goTo with a typed route instance:

import 'package:get_it/get_it.dart';
import 'package:flutter_slick/navigation/logic.dart';

final navigation = GetIt.I<NavigationLogic>();

navigation.goTo(const SampleRoute());                  // push onto the stack
navigation.goTo(const UrlParamRoute(name: 'slick'));   // with a path param
navigation.goReplace(const HomeRoute());               // replace current route
navigation.goBack();                                   // back through Slick history
navigation.goUp();                                     // up to the parent route (/a/b -> /a)

Inside a ViewModel the same instance is available as navigation, and navigateBack(context) is provided as a convenience.

NavigationLogic also exposes canGoBack, canGoUp, currentPath, an isLoading flag, and addRouteChangeListener / removeRouteChangeListener for reacting to navigation. Set useSmartBack: true (and optionally smartBackOrder) on the constructor to make goBack() prefer going up the hierarchy before popping history.

Redirects

Override onRedirect on a route to guard it. Return a path to redirect, or null to allow navigation. This is the canonical place for auth and sync checks:

@TypedGoRoute<DashboardRoute>(path: '/dashboard')
class DashboardRoute extends SlickRoute with $DashboardRoute {
  const DashboardRoute();

  @override
  Widget build(BuildContext context, GoRouterState state) => const DashboardScreen();

  @override
  FutureOr<String?> onRedirect(BuildContext context, GoRouterState state) async {
    final user = await context.read<MyAuthenticationService>().currentUser;
    if (user == null) return const SignInRoute().location;
    return null; // allow
  }
}

Override redirect is reserved by Slick — always use onRedirect.

Other overridable members on SlickRoute:

  • previousPath — put a different route on the history stack when this one is entered.
  • menuItems / tabItems — per-route menu/tab definitions (see Navigation menus).

Custom transitions

Page transitions come from Material's PageTransitionsTheme. Set them globally on your ThemeData, per route, or per loading view (see MVVM):

MaterialApp.router(
  theme: ThemeData(
    pageTransitionsTheme: const PageTransitionsTheme(
      builders: {
        TargetPlatform.android: CupertinoPageTransitionsBuilder(),
        TargetPlatform.iOS: CupertinoPageTransitionsBuilder(),
      },
    ),
  ),
  routerConfig: logic.goRouter,
);

MVVM

Slick's MVVM splits each screen into a ViewModel (state + logic) and a ViewModelWidget (the view). The widget owns the view-model lifecycle, shows a loading state while the view model initialises, and rebuilds whenever the view model notifies.

Defining a ViewModel

Extend ViewModel and implement initState() (async — runs once, before the view is shown). Call redraw() to rebuild the view, and loading() / idle() to toggle a loading indicator:

import 'package:flutter_slick/navigation/view_model.dart';

class VehicleListViewModel extends ViewModel {
  List<Vehicle> vehicles = [];

  @override
  Future<void> initState() async {
    vehicles = await VehicleService.getAll();
  }

  Future<void> refresh() async {
    loading();                       // show a loading indicator
    vehicles = await VehicleService.getAll();
    idle();                          // hide it
  }

  void search(String query) {
    vehicles = VehicleService.search(query);
    redraw();                        // rebuild the view (instead of setState)
  }
}

ViewModel also gives you:

  • navigation — the NavigationLogic singleton; navigateBack(context).
  • isLoading, isInitialised, loadingIndicatorStyle (auto, overlay, none, fullScreen).
  • dirtyStateMonitor — unsaved-changes tracking (form fields mark it dirty automatically).
  • showInfoMessage / showWarningMessage / showErrorMessage — routed through the AlertController.

Binding it to a view

Extend ViewModelWidget<T>, implement createViewModel() and buildView(), and read state from the vm getter:

import 'package:flutter/material.dart';
import 'package:flutter_slick/navigation/view_model_widget.dart';
import 'package:flutter_slick/components/layout/spaced_column.dart';

class VehicleListScreen extends ViewModelWidget<VehicleListViewModel> {
  VehicleListScreen({super.key});

  @override
  VehicleListViewModel createViewModel() => VehicleListViewModel();

  @override
  Widget buildView(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Vehicles')),
      body: SpacedColumn(
        children: [
          for (final v in vm.vehicles) Text(v.registration),
          ElevatedButton(onPressed: vm.refresh, child: const Text('Refresh')),
        ],
      ),
    );
  }
}

While initState() is running (or loading() is active), the widget shows buildLoading(context) — by default the defaultLoading widget you configured on NavigationLogic. Override buildLoading / buildLoadingOverlay for a custom loading view, and transitionTheme to animate the loading→view swap.

ViewModelWidget includes handy view helpers:

  • popDialog(context, dirtyWarning: true) — pops, prompting to discard unsaved changes.
  • showActionDialog(context, title, content, actionLabel) and showDiscardDialog.
  • showMessage(context, text) and showErrorSnackBar(context, text).

ViewModelWidgets nest freely — a view can build another ViewModelWidget and each manages its own lifecycle.

Authentication

Extend AuthenticationService<User> with your own user type. The base class already provides broadcast authState / tokenState streams (via authStateChangeController and tokenChangeController) and a dispose() that closes them — you implement the provider-specific methods and emit on the controllers:

import 'package:flutter_slick/services/authentication.dart';

class MyAuthenticationService extends AuthenticationService<AuthUser> {
  AuthUser? _user;

  @override
  AuthUser? get user => _user;

  @override
  Future<void> signIn(String email, String password) async {
    _user = await api.login(email, password);
    authStateChangeController.add(_user);          // notify listeners
    tokenChangeController.add(_user?.token);
  }

  @override
  Future<void> signOut() async {
    _user = null;
    authStateChangeController.add(null);
  }

  @override
  Future<void> refreshToken(AuthUser user) async { /* ... */ }
  @override
  Future<void> createUser(String email, String password) async { /* ... */ }
  @override
  Future<void> resetPasswordReset(String email) async { /* ... */ }
  @override
  Future<void> confirmPasswordReset(String code, String pw) async { /* ... */ }
  @override
  Future<void> sendResetPasswordEmail(String? email) async { /* ... */ }
}

Wrap provider/back-end errors in AuthenticationException so callers can handle them uniformly. Pass the service's authState stream to NavigationLogic(authState: ...) so routing waits for the first auth event before evaluating redirects.

Feature access & permissions

FeatureAccessController decides which routes a user may visit, based on a list of permitted path prefixes. Initialise it once with a callback that returns the current user's access, then hand it to NavigationLogic:

import 'package:flutter_slick/services/feature_access_controller.dart';

final access = await FeatureAccessController.initialise(() async {
  final perms = await api.getMyPermissions();
  return FeatureAccessDetails(
    userId: perms.userId,
    availableAccess: perms.paths,          // e.g. ['/dashboard', '/vehicles']
    readOnlyAccess: perms.readOnlyPaths,   // optional
  );
});

NavigationLogic(
  routes: $appRoutes,
  featureAccessController: access,
  signInPath: const SignInRoute().location,      // required with access control
  accessDeniedPath: const AccessDeniedRoute().location, // required with access control
);

With a controller set, NavigationLogic automatically redirects unauthenticated users to signInPath and authenticated-but-unauthorised users to accessDeniedPath. Paths match by prefix, and snake_case URL segments are matched against camelCase permissions.

  • access.hasPermission(path) / hasReadOnlyPermission(path) — check access directly.
  • await access.refreshAvailableAccess() — re-fetch after a role change.
  • NavigationLogic.isReadOnly() — is the current route read-only for this user?
  • Menus are filtered automatically — items the user can't reach are hidden (see below).

Form fields & validation

Slick fields are drop-in form widgets that integrate with Form/GlobalKey<FormState>, report typed values through onChanged, and mark the DirtyStateMonitor dirty on edit.

import 'package:flutter/material.dart';
import 'package:flutter_slick/components/fields/text.dart';
import 'package:flutter_slick/components/fields/email.dart';
import 'package:flutter_slick/components/fields/password.dart';
import 'package:flutter_slick/components/fields/number.dart';
import 'package:flutter_slick/components/fields/currency.dart';
import 'package:flutter_slick/components/fields/date.dart';
import 'package:flutter_slick/components/fields/checkbox.dart';
import 'package:flutter_slick/components/fields/dropdown.dart';
import 'package:flutter_slick/components/layout/spaced_column.dart';
import 'package:flutter_slick/utils/validators.dart';

final _formKey = GlobalKey<FormState>();
String _name = '';
double? _price;
Country? _country;

Form(
  key: _formKey,
  child: SpacedColumn(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
      StandardTextField(
        labelText: 'Name',
        initialValue: _name,
        onChanged: (v) => _name = v?.trim() ?? '',
        validator: CombinedFieldValidator([
          RequiredValidator(),
          LengthValidator(min: 1, max: 50),
        ]),
      ),
      EmailField(
        labelText: 'Email',
        onChanged: (v) => _email = v,
        validator: EmailFieldValidator(),
      ),
      PasswordField(
        labelText: 'Password',
        onChanged: (v) => _password = v,
      ),
      NumberField(
        labelText: 'Quantity',
        onChanged: (v) => _qty = v,            // int?
        validator: RangeValidator(min: 1, max: 99),
      ),
      CurrencyField(
        labelText: 'Price',
        currency: 'R',
        onChanged: (v) => _price = v,          // double?
      ),
      StandardDateField(
        labelText: 'Start date',
        onChanged: (v) => _start = v,          // DateTime?
        firstDate: DateTime(2020),
        lastDate: DateTime(2030),
      ),
      DropdownField<Country>(
        labelText: 'Country',
        items: countries,
        selectedItem: _country,
        itemAsString: (c) => c.name,
        showSearchBox: true,
        onChanged: (c) => _country = c,
      ),
      CheckboxField(
        labelText: 'I agree to the terms',
        initialValue: false,
        onChanged: (v) => _agreed = v,
      ),
    ],
  ),
);

// Validate and read the values:
if (_formKey.currentState!.validate()) {
  // submit _name, _price, _country, ...
}

Common fields and their value types:

Widget File Value Notes
StandardTextField fields/text.dart String? maxLength, maxLines, prefixIcon, inputFormatters, readOnly, keyboardType
EmailField fields/email.dart String? pairs with EmailFieldValidator
PasswordField fields/password.dart String? obscured with a show/hide toggle
NumberField fields/number.dart int?
CurrencyField fields/currency.dart double? requires currency symbol; readOnly
StandardDateField fields/date.dart DateTime? firstDate/lastDate, includeTime
StandardDurationField fields/duration.dart Duration HH:MM input
CheckboxField fields/checkbox.dart bool
DropdownField<T> fields/dropdown.dart T? showSearchBox, readOnly, custom compare
InlineMultiSelectField<T> fields/inline_multi_select/inline_multi_select_input.dart List<T> async suggestionCallback, inline chips

Set autoValidate: true on any field to validate when focus is lost.

Validators

Validators implement FieldValidator<T> and are callable, so they slot straight into a field's validator. Built-ins:

  • RequiredValidator() — non-null / non-empty.
  • LengthValidator(min:, max:) — string length; ListLengthValidator for lists.
  • EmailFieldValidator() — valid email.
  • RangeValidator(min:, max:) — numeric range; DateRangeValidator(min:, max:) for dates.
  • FieldMatchValidator(name, getValue) — matches another field (e.g. confirm password).
  • CombinedFieldValidator([...]) — runs validators in order, returns the first error.
  • DependentValidator(() => ...) — builds the validator lazily, for cross-field rules.

Write your own by extending FieldValidator<T>:

class PhoneValidator extends FieldValidator<String> {
  @override
  String? call(String? value) {
    if (value == null || value.isEmpty) return null;
    return RegExp(r'^\+?\d{7,15}$').hasMatch(value) ? null : 'Invalid phone number';
  }
}

Multi-select with inline chips

import 'package:flutter_slick/components/fields/inline_multi_select/inline_multi_select_input.dart';

InlineMultiSelectField<Driver>(
  label: 'Search for drivers',
  items: allDrivers,
  initialValues: selectedDrivers,
  dropDownMode: true,
  prefixIcon: const Icon(Icons.person),
  suggestionCallback: (query) async =>
      allDrivers.where((d) => d.name.toLowerCase().contains(query.toLowerCase())).toList(),
  chipLabelBuilder: (d) => Text(d.name),
  onChanged: (selected) => setState(() => selectedDrivers = selected),
);

Buttons

Slick's standard buttons accept an async onPressed and show an inline loading indicator while it runs, with an optional onError handler:

import 'package:flutter_slick/components/buttons/standard_elevated_button.dart';
import 'package:flutter_slick/components/buttons/standard_outlined_button.dart';
import 'package:flutter_slick/components/buttons/standard_text_button.dart';

StandardElevatedButton(
  onPressed: () async => await save(),       // spinner shown until this completes
  onError: () async => showError('Save failed'),
  child: const Text('Save'),
);

StandardOutlinedButton.icon(
  icon: const Icon(Icons.download),
  onPressed: () async => await export(),
  child: const Text('Export'),
);

StandardTextButton(
  onPressed: () async => await cancel(),
  child: const Text('Cancel'),
);

Also available: PrimaryActionButton / SecondaryActionButton (extended FABs taking iconData + labelText) and WordedToggle (an animated labelled on/off switch backed by a ValueNotifier<bool>).

Layout

Spacing

SpacedColumn and SpacedRow insert consistent gaps between children (default 16.0) — no manual SizedBoxes. These are the most-used widgets in Slick apps.

import 'package:flutter_slick/components/layout/spaced_column.dart';
import 'package:flutter_slick/components/layout/spaced_row.dart';

SpacedColumn(
  crossAxisAlignment: CrossAxisAlignment.start,
  spaceSize: 12,
  children: [nameField, surnameField, phoneField],
);

SpacedRow(
  mainAxisAlignment: MainAxisAlignment.end,
  children: [cancelButton, saveButton],
);

Responsive layouts

MediaLayout rebuilds with a MediaConstraints describing the current screen, so you can branch on size. You can also construct MediaConstraints(context) directly:

import 'package:flutter_slick/components/layout/media_layout.dart';

MediaLayout(
  builder: (context, constraints) {
    return constraints.isSmall
        ? const _MobileLayout()
        : const _DesktopLayout();
  },
);

// or, ad-hoc:
final constraints = MediaConstraints(context);
final columns = constraints.isSmall ? 1 : 2;

MediaConstraints exposes screenWidth, isSmall, isLarge, isSmallScreen, and accepts forceMobileOnlyView: true to force the mobile branch.

Dynamic columns

DynamicColumnLayout wraps a set of widgets into rows using one of three strategies — explicit widths, ratios (with a separate mobile ratio set), or a fixed count per row:

import 'package:flutter_slick/components/layout/dynamic_column_layout.dart';

// Even rows of N widgets, wrapping as needed:
DynamicColumnLayout(
  widgetsPerRow: 3,
  spacing: 20,
  runSpacing: 20,
  children: cards,
);

// Proportional widths, with a mobile override:
DynamicColumnLayout(
  widgetRatios: const [2, 1],            // 2:1 split on desktop
  widgetRatiosMobile: const [[1], [1]],  // stacked on mobile
  children: [main, sidebar],
);

A StaggeredGridView (components/layout/grid/staggered_grid_view.dart) is also provided for Pinterest-style staggered grids, with crossAxisSpacing / mainAxisSpacing.

Data grids

StandardGrid<T> renders a responsive data table with optional row selection, expandable detail rows, per-row actions, striping and a mobile card view. Cells are arbitrary widgets; items ties each row back to a typed model for actions and expansion.

import 'package:flutter_slick/components/layout/grid/standard_grid.dart';

StandardGrid<Vehicle>(
  title: 'Fleet',
  headers: const ['Registration', 'Drivers', 'Status', 'Fleet #'],
  highlightedIndices: const [0],            // emphasise these columns
  items: vm.vehicles,                       // typed models, one per row
  data: vm.vehicles.map((v) => [
    Text(v.registration),
    Text('${v.driverCount}'),
    Text(v.status),
    Text('Fleet ${v.fleetNumber}'),
  ]).toList(),
  stripedRows: true,
  onExpandRow: (index) async => VehicleDetail(vm.vehicles[index]),   // lazy detail row
  mobileViewBuilder: (index, context) => VehicleCard(vm.vehicles[index]),
  rowActions: (index) => [
    TableAction<Vehicle>(
      title: 'Edit',
      icon: Icons.edit,
      action: (vehicle) => vm.navigation.goTo(EditVehicleRoute(id: vehicle.id)),
    ),
  ],
);

Theming hooks:

  • gridRowColors: GridRowColors(evenRow:, oddRow:, hoverOrExpanded:) — brand row colours (falls back to theme colours when null).
  • headerColor — explicit header background.
  • columnWidths, rowBorder, isSelectable + onSelectionChanged.

Describe your app's menu as a tree of NavigationItems. Each item points at either a routePath or has children (not both). Pass them to NavigationLogic (defaultMenuItems / defaultTabItems) or per route (SlickRoute.menuItems):

import 'package:flutter_slick/navigation/item.dart';

final menuItems = [
  NavigationItem(
    labelText: 'Dashboard',
    iconData: Icons.dashboard,
    routePath: const DashboardRoute(),
  ),
  NavigationItem(
    labelText: 'Fleet',
    iconData: Icons.directions_car,
    children: [
      NavigationItem(labelText: 'Vehicles', routePath: const VehiclesRoute()),
      NavigationItem(labelText: 'Drivers', routePath: const DriversRoute()),
    ],
  ),
];

When a FeatureAccessController is configured, read NavigationLogic.filteredMenuItems (or SlickRoute.filteredMenuItems) to get the tree with items the user can't access already removed — including parents whose children are all hidden.

Render items with StandardMenuItem (in a Drawer, NavigationRail, etc.). It supports nested children, selected styling, indicator icons, and omits the tap target for items with a null routePath (store-compliance friendly). Use routePathSelection on a NavigationItem to keep it highlighted across a set of related routes.

Wizard

Wizard is a ready-made multi-step flow. Give it a list of WizardPages; each page controls its own Next/Back behaviour with async hooks (return false from onNext to block advancing — e.g. on a failed validation). The Next button shows a spinner while an async onNext runs.

import 'package:flutter_slick/components/wizard/wizard_ui.dart';
import 'package:flutter_slick/components/wizard/wizard_page.dart';

Wizard(
  title: const Text('Create vehicle'),
  actions: const [],
  onCancel: () => navigation.goBack(),
  pages: [
    WizardPage(
      title: 'Details',
      child: const VehicleDetailsForm(),
      onNext: () async => await saveDraft(),   // false blocks navigation
    ),
    WizardPage(
      title: 'Drivers',
      child: const AssignDriversForm(),
      hideCancelButton: true,
    ),
    WizardPage(
      title: 'Review',
      child: const ReviewStep(),
      nextButtonText: 'Finish',
      onNext: () async => await submit(),
    ),
  ],
);

Pages can hide individual buttons (hideNextButton, hideBackButton, hideCancelButton) and customise their labels. Set isModal: true when hosting the wizard in a dialog.

Add a breadcrumb trail driven by the current route. Register a BreadcrumbService (with optional resolvers that turn entity IDs in the URL into friendly names), then drop a BreadcrumbNavigation widget into your scaffold:

import 'package:flutter_slick/navigation/breadcrumb_setup.dart';
import 'package:flutter_slick/components/navigation/breadcrumb_navigation.dart';

final breadcrumbs = BreadcrumbSetup.register(
  entityNameResolver: myEntityNameResolver,   // id -> display name
  entityTypeResolver: myEntityTypeResolver,
  excludedSegments: const ['edit'],
  minimumDepthToShow: 2,
);

// In a screen:
BreadcrumbNavigation(
  breadcrumbService: breadcrumbs,
  separatorIcon: Icons.chevron_right,
);

Dialogs & alerts

StandardDialog wraps AlertDialog and integrates with dirty-state tracking via a beforeClose guard:

import 'package:flutter_slick/components/dialogs/dialog.dart';

showDialog(
  context: context,
  builder: (_) => StandardDialog(
    title: const Text('Confirm'),
    content: const Text('Delete this vehicle?'),
    beforeClose: () async => true,   // return false to keep the dialog open
    actions: [
      TextButton(onPressed: () => Navigator.pop(context), child: const Text('Cancel')),
      FilledButton(onPressed: confirmDelete, child: const Text('Delete')),
    ],
  ),
);

For view-model–driven alerts, call showInfoMessage / showWarningMessage / showErrorMessage on a ViewModel. By default these queue and show a simple dialog; to route them through your own UI (snackbars, toasts, branded dialogs), register an AlertController:

import 'package:flutter_slick/services/alert_controller.dart';

AlertController(
  onShowError: ({required message, title}) => myToast.error(message),
  onShowInfo: ({required message, title}) => myToast.info(message),
  showWarning: ({required message, title}) => myToast.warn(message),
);

Text components

Lightweight typed text widgets with consistent styling and formatting:

import 'package:flutter_slick/components/text/title.dart';
import 'package:flutter_slick/components/text/body.dart';
import 'package:flutter_slick/components/text/label.dart';
import 'package:flutter_slick/components/text/currency.dart';
import 'package:flutter_slick/components/text/percent.dart';
import 'package:flutter_slick/components/text/error.dart';
import 'package:flutter_slick/components/text/nullable.dart';
import 'package:flutter_slick/components/text/readonly_field.dart';
import 'package:flutter_slick/components/text/link.dart';

TitleText('Vehicle details');         // 18px
BodyText('Some body copy');           // 16px
LabelText('REGISTRATION');            // 12px
CurrencyText(1234.5);                 // formatted currency
PercentText(0.42);                    // formatted percentage
ErrorText('Something went wrong');    // error-coloured
NullableText(maybeValue);             // 'n/a' when null
ReadonlyField(label: 'Status', text: 'Active');
LinkText('Open dashboard', const DashboardRoute());  // tappable, navigates

Utilities

Formatting (utils/format.dart)

import 'package:flutter_slick/utils/format.dart';

formatDate(date);            // date only
formatDateTime(date);        // date + time
formatCurrency(1234.5);      // currency string
formatPercent(0.42);         // percentage
formatNumber(1000);          // grouped number
formatNullable(value);       // 'n/a' when null
formatInitials('Greg van Berkel');

// Extension methods on double:
(1234.5).formatCurrency(includeSymbol: true, includeCents: true);
(0.1234).format(2);          // 2 decimal places

Dates (utils/slick_datetime.dart)

SlickDateTime is a DateTime wrapper with formatting helpers and a sentinel "min" concept (handy with code-generated API clients). Convert with the .slick extension:

import 'package:flutter_slick/utils/slick_datetime.dart';

final d = DateTime.now().slick;     // SlickDateTime?
d.formatShortDate();
d.formatDateTime();
d.monthName();

Colours (utils/color_extensions.dart)

import 'package:flutter_slick/utils/color_extensions.dart';

theme.primary.darken(20);
theme.primary.lighten(40);
colorA.avg(colorB);

Other utilities: utils/parse.dart (duration parsing/formatting), utils/strings.dart (normalizePath() extension), and the validators covered above.

Other services

  • ConnectionService (services/connection_service.dart) — wraps connectivity_plus; hasConnectivity() and an onHasConnectionStateChanged callback.
  • ErrorService (services/error.dart) — an app-wide error broadcast stream; raise(error).
  • ApiService<User, UserModel> (services/api.dart) — a base for API clients that react to auth state and manage a token.

Example app

A runnable example covering app setup, typed routes (path/query params, redirects), view-model loading states and custom transitions lives in the example/ directory. Start with example/lib/main.dart, example/lib/application.dart and example/lib/routes.dart.

License

See the repository for license details.

Libraries

components/buttons/primary_action
components/buttons/secondary_action
components/buttons/standard_elevated_button
components/buttons/standard_outlined_button
components/buttons/standard_text_button
components/buttons/worded_toggle
components/dialogs/dialog
components/feedback/default_loading
components/fields/checkbox
components/fields/currency
components/fields/date
components/fields/duration
components/fields/email
components/fields/file
components/fields/inline_multi_select/chip_input
components/fields/inline_multi_select/inline_multi_select_input
components/fields/number
components/fields/password
components/fields/text
components/indicator_icon
components/layout/dynamic_column_layout
components/layout/grid/action_row
components/layout/grid/data_row
components/layout/grid/staggered_grid_view
components/layout/grid/standard_grid
components/layout/grid/table_text_cell
components/layout/media_layout
components/layout/spaced_column
components/layout/spaced_row
components/standard_menu_item
components/text/body
components/text/currency
components/text/error
components/text/label
components/text/nullable
components/text/percent
components/text/readonly_field
components/text/title
components/wizard/wizard_page
components/wizard/wizard_ui
components/wizard/wizard_vm
flutter_slick
navigation/item
navigation/logic
navigation/slick_route
navigation/view_model
navigation/view_model_widget
services/alert_controller
services/alert_request
services/api
services/authentication
services/connection_service
services/dirty_state_monitor
services/error
services/feature_access_controller
services/view_model_state_manager
utils/action_dialog_options
utils/color_extensions
utils/format
utils/go_router_extensions
utils/parse
utils/slick_datetime
utils/strings
utils/validators