Stacked Framework

repository·master·Indexed 21 days ago

https://github.com/stacked-org/stacked

A comprehensive frontend architecture framework for Flutter designed to facilitate the creation of testable and maintainable production applications. It provides a structured approach to organizing codebases, including tools for reactive state management via ReactiveList and ReactiveValue, route lifecycle monitoring with RouteAware and RouteAwareStateMixin, and advanced routing capabilities through NestedRouterDelegate and StackedRouteGuard.

Tokens
19.7K
Snippets
63
Records
76
Agent score
76%

What's inside Stacked

  1. What is Stacked?

    master
    Stacked is a complete frontend architecture framework for Flutter designed to help developers build production-ready, testable, and maintainable applications. It provides a structured approach to organizing Flutter codebases.
  2. What is Stacked architecture

    master

    Stacked is an architecture for Flutter applications based on MVVM (Model-View-ViewModel) principles. It is designed to provide common functionalities that make app development easier while enforcing code principles that ensure maintainability.

    The architecture is built around three major components:

    1. View: A UI representation that shows the interface to the user. Views should contain minimal to no logic and should only render the state provided by their ViewModel.
    2. ViewModel: Manages the state of the View, handles business logic, and processes user interactions. It interacts with Services to perform tasks.
    3. Services: Wrappers for specific functionalities or feature sets (e.g., API integration, database access, or showing dialogs).

    Core Principles:

    • Views must never use Services directly.
    • Views should only render the state from their ViewModel.
    • ViewModels should not know about other ViewModels.
    • ViewModels for page-level views are typically bound to a single View, but can be reused for widgets if the UI requires the same functionality.
  3. How to implement reactivity between Services and ViewModels

    master

    Stacked provides a mechanism for ViewModels to react to state changes in shared services using the ReactiveServiceMixin and ReactiveViewModel.

    1. In the Service: Use ReactiveServiceMixin to allow the service to notify listeners. When updating state, call notifyListeners().
    2. In the ViewModel: Extend ReactiveViewModel and override the reactiveServices getter to return a list of the services this ViewModel needs to listen to.

    This ensures that whenever a service calls notifyListeners(), any ViewModel that has registered that service in its reactiveServices list will be notified and can trigger UI updates.

    // 1. The Service
    class PostsService with ReactiveServiceMixin {
      int _postCount = 0;
      int get postCount => _postCount;
    
      void setPostCount(int count) {
        _postCount = count;
        notifyListeners(); // Notifies registered ViewModels
      }
    }
    
    // 2. The ViewModel
    class AnyViewModel extends ReactiveViewModel {
      final _postsService = locator<PostsService>();
      int get postCount => _postsService.postCount;
    
      @override
      List<ReactiveServiceMixin> get reactiveServices => [_postsService];
    }
  4. How ViewModelBuilder works in Stacked

    master

    The ViewModelBuilder creates the binding between a ViewModel (a class extending ChangeNotifier) and a View. It wraps ChangeNotifierProvider logic to trigger widget rebuilds when notifyListeners() is called within the ViewModel.

    There are two primary modes of binding:

    1. Reactive: The default mode. The builder function re-executes whenever the ViewModel calls notifyListeners(), allowing the UI to stay in sync with the state.
    2. Non-Reactive: Used when you want to provide a ViewModel to multiple child widgets without triggering a rebuild of the entire parent tree. In this mode, you use ViewModelWidget or SelectorViewModelWidget in the children to specify exactly which parts of the UI should rebuild when the state changes.
    // Reactive binding example
    return ViewModelBuilder<HomeViewModel>.reactive(
      viewModelBuilder: () => HomeViewModel(),
      onModelReady: (viewModel) => viewModel.initialise(),
      builder: (context, viewModel, child) => Scaffold(
        body: Center(child: Text(viewModel.title)),
      ),
    );
  5. Implement Nested Navigation

    master

    Nested navigation is achieved by declaring routes within the children property of a parent route in the @StackedApp annotation.

    1. Define Routes: Add child routes to a parent MaterialRoute.
    2. Render Navigator: In the parent view, use the ExtendedNavigator widget. The router name is generated by appending Router to the parent page name (e.g., OtherNavigator becomes OtherNavigatorRouter).
    3. Navigate: Use NavigationService.navigateTo providing the generated route and an id that matches the nestedNavigationKey used in the ExtendedNavigator.
    // 1. Route Definition
    @StackedApp(routes: [
        MaterialRoute(page: HomeView, initial: true),
        MaterialRoute(page: OtherNavigator, children: [
          MaterialRoute(page: OtherView, initial: true),
          MaterialRoute(page: OtherNestedView),
        ]),
      ],
    )
    
    // 2. Rendering the Nested Navigator
    class OtherNavigator extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          body: ExtendedNavigator(router: OtherNavigatorRouter(), navigatorKey: StackedService.nestedNavigationKey(1)),
        );
      }
    }
    
    // 3. Navigating
    _navigationService.navigateTo(OtherNavigatorRoutes.otherNestedView, id: 1);
  6. Getting started with the Navigator example project

    master
    The navigator_example project serves as a starting point for a Flutter application demonstrating the new architecture. If you are new to Flutter, it is recommended to follow the official Flutter codelabs and cookbooks to understand the fundamentals of mobile development before diving into the specific architecture of this project.
  7. Implement per-field validation

    master

    Stacked provides automated per-field validation when using @FormView. For every field defined, the generator creates specific methods to manage its error state:

    • set[FieldName]ValidationMessage(String? message): Call this in setFormStatus to set the error message for a specific field. Pass null if the field is valid.
    • has[FieldName]ValidationMessage: A boolean flag to check if the specific field has an error.
    • [fieldName]ValidationMessage: The error string to display in the UI for that field.

    Example logic: A validator function should return a String containing the error message or null if the input is valid.

    class ExampleFormViewModel extends FormViewModel {
    
      @override
      void setFormStatus() {
        // Set the validation message per field
        setPasswordValidationMessage(passwordValidator(value: passwordValue));
    
        // Set a validation message for the entire form if a field is invalid
        if (hasPasswordValidationMessage) {
          setFormValidationMessage('Error in the form, please check again');
        }
      }
    }
    
    // In the View
    if (viewModel.hasPasswordValidationMessage)
      Text(
        viewModel.passwordValidationMessage!,
        style: TextStyle(color: Colors.red),
      ),
  8. Initialize the Locator

    master

    After running the code generator, a setupLocator() function is created in app.locator.dart. This must be called in your main.dart before runApp().

    If you have any Presolve dependencies, main() must be an async function and you must await setupLocator().

    void main() {
      setupLocator();
      runApp(MyApp());
    }
    
    // If using Presolve dependencies:
    Future main() async {
      await setupLocator();
      runApp(MyApp());
    }
  9. Handle errors in BaseViewModel

    master

    When using runBusyFuture or runErrorFuture, Stacked automatically catches exceptions, sets the busy state to false, and stores the error for you.

    Retrieving Errors

    • Global error: If no key was provided to the future, check viewModel.hasError and retrieve the exception via viewModel.modelError.
    • Keyed error: If a busyObject key was provided, check for the error using viewModel.hasErrorForKey(key) and retrieve it via viewModel.error(key).

    Reacting to Errors

    You can override onFutureError(Exception error, dynamic key) in your ViewModel to perform specific logic when a future fails.

    class ErrorExampleViewModel extends BaseViewModel {
      Future longUpdateStuff() async {
        // Automatically catches errors and sets busy to false
        await runBusyFuture(updateStuff());
      }
    
      Future updateStuff() async {
        await Future.delayed(const Duration(seconds: 3));
        throw Exception('Things went wrong');
      }
    }
    
    // Accessing the error in UI
    if (viewModel.hasError) {
      print(viewModel.modelError);
    }
    
    // Accessing keyed error
    if (viewModel.hasErrorForKey(BusyObjectKey)) {
      print(viewModel.error(BusyObjectKey));
    }
  10. Implement global form validation

    master

    To implement validation for an entire form, extend FormViewModel. You can manage the global validation state using these methods within the setFormStatus override:

    • setFormValidationMessage(String message): Sets a global error message for the entire form.
    • showFormValidationMessage: A boolean flag used in the View to determine if a global error message should be displayed.
    • formValidationMessage: The actual string message to display in the UI.

    In the View, check viewModel.showFormValidationMessage to conditionally render the error text.

    class ExampleFormViewModel extends FormViewModel {
    
      @override
      void setFormStatus() {
        // Set a validation message for the entire form
        if (<any unmet condition>) {
          setFormValidationMessage('Error in the form, please check again');
        }
      }
    }
    
    // In the View
    if (viewModel.showFormValidationMessage)
      Text(
        viewModel.formValidationMessage!,
        style: TextStyle(color: Colors.red),
      ),
  11. Generate form fields with @FormView

    master

    You can automate form field generation using the stacked_generator package. By adding the @FormView decoration to your View class, you can define fields like FormTextField, FormDateField, and FormDropdownField.

    After defining the annotation, run the following command to generate the necessary mixin:

    flutter pub run build_runner build --delete-conflicting-outputs

    Once generated, use the mixin in your View (e.g., with $ExampleFormView). To ensure the form values are tracked and cleaned up, use the onModelReady and onDispose callbacks in your ViewModelBuilder:

    onModelReady: (viewModel) => listenToFormUpdated(viewModel),
    onDispose: (_) => disposeForm(),

    To access the actual values (e.g., emailValue, passwordValue) in your ViewModel, import the generated file.

    @FormView(fields: [
      FormTextField(name: 'email', initialValue: "Lorem"),
      FormTextField(name: 'password', isPassword: true),
      FormTextField(name: 'shortBio'),
      FormDateField(name: 'birthDate'),
      FormDropdownField(
        name: 'doYouLoveFood',
        items: [
          StaticDropdownItem(title: 'Yes', value: 'YesDr'),
          StaticDropdownItem(title: 'No', value: 'NoDr'),
        ],
      )
    ])
    class ExampleFormView extends StatelessWidget with $ExampleFormView {
      ExampleFormView({Key? key}) : super(key: key);
    }