-
-
Save HansMuller/a3a6d520c6a24238bf1b1b9e3d473bf5 to your computer and use it in GitHub Desktop.
/* | |
ModelBinding<T>: a simple class for binding a Flutter app to a immutable | |
model of type T. | |
This is a complete application. The app shows the state of an instance of | |
MyModel in a button's label, and replaces its MyModel instance when the | |
button is pressed. | |
ModelBinding is an inherited widget that must be created in a context above | |
the widgets that will depend on it. A ModelBinding is created like this: | |
ModelBinding<MyModel>( | |
initialState: MyModel(), | |
child: child, | |
) | |
ModelBinding provides a static method 'of<T>(context)' that can be used | |
by any descendant to retrieve the current model instance, and a similar | |
static 'update<T>(context, T newModel)' method for replacing the | |
current model. Both methods implicitly create a dependency on the model: | |
when the model changes the corresponding context will be rebuilt. | |
In this example the model is retrieved and updated like this: | |
RaisedButton( | |
onPressed: () { | |
final MyModel model = ModelBinding.of<MyModel>(context); | |
ModelBinding.update<MyModel>(context, MyModel(value: model.value + 1)); | |
}, | |
child: Text('Hello World ${ModelBinding.of<MyModel>(context).value}'), | |
) | |
To use ModelBinding in your own app define an immutable model class like | |
MyModel that contains the application's state. Add the three ModelBinding | |
classes below (_ModelBindingScope, ModelBinding, _ModelBindingState) to a file | |
and import that where it's needed. | |
*/ | |
import 'package:flutter/material.dart'; | |
class MyModel { | |
const MyModel({ this.value = 0 }); | |
final int value; | |
@override | |
bool operator ==(Object other) { | |
if (identical(this, other)) | |
return true; | |
if (other.runtimeType != runtimeType) | |
return false; | |
final MyModel otherModel = other; | |
return otherModel.value == value; | |
} | |
@override | |
int get hashCode => value.hashCode; | |
} | |
class _ModelBindingScope<T> extends InheritedWidget { | |
const _ModelBindingScope({ | |
Key key, | |
this.modelBindingState, | |
Widget child | |
}) : super(key: key, child: child); | |
final _ModelBindingState<T> modelBindingState; | |
@override | |
bool updateShouldNotify(_ModelBindingScope oldWidget) => true; | |
} | |
class ModelBinding<T> extends StatefulWidget { | |
ModelBinding({ | |
Key key, | |
@required this.initialModel, | |
this.child, | |
}) : assert(initialModel != null), super(key: key); | |
final T initialModel; | |
final Widget child; | |
_ModelBindingState<T> createState() => _ModelBindingState<T>(); | |
static Type _typeOf<T>() => T; // https://github.com/dart-lang/sdk/issues/33297 | |
static T of<T>(BuildContext context) { | |
final Type scopeType = _typeOf<_ModelBindingScope<T>>(); | |
final _ModelBindingScope<T> scope = context.inheritFromWidgetOfExactType(scopeType); | |
return scope.modelBindingState.currentModel; | |
} | |
static void update<T>(BuildContext context, T newModel) { | |
final Type scopeType = _typeOf<_ModelBindingScope<T>>(); | |
final _ModelBindingScope<dynamic> scope = context.inheritFromWidgetOfExactType(scopeType); | |
scope.modelBindingState.updateModel(newModel); | |
} | |
} | |
class _ModelBindingState<T> extends State<ModelBinding<T>> { | |
T currentModel; | |
@override | |
void initState() { | |
super.initState(); | |
currentModel = widget.initialModel; | |
} | |
void updateModel(T newModel) { | |
if (newModel != currentModel) { | |
setState(() { | |
currentModel = newModel; | |
}); | |
} | |
} | |
@override | |
Widget build(BuildContext context) { | |
return _ModelBindingScope<T>( | |
modelBindingState: this, | |
child: widget.child, | |
); | |
} | |
} | |
class ViewController extends StatelessWidget { | |
@override | |
Widget build(BuildContext context) { | |
return RaisedButton( | |
onPressed: () { | |
final MyModel model = ModelBinding.of<MyModel>(context); | |
ModelBinding.update<MyModel>(context, MyModel(value: model.value + 1)); | |
}, | |
child: Text('Hello World ${ModelBinding.of<MyModel>(context).value}'), | |
); | |
} | |
} | |
class App extends StatelessWidget { | |
@override | |
Widget build(BuildContext context) { | |
return MaterialApp( | |
home: ModelBinding<MyModel>( | |
initialModel: const MyModel(), | |
child: Scaffold( | |
body: Center( | |
child: ViewController(), | |
), | |
), | |
), | |
); | |
} | |
} | |
void main() { | |
runApp(App()); | |
} |
Hi Hans,
Thank you for writing the article. Can you please explain why the Model must be immutable?
Put another way, would you please explain why we must use three classes to share mutable data down the tree? If InheritedWidget had a setState() method and allowed non-final data members then my app's structure could be very straightforward and clear. I am interested in reading the discussion of the decision to disallow this. Would you please share it?
My app's model may be large (>100 KiB) and I would like to avoid having the app iterate through it to check for changes. In your example here, it seems that comparison is done only as an optimization to avoid calling setState() and re-building the descendants when _ModelBindingState.updateModel() is called with a model that is unchanged. Can you confirm this?
I just found the scoped_model package which seems to let me use a mutable model:
https://flutter.dev/docs/development/data-and-backend/state-mgmt/simple
I will use that for now.
Best,
Michael
i see that when moving the ModelBinding to ViewController which will be like this:
class ViewController extends StatelessWidget {
@OverRide
Widget build(BuildContext context) {
return ModelBinding(
initialModel: const MyModel(),
child: Scaffold(
body: Center(
child: RaisedButton(
onPressed: () {
final MyModel model = ModelBinding.of(context);
ModelBinding.update(
context, MyModel(value: model.value + 1));
},
child:
Text('Hello World ${ModelBinding.of(context).value}'),
),
),
),
);
}
}
class App extends StatelessWidget {
@OverRide
Widget build(BuildContext context) {
return MaterialApp(home: ViewController());
}
}
will have error that the variable scope in method static T of(BuildContext context) of class ModelBinding will be null.
So the code get value of model and code binding model must not in same class ??
Thanks for your code. I tried to use ModelBinding with a FutureBuilder but unfortunately it doesn't work. The returned scope is null in ModelBinding.of.
class AppBug extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: ModelBinding<MyModel>(
initialModel: const MyModel(),
child: Scaffold(
body: FutureBuilder(
future: () {
var model = ModelBinding.of<MyModel>(context); // BUG HERE
var url = 'http://jsonplaceholder.typicode.com/posts/${model.value + 1}';
return http.get(url).then((response) => jsonDecode(response.body)['title']);
}(),
builder: (context, snapshot) {
if (snapshot.hasError) throw snapshot.error;
if (!snapshot.hasData) return Center(child: CircularProgressIndicator());
return Center(
child: FlatButton(
onPressed: () {
final MyModel model = ModelBinding.of<MyModel>(context);
ModelBinding.update<MyModel>(context, MyModel(value: model.value + 1));
},
child: Text(snapshot.data),
),
);
},
),
),
),
);
}
}
Thank you for share.
I have one question, How to use in other screens this model.