Provider State Management – An Awesome Guide for Flutter App

While mind-mapping myself and trying to find a new idea for my next Flutter App, most of the time I get stuck just thinking about what State Management I should try. Would it be related to Bloc, Provider, GetX or so on… Well, this goes on and on for almost every project, and I am sure I am not alone in this. So, today’s topic is a hot one; i.e. Provider State Management – An Awesome Guide for Flutter App. In this blog post, we shall be developing a new application from scratch with this awesome technique, vis-à-vis getting hands-on experience with some architectural patterns as well.


Table of Contents

– Provider

It is a State Management solution that helps manage the state of an application and makes it easy to share data and functionality between various parts of an app.

Additionally, a provider follows the path of InheritedWidget, which allows the data to propagate down the widget tree, and rebuild parts of your user interface when data gets changed.

Furthermore, it is a simplified package that takes care of the state and provides optimal solutions whether your app is simple or complex.

Moreover, the root concept behind the provider is ChangeNotifier, a class that you extend to create your custom data models. These models can then be accessed and updated from various widgets without needing to pass the data through multiple levels manually.

Let’s now look at the application overview.

– Application Overview

We are going to develop a simple application that will parse HTTP requests. Apart from that, we shall make an appropriate MVVM structure, followed by exception classes, utils, and constants as well.

More on MVVM:

lib
- main.dart
- app.dart
- res/
  - view/
  - view_model
  - models/
  - repostitory/
  - utils/
  - services/
    - app_exceptions.dart
    - network_api_service.dart
    - base_api_service.dart
  - widgets/

Let’s now head to the implementation stuff.

– Implementation

Create a new Flutter project.

Under the pubspec.yaml configuration file, add a new package:

provider: any
http: any

Firstly, let’s set up our network service structure to parse various HTTP requests.

Under the services directory, base_api_service.dart, add the following snippet:

Note: We shall make this class an abstract one to provide a base setup.

import '../response/api_response.dart';

abstract class BaseApiServices {

  Future<ApiResponse> postApiResponse(String url, Map<String, String> body, 
  {Map<String, String>? headers});

  Future<ApiResponse> putApiResponse(String url, Map<String, String> body, 
  {Map<String, String>? headers});

  dynamic getApiResponse(String url);

  Future<ApiResponse> deleteApiResponse(String url, Map<String, String> body, 
  {Map<String, String>? headers});
}

Based on this class, let’s build the content for network_api_service.dart.

Code Snippet:

import '../project/exports.dart';

String baseUrl = "https://reqres.in/";
String prefix = "api/";

class NetworkApiService extends BaseApiServices {
  @override

  dynamic getApiResponse(String url) async {
    dynamic jsonResponse;

    try{
      final res = await http.get(Uri.parse(baseUrl+ prefix + 
      url));

      jsonResponse = jsonOutput(res);
    }
    on SocketException {
      throw FetchDataException("No Internet found");
    }

    return jsonResponse;
  }

  @override
  dynamic postApiResponse(String url, Map<String, String> body,
  {Map<String, String>? headers}) async {

    dynamic jsonResponse;

    try {
      final res = await http.post(Uri.parse(baseUrl + prefix +
      url), body: body, headers: headers);

      jsonResponse = res.body;
    }
    on SocketException {
      throw FetchDataException("No Internet found");
    }
    return jsonResponse;
  }

  /* This method instantly converts the response to json for model parsing */
  dynamic jsonOutput(http.Response response) {
    switch(response.statusCode) {
      case 200:
        dynamic res = jsonDecode(response.body);
        return res;
      case 500:
        throw FetchDataException("Internal server error ${response.body}");
      default:
        throw BadRequestException("Bad request ${response.body}");
    }
  }
}

The above class extends the base class that represents various methods that have been overridden.

exception.dart:

class AppExceptions implements Exception {

  final String message, prefix;
  AppExceptions(this.message, this.prefix);

  @override
  String toString() {
    // TODO: implement toString
    return '$prefix - $message';
  }
}

class FetchDataException extends AppExceptions {
  FetchDataException([String? message]) : super(message!, "Error during communication");
}

class BadRequestException extends AppExceptions {
  BadRequestException([String? message]) : super(message!, "Bad request");
}

class UnauthorizedException extends AppExceptions {
  UnauthorizedException([String? message]) : super(message!, "Unauthorized");
}

At this point, our base setup is completed.

Let’s now make the model class and parse it accordingly.

Under the models’ directory, add a new file users_model.dart and paste the code there.

This would be the structural pattern.

Network Service -> Repository -> View Model -> View (+ Custom Widgets)

Now, under the repository directory, add a new file users_repository.dart and add this snippet:

class UserRepository {
  final NetworkApiService service = NetworkApiService();

  dynamic getUsers() async {
    final res = await service.getResponse('users');
    return res;
  }
}

This code is self-explanatory, so let’s head forward.

Next, under the view_model directory, create a new file users_view_model.dart.

Code Snippet:

class UsersViewModel extends ChangeNotifier {
  
  UsersViewModel(this.userRepository);
  final UserRepository userRepository;
  
  List<Users> allUsers = [];
  bool isLoading = false;
  
  Future<void> getUsers() async {
    isLoading = true;
    notifyListeners();

    dynamic res = await userRepository.getUsers();

    final users = UsersModel.fromJson(res);
    allUsers = users.users;

    isLoading = false;
    notifyListeners();
  }
}

In the above snippet, we are getting the response from the relevant repository, and converting it to its suitable model.

We are now left with the view i.e. the presentation stuff only.

So, in the view directory, add a new file user_list_view.dart.

Note: Be sure to make this class a Stateful Widget to fetch the user list.

Code Snippet:

class UsersListView extends StatefulWidget {
  const UsersListView({super.key});

  @override
  State<UsersListView> createState() => _UsersListViewState();
}

class _UsersListViewState extends State<UsersListView> {

  late UsersViewModel viewModel;

  @override
  void initState() {
    // TODO: implement initState

    viewModel = Provider.of<UsersViewModel>(context, listen: false);

    /* This method gets called after build */
    WidgetsBinding.instance.addPostFrameCallback((_) {
      viewModel.getUsers();
    });
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("Users"),
      ),
      body: Consumer<UsersViewModel>(
        builder: (context, _, child) {
          return viewModel.isLoading ? 
            const Center(child: CircularProgressIndicator()) : 
            ListView.separated(
            itemCount: viewModel.allUsers.length,
            itemBuilder: (context, i) {

              Users item = viewModel.allUsers[i];
              return UsersCustomWidgetView(item: item);
            },
            separatorBuilder: (context, i) => const SizedBox(height: 10),
          );
        },
      ),
    );
  }
}

Here, the Consumer is used to allowing a part of the Widget to be rebuilt.

Note: Be sure to register this ViewModel inside the app.dart file, as a root of MaterialApp.

ChangeNotifierProvider(
      create: (context) => UsersViewModel(UserRepository()),
      child: MaterialApp(
        debugShowCheckedModeBanner: false,
        home: UsersListView(),
      ),
    );

It’s time to run the App.

Preview:

Users List preview

Let’s now do it for a post request as well.

Add a new file add_user.dart.

viewModel.isLoading ? const Center(child: CircularProgressIndicator()) : 
    ElevatedButton(
        child: const Text("Add User"),
        onPressed: () {

           Map<String, String> body = {
              "name": _username.text,
              "job": _job.text,
            };

           viewModel.addUser(context, body);
     },
 ),

Moreover, in the view_model class, add the following snippet:

  addUser(BuildContext context, Map<String, String> body) async {
    isLoading = true;
    notifyListeners();

    await userRepository.addUser(body);

    Navigator.of(context).pop();
    ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("User Added")));
    getUsers();

    isLoading = false;
    notifyListeners();
  }

Furthermore, in the repository itself, add the following snippet:

  dynamic addUser(Map<String, String> body) async {
    final res = await service.postApiResponse('users', body);
    return res;
  }

At this point, our implementation stuff is now completed, so as for our Provider State Management too.

Note: This project has been made to work for all major platforms that Flutter currently supports.

Thanks for being with me till here.

Hope you enjoyed reading it...


Summing Up

Provider State Management – An Awesome Guide for Flutter App was all about an easy and effective approach to understanding Provider, vis-à-vis, its core and vital concepts in the form of a mid-level application following an excellent architectural pattern. In addition to that, useful snippets, and previews in-between as well.

Apart from all of these, if you feel that something was missed or wrongly mentioned in this post or previous ones, be sure to jot it down in the comments below. I would love to hear from you all.

In the meantime,

Link to the Repository

Link to my YouTube handle

– Previous Blog Posts —

Also read – Exploring the hidden ventures of Url Launcher in Flutter

Thanks much for your precious time!

Leave a Comment