Manage state in Flutter using Scoped Model

Flutter-State-Management-With-Scoped-Model

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.

Prerequisite.

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.

Installations/Project Setup.

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.

  1. scoped_model: – This will provide all the required features to manage our application’s state.
  2. uuid: ^2.0.2 – We will require this package to generate some random IDs for out Todos List.

Now it should look like this in your file,

Dependencies

Dependencies

Step 1: Building The Layout.

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

main.dart
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);
                },
              )
            ],
          );
        });
  }
}
Basic Layout

Basic Layout

Step 2: Let’s Setup Our Structure For Models & Scoped Models.

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.

Step 3: Create a ToDo Model.

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;

}

Step 4: Create A Scoped Model To Manage The State.

Create a file named todos.dart in the /lib/scoped-models directory which will accommodate all our state & business logic for the applications.

todos.dart
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.

  1. todos(): This is actually a getter method marked with the get identifier which returns the list of the tasks we have in our state.
  2. add(): Takes one argument which is the title of any new task we want to add in the _todos list.
  3. remove(): Removes an item from the list with the given index.
  4. fetchTodos(): This is an important method which will actually initialize our list. You can make any networks calls here to fetch any data from the server.

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.

Step 5: Using the Scoped Model.

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

Step 6: Refactoring Our Code.

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.

Step 7: Accessing Multiple ScopedModels.

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.

Step 8: Access The ScopedModel Without Using ScopedModelDescendant.

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

If you have any suggestions or if you face any problem please write it down in the comment box…

Happy Coding…:)

Download Project From GitHub

 

Ropali Munshi
Ropali Munshi
Ropali Munshi is fullstack PHP Developer. He is passionate developer who loves to learn and expirement with new programming languages , libraries and frameworks. Nowdays he is more into the JavaScript realm.

Leave a Reply

Your email address will not be published. Required fields are marked *