Open In App

Flutter – Using Nested Models and Providers

Last Updated : 20 Jul, 2023
Improve
Improve
Like Article
Like
Save
Share
Report

The provider is a simple state management technique that is used for managing a piece of data around the app. It is basically a wrapper around inherited widgets but it is easy to understand and manage. Before going ahead we have to understand some concepts in provider –

  • ChangeNotifier: It is basically a simple class that is used for notifying any changes that happen to a given model to the part of the app that is listening for them. We can describe them as a subscription service. i.e. any changes to data get updated to all the parts of the app that are subscribed to it.
  • ChangeNotifierProvider: It is a widget available in the provider package that provides an instance of the ChangeNotifier to its children. It is basically placed above all the package that needs the data of the model.
  • Consumer: We can use the data of the model via Consumer as the ChangeNotfierProvider is present as the parent of it. We have to specify the generics (<TaskData> here) so that the provider knows which data to fetch. It has one argument builder. Builder is a function that is called when there is a change in data. It has three arguments context which is the current context of the builder, then the instance of ChangeNotifier (here data), and the third is a child which is used for large widget for optimization purpose.
Syntax:
return Consumer<TaskData>(
  builder: (context, data, child) {
    return Text(data.task);
  },
);

Setting up the App:

Now that we have covered the basic concepts of Provider. Now we will understand each of these topics with the help of a sample app. We will be building a sample task management app.

First, we need to add the provider package to the dependencies section of pubspec.yaml.

dependencies:
  flutter:
    sdk: flutter
  provider: ^4.3.2+4 #ADD

We will create a model directory inside the lib directory and add task.dart and task_data.dart file inside it.

Dart




import 'package:flutter/foundation.dart';
 
class Task {
  String task;
  bool completed;
  Task({@required this.task, this.completed = false});
  void toggle() {
    completed = !completed;
  }
}


The task.dart file has a class Task with two fields. 

  1. String task which stores the name of the task to be displayed on the app when added.
  2. bool completed which will toggle according to the task as it is completed or not.

There is also a toggle function that switches the tasks from true to false and vice-versa.

Dart




import 'dart:collection';
import 'package:flutter/foundation.dart';
import 'package:task_management/model/task.dart';
 
class TaskData with ChangeNotifier {
  List<Task> _tasks = [];
  UnmodifiableListView<Task> get tasks => UnmodifiableListView(_tasks);
 
  void addTask(Task task) {
    _tasks.add(task);
    notifyListeners();
  }
 
  void toggleTask(Task task) {
    task.toggle();
    notifyListeners();
  }
 
  void removeTask(Task task) {
    _tasks.remove(task);
    notifyListeners();
  }
}


The task_data.dart file has the TaskData class which is used to manage the data. We define this class with ChangeNotifier to make the data available to the parts of the app that are listening for it. The components of the class are

  • List of Task (_tasks) which is a private member so that the data cant be changed from outside the app.
  • There is an unmodifiable version of that list (tasks) so that the data is available for use inside the app.
  • The addTask method is used to add a new task to the app.
  • The toggleTask is used to change the task from completed to non-completed and vice-versa.
  • There is also a removeTask to remove a particular task from the list of tasks.
  • There is a notifyListeners() call at the end of each method so that listeners of the data get notified when data changes.

We are going to add the ChangeNotifierProvider in the main.dart above all the widgets. As our app is quite simple with just two screens we have to place it in the main.dart so that it is available to its descendants. The main.dart is as follows-

Dart




import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
 
import 'model/task_data.dart';
import 'screens/home.dart';
 
void main() {
  runApp(MyApp());
}
 
class MyApp extends StatelessWidget {
   
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (context) => TaskData(),
      child: MaterialApp(
        debugShowCheckedModeBanner: false,
        title: 'Provider Demo',
        theme: ThemeData(
          primarySwatch: Colors.green,
        ),
        home: Home(),
      ),
    );
  }
}


The MaterialApp widget is the child of ChangeNotifierProvider which takes a create field to get the instance of TaskData. We create two screens after this which are defined in the screens directory which is defined in the lib directory and two files i.e. home.dart and add_task.dart in it.

  • The Home screen is used to display the content of the List of tasks
  • Add Task Screen which is used to add a new task to lists of tasks.

Dart




import 'package:flutter/material.dart';
import 'package:task_management/task_list.dart';
import 'add_task.dart';
 
class Home extends StatelessWidget {
   
  // create the appbar
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('GeeksforGeeks'),
      ),
      body: Container(
        padding: EdgeInsets.all(20),
        child: TaskList(),
      ),
      floatingActionButton: FloatingActionButton(
        child: Icon(
          Icons.add,
        ),
        onPressed: () {
          Navigator.push(
              context, MaterialPageRoute(builder: (context) => AddTask()));
        },
      ),
    );
  }
}


The Home screen contains an App Bar and then Container which has child TaskList widgets that show the list of tasks, and a FloatingActionButton to add new tasks to the app.

Dart




import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:task_management/model/task.dart';
import 'package:task_management/model/task_data.dart';
 
class AddTask extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    String text = '';
    return Scaffold(
      appBar: AppBar(
        title: Text('Add Task'),
      ),
      body: Container(
        padding: EdgeInsets.all(18),
        child: Column(
          children: [
            TextField(
              onChanged: (c) => text = c,
              decoration: InputDecoration(
                contentPadding: EdgeInsets.symmetric(horizontal: 10),
                hintText: 'Enter Task',
                border: const OutlineInputBorder(
                  borderRadius: BorderRadius.all(Radius.circular(8.0)),
                ),
              ),
            ),
             
            // add button
            RaisedButton(
              shape: RoundedRectangleBorder(
                borderRadius: BorderRadius.all(
                  Radius.circular(8),
                ),
              ),
              child: Text(
                'Submit',
                style: TextStyle(
                    fontWeight: FontWeight.bold,
                    fontSize: 18,
                    color: Colors.black),
              ),
               
              // assign action
              onPressed: () {
                Provider.of<TaskData>(context, listen: false)
                    .addTask(Task(task: text));
                Navigator.pop(context);
              },
            )
          ],
        ),
      ),
    );
  }
}


The AddTask screen lets us add a new task with the default switch to be off (i.e. The task is not done). Here we use Provider.of, with the listen parameter set too false to add the data in the list. We have not used Consumer here as we don’t need the data.

Now we are going to make the widget that is going to render the tasks on the screen. So we define the following widget TaskList in lib called task_list.dart as:

Dart




import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:task_management/model/task_data.dart';
 
class TaskList extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Consumer<TaskData>(builder: (context, data, child) {
      return ListView.builder(
        scrollDirection: Axis.vertical,
        shrinkWrap: true,
        itemCount: data.tasks.length,
        itemBuilder: (context, index) {
          final task = data.tasks[index];
           
          // gesture detection
          return GestureDetector(
            onLongPress: () => data.removeTask(task),
            child: Container(
              margin: EdgeInsets.only(bottom: 10),
              padding: EdgeInsets.fromLTRB(12, 5, 8, 5),
              width: double.infinity,
              decoration: BoxDecoration(
                  color: Colors.black12,
                  borderRadius: BorderRadius.circular(8)),
              child: Row(
                mainAxisAlignment: MainAxisAlignment.spaceBetween,
                children: [
                   
                  // text field
                  Text(
                    task.task,
                    style: TextStyle(
                        decoration:
                            task.completed ? TextDecoration.lineThrough : null,
                        fontSize: 16,
                        fontWeight: FontWeight.bold),
                  ),
                   
                  // switch case
                  Switch(
                    value: task.completed,
                    onChanged: (c) => data.toggleTask(task),
                  ),
                ],
              ),
            ),
          );
        },
      );
    });
  }
}


Output:

Here we have used the Consumer to get the data of TaskList we also provide the functionalities for removing and toggling the task as long press to remove and toggle to mark as done.



Like Article
Suggest improvement
Previous
Next
Share your thoughts in the comments

Similar Reads