So many providers in RiverPod ๐Ÿ˜ฑ๐Ÿ˜ฑ Which one should I use?

So many providers in RiverPod ๐Ÿ˜ฑ๐Ÿ˜ฑ Which one should I use?

ยท

6 min read

I hardly pay attention in my English class back in High School.

I thought I wouldn't need it.

After all, I would be majoring in Engineering. "Just focus on your Science-based subject", which I did, and thankfully, I got admitted to the most competitive school in my home country.

However, my time at the university taught me that I was wrong about my view of the English Language, so I struggled when writing Lab Reports and giving a Presentation on a Project. And it's been one of the sources of the biggest setback I've had in my career.

I've forgotten a lot of things from my time in High School, but one lesson stays stuck to date. And it is...

No two English words mean the exact same thing

It's something my English Teacher would say repeatedly when we do some fill-in-the-gap textbook exercises. In his words, "You can use some words interchangeably, but not in all situations".

I like to think of providers in Riverpod the same way.

No two providers have the exact same function

You can use them interchangeably, yes. But only in certain situations. So I will never understand people who talk about "8 types of providers doing the same work". All it screams to me is they don't understand the tool in the first place and don't want to spend time learning it.

What's worse though, is that most of these complaints originate from people who will never change to Riverpod even if you point a gun directly at their skull, partly for the dreaded fear of being a beginner again.

But you are different.

You know better. You know that the creator of Freezed, Flutter_hooks, and Provider definitely has more productive uses for their time than developing providers with overlapping functionality. And you want to learn and use it to design Functional mobile apps. So let's start the article proper!!!.

Each Provider explained

For easy understanding, I like to group all Providers into two classes:

  1. Function Variants.
  2. Class Variants.

In Function Variants, you define providers as functions.

These functions return the data you want to provide. You can use various types of providers, like StateProvider, FutureProvider, and StreamProvider. They are easy to set up and are great for simple use cases.

Function Variant:

  1. Provider
  2. StateProvider
  3. FutureProvider
  4. StreamProvider

So let's dive into each.

1. Provider

This is the simplest of them all.

It's what you would normally use to provide simple immutable objects( data that doesn't change) to multiple parts of your app.

Examples of this data are a Websocket connection, Database connection, or an Authenticated User Instance โ€” basically, anything you'd want to represent as a singleton.

import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';

final nameProvider = Provider<String>((ref) {
  return "Daniel Asaboro";
});

class RandomPage extends StatelessWidget {
  const RandomPage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Consumer(
          builder: (context, ref, child) {
            final name = ref.watch(nameProvider);

            return Text(name);  //Daniel Asaboro
          },
        ),
      ),
    );
  }
}

2. StateProvider

StateProvider is the next rung after the basic Provider.

It's a Provider that lets you store simple mutable objects so you can modify its state. However, it exists primarily for working with simple variables such as booleans, integers, and Strings.

import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';

final nameProvider = StateProvider<String>((ref) {
  return "";
});

class RandomPage extends ConsumerWidget {
  const RandomPage({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final name = ref.watch(nameProvider);
    return Scaffold(
      body: Center(
        child: Column(
          children: [
            Text(name),
            ElevatedButton(
              onPressed: () {
                // This will change the output in Text widget to "Daniel Asaboro"
                ref.read(nameProvider.notifier).state = "Daniel Asaboro";
              },
              child: Text("Change Name"),
            ),
          ],
        ),
      ),
    );
  }
}

Its use is heavily discouraged because it allows easy modification from anywhere and from any widget โ€” don't too many cooks spoil the broth?

This is why it's only useful for maintaining local state i.e. state that doesn't need to be shared across the entire application e.g. Passing the Index of a selected item in a List to another function or Provider which we will talk about later in the series.

3. FutureProvider

FutureProvider is your guy when you need to supply values that are resolved asynchronously, such as API network calls or database queries. FutureProvider makes working with futures and async operations a work in the park.

import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:http/http.dart as http;
import 'dart:convert';

final nameProvider = FutureProvider<Map<String, dynamic>>((ref) async {
  final response =
      await http.get(Uri.parse('https://jsonplaceholder.typicode.com/users/5'));
  if (response.statusCode == 200) {
    return json.decode(response.body);
  } else {
    throw Exception('Failed to load data');
  }
});

class RandomPage extends ConsumerWidget {
  const RandomPage({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return ref.watch(nameProvider).when(
          data: (data) {
            return Scaffold(
              body: Center(
                child: Text(data["name"]), //"Chelsey Dietrich"
              ),
            );
          },
          error: ((error, stackTrace) {
            return Text("${error.toString()}, $stackTrace");
          }),
          loading: () => CircularProgressIndicator(),
        );
  }
}

4. StreamProvider

StreamProvider is used for providing values that come from a continuous stream of data like real-time stock market updates, football match live scores, or WebSocket events.

A common use-case for StreamProvider is providing stream data from a real-time database like Firestore. Some use it in chat applications, and some in Video-calls.

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:socket_io_client/socket_io_client.dart' as IO;

class SocketService {
  final String serverUrl = "http://your.socket.io.server.url";
  IO.Socket socket;

  // Stream controller for broadcasting live weather data
  StreamController<String> _weatherDataController = StreamController<String>();

  SocketService() {
    // Initialize the Socket.IO connection
    socket = IO.io(serverUrl, <String, dynamic>{
      'transports': ['websocket'],
      'autoConnect': true,
    });

    // Define an event to receive live weather data from the server
    socket.on('weatherData', (data) {
      // Add the data to the stream
      _weatherDataController.add(data);
    });
  }

  Stream<String> getLiveWeatherData() {
    return _weatherDataController.stream;
  }

  void dispose() {
    _weatherDataController.close();
    socket.disconnect();
  }
}

final socketProvider = Provider<SocketService>((ref) {
  return SocketService();
});

final socketStreamProvider = StreamProvider((ref) async* {
  final streamData = ref.watch(socketProvider).getLiveWeatherData();
  await for (final eachLiveData in streamData) {
    yield eachLiveData;
  }
});

class RandomPage extends ConsumerWidget {
  const RandomPage({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return ref.watch(socketStreamProvider).when(
          data: (data) {
            return Scaffold(
              body: Center(
                child: Text(data), //some string;
              ),
            );
          },
          error: ((error, stackTrace) {
            return Text("${error.toString()}, $stackTrace");
          }),
          loading: () => const CircularProgressIndicator(),
        );
  }
}

As you can see, it's quite similar to FutureProvider as they both deal with asynchronous data. And Riverpod gives us an easy way to handle all its possible states from Loading, to when it gets Data or Errors out;

We've come a long way

We've spoke about the four fundamental providers in Flutter: Provider, StateProvider, FutureProvider, and StreamProvider. While they've all proven to be invaluable tools in managing data, state, and asynchronous operations in your app easily, they do have certain limitations.

Most are read-only and only focus on data distribution.

What if you need more than that?

Say you need to retrieve a User biodata from the server, edit and update it, and then send it back to the server, that's a lot of operations โ€” none of the Providers we've talked about can do that.

This is the gap Class variant Providers fill.

As its name implies, it lets you group related methods for working on a particular data together. It will be the focus of the next episode in this series. We will take a deep dive, and talk about why they are useful, when to use them, and how.

Till then...bye.

ย