app_core

A shared Flutter package providing configurable theme, reusable widgets, network client, secure storage, and utilities — designed to be consumed by multiple apps from a single codebase.


Installation

In your app's pubspec.yaml:

dependencies:
  app_core:
    path: ../app_core   # local path
    # or via git:
    # git:
    #   url: https://github.com/your-org/app_core.git

Then import everything with one line:

import 'package:app_core/app_core.dart';

Theme

Basic setup

MaterialApp(
  theme: AppTheme.light(),
  darkTheme: AppTheme.dark(),
);

Only primary and secondary are required. Everything else has sensible defaults.

final scheme = AppColorScheme(
  primary: Color(0xFFFF6D00),
  secondary: Color(0xFF00BFA5),
);

MaterialApp(
  theme: AppTheme.light(scheme),
  darkTheme: AppTheme.dark(scheme),
);

Full customisation

final scheme = AppColorScheme(
  primary: Color(0xFF4A148C),
  onPrimary: Colors.white,
  secondary: Color(0xFF00C853),
  onSecondary: Colors.black,   // if secondary is light
  error: Color(0xFFB71C1C),
  warning: Color(0xFFF57F17),
  info: Color(0xFF0288D1),
  success: Color(0xFF2E7D32),  // defaults to secondary if omitted
);

Accessing extra theme tokens in widgets

// success, warning, info, divider colors
final ext = context.appTheme;
Container(color: ext.success);
Divider(color: ext.divider);

Widgets

Buttons

AppButton(label: 'Submit', onPressed: _submit)
AppButton(label: 'Cancel', onPressed: _cancel, variant: AppButtonVariant.secondary)
AppButton(label: 'Delete', onPressed: _delete, variant: AppButtonVariant.text)
AppButton(label: 'Loading', onPressed: null, isLoading: true)
AppButton(label: 'Icon', onPressed: _go, icon: Icon(Icons.send), size: AppButtonSize.small)

AppIconButton(icon: Icon(Icons.edit), onPressed: _edit)
AppIconButton(icon: Icon(Icons.delete), onPressed: _delete, variant: AppIconButtonVariant.filled)

Fields

// Standard text field
AppTextField(
  label: 'Email',
  prefixIcon: Icon(Icons.email),
  validator: Validators.compose([Validators.required, Validators.email]),
)

// Password field — visibility toggle built in
AppPasswordField(
  label: 'Password',
  validator: (v) => Validators.minLength(v, 8),
)

// Search field — clear button built in
AppSearchField(
  hint: 'Search products...',
  onChanged: (v) => bloc.add(SearchChanged(v)),
  onSearch: (v) => bloc.add(SearchSubmitted(v)),
)
AppDropdown<String>(
  label: 'Country',
  value: _selected,
  items: ['US', 'UK', 'AU'].map((e) => DropdownMenuItem(value: e, child: Text(e))).toList(),
  onChanged: (v) => setState(() => _selected = v),
)

Card

AppCard(
  header: Text('Title', style: context.textTheme.titleMedium),
  footer: AppButton(label: 'Action', onPressed: _action),
  onTap: _onTap,
  child: Text('Card body content'),
)

Dialog

AppDialog.show(
  context,
  title: 'Delete item?',
  content: Text('This cannot be undone.'),
  confirmLabel: 'Delete',
  isDestructive: true,
  onConfirm: _delete,
);

Bottom Sheet

AppBottomSheet.show(
  context,
  title: 'Options',
  child: Column(children: [...]),
);

States

AppStateWidget.empty(message: 'No results found')
AppStateWidget.error(onRetry: _reload)
AppStateWidget.noInternet(onRetry: _reload)

Loaders

AppLoader()                        // circular
AppShimmer(child: YourWidget())    // shimmer effect

Network

final client = ApiClient.create(
  baseUrl: 'https://api.example.com',
  tokenProvider: () => storage.read(AppConstants.keyAccessToken),
  tokenRefresher: () => authService.refresh(),
  enableLogging: kDebugMode,
);

// All calls return ApiResponse<T> — no try/catch needed
final response = await client.get(
  '/users/me',
  fromJson: (data) => User.fromJson(data),
);

response.when(
  success: (user) => print(user.name),
  failure: (e) => print(e.message),
);

Network connectivity

final networkInfo = NetworkInfoImpl(Connectivity());
if (await networkInfo.isConnected) { ... }

Storage

Wraps flutter_secure_storage — values are encrypted on device.

final storage = AppStorage();

await storage.write(AppConstants.keyAccessToken, token);
final token = await storage.read(AppConstants.keyAccessToken);
await storage.delete(AppConstants.keyAccessToken);
await storage.deleteAll();

Validators

// Single
AppTextField(validator: Validators.required)
AppTextField(validator: Validators.email)

// Composed — first error wins
AppTextField(
  validator: Validators.compose([
    Validators.required,
    Validators.email,
  ]),
)

Extensions

// String
'hello world'.titleCase   // 'Hello World'
'test@x.com'.isEmail      // true
'  '.isBlank              // true

// BuildContext
context.colors.primary
context.textTheme.bodyLarge
context.isDark
context.screenWidth
context.showSnackBar('Saved!', isError: false)

// Num spacing
16.verticalSpace       // SizedBox(height: 16)
8.horizontalSpace      // SizedBox(width: 8)
16.allPadding          // EdgeInsets.all(16)

Constants

AppConstants.keyAccessToken    // 'access_token'
AppConstants.keyRefreshToken   // 'refresh_token'
AppConstants.defaultPageSize   // 20
AppConstants.animNormal        // Duration(milliseconds: 300)

What each app needs to do

  1. Add app_core to pubspec.yaml
  2. Create an AppColorScheme with your brand primary and secondary
  3. Pass it to AppTheme.light(scheme) and AppTheme.dark(scheme)
  4. Create an ApiClient with your baseUrl and token callbacks
  5. That's it — all widgets, utils, and network are ready to use