Skip to main content

flutterx_application 3.0.0 copy "flutterx_application: ^3.0.0" to clipboard
flutterx_application: ^3.0.0 copied to clipboard

PlatformiOSweb

Application foundation for Flutter apps: fragment navigation with optional go_router sync, view models, token handling and common widgets.

flutterx_application #

Application foundation for Flutter apps: fragment navigation with optional go_router sync, view models with task tracking, token handling with refresh, and common widgets.

Installation #

flutter pub add flutterx_application
import 'package:flutterx_application/flutterx_application.dart';

Fragments #

A Fragment is a page of a FragmentHost, which shows one at a time and animates between them. Each fragment has a constant FragmentKey and a FragmentState:

class HomeFragment extends Fragment {
  static const FragmentKey fragmentKey = FragmentKey('home');

  const HomeFragment({super.key = fragmentKey});

  @override
  FragmentState<HomeFragment> createState() => _HomeFragmentState();
}

class _HomeFragmentState extends FragmentState<HomeFragment> {
  @override
  void onResume() {
    super.onResume();   // required
    // the fragment is now the shown one
  }

  @override
  void onPause() {
    // the fragment is no longer shown, or was removed while shown
    super.onPause();    // required
  }

  @override
  Widget build(BuildContext context) => Text('${args<RouteArgs?>()}');
}

onResume and onPause are the only hooks; active tells whether the fragment is between them. args<X>() reads the arguments of the entry that showed the fragment (null before the first one: use args<RouteArgs?>()).

The host is driven by a FragmentManager, created and kept by the owner of the host:

final manager = FragmentManager(key: 'main');

FragmentHost(
  manager: manager,
  storage: localStorage,                         // optional: persists the history
  fragments: const [HomeFragment(), SettingsFragment()],
);

Inside a fragment, manager (or FragmentManager.of(context)) navigates:

manager.navigateToKey(SettingsFragment.fragmentKey, args: {'tab': 2});
manager.navigateTo<SettingsFragment>(replace: true);   // first fragment of that type
manager.back();                                        // false if there is no history
manager.forward();

manager.canGoBack and canGoForward are LiveData<bool>. manager.currentFragment is the FragmentStack: the entry shown plus the history. With a storage, history and index are saved at every change under <manager key>_fragment_stack and restored when the host starts. The system back pops the history; once empty, the route pops if canPop is true.

Router sync #

With a GoRouter above the host and syncLocationFragment: true (the default), the shown fragment is mirrored in the fragment of the location (/page#settings) and a location with a known fragment selects it. Without GoRouter the flag does nothing; set it to false to leave the location alone.

Unknown entries #

unknownEntryStrategy decides what to do with an entry whose fragment is not in fragments (a restored history of an older version, a link): FragmentHost.unknownEntryStrategyIdle (default) keeps the current fragment and leaves no trace in the history, unknownEntryStrategyInitial shows initialFragment or the first fragment, unknownEntryStrategyCrash throws a StateError. A custom strategy returns the entry to show instead, or null.

View models #

ViewModelScope holds the view models of its subtree and clears them when it leaves the tree. Put one above the app:

class CounterViewModel extends ViewModel with TaskViewModel {
  final MutableLiveData<int> count = MutableLiveData(value: 0);

  CounterViewModel(super.provider);

  static CounterViewModel of(BuildContext context) => context.viewModel(CounterViewModel.new);

  Future<void> increment() async {
    await Future<void>.delayed(const Duration(milliseconds: 800)).notify(this);
    count.value++;
  }
}

ViewModelScope(child: MaterialApp(home: const HomePage()));

context.viewModel(factory, key:) creates the view model the first time and returns the same instance afterwards; view models are indexed by type and key, so several of one type coexist with different keys. Pass the constructor tear-off: the type is inferred from it and must be the exact runtime type.

TaskViewModel tracks futures with future.notify(this, tag:): loading and loadingWithTag are LiveData<bool> for loaders, activeTasks lists the running tasks, loadingDependsOn(other) makes a view model loading while another is. The error of a failed task is rethrown to the caller.

TokenHandler #

Keeps a token, refreshes it when it expires and turns it into the authorization header value. Reads and updates are serialized, so concurrent requests refresh it once:

final tokens = TokenHandler<Session>.storage(
  key: 'session',
  storage: localStorage,
  toJson: (session) => session.toJson(),
  fromJson: Session.fromJson,
  isTokenValid: (session) => session.expiresAt.isAfter(DateTime.now()),
  authorize: (session) => 'Bearer ${session.accessToken}',
  refreshToken: (session) => api.refresh(session.refreshToken),
  errorShouldClearToken: (error) => error is XHttpException && error.response?.statusCode == 401,
);

await tokens.updateToken(() => api.login(user, password));
final header = await tokens.authorization;   // null when logged out
await tokens.clear();

TokenHandler(...) holds the token in memory only. requireAuthorization throws a TokenException when there is no token; authorization throws it when the token cannot be renewed. Without refreshToken an expired token is dropped. A failed refresh keeps the token unless errorShouldClearToken returns true. token and hasToken are LiveData.

Widgets #

ActionButton<void>(label: 'Save', onAction: save, buttonBuilder: ActionButton.buttonFilled);
ActionButton<User>.positive(onAction: load, onActionResult: show);

ActionButton runs onAction and stays disabled until it completes; enabled adds an external condition. Define the content with exactly one of label, child or builder.

  • NiceDialog<T>: an AlertDialog with cancel and submit actions; submitForm validates (return true to close), submitResult is the value the dialog closes with. Show it with showDialog.
  • Message: a banner, Message.success(title: 'Saved').show(context); also info, warning, error.
  • FloatingMenu: an anchor (builder) with a menu (overlayBuilder); both receive the OverlayPortalController.
  • Initializer<T>: runs initialize once and builds with the result, a placeholder meanwhile and an error state if it fails.
  • SimpleLoader, AppBarLoader(loading: viewModel.loading): circular and linear progress.
  • FooterView: a footer kept at the bottom when the body is short.
  • KeepAliveBuilder: keeps its subtree alive in a lazy list.
  • ScopedData: creates a value once per state and builds with it.
  • SystemUiOverlayStyleTheme: applies a SystemUiOverlayStyle to the system bars and to the app bars below.

Behavior #

  • onResume of a fragment runs at its first didChangeDependencies, after its whole initState.
  • FragmentKey is a LocalKey equal by name: two hosts can hold fragments with the same name independently.
  • A navigation superseded by a newer one has no effect: only the last entry is shown.
  • ActionButton is disabled while onAction runs; its errors go to FlutterError.reportError, onActionResult is not called and the button is enabled again.
  • Initializer restarts initialize after an error whenever a new callback is given: pass a tear-off, not an inline closure, or every rebuild of the parent retries.
  • FloatingMenu owns its controller and animation and releases them; whoever creates it releases nothing.
  • The spacing constants m4…m80 are no longer in the library: the app defines them in its own theme.

Claude Code plugin #

The repository ships a plugin with skills that teach Claude Code to integrate the library:

/plugin marketplace add https://gitlab.com/dade.sliep/flutterx_application
/plugin install flutterx-application@flutterx-application
4
likes
140
points
675
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Application foundation for Flutter apps: fragment navigation with optional go_router sync, view models, token handling and common widgets.

Repository (GitLab)
View/report issues

License

BSD-3-Clause (license)

Dependencies

collection, flutter, flutterx_live_data, flutterx_preferences, flutterx_utils, go_router, pointer_interceptor

More

Packages that depend on flutterx_application