Flutter State & Scoped Model Approach
Approach
This approach removes most of the requirements to use Stateful Widgets, enabling the user to use Stateless Widgets instead in many cases.ScopedModel has been mentioned in many articles as an alternative to just using InheritedWidget. At first sight, it looks like the ScopedModel package is basically InheritedWidget, only made easier to use.
Package
ScopedModel is a Dart package and it is available here: https://pub.dartlang.org/packages/scoped_modelAs it is a package you will have to install it:
https://pub.dartlang.org/packages/scoped_model - -installing-tab-
Package Readme
The package README.md file includes the following text:A set of utilities that allow you to easily pass a data Model from a parent Widget down to its descendants. In addition, it also rebuilds all of the children that use the model when the model is updated. This library was originally extracted from the Fuchsia codebase.
This package provides three main classes:
1. Model
You will extend this class to create your own Models, such as SearchModel or UserModel.You can listen to Models for changes!
2. ScopedModel Widget.
If you need to pass a Model deep down your Widget hierarchy, you can wrap your Model in a ScopedModel Widget.This will make the Model available to all descendant Widgets
3. ScopedModelDescendant Widget.
Use this Widget to find the appropriate ScopedModel in the Widget tree.
It will automatically rebuild whenever the Model notifies that change has taken place.
Multiple Models
At first glance, it looks as if this package allows the user to use multiple State Models. This certainly makes it a better candidate for working with larger applications. You could have User data in one model, Transaction data in another etc.Exercise – ‘state_and_scoped_model’
The code below is not perfect by any means (you can add the same car twice and when you tap on it, it selects both) but it demonstrates how to get an app up and working with ScopedModel and how you can maintain separate states in separate models.In this exercise, I use the ScopedModel to handle two separate state
models:
1. a list of cars (to which we can add cars)
2. the currently selected car (which you can change by tapping on a car).
There is more code for you to copy and paste in this example.
However, this app does more than some of the previous examples: it allows you to add cars and allows you to select cars.
Step 1 – Create Default Flutter App
Follow the instructions in Generate Your First App Leave project open.
Step 2 – Replace Application Code
Replace contents of file ‘main.dart’ in folder ‘lib’ with the following:
import 'package:flutter/material.dart';
import 'package:scoped_model/scoped_model.dart';
void main() => runApp(new CarAppWidget());
class Car {
String _make;
String _model;
String _imageSrc;
Car(this._make, this._model, this._imageSrc);
operator ==(other) =>
(other is Car) && (_make == other._make) && (_model == other._model);
int get hashCode => _make.hashCode ^ _model.hashCode ^
_imageSrc.hashCode;
}
class CarListModel extends Model {
List<Car> _carList = [
Car(
"Bmw",
"M3",
"Https://media.ed.edmundsmedia.com/bmw/m3/2018/oem/2018_bmw_m3_sedan_base_fq_oem_4_150.jpg",
),
Car(
"Nissan",
"GTR",
"Https://media.ed.edmunds-media.com/nissan/gt-r/2018/oem/2018_nissan_gtr_coupe_nismo_fq_oem_1_150.jpg",
),
Car(
"Nissan",
"Sentra",
"Https://media.ed.edmundsmedia.com/nissan/sentra/2017/oem/2017_nissan_sentra_sedan_srturbo_fq_oem_4_150.jpg",
)
];
List<Car> get carList => _carList;
void add(String make, String model, String imageSrc) {
_carList.add(Car(make, model, imageSrc));
notifyListeners();
}
}
class CarSelectionModel extends Model {
Car _selectedCar;
Car get selectedCar => _selectedCar;
void set selectedCar(Car selectedCar) {
_selectedCar = selectedCar;
notifyListeners();
}
bool isSelected(Car car) {
if (_selectedCar == null) {
return false;
} else {
return car == _selectedCar;
}
}
}
class CarAppWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Car App',
theme: new ThemeData(
primarySwatch: Colors.blue,
),
home: ScopedModel<CarListModel>(
model: CarListModel(),
child: ScopedModel<CarSelectionModel>(
model: CarSelectionModel(),
child: CarAppLayoutWidget(title: 'Cars'))));
}
}
class CarAppLayoutWidget extends StatelessWidget {
CarAppLayoutWidget({Key key, this.title}) : super(key: key);
final String title;
_addCar(BuildContext context) {
ScopedModel.of<CarListModel>(context, rebuildOnChange: true).add(
"Subaru",
"WRX",
"Https://media.ed.edmunds-media"
".com/subaru/wrx/2018/oem/2018_subaru_wrx_sedan_stilimited_s_oem_1_150"
".jpg");
}
String _calculateSelectedCarName(BuildContext context) {
Car selectedCar =
ScopedModel.of<CarSelectionModel>(context, rebuildOnChange: true)
.selectedCar;
if (selectedCar == null) {
return "No car selected.";
} else {
return "Selected: ${selectedCar._make} ${selectedCar._model}";
}
}
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text(title),
),
body: Center(child: CarListWidget()),
persistentFooterButtons: <Widget>[
Text(_calculateSelectedCarName(context)),
IconButton(
icon: Icon(Icons.add),
onPressed: () {
_addCar(context);
}),
]);
}
}
class CarListWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
final carList =
ScopedModel.of<CarListModel>(context, rebuildOnChange: true).carList;
List<CarWidget> carWidgets = carList.map((Car car) {
return CarWidget(car);
}).toList();
return new ListView(children: carWidgets);
}
}
class CarWidget extends StatelessWidget {
CarWidget(this._car) : super();
final Car _car;
_buildCarWidget(context, child, CarSelectionModel selectionModel) {
return GestureDetector(
onTap: () => selectionModel.selectedCar = _car,
child: Padding(
padding: EdgeInsets.all(20.0),
child: Container(
decoration: BoxDecoration(
border: Border.all(),
color: selectionModel.isSelected(_car)
? Colors.blue
: Colors.white),
padding: EdgeInsets.all(20.0),
child: Center(
child: Column(children: <Widget>[
Text('${_car._make} ${_car._model}',
style: TextStyle(fontSize: 24.0)),
Padding(
padding: EdgeInsets.only(top: 20.0),
child: Image.network(_car._imageSrc))
])))));
}
@override
Widget build(BuildContext context) {
return ScopedModelDescendant<CarSelectionModel>(
builder: (context, child, selectionModel) =>
_buildCarWidget(context, child, selectionModel));
}
}
Step 3 – Open Emulator & Run
Follow the instructions in Open Android Emulator & Run Your First App
If you tap on the ‘+’ button at the bottom it adds another car.
If you tap on a car it selects the car (adding a blue background) and sets the text of the selected car at the bottom.