fbae_core 0.1.0 copy "fbae_core: ^0.1.0" to clipboard
fbae_core: ^0.1.0 copied to clipboard

Flutter Base App Engine — pluggable infrastructure package.

fbae_core #

Flutter Base App Engine — pluggable infrastructure package.

Add fbae_core to your app and call FbaeCore.init() in main() to get navigation, HTTP, theming, env config, and logging out of the box.

pub package License: MIT

Quick Start #

import 'package:fbae_core/fbae_core.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();

  await FbaeCore.init(
    config: FbaeConfig.dev(baseUrl: 'https://dev.api.example.com'),
  );

  runApp(const MyApp());
}

Environment Setup #

fbae_core does not depend on envied. Environment variables are injected at build time in your app — fbae_core only consumes the resolved values.

1. Add envied to your app #

# your_app/pubspec.yaml
dev_dependencies:
  envied: ^1.3.4
  envied_generator: ^1.3.4
  build_runner: ^2.4.0

2. Create .env files #

# .env.dev
API_BASE_URL=https://dev.api.example.com
DARK_MODE_ENABLED=true

# .env.prod
API_BASE_URL=https://api.example.com
DARK_MODE_ENABLED=false

Add .env.* files to .gitignore.

3. Define an @Envied class #

import 'package:envied/envied.dart';

@Envied(path: '.env.prod', obfuscate: true)
abstract class Env {
  @EnviedField(varName: 'API_BASE_URL')
  static final String baseUrl = _Env.baseUrl;

  @EnviedField(varName: 'DARK_MODE_ENABLED', defaultValue: false)
  static final bool darkModeEnabled = _Env.darkModeEnabled;
}

Run dart run build_runner build to generate env.g.dart.

4. Wire into FbaeConfig #

// main.dart
await FbaeCore.init(
  config: FbaeConfig.production(
    baseUrl: Env.baseUrl,
    featureFlags: {'dark_mode': Env.darkModeEnabled},
  ),
);

For dev builds, swap the factory:

await FbaeCore.init(
  config: FbaeConfig.dev(
    baseUrl: Env.baseUrl,  // points to .env.dev values
    featureFlags: {'dark_mode': Env.darkModeEnabled},
  ),
);

5. Custom logger (optional) #

Swap the default TalkerAppLogger without changing any call sites:

await FbaeCore.init(
  config: FbaeConfig.production(baseUrl: Env.baseUrl),
  logger: MyCrashlyticsLogger(),  // implements AppLogger
);

HTTP Client #

fbae_core ships a typed HTTP client backed by Dio. After FbaeCore.init() completes, access it via FbaeCore.instance.httpClient. All methods throw ApiError on non-2xx responses so there is no need to inspect status codes at call sites — just catch ApiError.

Accessing the client #

final client = FbaeCore.instance.httpClient;

GET request #

final products = await FbaeCore.instance.httpClient.get<List<dynamic>>(
  '/products',
  queryParams: {'limit': '10'},
  fromJson: (data) => (data['products'] as List),
);

POST request #

final result = await FbaeCore.instance.httpClient.post<Map<String, dynamic>>(
  '/auth/login',
  body: {'username': 'emilys', 'password': 'emilyspass', 'expiresInMins': 30},
  fromJson: (data) => data as Map<String, dynamic>,
);
final accessToken = result['accessToken'] as String;

Error handling #

try {
  final user = await FbaeCore.instance.httpClient.get<User>(
    '/users/1',
    fromJson: User.fromJson,
  );
} on ApiError catch (e) {
  if (e.statusCode == 401) navigateToLogin();
  if (e.statusCode == 404) showNotFound();
}

TokenProvider — attaching Bearer tokens #

Pass a TokenProvider callback to FbaeCore.init(). It is called on every outbound request and its return value is attached as Authorization: Bearer <token>. Return null for unauthenticated requests — the header is omitted automatically.

// In-memory token store (replace with your secure storage)
String? _accessToken;

await FbaeCore.init(
  config: FbaeConfig.production(baseUrl: 'https://api.example.com'),
  tokenProvider: () async => _accessToken,
  onRefreshToken: () async {
    // Call your refresh endpoint and persist new tokens
    final response = await http.post(Uri.parse('https://api.example.com/auth/refresh'));
    _accessToken = jsonDecode(response.body)['accessToken'] as String;
    return _accessToken!;
  },
);

onRefreshToken is optional. When provided, fbae_core automatically retries a 401 response with a refreshed token — no manual retry logic needed in your cubits.

fbae_core provides FbaeShell — a pre-built shell widget that renders either a Material 3 NavigationBar (bottom tabs) or a Drawer based on the ShellNavigationConfig you supply. FbaeRouter.buildRoutes() wires FbaeShell into GoRouter for you. The consumer always owns the GoRouter instance — fbae_core never creates one.

Bottom-tab navigation #

Define a TabConfig with one TabItemConfig per branch, then pass it alongside matching StatefulShellBranch entries to FbaeRouter.buildRoutes():

import 'package:fbae_core/fbae_core.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';

final router = GoRouter(
  initialLocation: '/products',
  routes: FbaeRouter.buildRoutes(
    config: TabConfig([
      TabItemConfig(
        icon: const Icon(Icons.shopping_bag_outlined),
        activeIcon: const Icon(Icons.shopping_bag),
        label: 'Products',
        initialLocation: '/products',
      ),
      TabItemConfig(
        icon: const Icon(Icons.search_outlined),
        activeIcon: const Icon(Icons.search),
        label: 'Search',
        initialLocation: '/search',
      ),
      TabItemConfig(
        icon: const Icon(Icons.settings_outlined),
        activeIcon: const Icon(Icons.settings),
        label: 'Settings',
        initialLocation: '/settings',
      ),
    ]),
    branches: [
      StatefulShellBranch(routes: [GoRoute(path: '/products', builder: (_, __) => const ProductsPage())]),
      StatefulShellBranch(routes: [GoRoute(path: '/search',   builder: (_, __) => const SearchPage())]),
      StatefulShellBranch(routes: [GoRoute(path: '/settings', builder: (_, __) => const SettingsPage())]),
    ],
  ),
);

Branch order must match TabItemConfig order — the nth branch corresponds to the nth tab item. Each branch uses StatefulShellRoute.indexedStack internally, so tab state is preserved across tab switches without any extra configuration.

Drawer navigation #

Swap TabConfig for DrawerConfig — everything else stays the same:

config: DrawerConfig(
  header: const DrawerHeader(child: Text('My App')),
  items: [
    DrawerItemConfig(icon: const Icon(Icons.home), label: 'Home',     location: '/home'),
    DrawerItemConfig(icon: const Icon(Icons.info), label: 'About',    location: '/about'),
  ],
),

Theming #

fbae_core ships FbaeColors (a ThemeExtension with 13 colour slots), FbaeTheme.build() (a ThemeData factory), FbaeThemeProvider (a BlocProvider<ThemeCubit> wrapper), and ThemeCubit (a cubit that manages ThemeMode).

Wire up themes in MaterialApp.router #

Wrap your MaterialApp.router with FbaeThemeProvider and read ThemeCubit state to drive themeMode:

import 'package:fbae_core/fbae_core.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

final _lightColors = FbaeColors(brightness: Brightness.light);
final _darkColors  = FbaeColors(
  brightness:  Brightness.dark,
  primary:     const Color(0xFFD0BCFF),
  onPrimary:   const Color(0xFF381E72),
  background:  const Color(0xFF1C1B1F),
  surface:     const Color(0xFF49454F),
  textPrimary: const Color(0xFFE6E1E5),
);

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

  @override
  Widget build(BuildContext context) {
    return FbaeThemeProvider(
      child: BlocBuilder<ThemeCubit, ThemeMode>(
        builder: (context, themeMode) {
          return MaterialApp.router(
            theme:      FbaeTheme.build(colors: _lightColors),
            darkTheme:  FbaeTheme.build(colors: _darkColors),
            themeMode:  themeMode,
            routerConfig: router,
          );
        },
      ),
    );
  }
}

Toggling the theme #

Call ThemeCubit.toggle() from any widget that has BuildContext:

ElevatedButton(
  onPressed: () => context.read<ThemeCubit>().toggle(),
  child: const Text('Toggle theme'),
),

toggle() cycles: system → light, light → dark, dark → light. Use setMode(ThemeMode.system) to return to system preference explicitly.

Accessing FbaeColors in widgets #

FbaeColors is registered as a ThemeExtension. Access it anywhere below MaterialApp:

final colors = Theme.of(context).extension<FbaeColors>()!;
Text('Hello', style: TextStyle(color: colors.textPrimary));
Container(color: colors.surface);

This keeps custom colours type-safe and co-located with ThemeData — no InheritedWidget needed.

State Management #

fbae_core provides AsyncState<T> — a sealed, typed async lifecycle — and BaseCubit<T> which manages the AsyncInitial → AsyncLoading → AsyncData | AsyncError transition for you. FbaeBlocObserver is auto-wired during FbaeCore.init() and logs all BLoC/Cubit transitions.

AsyncState variants #

AsyncInitial<T>   — not yet started (initial state)
AsyncLoading<T>   — in-flight async operation
AsyncData<T>      — successful result; holds .value
AsyncError<T>     — failure; holds .error and .stackTrace

Extending BaseCubit #

Create a cubit by extending BaseCubit<T> and delegating to run():

import 'package:fbae_core/fbae_core.dart';

class ProductsCubit extends BaseCubit<List<Product>> {
  ProductsCubit(this._client);

  final FbaeHttpClient _client;

  Future<void> load() => run(
    () async {
      final data = await _client.get<List<dynamic>>('/products', fromJson: (d) => d['products']);
      return (data as List).map(Product.fromJson).toList();
    },
  );

  /// Silent reload — preserves the current AsyncData state while fetching.
  Future<void> refresh() => run(
    () async {
      final data = await _client.get<List<dynamic>>('/products', fromJson: (d) => d['products']);
      return (data as List).map(Product.fromJson).toList();
    },
    silent: true,
  );
}

Consuming state in widgets #

Use the exhaustive switch pattern — the compiler enforces all four variants:

BlocBuilder<ProductsCubit, AsyncState<List<Product>>>(
  builder: (context, state) => switch (state) {
    AsyncInitial() => const SizedBox.shrink(),
    AsyncLoading() => const CircularProgressIndicator(),
    AsyncData(:final value) => ListView.builder(
        itemCount: value.length,
        itemBuilder: (_, i) => ListTile(title: Text(value[i].title)),
      ),
    AsyncError(:final error) => Text('Error: $error'),
  },
),

FbaeBlocObserver #

FbaeBlocObserver is registered automatically during FbaeCore.init(). In debug builds it logs every onChange, onTransition, and onError event through AppLogger — no configuration required. In profile/release builds the verbosity is reduced to errors only, matching the LogLevel from FbaeConfig.


See the example app for a full working demo.

1
likes
0
points
147
downloads

Publisher

unverified uploader

Weekly Downloads

Flutter Base App Engine — pluggable infrastructure package.

Repository (GitHub)
View/report issues

License

unknown (license)

Dependencies

dio, flutter, flutter_bloc, get_it, go_router, talker, talker_dio_logger, talker_flutter

More

Packages that depend on fbae_core