State management is the key concept in the Flutter framework. State what makes the Flutter application reactive.
This can also be the pain point for many developers because there are many ways to manage the state of the Flutter application. By default, Flutter provides you one way to manage state by passing the properties down every widget. But this is not suitable for intermediate to large applications.
There are many third-party packages available for state management for more complex applications. One of them is scoped_model. It is the most convenient and easy to learn package to manage the state of your application.
In this tutorial, I will build a ToDO List App using the scoped_model package.
You need to familiar with the Flutter framework (Of course you are). You don’t need any other things to follow along with this tutorial. So let’s dive into it.
Now open your terminal inside any directory where you keep all your project files & fire this command to create a new flutter project. Replace <YOUR_PROJECT_NAME> with any name you want to give your project.
> flutter create <YOUR_PROJECT_NAME>
Now after Flutter has finished creating your project open the project folder with any of your favorite code editor.
Now we need to install some dependencies, open pubspec.yaml file & below the dependencies add these two package names.
Now it should look like this in your file,
Now open the main.dart file & delete all the code so that we can write our own code for layout & other logics.
I will build a basic list of ToDos which can be deleted by right swipe. To build this layout I have used Dissmissible widget with the LisView widget. To add a new task in the list I have used the FloatinAction button which opens a dialog to enter a new task.
All the ToDos details are maintained using the List of Maps. I have already initialized a list with some random tasks
import 'package:flutter/material.dart'; import 'package:uuid/uuid.dart'; void main() => runApp(MyApp()); class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( theme: ThemeData( primarySwatch: Colors.blue, ), home: MyHomePage(), ); } } class MyHomePage extends StatefulWidget { @override _MyHomePageState createState() => _MyHomePageState(); } class _MyHomePageState extends State<MyHomePage> { // Prefilled list of todos final List<Map<String, dynamic>> _todos = [ {"title": "tweet milk", "done": false, "id": Uuid().v4()}, {"title": "try to clean needle work", "done": false, "id": Uuid().v4()}, {"title": "help crash fishing rod", "done": false, "id": Uuid().v4()}, {"title": "forget fishing rod", "done": false, "id": Uuid().v4()}, {"title": "pretend to pick chess", "done": false, "id": Uuid().v4()}, {"title": "clean laptop", "done": false, "id": Uuid().v4()}, {"title": "avoid distant relatives", "done": false, "id": Uuid().v4()}, {"title": "pick needle work", "done": false, "id": Uuid().v4()}, {"title": "try to sell fishing rod", "done": false, "id": Uuid().v4()}, {"title": "avoid forgeting milk", "done": false, "id": Uuid().v4()}, {"title": "exercise Italian", "done": false, "id": Uuid().v4()}, {"title": "buy knife", "done": false, "id": Uuid().v4()}, ]; TextEditingController _textFieldController = TextEditingController(); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Scoped Modal Demo'), ), body: ListView.builder( itemBuilder: (BuildContext context, int index) { return Dismissible( key: Key(_todos[index]['id']), direction: DismissDirection.startToEnd, child: ListTile( title: Text(_todos[index]['title']), leading: Icon(Icons.check), ), background: Container( alignment: Alignment.centerLeft, padding: EdgeInsets.only(left: 10.0), color: Colors.red, child: Icon( Icons.delete, color: Colors.white, ), ), onDismissed: (DismissDirection direction) => _onDismissed(direction, index), ); }, itemCount: _todos.length, ), floatingActionButton: FloatingActionButton( onPressed: (){ _addTodo(context); }, tooltip: 'Add', child: Icon(Icons.add), ), ); } void _onDismissed(DismissDirection direction, int todoIndex) { if (direction == DismissDirection.startToEnd) { setState(() { _todos.removeAt(todoIndex); }); } } void _addTodoInList(BuildContext context){ String val = _textFieldController.text; final newTodo = { "title": val, "id": Uuid().v4(), "done": false }; setState(() { _todos.insert(0, newTodo); }); _textFieldController.clear(); Navigator.pop(context); } void _addTodo(BuildContext context) { showDialog( context: context, builder: (context) { return AlertDialog( title: Text('Add New Todo'), content: TextField( controller: _textFieldController, decoration: InputDecoration(hintText: "Enter title"), ), actions: <Widget>[ FlatButton( child: Text('CANCEL'), onPressed: () { _textFieldController.clear(); Navigator.of(context).pop(); }, ), FlatButton( child: Text('ADD'), onPressed: () { _addTodoInList(context); }, ) ], ); }); } }
I have created two folders inside the lib directory, one is models & the second one is the scoped-models. One is t be used to all the Model classes & others to be used to store all the scoped_model classes which will actually manage the state. You might be a bit confused between the Model & scoped_model classes, Let me explain.
Model is a single class which represents any entity in our case it represents a single ToDo task. Models are the best way to manage & represent your data in your application.
It provides a flexible way to manage any data because you know exactly how your data look like & since It is just a plain class you can add any method to manipulate your data. You can create different constructors of that class to handle different scenarios
Scoped Models are also dart classes which extend Model class provided by the scoped_model package. In these classes, we are going to manage all our state & the business logic related to our application. This package also helps us to separate our designs from the business logic which is considered as best practice in the industries.
I have already discussed what the Model is & what it does. So let’s create our own model class to represent the ToDo tasks rather managing it as Map.
Let’s create a file named ToDo.dart inside the /lib/models directory.
ToDo.dart
import 'package:flutter/material.dart'; class Todo { final String title; final String id; final bool done; Todo({ @required this.title, @required this.id, this.done = false }); // A special constructor which // can be used to create a Todo object from the JSON data or Maps Todo.fromJson(Map<String, dynamic> json) : title = json['title'], id = json['id'], done = false; }
Create a file named todos.dart in the /lib/scoped-models directory which will accommodate all our state & business logic for the applications.
import 'package:scoped_model/scoped_model.dart'; import 'package:uuid/uuid.dart'; import '../models/Todo.dart'; class TodosModel extends Model { List<Todo> _todos = []; List<Todo> get todos { return List.from(_todos); } void add(String val) { final newTodo = {"title": val, "id": Uuid().v4()}; _todos.insert(0, Todo.fromJson(newTodo)); notifyListeners(); } void remove(int index){ _todos.removeAt(index); notifyListeners(); } void fetchTodos() { // In this method you actualyy perform any network related job // Like if your fetching the list from your server // This will initialize out state at the start of the application final List<Map<String, dynamic>> todos = [ {"title": "tweet milk", "done": false, "id": Uuid().v4()}, {"title": "try to clean needle work", "done": false, "id": Uuid().v4()}, {"title": "help crash fishing rod", "done": false, "id": Uuid().v4()}, {"title": "forget fishing rod", "done": false, "id": Uuid().v4()}, {"title": "pretend to pick chess", "done": false, "id": Uuid().v4()}, {"title": "clean laptop", "done": false, "id": Uuid().v4()}, {"title": "avoid distant relatives", "done": false, "id": Uuid().v4()}, {"title": "pick needle work", "done": false, "id": Uuid().v4()}, {"title": "try to sell fishing rod", "done": false, "id": Uuid().v4()}, {"title": "avoid forgeting milk", "done": false, "id": Uuid().v4()}, {"title": "exercise Italian", "done": false, "id": Uuid().v4()}, {"title": "buy knife", "done": false, "id": Uuid().v4()}, ]; todos.forEach((todoMap)=> _todos.add(Todo.fromJson(todoMap))); notifyListeners(); } }
As you can see in the code, I have defined the four methods for performing any operation related to our ToDo task.
notifyListeners() method is the magic of ScopedModel. It rebuilds your widget every time your Model is changed so now you don’t need to call the setState() method.
The concept behind the scoped model is simple. All you have to do is that you wrap your app around this model then all your widgets will have any scoped model classes you want.
In this case, we have to wrap our MaterialApp widget with ScopedModel widget, like this,
class MyApp extends StatelessWidget { final TodosModel model = TodosModel(); @override Widget build(BuildContext context) { return ScopedModel( model: model, child: MaterialApp( theme: ThemeData( primarySwatch: Colors.blue, ), home: MyHomePage(model), ), ); } }
The ScopedModel widget takes two named parameters, one is the model and other is the child.
The model takes the scoped models which we had defined earlier & child is the widget that needs to have access to the model. Here we could have wrapped our MyHomePage() class which is also a widget but the general practice is that we must wrap MaterialApp widget because that is the highest level of the widget in the widgets tree.
Next, we have passed that model to the MyHomePage so that we can have access to it. After that in the _MyHomePageState class, we need to define the init() method which is a lifecycle method & invoked when the app starts up, So in that method, we will initialize our state, like this,
@override void initState() { // Initialize the ToDo list widget.model.fetchTodos(); super.initState(); }
Now I have created one more file named todos_page.dart which will contain all the code related to displaying the list of ToDos. This will also make sense if we want to use the scoped model features. Because your app may have multiple pages or screens which require access to some common data, hence scoped model provides a central repository of your data which you can access to it from any pages.
To give your widgets access to the data, you have to wrap your widget with another widget called ScopedModelDescendant<T>. It takes only one parameter that is builder which is a method which also three parameters. See the code below,
todos_page.dart
import 'package:flutter/material.dart'; import 'package:scoped_model/scoped_model.dart'; import './models/Todo.dart'; import 'scoped-models/todos.dart'; class TodosPage extends StatelessWidget { @override Widget build(BuildContext context) { return ScopedModelDescendant<TodosModel>( builder: (BuildContext context, Widget child, TodosModel model) { print(child.toString()); return ListView.builder( itemBuilder: (BuildContext context, int index) { Todo todo = model.todos[index]; return Dismissible( key: Key(todo.id), direction: DismissDirection.startToEnd, child: ListTile( title: Text(todo.title), leading: Icon(Icons.check), ), background: Container( alignment: Alignment.centerLeft, padding: EdgeInsets.only(left: 10.0), color: Colors.red, child: Icon( Icons.delete, color: Colors.white, ), ), onDismissed: (DismissDirection direction) => _onDismissed(direction, index, model), ); }, itemCount: model.todos.length, ); }, ); } void _onDismissed( DismissDirection direction, int todoIndex, TodosModel model) { if (direction == DismissDirection.startToEnd) { model.remove(todoIndex); } } }
In ScopedModelDescendant you have to specify which Model you want to have access to. In the builder method, you get access to that Model. In our case, we wanted to have access to our TodosModel.
This same principle applies to any widget where you want to have access to some model. It could any widget from Scaffold to button.
There is a strong possibility that you need to create multiple ScopedModels from a maintainability point of view, you can’t just include everything in one Model.
For example, you may have another ScopdeModel which handles all the data related to your users. In that case, you also need to have access to the UsersModels.
So how do we resolve this? ScopedModel allows you to wrap a ScopedModel with another ScopedModel widget to create some kind of nested ScopedModels.
For demo purpose let’s create another Model class which will only store the static username, So you can retrieve the username & display it on our app bar. Create a file users.dart in the scopde-models folder.
users.dart
import 'package:scoped_model/scoped_model.dart'; class UsersModel extends Model{ final String username = "NewCodingEra"; // Getter method to get the username String get getUsername{ return username; } }
Now to use this ScopedModel, We need to wrap our MaterialApp widget again with this one, like this
class MyApp extends StatelessWidget { final TodosModel model = TodosModel(); @override Widget build(BuildContext context) { return ScopedModel( model: UsersModel(), child: ScopedModel( model: model, child: MaterialApp( theme: ThemeData( primarySwatch: Colors.blue, ), home: MyHomePage(model), ), ), ); } }
This allows you to access the UsersModel data from anywhere now.
At some point, you just want to get the value from any variable inside the ScopedModel & in that case you might not want to wrap your widget with the ScopedModelDescendant. To access just a variable or a method from any ScopedModel this package provides a convenient way to do that like this,
String username = ScopedModel.of<UsersModel>(context, rebuildOnChange: true).getUsername;
After this our build method in the main.dart file will look like this,
Widget build(BuildContext context) { String username = ScopedModel.of<UsersModel>(context, rebuildOnChange: true).getUsername; return Scaffold( appBar: AppBar( title: Text('Hi, $username'), ), body: TodosPage(), floatingActionButton: ScopedModelDescendant<TodosModel>( builder: (BuildContext context, Widget child, TodosModel model) { return FloatingActionButton( onPressed: () { _addTodo(context, model); }, tooltip: 'Add', child: Icon(Icons.add), ); }, ), ); }
Happy Coding…:)
Download Project From GitHub