Skip to content

Instantly share code, notes, and snippets.

@HansMuller
Created December 14, 2018 23:05
Show Gist options
  • Star 16 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save HansMuller/a3a6d520c6a24238bf1b1b9e3d473bf5 to your computer and use it in GitHub Desktop.
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());
}
@rmzntimur
Copy link

Thank you for share.
I have one question, How to use in other screens this model.

@mleonhard
Copy link

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

@vandong9
Copy link

vandong9 commented Aug 25, 2019

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 ??

@Z6P0
Copy link

Z6P0 commented Sep 11, 2020

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),
                ),
              );
            },
          ),
        ),
      ),
    );
  }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment