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
- Importing — there is no barrel file
- Quick start — wire up an app in three files
- Routing —
SlickRoute, parameters, redirects, navigation - MVVM —
ViewModel+ViewModelWidget - Authentication
- Feature access & permissions
- Form fields & validation
- Buttons
- Layout —
SpacedColumn,SpacedRow,MediaLayout,DynamicColumnLayout - Data grids —
StandardGrid - Navigation menus —
NavigationItem,StandardMenuItem - Wizard
- Breadcrumbs
- Dialogs & alerts
- Text components
- Utilities — formatting, validators, dates, colours
- Example app
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_builderandbuild_runnerto yourdev_dependenciesfor 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');
}
Navigating
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
redirectis reserved by Slick — always useonRedirect.
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— theNavigationLogicsingleton;navigateBack(context).isLoading,isInitialised,loadingIndicatorStyle(auto,overlay,none,fullScreen).dirtyStateMonitor— unsaved-changes tracking (form fields mark it dirty automatically).showInfoMessage/showWarningMessage/showErrorMessage— routed through theAlertController.
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)andshowDiscardDialog.showMessage(context, text)andshowErrorSnackBar(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;ListLengthValidatorfor 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.
Navigation menus
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.
Breadcrumbs
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) — wrapsconnectivity_plus;hasConnectivity()and anonHasConnectionStateChangedcallback.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/worded_toggle
- components/dialogs/dialog
- components/feedback/default_loading
- components/fields/checkbox
- components/fields/currency
- components/fields/date
- components/fields/dropdown
- 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/text/body
- components/text/currency
- components/text/error
- components/text/label
- components/text/link
- 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