0 purchases
flutter mediator
Flutter Mediator #
Flutter Mediator
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 #
Global Mode
Steps
Case 1: Int
Case 2: List
Case 3: Locale setting with Persistence by SharedPreferences
Case 4: Scrolling effect
Case 5: Computed Mediator Variable
Recap
Signal
Global Get
Case 1: By Type
Case 2: By tag
Global Broadcast
Versions
Example: Logins to a REST server
Model Mode
Three main classes: Pub, Subscriber, Host
Flow chart
Flutter Widget of the Week: InheritedModel explained
Key contepts
Subscribe and Publish
Rx Variable
Widget Aspects
Rx Related Widget
Rx Automatic Aspect
View Map
Getting Started Quick Steps
1. Model:
2. Host:
3. View: Subscribe widgets
4. Controller:
Access the underlying value of rx variables
Visual Studio Code snippets
View Map - one step further of dependency injection
Original View
After using the View Map
Here's how to use View Map.
Summing up
Use Case - explain how the package works
Case 1: use rx automatic aspect
Case 2: with specific aspect
Case 3: manual publish aspect
Use Case - i18n with View Map
Setting up #
Add the following dependency to pubspec.yaml of your flutter project:
dependencies:
flutter_mediator: "^2.2.5"
copied to clipboard
Import flutter_mediator in files that will be used:
import 'package:flutter_mediator/mediator.dart';
copied to clipboard
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 #
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, or MultiHost.create if you want to use Model Mode together, at the top of the widget tree.
Create a consumer widget with globalConsume or watchedVar.consume to register the watched variable to the host to rebuild it when updating.
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);
copied to clipboard
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(),
),
);
}
copied to clipboard
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,
),
),
// ...
copied to clipboard
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,
),
copied to clipboard
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>[]);
copied to clipboard
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,
// ...
copied to clipboard
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));
}
copied to clipboard
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);
}
copied to clipboard
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(
// ...
);
}
copied to clipboard
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,
// ...
),
// ...
),
],
copied to clipboard
Step 1-4: Add assets in pubspec.yaml and prepare locale files in the folder
flutter:
# ...
assets:
- assets/images/
- assets/flutter_i18n/
copied to clipboard
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),
// ...
],
),
);
copied to clipboard
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);
}
}
copied to clipboard
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);
copied to clipboard
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),
// ...
),
);
}
}
copied to clipboard
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();
}
copied to clipboard
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;
copied to clipboard
Step 2: Create a consumer widget using locstr which is _locstr.value.
globalConsume(
() => Text(
locstr,
style: const TextStyle(fontSize: 16),
),
),
copied to clipboard
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);
copied to clipboard
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);
copied to clipboard
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',
),
// ...
copied to clipboard
Table of Contents
Case 2: By tag #
//* Step1: Declare the watched variable with `globalWatch`.
final touchCount = globalWatch(0, tag: 'tagCount');
copied to clipboard
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',
),
// ...
copied to clipboard
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 #
Flutter Mediator: Global Mode + Model Mode.
Lite: Global Mode only.
Persistence: Lite + built in persistence.
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.
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.
Rx Related Widget
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;
}
}
copied to clipboard
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(),
),
);
}
copied to clipboard
Or, use the generic form.
MultiHost.create( // Generic form
hosts: [
Host<MyModel>(model: MyModel(updateMs: 1000)),
Host<ListModel>(model: ListModel(updateMs: 500)),
],
child: MyApp(),
),
copied to clipboard
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}'))
copied to clipboard
Case 2: With specific aspect 'int1'.
'int1'.subModel<MyModel>((context, model) => Text('Int1 is ${model.int1}'))
copied to clipboard
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');
})
copied to clipboard
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++`
),
)
copied to clipboard
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++`
),
);
}
copied to clipboard
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';
}
copied to clipboard
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.');
},
),
],
),
);
}
/// ...
copied to clipboard
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
],
),
);
}
/// ...
copied to clipboard
Isn't it cleaner.
Here's how to use View Map. #
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();
}
copied to clipboard
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.');
});
copied to clipboard
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
],
),
copied to clipboard
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.
}
}
copied to clipboard
/// main.dart
void main() {
runApp(
MultiHost.create1(
MyModel(updateMs: 1000), // model extends from Pub
child: MyApp(),
),
);
}
copied to clipboard
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(),
],
);
}
copied to clipboard
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(),
],
);
}
copied to clipboard
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(),
],
);
}
copied to clipboard
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.
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/
copied to clipboard
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",
"~": ""
}
}
copied to clipboard
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.
Add an i18n extension to the setting_model.dart.
//* i18n extension
extension StringI18n on String {
String i18n(BuildContext context) {
return FlutterI18n.translate(context, this);
}
}
copied to clipboard
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.');
});
//...
copied to clipboard
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
);
}
}
copied to clipboard
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',
];
copied to clipboard
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 #
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
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(),
),
);
}
copied to clipboard
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(),
),
);
}
copied to clipboard
Or, use the generic form.
MultiHost.create( // Generic form
hosts: [
Host<MyModel>(model: MyModel(updateMs: 1000)),
Host<ListModel>(model: ListModel(updateMs: 500)),
],
child: MyApp(),
),
copied to clipboard
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.
}
copied to clipboard
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.
}
copied to clipboard
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);
}
copied to clipboard
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
}
copied to clipboard
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'
}
copied to clipboard
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
}
copied to clipboard
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
}
copied to clipboard
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
}
copied to clipboard
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.
}
copied to clipboard
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.
}
copied to clipboard
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);
}
}
copied to clipboard
/// 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);
}
}
copied to clipboard
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>();
copied to clipboard
with user extension
final model = getMyModel();
copied to clipboard
Get current triggered frame aspects of the model. See also [email protected].
final model = Host.model<MyModel>();
final aspects = model.frameAspects;
copied to clipboard
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;
copied to clipboard
/// main.dart
rxSub<MyModel>((context, model) {
return Text('tick1 is ${model.tick1}');
}),
copied to clipboard
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');
})
copied to clipboard
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}')),
copied to clipboard
original form
Subscriber<MyModel>(
aspects: 'int1',
create: (context, model) {
return Text('Int1 is ${model.int1}');
},
),
copied to clipboard
with user extension
'int1'.subMyModel((context, model) => Text('Int1 is ${model.int1}')),
copied to clipboard
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,
),
),
copied to clipboard
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,
);
},
),
copied to clipboard
with user extension
['int1', 'star'].subMyModel(
(context, model) => Text(
'Int1 is ${model.int1} and Star is ${model.star}',
softWrap: true,
textAlign: TextAlign.center,
),
),
copied to clipboard
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 [email protected].
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);
},
),
copied to clipboard
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);
},
),
copied to clipboard
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);
},
),
copied to clipboard
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,
}
copied to clipboard
Then everything is the same as String aspect, just to replace the String with enum.
See also [email protected].
simple form
ListEnum.ListUpdate.subModel<ListModel>((context, model) {
/* create method */
}),
copied to clipboard
original form
Subscriber<ListModel>(
aspects: ListEnum.ListUpdate,
create: (context, model) {
/* create method */
}),
copied to clipboard
with user extension
ListEnum.ListUpdate.subMyModel((context, model) {
/* create method */
}),
copied to clipboard
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;
copied to clipboard
/// 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);
},
);
});
}
copied to clipboard
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();
}
copied to clipboard
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.
For personal and professional use. You cannot resell or redistribute these repositories in their original state.
There are no reviews.