Flutter Mediator

Flutter Mediator pub.dev License Build Status

Flutter mediator is a state management package base on the InheritedModel with automatic aspect management to make it simpler and easier to use and rebuild widgets only when necessary.



Table of Contents


Setting up

Add the following dependency to pubspec.yaml of your flutter project:

dependencies:
  flutter_mediator: "^2.2.5"

Import flutter_mediator in files that will be used:

import 'package:flutter_mediator/mediator.dart';

For help getting started with Flutter, view the online documentation.

Table of Contents

Global Mode

As of v2.1.0 introduces a Global Mode to support a super easy way to use the state management.

Steps

  1. Declare the watched variable with globalWatch.
    Suggest to put the watched variables into a file var.dart and then import it.

  2. Create the host with globalHost, or MultiHost.create if you want to use Model Mode together, at the top of the widget tree.

  3. Create a consumer widget with globalConsume or watchedVar.consume to register the watched variable to the host to rebuild it when updating.

  4. Make an update to the watched variable, by watchedVar.value or watchedVar.ob.updateMethod(...).

Table of Contents

Case 1: Int

example_global_mode/lib/main.dart

Step 1: Declare variable in var.dart.

//* Step1: Declare the watched variable with `globalWatch` in the var.dart.
//* And then import it in the file.
final touchCount = globalWatch(0);

Step 2: Initialize the persistent watched variable and create the Host.

Future<void> main() async {
  //* Initialize the persistent watched variables
  //* whose value is stored by the SharedPreferences.
  await initVars();

  runApp(
    //* Step2: Create the host with `globalHost`
    //* at the top of the widget tree.
    globalHost(
      child: MyApp(),
    ),
  );
}

Step 3: Create a consumer widget.

Scaffold(
  appBar: AppBar(title: const Text('Global Mode:Int Demo')),
  body: Column(
    mainAxisAlignment: MainAxisAlignment.center,
    children: [
      const Text('You have pushed the button this many times:'),
      //* Step3: Create a consumer widget with
      //* `globalConsume` or `watchedVar.consume` to register the
      //* watched variable to the host to rebuild it when updating.
      globalConsume(
        () => Text(
          '${touchCount.value}',
          style: Theme.of(context).textTheme.headlineMedium,
        ),
      ),
   // ...

Step 4: Implement update function.

FloatingActionButton(
  //* Stet4: Make an update to the watched variable.
  onPressed: () => touchCount.value++,
  tooltip: 'Increment',
  child: const Icon(Icons.add),
  heroTag: null,
),

Table of Contents

Case 2: List

example_global_mode/lib/pages/list_page.dart

Step 1: Declare variable in var.dart.

//* Step1: Declare the watched variable with `globalWatch` in the var.dart.
//* And then import it in the file.
final data = globalWatch(<ListItem>[]);

Step 3: Create a consumer widget.

return Scaffold(
  appBar: AppBar(title: const Text('Global Mode:List Demo')),
  //* Step3: Create a consumer widget with
  //* `globalConsume` or `watchedVar.consume` to register the
  //* watched variable to the host to rebuild it when updating.
  body: globalConsume(
    () => GridView.builder(
      itemCount: data.value.length,

    // ...

Step 4: Implement update function.

void updateListItem() {
  // ...

  //* Step4: Make an update to the watched variable.
  //* watchedVar.ob = watchedVar.notify() and then return the underlying object
  data.ob.add(ListItem(itemName, units, color));
}

Table of Contents

Case 3: Locale setting with Persistence by SharedPreferences

Or use Flutter Mediator Persistence for built in persistence support.
Please see Flutter Mediator Persistence: use case 3 for details.

example_global_mode/lib/pages/locale_page.dart

Step 1-1: Declare variable in var.dart.

//* Declare a global scope SharedPreferences.
late SharedPreferences prefs;

//* Step1B: Declare the persistent watched variable with `late Rx<Type>`
//* And then import it in the file.
const defaultLocale = 'en';
late Rx<String> locale; // local_page.dart

/// Initialize the persistent watched variables
/// whose value is stored by the SharedPreferences.
Future<void> initVars() async {
  // To make sure SharedPreferences works.
  WidgetsFlutterBinding.ensureInitialized();

  prefs = await SharedPreferences.getInstance();
  locale = globalWatch(prefs.getString('locale') ?? defaultLocale);
}

Step 1-2: Initialize the persistent watched variables in main.dart.

Future<void> main() async {
  //* Step1-2: Initialize the persistent watched variables
  //* whose value is stored by the SharedPreferences.
  await initVars();

  runApp(
    // ...
  );
}

Step 1-3: Initialize the locale in main.dart.

//* Initialize the locale with the persistent value.
localizationsDelegates: [
  FlutterI18nDelegate(
    translationLoader: FileTranslationLoader(
      forcedLocale: Locale(locale.value),
      fallbackFile: defaultLocale,
      // ...
    ),
    // ...
  ),
],

Step 1-4: Add assets in pubspec.yaml and prepare locale files in the folder

flutter:
  # ...
  assets:
    - assets/images/
    - assets/flutter_i18n/

Step 3: Create a consumer widget

return SizedBox(
  child: Row(
    children: [
      //* Step3: Create a consumer widget with
      //* `globalConsume` or `watchedVar.consume` to register the
      //* watched variable to the host to rebuild it when updating.
      //* `watchedVar.consume()` is a helper function to
      //* `touch()` itself first and then `globalConsume`.
      locale.consume(() => Text('${'app.hello'.i18n(context)} ')),
      Text('$name, '),
      //* Or use the ci18n extension
      'app.thanks'.ci18n(context),
      // ...
    ],
  ),
);

Step 4: Implement update function in var.dart.

Future<void> changeLocale(BuildContext context, String countryCode) async {
  if (countryCode != locale.value) {
    final loc = Locale(countryCode);
    await FlutterI18n.refresh(context, loc);
    //* Step4: Make an update to the watched variable.
    locale.value = countryCode; // will rebuild the registered widget

    await prefs.setString('locale', countryCode);
  }
}

Table of Contents

Case 4: Scrolling effect

example_global_mode/lib/pages/scroll_page.dart

Step 1: Declare variable in var.dart.

//* Step1: Declare the watched variable with `globalWatch` in the var.dart.
//* And then import it in the file.
final opacityValue = globalWatch(0.0);

Step 3: Create a consumer widget.

class CustomAppBar extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    //* Step3: Create a consumer widget with
    //* `globalConsume` or `watchedVar.consume` to register the
    //* watched variable to the host to rebuild it when updating.
    return globalConsume(
      () => Container(
        color: Colors.black.withOpacity(opacityValue.value),
        // ...
      ),
    );
  }
}

Step 4: Add an offset change listener.

class _ScrollPageState extends State<ScrollPage> {
  // ...

  @override
  void initState() {
    _scrollController.addListener(() {
      //* Step4: Make an update to the watched variable.
      opacityValue.value =
          (_scrollController.offset / 350).clamp(0, 1).toDouble();
    });
    super.initState();
  }

Table of Contents

Case 5: Computed Mediator Variable

Step 1: Declare the computed variable _locstr with a computed function in var.dart.

Specify the return type of the computed function as dynamic if the return type along with the function will change.

/// Computed Mediator Variable: locstr
final _locstr = Rx(() => "locale: ${locale.value}" as dynamic);
get locstr => _locstr.value;
set locstr(value) => _locstr.value = value;

Step 2: Create a consumer widget using locstr which is _locstr.value.

        globalConsume(
          () => Text(
            locstr,
            style: const TextStyle(fontSize: 16),
          ),
        ),

Table of Contents

Recap

  • At step 1, globalWatch(variable) creates a watched variable from the variable.

  • At step 2, MultiHost works with both Global Mode and Model Mode.

  • At step 3, create a consumer widget and register it to the host to rebuild it when updating,
    use globalConsume(() => widget) if the value of the watched variable is used inside the consumer widget;
    or use watchedVar.consume(() => widget) to touch() the watched variable itself first and then globalConsume(() => widget).

  • At step 4, update to the watchedVar.value will notify the host to rebuild; or the underlying object would be a class, then use watchedVar.ob.updateMethod(...) to notify the host to rebuild.
    watchedVar.ob = watchedVar.notify() and then return the underlying object.

Table of Contents

Signal

Mediator variables can be initialled by the Signal annotation, through type alias.

For example,

final _int1 = 0.signal;
final _int2 = Signal(0); 
final _int3 = Signal(0); 
// computed mediator variable
final _sum = Signal(() => int1 + int2 + int3);

Table of Contents

Global Get

Note: Suggest to put the watched variables into a file var.dart and then import it.

globalGet<T>({Object? tag}) to retrieve the watched variable from another file.

  • With globalWatch(variable), the watched variable will be retrieved by the Type of the variable, i.e. retrieve by globalGet<Type>().

  • With globalWatch(variable, tag: object), the watched variable will be retrieved by the tag, i.e. retrieve by globalGet(tag: object).

Table of Contents


Case 1: By Type

//* Step1: Declare the watched variable with `globalWatch`.
final touchCount = globalWatch(0);

lib/pages/locale_page.dart example_global_mode/lib/pages/locale_page.dart

class LocalePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    //* Get the watched variable by it's [Type] from `../main.dart`
    final mainInt = globalGet<int>();

    return Container(
      // ...
          const SizedBox(height: 25),
          //* `globalConsume` the watched variable from `../main.dart`
          globalConsume(
            () => Text(
              'You have pressed the button at the first page ${mainInt.value} times',
            ),
      // ...

Table of Contents

Case 2: By tag

//* Step1: Declare the watched variable with `globalWatch`.
final touchCount = globalWatch(0, tag: 'tagCount');

lib/pages/locale_page.dart example_global_mode/lib/pages/locale_page.dart

class LocalePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    //* Get the watched variable by [tag] from `../main.dart`
    final mainInt = globalGet('tagCount');

    return Container(
      // ...
          const SizedBox(height: 25),
          //* `globalConsume` the watched variable from `../main.dart`
          globalConsume(
            () => Text(
              'You have pressed the button at the first page ${mainInt.value} times',
            ),
      // ...

Note

  • Make sure the watched variable is initialized, only after the page is loaded.

  • When using Type to retrieve the watched variable, only the first one of the Type is returned.

Table of Contents

Global Broadcast

  • globalBroadcast(), to broadcast to all the consumer widgets.
  • globalConsumeAll(Widget Function() create, {Key? key}), to create a consumer widget which will be rebuilt whenever any watched variables changes are made.
  • globalFrameAspects, a getter, to return the updated aspects of the Global Mode.
  • globalAllAspects, a getter, to return all the aspects that has been registered to the Global Mode.

Table of Contents

Versions

Table of Contents

Example: Logins to a REST server

A boilerplate example that logins to a REST server with i18n, theming, persistence and state management.

Please see the login to a REST server example for details.

Table of Contents


Model Mode

Three main classes: Pub, Subscriber, Host

  • Pub : The base class of implementing a model, to publish aspects.
  • Subscriber : The widget class that register to the host to subscribe aspects, being notified to rebuild when updating.
  • Host : The InheritedModel widget, to place at the top of the widget tree, to dispatch aspects.

Flow chart

Initialization:

Updating:


Flutter Widget of the Week: InheritedModel explained

InheritedModel provides an aspect parameter to its descendants to indicate which fields they care about to determine whether that widget needs to rebuild. InheritedModel can help you rebuild its descendants only when necessary.

Flutter Widget of the Week: InheritedModel Explained

Table of Contents

Key contepts

Subscribe and Publish

A widget subscribes with aspects and will rebuild whenever a model controller publishs any of those aspects.

Rx Variable

The watched variable in the Global Mode.

A proxy object, by design pattern, proxy provides a surrogate or placeholder for another object to control access to it. Variables in the model can turn into a proxy object by denoting .rx

Widget Aspects

Aspects which the widget is listen to. The widget will rebuild whenever any of these aspects is published.

When subscribing a widget, any rx variables used inside the create method will automatically rebuild the widget when updating.

Rx Automatic Aspect

By using rxSub<Model> to subscribe a widget, the package will generate aspects for the widget automatically, provides there is at least one rx variable used or use model.rxVar.touch() inside the create method to activate rx automatic aspect. (and so this widget is a rx related widget)

View Map

View map consists of two maps of create methods, Subscriber and Controller, that build upon rx automatic aspect and try to go one step further to make the UI view cleaner.

Table of Contents

Getting Started Quick Steps

Host, Model, View, Controller:

1. Model:

1-1. Implement the model by extending Pub .
  1-2. Use .rx to turn the model variable into a rx variable which will automatically rebuild related widgets when updating.
  1-3. Implement the controller method of the variable.

For example,

/// my_model.dart
class MyModel extends Pub {
  /// `.rx` make the var automatically rebuild related widgets when updating.
  final _int1 = 0.rx;
  /// Implement getter and setter of the rx variable.
  int get int1 => _int1.value;
  set int1(int v) => _int1.value = v;

  void updateInt1() {
    /// `int1` is a rx variable which will automatically rebuild realted widgets when updating.
    int1 += 1;
  }
}

Get the model by using Host.model<Model>()

Note that you don't need context to get the model, this provides you the flexibility to do things anywhere.

2. Host:

Register the models to the Host, and place it at the top level of the widget tree.
MultiHost.create1 to MultiHost.create9 are provided by the package, use it with the number of the amount of models.
For example, register 2 models, MyModel and ListModel, to the host.

void main() {
  runApp(
    MultiHost.create2(
      MyModel(updateMs: 1000), // model extends from Pub
      ListModel(updateMs: 500),// model extends from Pub
      child: MyApp(),
    ),
  );
}

Or, use the generic form.

    MultiHost.create( // Generic form
      hosts: [
        Host<MyModel>(model: MyModel(updateMs: 1000)),
        Host<ListModel>(model: ListModel(updateMs: 500)),
      ],
      child: MyApp(),
    ),

3. View: Subscribe widgets

There are two ways to subscribe a widget:

  • Rx Automatic Aspect: (Recommend)

    • The package will generate aspects for the widget automatically, provides there is at least one rx variable used or use model.rxVar.touch() inside the create method to activate rx automatic aspect. (and so this widget is a rx related widget)
      rxSub<Model>((context, model) {/*create method */})
  • With Specific Aspect:

    • Subscribe an aspect:
      aspect.subModel<Model>((context, model) {/*create method */})
    • Subscribe multiple aspects: (Place aspects in a list)
      [a1, a2].subModel<Model>((context, model) {/*create method */})
    • Broadcast to all aspects of the model: (Subscribe with null aspect to broadcast)
      null.subModel<Model>((context, model) {/*create method */})

Place that Subscriber in the widget tree then any rx variables used inside the create method will automatically rebuild related widgets when updating. (triggered by getter and setter)

For example, subscribes a widget with model class <MyModel>

  • Case 1: Use rx automatic aspect.
rxSub<MyModel>((context, model) => Text('Int1 is ${model.int1}'))
  • Case 2: With specific aspect 'int1'.
'int1'.subModel<MyModel>((context, model) => Text('Int1 is ${model.int1}'))
  • Case 3: When using rx automatic aspect, but the create method does not use any rx variables, then you can use model.rxVar.touch() which the widget depends on that rxVar to activate rx automatic aspect.
    For example, when changing locale, the create method doesn't have to display the value of the locale, then you can use model.locale.touch() to activate rx automatic aspect.
rxSub<MyModel>((context, model) {
  model.locale.touch();
  final hello = 'app.hello'.i18n(context);
  return const Text('$hello');
})

4. Controller:

Place the controller in the widget tree.
For example, to get the model class <MyModel> and execute its controller method within a ElevatedButton.

Controller<MyModel>(
  create: (context, model) => ElevatedButton(
    child: const Text('Update Int1'),
    onPressed: () => model.updateInt1(), // or simplely, `model.int1++`
  ),
)

Or implement a controller function of MyModel.updateInt1(), then place it in the widget tree.

Widget int1Controller() {
  return Controller<MyModel>(
    create: (context, model) => ElevatedButton(
      child: const Text('Update Int1'),
      onPressed: () => model.updateInt1(), // or simplely, `model.int1++`
    ),
  );
}

Works automatically!

Then whenever the rx variable updates, the related widgets will rebuild automatically!

Table of Contents

Access the underlying value of rx variables

Sometimes, an operation of a rx variable can not be done, then you need to do that with the underlying value by denoting .value .
For example,

/// my_model.dart
final _int1 = 0.rx;   // turn _int1 into a rx variable (i.e. a proxy object)
final _str1 = 'A'.rx; // turn _str1 into a rx variable (i.e. a proxy object)
void updateInt1() {
  _int1.value *= 5;
  _str1.value = 'B';
}

Table of Contents

Visual Studio Code snippets

These are code snippets, for example, for visual studio code to easy using the package.
To add these code snippets in visual studio code, press

control+shift+p => Preferences: Configure user snippets => dart.json
Then add the content of vscode_snippets.json into the dart.json.

Now you can type these shortcuts for code templates to easy using the package:

  • mmodel - Generate a Model Boilerplate Code of Flutter Mediator.
  • getmodel - Get the Model of Flutter Mediator.
  • pubmodel - Get the Model of Flutter Mediator, the same as getmodel.

View Map shortcuts: (See View Map)

  • addsub - Add a Creator to the Subscriber Map of the Model.
  • addcon - Add a Creator to the Controller Map of the Model.
  • pubsub - Create a Subscriber Widget from the Subscriber Map of the Model.
  • pubcon - Create a Controller Widget from the Controller Map of the Model.

Shortcuts:

  • controller - Create a Flutter Mediator Controller Function.
  • subscriber - Create a Flutter Mediator Subscriber Function with Aspect.
  • rxfun - Create a Flutter Mediator Subscriber Function with RX Automatic Aspect.
  • submodel - Create a Flutter Mediator Subscriber with Aspect.
  • rxsub - Create a Flutter Mediator Subscriber with RX Automatic Aspect.

Table of Contents

View Map - one step further of dependency injection

View map consists of two maps of create methods, Subscriber and Controller, which build upon rx automatic aspect and try to go one step further to make the UI view cleaner.

First, let's see what's the difference by an original view and after using the view map.

Original View

/// Original view
class LocalePanel extends StatelessWidget {
  const LocalePanel({Key key}) : super(key: key);

  Widget txt(BuildContext context, String name) {
    return SizedBox(
      width: 250,
      child: Row(
        children: [
          rxSub<ListModel>(
            (context, model) {
              model.locale.touch(); // to activate rx automatic aspect
              final hello = 'app.hello'.i18n(context);
              return Text('$hello ');
            },
          ),
          Text('$name, '),
          rxSub<ListModel>(
            (context, model) {
              model.locale.touch(); // to activate rx automatic aspect
              final thanks = 'app.thanks'.i18n(context);
              return Text('$thanks.');
            },
          ),
        ],
      ),
    );
  }
/// ...

After using the View Map

/// After using the View Map
class LocalePanel extends StatelessWidget {
  const LocalePanel({Key key}) : super(key: key);

  Widget txt(BuildContext context, String name) {
    return SizedBox(
      width: 250,
      child: Row(
        children: [
          Pub.sub<ListModel>('hello'), // use `pubsub` shortcut for boilerplate
          Text('$name, '),
          Pub.sub<ListModel>('thanks'), // use `pubsub` shortcut for boilerplate
        ],
      ),
    );
  }
/// ...

Isn't it cleaner.

Here's how to use View Map.

  1. Add these code into the model and change <Model> to the class name of the model.

    Use the code snippet shortcut, mmodel, to generate these boilerplate code.

/// some_model.dart
  void addSub(Object key, CreatorFn<Model> sub) => regSub<Model>(key, sub);
  void addCon(Object key, CreatorFn<Model> con) => regCon<Model>(key, con);

  @override
  void init() {
    // addSub('', (context, model) {
    //   return Text('foo is ${model.foo}');
    // });

    // addCon('', (context, model) {
    //   return ElevatedButton(child: const Text('Update foo'),
    //     onPressed: () => model.increaseFoo(),);
    // });

    super.init();
  }
  1. Use the addsub or addcon shortcut to add create methods of Subscriber or Controller in the init() method.

    'hello' and 'thanks' are the keys to the map, later, you can use these keys to create corresponding widgets.

/// in the init() of some_model.dart
    // use `addsub` shortcut to generate boilerplate code
    addSub('hello', (context, model) {
      model.locale.touch(); // to activate rx automatic aspect
      final hello = 'app.hello'.i18n(context);
      return Text('$hello ');
    });

    // use `addsub` shortcut to generate boilerplate code
    addSub('thanks', (context, model) {
      model.locale.touch(); // to activate rx automatic aspect
      final thanks = 'app.thanks'.i18n(context);
      return Text('$thanks.');
    });
  1. Then use the pubsub shortcut to place the Subscriber widget in the widget tree.

    Change <Model> to the class name of the model.

/// in the widget tree
      child: Row(
        children: [
          Pub.sub<Model>('hello'), // use `pubsub` shortcut for boilerplate
          Text('$name, '),
          Pub.sub<Model>('thanks'),// use `pubsub` shortcut for boilerplate
        ],
      ),

Now you just need to use these shortcuts, or commands, to do state management.

  • mmodel - Generate a Model Boilerplate Code.
  • addsub - Add a Creator to the Subscriber Map of the Model.
  • addcon - Add a Creator to the Controller Map of the Model.
  • pubsub - Create a Subscriber Widget from the Subscriber Map of the Model.
  • pubcon - Create a Controller Widget from the Controller Map of the Model.

Plus with,

  • .rx - Turn model variables into rx variables, thus, you can use rx automatic aspect.
  • rxVar.touch() - Used when the create method doesn't have to display the value of that rx variable, then you touch() that rx variable to activate rx automatic aspect.
  • getmodel - Get the model. (Note that context is not needed to get the model.)

Table of Contents

Summing up

  • Subscriber: Use at least one rx variable or model.rxVar.touch() which the widget depends on that rxVar to activate rx automatic aspect.

  • Controller: To publish the aspect, it's automatically done with the rx variables, or publish the aspect manually.

To custom a rx class please see Detail: 21 implement a custom rx class.

Table of Contents

Use Case - explain how the package works

This use case explains how the package works, you can skip it. There is an use case for i18n with View Map, which is much more straight forward to use.

First of all, implement the Model and place the Host at the top level of the widget tree,

/// my_model.dart
class MyModel extends Pub {
  /// `.rx` make the var automatically rebuild related widgets when updating.
  final _int1 = 0.rx;
  int get int1 => _int1.value;
  set int1(int v) => _int1.value = v;

  // controller function for int1
  void updateInt1() {
    int1 += 1; // Automatically rebuild related widgets when updating.
  }


  /// ordinary variable
  var m = 0;

  // controller function for ordinary variable
  void increaseManual(Object aspect) {
    m++;
    publish(aspect); // `m` is an ordinary variable which needs to publish the aspect manually.
  }
}
/// main.dart
void main() {
  runApp(
    MultiHost.create1(
      MyModel(updateMs:  1000), // model extends from Pub
      child: MyApp(),
    ),
  );
}

Case 1: use rx automatic aspect

Implement the Subscriber and Controller functions, and place them in the widget tree.

/// main.dart
/// Subscriber function
Widget Int1Subscriber() {
  return rxSub<MyModel>((context, model) {
    return Text('int1: ${model.int1}');
  });
}
/// Controller function
Widget Int1Controller() {
  return Controller<MyModel>(
    create: (context, model) => ElevatedButton(
      child: const Text('Int1'),
      onPressed: () => model.UpdateInt1(),
    ),
  );
}
/// widget tree
Widget mainPage() {
  return Column(
    children: [
      Int1Subscriber(),
      Int1Controller(),
    ],
  );
}

Table of Contents

Case 2: with specific aspect

Specific an aspect, for example 'Int1', implement the Subscriber and Controller functions of the aspect, and place them in the widget tree.

/// main.dart
/// Subscriber function
Widget Int1Subscriber() {
  return 'Int1'.subModel<MyModel>((context, model) {
    return Text('Int1: ${model.int1}');
  });
}
/// Controller function
Widget Int1Controller() {
  return Controller<MyModel>(
    create: (context, model) => ElevatedButton(
      child: const Text('update int1'),
      onPressed: () => UpdateInt1(), // or simplely model.star++,
    ),
  );
}
/// widget tree
Widget mainPage() {
  return Column(
    children: [
      Int1Subscriber(),
      Int1Controller(),
    ],
  );
}

Table of Contents

Case 3: manual publish aspect

Specific an aspect, for example 'manual', implement the Subscriber and Controller functions of the aspect, and place them in the widget tree, then publish the aspect in the controller function.

/// main.dart
/// Subscriber function
Widget manualSubscriber() {
  return 'manual'.subModel<MyModel>((context, model) {
    return Text('manual: ${model.manual}');
  });
}
/// Controller function
Widget manualController() {
  return Controller<MyModel>(
    create: (context, model) => ElevatedButton(
      child: const Text('update manual'),
      onPressed: () => increaseManual('manual'),
    ),
  );
}
/// widget tree
Widget mainPage() {
  return Column(
    children: [
      manualSubscriber(),
      manualController(),
    ],
  );
}

Table of Contents

Use Case - i18n with View Map

For example, to write an i18n app using flutter_i18n with View Map.

These are all boilerplate code, you may just need to look at the lines with comments, that's where to put the code in.

  1. Edit pubspec.yaml to use flutter_i18n and flutter_mediator.
dependencies:
  flutter_i18n: ^0.32.4
  flutter_mediator: ^2.2.1

flutter:
  assets:
    - assets/flutter_i18n/
  1. Create the i18n folder asserts/flutter_i18n and edit the locale files, see folder.
    For example, an en.json locale file.
{
  "app": {
    "hello": "Hello",
    "thanks": "Thanks",
    "~": ""
  }
}
  1. Create a folder models then new a file setting_model.dart in the folder and use mmodel shortcut to generate a model boilerplate code with the class name Setting.

  2. Add an i18n extension to the setting_model.dart.

//* i18n extension
extension StringI18n on String {
  String i18n(BuildContext context) {
    return FlutterI18n.translate(context, this);
  }
}
  1. Add the locale variable and make it a rx variable along with the changeLocale function, then add create methods to the Setting model. (in the init() method)
    Add the SettingEnum to represent the map keys of the view map.
/// setting_model.dart
enum SettingEnum {
  hello,
  thanks,
}

class Setting extends Pub {
  //* member variables
  var locale = 'en'.rx;

  //* controller function
  Future<void> changeLocale(BuildContext context, String countryCode) async {
    final loc = Locale(countryCode);
    await FlutterI18n.refresh(context, loc);
    locale.value = countryCode;
    // `locale` is a rx variable which will rebuild related widgets when updating.
  }

  //* View Map:
  // ...

  @override
  void init() {
    addSub(SettingEnum.hello, (context, model) { // SettingEnum.hello is the map key
      model.locale.touch(); // to activate rx automatic aspect
      final hello = 'app.hello'.i18n(context); // app.hello is the json field in the locale file
      return Text('$hello ');
    });

    addSub(SettingEnum.thanks, (context, model) { // SettingEnum.thanks is the map key
      model.locale.touch(); // to activate rx automatic aspect
      final thanks = 'app.thanks'.i18n(context); // app.thanks is the json field in the locale file
      return Text('$thanks.');
    });
    //...
  1. Setup main.dart.
    Import files, add Setting model to the host, i18n stuff and set home to infoPage().
/// main.dart
import 'package:flutter/material.dart';
import 'package:flutter_i18n/flutter_i18n.dart';
import 'package:flutter_i18n/loaders/decoders/json_decode_strategy.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_mediator/mediator.dart';

import 'models/setting_model.dart';

void main() {
  runApp(
    MultiHost.create1(
      Setting(), // add `Setting` model to the host
      child: MyApp(),
    ),
  );
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Mediator Demo',
      theme: ThemeData(primarySwatch: Colors.blue),
      // add flutter_i18n support, i18n stuff
      localizationsDelegates: [
        FlutterI18nDelegate(
          translationLoader: FileTranslationLoader(
            decodeStrategies: [JsonDecodeStrategy()],
          ),
          missingTranslationHandler: (key, locale) {
            print('--- Missing Key: $key, languageCode: ${locale!.languageCode}');
          },
        ),
        GlobalMaterialLocalizations.delegate,
        GlobalWidgetsLocalizations.delegate,
      ],
      home: infoPage(), // set `infoPage` as home page
    );
  }
}

  1. Implement infoPage() with View Map.

    These are boilerplate code, just look at the lines with comments, that's where to put the code in.

/// main.dart
Widget infoPage() {
  return Scaffold(
    body: Column(
      children: [
        SizedBox(height: 50),
        Container(
          child: Row(
            mainAxisAlignment: MainAxisAlignment.start,
            crossAxisAlignment: CrossAxisAlignment.start,
            children: const [
              RadioGroup(),
              LocalePanel(),
            ],
          ),
        ),
      ],
    ),
  );
}

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

  Widget txt(String name) {
    return SizedBox(
      width: 250,
      child: Row(
        children: [
          Pub.sub<Setting>(SettingEnum.hello), // Use `pubsub` shortcut for boilerplate, SettingEnum.hello is the map key.
          Text('$name, '),
          Pub.sub<Setting>(SettingEnum.thanks), // Use `pubsub` shortcut for boilerplate, SettingEnum.thanks is the map key.
        ],
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.start,
      children: [for (final name in names) txt(name)],
    );
  }
}

class RadioGroup extends StatefulWidget {
  const RadioGroup({
    Key key,
  }) : super(key: key);

  @override
  _RadioGroupState createState() => _RadioGroupState();
}

class _RadioGroupState extends State<RadioGroup> {
  final locales = ['en', 'fr', 'nl', 'de', 'it', 'zh', 'jp', 'kr']; // locale values
  final languages = [ // the language options to let the user to select, need to be corresponded with the locale values
    'English',
    'français',
    'Dutch',
    'Deutsch',
    'Italiano',
    '中文',
    '日本語',
    '한국어',
  ];

  Future<void> _handleRadioValueChange1(String? value) async {
    final model = Host.model<Setting>(); // use `getmodel` shortcut to get the model
    await model.changeLocale(context, value!); // change the locale
    setState(() {
      /// model.locale.value = value; // changed in model.changeLocale
    });
  }

  @override
  Widget build(BuildContext context) {
    final model = Host.model<Setting>(); // use `getmodel` shortcut to get the model
    final _radioValue1 = model.locale.value; // get the locale value back to maintain state

    Widget panel(int index) {
      return Row(
        mainAxisAlignment: MainAxisAlignment.start,
        children: [
          Radio(
            value: locales[index],
            groupValue: _radioValue1,
            onChanged: _handleRadioValueChange1,
          ),
          Text(
            languages[index],
            style: const TextStyle(fontSize: 16.0),
          ),
        ],
      );
    }

    return Container(
      width: 130,
      child: Column(
        mainAxisAlignment: MainAxisAlignment.start,
        children: [for (var i = 0; i < locales.length; i++) panel(i)],
      ),
    );
  }
}


final names = [
  'Aarron',
  'Josh',
  'Ibraheem',
  'Rosemary',
  'Clement',
  'Kayleigh',
  'Elisa',
  'Pearl',
  'Aneesah',
  'Tom',
  'Jordana',
  'Taran',
  'Bethan',
  'Haydon',
  'Olivia-Mae',
  'Anam',
  'Kelsie',
  'Denise',
  'Jenson',
  'Piotr',
];
  1. Work completed. Now you get an app with i18n support.

Table of Contents

Example

You can find the example in the example/lib folder.


These steps can help you in most situations. The following details explain the package one step further, you can skip it.

Details

  1. Single model - host
  2. Multiple models - host
  3. Automatically rebuild the widget whenever the rx variable updates - Pub
  4. Access the underlying value of rx variables - Pub
  5. Update the rx variables by call style - Pub
  6. Manually publish an aspect - Pub
  7. Manually publish multiple aspects - Pub
  8. Broadcast to the model - Pub
  9. Publish aspects of a rx variable - Pub
  10. Future publish - Pub
  11. Rebuild only once a frame for the same aspect - Pub
  12. Writing model extension - Pub
  13. Get the model - Controller and Subscriber
  14. Subscribe with rx automatic aspect - rx automatic aspect - Subscriber
  15. Touch the rx variable - rx automatic aspect - Subscriber
  16. Subscribe an aspect - specific aspect - Subscriber
  17. Subscribe multiple aspects - specific aspect - Subscriber
  18. Subscribe all aspects - specific aspect - Subscriber
  19. Subscribe with enum aspects - specific aspect - Subscriber
  20. Manage rx aspects - Chain react aspects - advance topic
  21. Implement a custom rx class - advance topic
  22. Aspect type - terminology

1. Single model

Register a model to the Host, and place it at the top level of the widget tree.

/// main.dart
void main() {
  runApp(
    Host(
      model: AppModel(), // model extends from Pub
      child: MyApp(),
    ),
  );
}

back to details


2. Multiple models

Register multiple models to the Host, and place it at the top level of the widget tree.
MultiHost.create1 to MultiHost.create9 are provided by the package, use it with the number of the amount of models.

/// main.dart
void main() {
  runApp(
    MultiHost.create2(
      MyModel(updateMs: 1000),  // model extends from Pub
      ListModel(updateMs: 500), // model extends from Pub
      child: MyApp(),
    ),
  );
}

Or, use the generic form.

    MultiHost.create( // Generic form
      hosts: [
        Host<MyModel>(model: MyModel(updateMs: 1000)),
        Host<ListModel>(model: ListModel(updateMs: 500)),
      ],
      child: MyApp(),
    ),

back to details


3. Automatically rebuild the widget whenever the rx variable updates

Denoting .rx turns the variable of the model into a rx variable, a proxy object, which will automatically rebuild related widgets when updating. For Example,

rx int:

/// my_model.dart
class MyModel extends Pub {
  /// `.rx` make the var automatically rebuild related widgets when updating.
  final _int1 = 0.rx;
  int get int1 => _int1.value;
  set int1(int v) => _int1.value = v;

  void updateInt1() {
    int1 += 1; // Automatically rebuild related widgets when updating.
  }

rx list:

/// list_model.dart
class ListModel extends Pub {
  /// `.rx` turn the var into a rx variable(i.e. a proxy object)
  /// which will rebuild related widgets when updating.
  final data =  <ListItem>[].rx;

  void updateListItem() {
    // get new item data...
    final newItem = ListItem(itemName, units, color);
    data.add(newItem); // Automatically rebuild related widgets.
  }

rx variable of type int, double, num, string, bool, list, map, set are provided by the package.
See also RxInt class, RxList class, RxList.add

back to details


4. Access the underlying value of rx variables

  • rxVar.value : Return the underlying value.
  • rxVar.ob : Do a rxVar.notify() first to notify the host to rebuild then return the underlying object. Typically used with classes that aren't supported by the package.

For example,

/// my_model.dart
var _int1 = 0.rx;   // turn _int1 into a rx variable (i.e. a proxy object)
var _str1 = 'A'.rx; // turn _str1 into a rx variable (i.e. a proxy object)
void updateInt1() {
  _int1.value *= 5;
  _str1.value = 'B';
}

final customClass = CustomClass();
final data = customClass.rx; // turn customClass into a rx variable (i.e. a proxy object)
void updateData() {
  data.ob.add(5);
}

back to details


5. Update the rx variables by call style

Dart provides a call(T) to override, you can use rxVar(value) to update the underlying value.

/// my_model.dart
var _foo = 1.rx;
set foo(int value) {
  _foo(value); // update the rx variable by call() style
}

back to details


6. Manually publish an aspect

Use the publish() method of the model to manually publish an aspect.

/// my_model.dart
int manuallyInt = 0;
void manuallyPublishDemo(int value) {
  manuallyInt = value;
  publish('manuallyInt'); // manually publish aspect of 'manuallyInt'
}

back to details


7. Manually publish multiple aspects

Place aspects in a list to publish multiple aspects.

/// my_model.dart
int _foo = 0;
int _bar = 0;
void increaseBoth() {
  _foo += 1;
  _bar += 1;
  publish(['foo', 'bar']); // manually publish multiple aspects
}

back to details


8. Broadcast to the model

Publish null value to broadcast to all aspects of the model.

/// my_model.dart
void increaseAll() {
  //...
  publish(); // broadcasting, publish all aspects of the model
}

back to details


9. Publish aspects of a rx variable

Publish a rx variable to publish the aspects that rx variable attached.

/// my_model.dart
var int1 = 0.rx;
void publishInt1Related() {
  //...
  publish(int1); // publish the aspects that int1 attached
}

back to details


10. Future publish

Use rx variables within an async method.

/// my_model.dart
int int1 = 0.rx;
Future<void> futureInt1() async {
  await Future.delayed(const Duration(seconds: 1));
  int1 += 1; // `int1` is a rx variable which will automatically rebuild related widgets when updating.
}

back to details


11. Rebuild only once a frame

By using Set to accumulate aspects, the same aspect only causes the related widget to rebuild once.
The following code only causes the related widget to rebuild once.

/// my_model.dart
int int1 = 0.rx;
void incermentInt1() async {
  int1 += 1; // `int1` is a rx variable which will automatically rebuild related widgets when updating.
  publish('int1'); // Manually publish 'int1'.
  publish('int1'); // Manually publish 'int1', again.
  // Only cause the related widgets to rebuild only once.
}

back to details


12. Writing model extension

You can write model extensions to simplified the typing. For example,

Use shortcut mmodel will generate these extensions automatically.

/// MyModel extension
MyModel getMyModel(BuildContext context) => Host.model<MyModel>();

Subscriber<MyModel> subMyModel(CreatorFn<MyModel> create,
    {Key? key, Object? aspects}) {
  return Subscriber<MyModel>(key: key, aspects: aspects, create: create);
}

extension MyModelExtT<T> on T {
  Subscriber<MyModel> subMyModel(CreatorFn<MyModel> create,
      {Key? key}) {
    return Subscriber<MyModel>(key: key, aspects: this, create: create);
  }
}
/// ListModel extension
ListModel getListModel(BuildContext context) => Host.model<ListModel>();

Subscriber<ListModel> subListModel(CreatorFn<ListModel> create,
    {Key? key, Object? aspects}) {
  return Subscriber<ListModel>(key: key, aspects: aspects, create: create);
}

extension ListModelExtT<T> on T {
  Subscriber<ListModel> subListModel(CreatorFn<ListModel> create,
      {Key? key}) {
    return Subscriber<ListModel>(key: key, aspects: this, create: create);
  }
}

See also extension.dart for package extension.

back to details


13. Get the model

To get the model, for example, getting MyModel,

Note that you don't need context to get the model, this provides you the flexibility to do things anywhere.

  • original form
final model = Host.model<MyModel>();
  • with user extension
final model = getMyModel();

Get current triggered frame aspects of the model. See also allSubscriber@main.dart.

final model = Host.model<MyModel>();
final aspects = model.frameAspects;

back to details


14. Subscribe with rx automatic aspect

By using rxSub<Model> to subscribe a widget, the package will generate aspects for the widget automatically, provides there is at least one rx variable used or use model.rxVar.touch() inside the create method to activate rx automatic aspect. (and so this widget is a rx related widget)
For example,

/// my_model.dart
final _tick1 = 0.rx;
int get tick1 => _tick1.value;
set tick1(int v) => _tick1.value = v;
/// main.dart
rxSub<MyModel>((context, model) {
  return Text('tick1 is ${model.tick1}');
}),

back to details


15. Touch the rx variable

When using rx automatic aspect, but the create method does not use any rx variables, then you can use model.rxVar.touch() which the widget depends on that rxVar to activate rx automatic aspect.
For example, when changing locale, the create method doesn't have to display the value of the locale, then you can use model.locale.touch() to activate rx automatic aspect.

rxSub<MyModel>((context, model) {
  model.locale.touch();
  final hello = 'app.hello'.i18n(context);
  return const Text('$hello');
})

back to details


16. Subscribe an aspect

For example, subscribe to a String aspect 'int1' of class <MyModel>.

  • simple form
'int1'.subModel<MyModel>((context, model) => Text('Int1 is ${model.int1}')),
  • original form
Subscriber<MyModel>(
  aspects: 'int1',
  create: (context, model) {
    return Text('Int1 is ${model.int1}');
  },
),
  • with user extension
'int1'.subMyModel((context, model) => Text('Int1 is ${model.int1}')),

back to details


17. Subscribe multiple aspects

Place aspects in a list to subscribe multiple aspects.

  • simple form
['int1', 'star'].subModel<MyModel>(
  (context, model) => Text(
    'Int1 is ${model.int1} and Star is ${model.star}',
    softWrap: true,
    textAlign: TextAlign.center,
  ),
),
  • original form
Subscriber<MyModel>(
  aspects: ['int1', 'star'],
  create: (context, model) {
    return Text(
      'Int1 is ${model.int1} and Star is ${model.star}',
      softWrap: true,
      textAlign: TextAlign.center,
    );
  },
),
  • with user extension
['int1', 'star'].subMyModel(
  (context, model) => Text(
    'Int1 is ${model.int1} and Star is ${model.star}',
    softWrap: true,
    textAlign: TextAlign.center,
  ),
),

back to details


18. Subscribe all aspects

Provide no aspects parameter, or use null as aspect to subscribe to all aspects of the model.
See also allSubscriber@main.dart.

  • simple form
null.subModel<MyModel>( // null aspects means broadcasting to the model
  (context, model) {
    final aspects = model.frameAspects;
    final str = aspects.isEmpty ? '' : '$aspects received';
    return Text(str, softWrap: true, textAlign: TextAlign.center);
  },
),

  • original form
Subscriber<MyModel>(
   // aspects: , // no aspects parameter means broadcasting to the model
  create: (context, model) {
    final aspects = model.frameAspects;
    final str = aspects.isEmpty ? '' : '$aspects received';
    return Text(str, softWrap: true, textAlign: TextAlign.center);
  },
),
  • with user extension
null.subMyModel( // null aspects means broadcasting to the model
  (context, model) {
    final aspects = model.frameAspects;
    final str = aspects.isEmpty ? '' : '$aspects received';
    return Text(str, softWrap: true, textAlign: TextAlign.center);
  },
),

back to details


19. Subscribe with enum aspects

You can use enum as aspect.
For example, first, define the enum.

/// list_model.dart
enum ListEnum {
  ListUpdate,
}

Then everything is the same as String aspect, just to replace the String with enum.
See also cardPage@main.dart.

  • simple form
ListEnum.ListUpdate.subModel<ListModel>((context, model) {
  /* create method */
}),
  • original form
Subscriber<ListModel>(
  aspects: ListEnum.ListUpdate,
  create: (context, model) {
    /* create method */
}),
  • with user extension
ListEnum.ListUpdate.subMyModel((context, model) {
  /* create method */
}),

back to details


20. Manage rx aspects - Chain react aspects

Chain react aspects:

Supposed you need to rebuild a widget whenever a model variable is updated, but it has nothing to do with the variable. Then you can use chain react aspects.
For example, to rebuild a widget whenever str1 of class <MyModel> is updated, and chained by the aspect 'chainStr1'.

/// my_model.dart
final _str1 = 's'.rx..addRxAspects('chainStr1'); // to chain react aspects
String get str1 => _str1.value;
set str1(String v) => _str1.value = v;
/// main.dart
int httpResCounter = 0;
Future<int> _futureHttpTask() async {
  await Future.delayed(const Duration(milliseconds: 0));
  return ++httpResCounter;
}

//* Chain subscribe binding myModel.str1 with aspect 'chainStr1'.
Widget chainReactSubscriber() {
  return 'chainStr1'.subModel<MyModel>((context, model) {
    return FutureBuilder(
      future: _futureHttpTask(),
      initialData: httpResCounter,
      builder: (BuildContext context, AsyncSnapshot snapshot) {
        Widget child;
        if (snapshot.hasData) {
          child = Text('str1 chain counter: $httpResCounter');
        } else {
          child = Text('str1 init counter: $httpResCounter');
        }
        return Center(child: child);
      },
    );
  });
}

Then whenever str1 of class <MyModel> updates, the widget rebuild automatically.

Manage rx aspects:

  • Add aspects to the rx variable:

    • add an aspect: rxVar.addRxAspects('chained-aspect')
    • add multiple aspects: rxVar.addRxAspects(['chained-as1', 'chained-as2'])
    • add aspects from another rx variable: rxVar.addRxAspects(otherRxVar)
    • broadcast to the model: rxVar.addRxAspects()
  • Remove aspects from the rx variable:

    • remove an aspect: rxVar.removeRxAspects('chained-aspect')
    • remove multiple aspects: rxVar.removeRxAspects(['chained-as1', 'chained-as2'])
    • remove aspects from another rx variable: rxVar.removeRxAspects(otherRxVar)
    • don't broadcast to the model: rxVar.removeRxAspects()
  • Retain aspects in the rx variable:

    • retain an aspect: rxVar.retainRxAspects('chained-aspect')
    • retain multiple aspects: rxVar.retainRxAspects(['chained-as1', 'chained-as2'])
    • retain aspects from another rx variable: rxVar.retainRxAspects(otherRxVar)
  • Clear all rx aspects:

    • rxVar.clearRxAspects()

back to details


21. Implement a custom rx class

If you need to write your own rx class, see custom_rx_class.dart for example.
Or you can manipulate the underlying value directly. For example,

/// someclass.dart
class SomeClass {
  int counter = 0;
}

final rxClass = SomeClass().rx;
void updateSomeClass() {
  rxClass.value.counter++;
  rxClass.publishRxAspects();
}

By using the extension, every object can turn into a rx variable.

back to details


22. Aspect type

  • Widget aspects - Aspects which the widget is listen to.
  • Frame aspects - Aspects which will be sent to rebuild the related widgets in the next UI frame.
  • Registered aspects - Aspects that have been registered to the model.
  • RX aspects - Aspects that have been attached to the rx variable. The rx variable will rebuild the related widgets when updating.

back to details


Changelog

Please see the Changelog page.


License

Flutter Mediator is distributed under the MIT License. See LICENSE for more information.