Flutter Gadgets 2 🎩

Flutter Gadgets logo

A library for simplified state (model) management and service location in Flutter.

Migration to 0.3.0

  • The viewModel property and the init method have been removed from ViewModelGadget.

    Now the viewModel can be created and initialized using the create method.

    What used to be:

    ViewModelGadget<MyViewModel>(
      viewModel: MyViewModel(),
      init: (viewModel) => viewModel.controller = TextEditingController(),
      ...  
    )
    

    is now:

    ViewModelGadget<MyViewModel>(
      create: () => MyViewModel()..controller = TextEditingController(),
      ...
    )
    
  • The controller property and the init method have been removed from ViewGadget.

    Now the controller can be created and initialized using the create method.

    What used to be:

    ViewGadget<MyController>(
      controller: MyWelcomeViewController(),
      init: (controller) => ...,
      ...
    )
    

    is now:

    ViewGadget<MyController>(
      create: () => MyWelcomeViewController(),
      ...
    )
    

Introduction

State management in Flutter is tough. Flutter Gadgets provides a simple solution to that, along the same principles as Redux, but in our opinion much simpler to adopt.

In essence, the library provides a main "gadget", called ObservableModel, that is the main store for application state, i.e., atomic values or references to business-logic components. All of the state is stored within the ObservableModel, and Flutter Gadgets provides the needed infrastructure to access and modify the state from any application component.

This has the great advantage of keeping business-logic components clean from UI and widget-lifecycle-code.

With respect to other state-management solutions, Flutter Gadgets has the main advantage of having a very low initial learning step, and to be felt by developers as very similar to well-known state-management techniques from other platforms, like, for example, session management in J2EE Web app.

The library closes the gap between reactive and imperative programming, by providing a simple framework that takes the best from both worlds and mixes them in an easy and understandable way.

Flutter Gadgets also provides additional features, the most prominent one being a simple service-location framework.

How it works

Flutter Gadgets consists of seven main components:

  1. AppGadget
  2. ObservableModel
  3. SubscriberGadget
  4. ViewModelGadget
  5. ViewGadget
  6. ControllerGadget
  7. Service
Service Location

Service location is based on Service, a utility class that encapsulates a custom object that needs to be available everywhere in the widget tree.

Model Management

On the other hand, model management is insured by the usage of ObservableModel, combined with one or more SubscriberGadget(s).

By observing an object from the model, a subscriber gadget gets notified when its associated model instance changes and rebuilds instantly.

The advantage of this approach is that the business logic components will remain pure data holders with logic: in this way, model instances could be easily shared between frontend and backend projects. Moreover, by providing fine control over the subscription scope (instance level or property level), the number of rebuilds needed when an object is updated can always be minimized.


Counter Example

import 'package:flutter/material.dart';
import 'package:flutter_gadgets_2/flutter_gadgets_2.dart';

void main() => runApp(AppGadget(
      init: (model) => model.putValue<int>(value: 0),
      child: const MyApp(),
    ));

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Counter'),
        ),
        body: Center(
          child: SubscriberGadget<int>(
            builder: (_, value) => Text('Count: $value'),
          ),
        ),
        floatingActionButton: FloatingActionButton(
          onPressed: () => context.read<ObservableModel>().updateValue<int>(
                updateFunction: (value) => value + 1,
              ),
          child: const Icon(Icons.add),
        ),
      ),
    );
  }
}

When the application starts, AppGadget registers a new int instance in the ObservableModel: now the object is globally available.

To bind the value of the counter instance to a widget, a Text widget is wrapped in a SubscriberGadget: in this way, it gets access to the int object and will update when necessary.

The task of updating the model instances and the interface is delegated to the onPressedcallback: it will increment the current int value and will trigger the rebuild of the relative subscriber.


Components

AppGadget

The root of a Flutter Gadgets application.

It sets up the whole infrastructure, initializing the provided services and logic components.

void main() {
  runApp(AppGadget(
    init: (model) => model.putValue<MyValue>(value: MyValue()),
    services: [
      Service(create: (context) => MyService()),
    ],
    child: MyApp(),
  ));
}

The init function can be used to insert all the needed logic components into the ObservableModel before the application is started.

The services parameter is a List of Services that stores all the components of the application that need to be globally available in the widget tree (DAO, Repository, ...).

ObservableModel

A component used to store the shared state of the application.

It is automatically added as a service by AppGadget.

Whenever you need to access an instance stored in the model, you can use the getValue<T>({String? token}) method.

A component can be manually added to the model by using the putValue<T>({required T? value, String? token}) method.

If you need to update the value of a stored object and notify its change, the updateValue<T>({ required Update<T> updateFunction, T? updateIfNull, String? token}) can be used. When the requested value is not null, the updateFunction will be used, otherwise the optionally provided updateIfNull value will be stored in the model.

model.updateValue<MyValue>(
  updateFunction: (value) => value.doSomething(),
  updateIfNull: MyValue()
);

The notifyFor<T>({String? token}) method can also be used to simply notify the change of the internal state of a value identified by the given type and the optional given token.

SubscriberGadget

A gadget that observes a value stored in the ObservableModel.

It can watch a single value (or even single / multiple properties of it) and rebuild accordingly.

Whenever you need to access a logic component in a widget, you can wrap it in a SubscriberGadget:

SubscriberGadget<MyValue>(
  builder: (context, myBean) {
    return Text(myBean.internalState);
  }
)

In this way, when the ObservableModel will notify a change for MyValue it will automatically rebuild.

You can also specify a list of properties you want to observe using the propertiesSelector function:

SubscriberGadget<MyValue>(
  propertiesSelector: (value) => [value.first, value.second],
  builder: (context, value) {
    return Text('${value.first} ${value.second}');
  }
)

By specifing the properties, the subscriber will rebuild only if the ObservableModel notifies a change for the observed value and the specified properties changed.

When the useEquality flag is true, the rebuild will be triggerd only if the current value of the observed object and the previous one - compared using the == operator - are different.

SubscriberGadget<int>(
  useEquality: true,
  builder: (context, value) => Text('int value $value')
)

Remeber: if you just need to get access to a model instance without subscribing to it, you can always retrieve the bean directly from the ObservableModel by getting a reference to it from the BuildContext using the read<T>() method.

ViewModelGadget

A gadget that encapsulates a view model.

A view model is intended as the internal mutable state of a view. An example would be:

class MyViewModel {
  String errorText;
  TextEditingController controller;
}

In order to correctly initialize and dispose the view model, a ViewModelGadget can be created specifying the create and dispose callbacks, that get executed when the underlying stateful widget is initialized and disposed:

ViewModelGadget<MyViewModel>(
  create: () => MyViewModel()..controller = TextEditingController(),
  dispose: (viewModel) => viewModel.controller.dispose(),
  builder: (_, viewModel) => ...
)

When the ViewModelGadget is created, it automatically registers and handles a MyViewModel instance in the ObservableModel: in this way, MyView can access its state and subscribe to it by using:

SubscriberGadget<MyViewModel>(
  builder: (_, viewModel) => TextField(
    controller: viewModel.controller,
    decoration: InputDecoration(
      labelText: 'Name',
      hintText: 'Enter your name',
      errorText: viewModel.errorText,
    ),
  ),
)

If the whole ViewModelGadget subtree needs to be a subscriber of the view-model, the subscribeToViewModel parameter can be set to true when building the ViewModelGadget. In this way, the ViewModelGadget automatically registers itself as a subscriber for the relative view-model instance:

ViewModelGadget<MyViewModel>(
  viewModel: MyViewModel(),
  subscribeToViewModel: true,
  create: () => MyViewModel()..controller = TextEditingController(),
  dispose: (viewModel) => viewModel.controller.dispose(),
  builder: (_, viewModel) => ...
)

ViewGadget

A gadget that represents a view of your application.

A view is intended as a collection of widget that corresponds to a screen of the application: for example, the login screen of an app can be modeled as a view.

A ViewGadget works in association with a ControllerGadget and offers the create and dispose functions that can be used in order to create and dispose the controller.

By using the ViewGadget associated with a ControllerGadget, it's easy to architect your UIs using the Model - View - Controller (MVC) pattern:

ViewGadget<MyController>(
  create: () => MyWelcomeViewController(),
  builder: (context, controller) => Scaffold(
    body: // Scaffold body...
    floatingActionButton: FloatingActionButton(
      onPressed: controller.onPressed,
      child: // FAB child...,
    ),
  )
)

ControllerGadget

An abstract component that represents a controller associated to a ViewGadget.

It provides some useful shortcuts - like getting a reference to the ObservableModel or to some declared Service - and it's the components that contains all the callbacks required by a view.

To create a new controller, it's sufficient to extend the ControllerGadget class:

class MyController extends ControllerGadget {
  void callback() {
    // A controller can automatically obtain a reference to the model
    final value = model.getValue<MyValue>();
    // It can also easily obtain a reference to a previously declared service
    final service = read<MyDAO>();
    // Do stuff...
  }
}

Service

A utility class that encapsulates a Create and a Dispose function respectively used to initialize and dispose a component that needs to be globally available in your application.

The underlying system used is provider (no big surprises here 😜): each service generates a corrisponding Provider that makes the encapsulated value available in the widget tree.

Any service is istantiated when the AppGadget is created and can be easily accessed using the read<T>() method of ControllerGadget or the read<T>() method of BuildContext exposed from provider:

AppGadget(
  services: [
    Service(
      create: (context) => MyService(),
      dispose: (context, service) => service.dispose()
    )
  ]
  child: MyChild()
)
class MyController extends ControllerGadget {
  void callback() {
    final service = read<MyDAO>();
    // Do stuff using service...
  }
}

Libraries

flutter_gadgets_2