Flutter Mediator |
Flutter mediator is a signal 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.
- Global Mode
- Model Mode
- Three main classes:
Pub
,Subscriber
,Host
- Flow chart
- Flutter Widget of the Week: InheritedModel explained
- Key contepts
- Getting Started Quick Steps
- Access the underlying value of rx variables
- Visual Studio Code snippets
- View Map - one step further of dependency injection
- Use Case - explain how the package works
- Use Case - i18n with View Map
- Three main classes:
Run this command:
With Flutter:
$ flutter pub add flutter_mediator
This will add a line like this to your package's pubspec.yaml (and run an implicit flutter pub get):
dependencies:
flutter_mediator: "^2.2.5"
Alternatively, your editor might support flutter pub get. Check the docs for your editor to learn more.
Now in your Dart code, you can use:
import 'package:flutter_mediator/mediator.dart';
For help getting started with Flutter, view the online documentation.
As of v2.1.0 introduces a Global Mode
to support a super easy way to use the state management.
-
Declare the watched variable with
globalWatch
.
Suggest to put the watched variables into a file var.dart and then import it. -
Create the host with
globalHost
, orMultiHost.create
if you want to use Model Mode together, at the top of the widget tree. -
Create a consumer widget with
globalConsume
orwatchedVar.consume
to register the watched variable to the host to rebuild it when updating. -
Make an update to the watched variable, by
watchedVar.value
orwatchedVar.ob.updateMethod(...)
.
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,
),
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));
}
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);
}
}
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();
}
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),
),
),
-
At step 1,
globalWatch(variable)
creates a watched variable from the variable. -
At step 2,
MultiHost
works with bothGlobal Mode
andModel Mode
. -
At step 3, create a consumer widget and register it to the host to rebuild it when updating,
useglobalConsume(() => widget)
if the value of the watched variable is used inside the consumer widget;
or usewatchedVar.consume(() => widget)
totouch()
the watched variable itself first and thenglobalConsume(() => widget)
. -
At step 4, update to the
watchedVar.value
will notify the host to rebuild; or the underlying object would be a class, then usewatchedVar.ob.updateMethod(...)
to notify the host to rebuild.watchedVar.ob = watchedVar.notify() and then return the underlying object
.
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);
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 theType
of the variable, i.e. retrieve byglobalGet<Type>()
. -
With
globalWatch(variable, tag: object)
, the watched variable will be retrieved by the tag, i.e. retrieve byglobalGet(tag: object)
.
//* 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',
),
// ...
//* 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',
),
// ...
-
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 theType
is returned.
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.
- Flutter Mediator: Global Mode + Model Mode.
- Lite: Global Mode only.
- Persistence: Lite + built in persistence.
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.
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.
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.
A widget subscribes with aspects and will rebuild whenever a model controller publishs any of those aspects.
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
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.
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 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.
Host, Model, View, Controller:
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.
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(),
),
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
*/})
- The package will generate aspects for the widget automatically, provides there is at least one rx variable used or use
-
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
*/})
- Subscribe an aspect:
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 thatrxVar
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 usemodel.locale.touch()
to activate rx automatic aspect.
rxSub<MyModel>((context, model) {
model.locale.touch();
final hello = 'app.hello'.i18n(context);
return const Text('$hello');
})
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++`
),
);
}
Then whenever the rx variable updates, the related widgets will rebuild automatically!
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';
}
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 asgetmodel
.
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.
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
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
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.
- 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();
}
- Use the
addsub
oraddcon
shortcut to add create methods ofSubscriber
orController
in theinit()
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.');
});
- Then use the
pubsub
shortcut to place theSubscriber
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 youtouch()
that rx variable to activate rx automatic aspect.getmodel
- Get the model. (Note thatcontext
is not needed to get the model.)
-
Subscriber: Use at least one rx variable or
model.rxVar.touch()
which the widget depends on thatrxVar
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.
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(),
),
);
}
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(),
],
);
}
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(),
],
);
}
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(),
],
);
}
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.
- 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/
- Create the i18n folder
asserts/flutter_i18n
and edit the locale files, see folder.
For example, anen.json
locale file.
{
"app": {
"hello": "Hello",
"thanks": "Thanks",
"~": ""
}
}
-
Create a folder
models
then new a filesetting_model.dart
in the folder and usemmodel
shortcut to generate a model boilerplate code with the class nameSetting
. -
Add an
i18n
extension to thesetting_model.dart
.
//* i18n extension
extension StringI18n on String {
String i18n(BuildContext context) {
return FlutterI18n.translate(context, this);
}
}
- Add the
locale
variable and make it a rx variable along with thechangeLocale
function, then add create methods to theSetting
model. (in theinit()
method)
Add theSettingEnum
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.');
});
//...
- Setup
main.dart
.
Import files, addSetting
model to the host, i18n stuff and sethome
toinfoPage()
.
/// 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
);
}
}
- 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',
];
- Work completed. Now you get an app with i18n support.
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.
- Single model - host
- Multiple models - host
- Automatically rebuild the widget whenever the rx variable updates - Pub
- Access the underlying value of rx variables - Pub
- Update the rx variables by call style - Pub
- Manually publish an aspect - Pub
- Manually publish multiple aspects - Pub
- Broadcast to the model - Pub
- Publish aspects of a rx variable - Pub
- Future publish - Pub
- Rebuild only once a frame for the same aspect - Pub
- Writing model extension - Pub
- Get the model - Controller and Subscriber
- Subscribe with rx automatic aspect - rx automatic aspect - Subscriber
- Touch the rx variable - rx automatic aspect - Subscriber
- Subscribe an aspect - specific aspect - Subscriber
- Subscribe multiple aspects - specific aspect - Subscriber
- Subscribe all aspects - specific aspect - Subscriber
- Subscribe with enum aspects - specific aspect - Subscriber
- Manage rx aspects - Chain react aspects - advance topic
- Implement a custom rx class - advance topic
- Aspect type - terminology
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(),
),
);
}
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(),
),
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,
/// 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.
}
/// 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
rxVar.value
: Return the underlying value.rxVar.ob
: Do arxVar.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);
}
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
}
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'
}
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
}
Publish null value to broadcast to all aspects of the model.
/// my_model.dart
void increaseAll() {
//...
publish(); // broadcasting, publish all aspects of the model
}
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
}
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.
}
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.
}
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.
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;
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}');
}),
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');
})
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}')),
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,
),
),
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);
},
),
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 */
}),
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.
-
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()
- add an aspect:
-
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()
- remove an aspect:
-
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)
- retain an aspect:
-
Clear all rx aspects:
rxVar.clearRxAspects()
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.
- 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.
Please see the Changelog page.
Flutter Mediator is distributed under the MIT License. See LICENSE for more information.