Pull to refresh

Riverpod in Production

Level of difficultyEasy
Reading time5 min
Views1.2K

As a Flutter developer, I've always been on the lookout for efficient ways to manage app state and dependencies. In my journey, I've come across Riverpod, a powerful library that I like to think of as the Swiss Army knife of Flutter development. It offers elegant solutions for both state management and dependency injection, giving you the freedom to "cook" your app architecture just the way you like it.

This flexibility, while powerful, can sometimes be overwhelming. The main challenge often lies in finding the optimal way to use Riverpod for your specific needs. In this post, I want to share with you a production-ready example from my own experience. We'll explore Riverpod's key features and then dive into a real-world example of how to implement a clean, modular dependency injection system.

But before we dive in, let's take a look at the dependencies we'll be using. Here's the pubspec.yaml file for our project:

name: riverpod_di_example
description: "Example of using Riverpod for managing application dependencies"

publish_to: "none"

version: 1.0.0+1

environment:
  sdk: ">=3.4.3 <4.0.0"

dependencies:
  dio: ^5.5.0+1
  flutter:
    sdk: flutter
  flutter_hooks: ^0.20.5
  flutter_riverpod: ^2.5.1
  hooks_riverpod: ^2.5.1
  very_good_analysis: ^6.0.0

dev_dependencies:
  flutter_test:
    sdk: flutter

flutter:
  uses-material-design: true

Now that we have our dependencies set up, let's dive into what makes Riverpod such a versatile tool.

What is Riverpod?

Riverpod is a robust state management library for Flutter that offers a declarative approach to programming, easy implementation of complex UI patterns, and enhanced tooling support. Let's look at its key features:

1. Declarative Programming

Riverpod allows developers to write business logic in a manner similar to Stateless widgets. This approach makes the code:

  • Easily reusable

  • Highly composable

  • More maintainable

A standout feature is its ability to automatically recompute network requests when necessary, streamlining data management in your application.

2. Simplified Implementation of Complex UI Patterns

With Riverpod, implementing common yet complex UI patterns becomes significantly easier. Features like:

  • Pull to refresh

  • Search as you type

  • Real-time updates

These can be implemented with just a few lines of code, greatly reducing development time and complexity.

3. Enhanced Tooling Support

Riverpod goes beyond just state management by providing robust tooling support:

  • It enhances the compiler to catch common mistakes at compile-time, turning them into compilation errors.

  • Custom lint rules are provided to ensure code quality and consistency.

  • Built-in refactoring options help maintain clean code as your project evolves.

  • A command-line interface for generating documentation is included, aiding in project maintenance and onboarding.

4. Comprehensive Feature Set

Riverpod offers a wide array of features that cater to various aspects of modern app development:

  • Declarative Programming

  • Compile Safety

  • Versatility (works in plain Dart environments)

  • Custom Lint Rules

  • Logging

  • Native Network Request Support

  • Type-safe Query Parameters

  • Easily Combinable States

  • Built-in Refactorings

  • WebSocket Support

  • Automatic Loading/Error Handling

  • Test Ready

  • Built-in Support for Pull-to-Refresh

  • Hot-Reload Support

  • Documentation Generator

Now that we've covered the key features of Riverpod, let's dive into a practical example of how to use it in a production environment for dependency injection.

Implementing Dependency Injection with Riverpod

One of the most powerful features of Riverpod is its ability to provide a clean and modular dependency injection system. Let's look at how we can structure our DI setup using Dart's part directive for better organization and maintainability.

Project Structure

For our example, we'll use a Pokemon viewer app with the following structure for our DI setup:

lib/
└── src/
    └── di/
        ├── di.dart
        ├── manager_providers.dart
        ├── repository_providers.dart
        └── view_model_providers.dart

Main DI File

First, let's look at the main di.dart file:

// lib/src/di/di.dart
library di;

import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_di_example/src/api/api_manager.dart';
import 'package:riverpod_di_example/src/features/pokemons_feed/models/pokemon.dart';
import 'package:riverpod_di_example/src/features/pokemons_feed/pokemons_feed_view_model.dart';
import 'package:riverpod_di_example/src/features/pokemons_feed/pokemons_repository.dart';

part 'manager_providers.dart';
part 'repository_providers.dart';
part 'view_model_providers.dart';

abstract final class Di {
  static final viewModel = _ViewModelProviders();
  static final manager = _ManagerProviders();
  static final repository = _RepositoryProviders();
}

This file serves as the entry point for our DI setup. It imports necessary dependencies and uses the part directive to include other files that contain specific provider definitions.

Manager Providers

Next, let's look at the manager_providers.dart file:

// lib/src/di/manager_providers.dart
part of 'di.dart';

final class _ManagerProviders {
  late final api = Provider((ref) => ApiManager());
}

This file contains providers for manager classes, such as the ApiManager.

Repository Providers

The repository_providers.dart file contains providers for repositories:

// lib/src/di/repository_providers.dart
part of 'di.dart';

final class _RepositoryProviders {
  late final pokemons = Provider(
    (ref) => PokemonsRepository(
      ref.watch(Di.manager.api),
    ),
  );
}

Here, we define a provider for the PokemonsRepository, which depends on the ApiManager.

ViewModel Providers

Finally, the view_model_providers.dart file contains providers for view models:

// lib/src/di/view_model_providers.dart
part of 'di.dart';

final class _ViewModelProviders {
  late final feed = StateNotifierProvider<PokemonsFeedViewModel, List<Pokemon>>(
    (ref) => PokemonsFeedViewModel(
      ref.watch(
        Di.repository.pokemons,
      ),
    ),
  );
}

This file defines a StateNotifierProvider for the PokemonsFeedViewModel, which depends on the PokemonsRepository.

Benefits of This Structure

  1. Modularity: Each type of provider (managers, repositories, view models) is isolated in its own file, making the code easier to navigate and maintain.

  2. Encapsulation: The part directive allows us to keep these provider definitions private to the di.dart file, preventing unauthorized access from other parts of the app.

  3. Scalability: As the app grows, new providers can be easily added to the appropriate file without cluttering the main di.dart file.

  4. Clear Dependencies: The structure makes it easy to see the dependency chain from view models down to managers.

  5. Easy to Use: In other parts of the app, you can simply import the di.dart file and access all providers through the Di class.

Using the DI Setup in Your App

To use this DI setup in your Flutter app, you would typically do the following:

  1. Wrap your app with a ProviderScope in your main.dart file:

void main() {
  runApp(
    ProviderScope(
      child: MyApp(),
    ),
  );
}
  1. In your widgets, use the Consumer widget or the ref parameter to access your providers:

class PokemonFeedPage extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final viewModel = ref.watch(Di.viewModel.pokemonsFeed.notifier);
    final state = ref.watch(Di.viewModel.pokemonsFeed);

    useEffect(
      () {
        viewModel.fetchPokemons();
        return null;
      },
      [],
    );

    // Use viewModel and state to build your UI
    // See the full code on Github
  }
}

Conclusion

In my experience, Riverpod has proven to be an invaluable tool in my Flutter development toolkit. Its flexibility allows for clean, maintainable, and scalable code, especially when it comes to dependency injection and state management.

The approach we've outlined in this post is just one way to leverage Riverpod's power. It's a method that has worked well for me in production environments, but remember - Riverpod is your Swiss Army knife. Feel free to adapt and modify this approach to best fit your project's needs.

If you want to dive deeper into Riverpod, I highly recommend checking out the official documentation at https://riverpod.dev/. It's a goldmine of information and best practices.

For a complete, working example of the code we've discussed in this post, you can visit the GitHub repository I've set up: https://github.com/devasidmi/riverpod_di_example. Feel free to clone, fork, and experiment with it!

Thank you for reading! I hope this post has given you some insights into how you can use Riverpod to streamline your Flutter development process. Happy coding!

Tags:
Hubs:
Total votes 3: ↑3 and ↓0+5
Comments1

Articles