[Flutter Project] 001-Flutter State Management: Riverpod

【Flutter Project】001-Flutter State Management: Riverpod

Article directory

  • [Flutter Project] 001-Flutter State Management: Riverpod
  • I. Overview
    • 1. Official status management
    • 2. State management solution
    • 3. Why choose Riverpod
      • Riverpod Official Documentation
      • Several Providers provided by Riverpod
  • 2. Official example
    • 1. Installation
    • 2. Official example
    • 3. Code generation
    • 4. Official sample running results
  • 3. Basic use
    • 1. Transform `main.dart`
    • 2. Create `home_page.dart`
    • 3. Create `hello_state.dart`
    • 4. Running results
  • Fourth, use code generation
    • 1. Transform `hello_state.dart`
    • 2. Code generation
    • 3. Transform `home_page.dart`
    • 4. Running results
    • 5. Why use code generation in Riverpod

1. Overview

1. Official status management

State management deals with the key concepts of data flow and UI updates in an application. In a Flutter application, state management ensures that application UI and data stay in sync, shares and synchronizes data, and provides good code structure and maintainability.

Flutter provides StatefulWidget as the most basic state management method. Stateful components can store and update their own state, suitable for simple scenarios and local states.

However, StatefulWidget has the following problems:

  1. State Management Complexity: When the component tree is huge and the state needs to be shared among multiple components, state management becomes complicated and the code is difficult to understand and maintain.
  2. Performance issue: Compared with StatelessWidget, StatefulWidget will cause more components to rebuild when the state changes, which may affect application performance, although Flutter has Optimized for performance.
  3. Lifecycle Management Complexity: StatefulWidget has a complex lifecycle and needs to handle multiple lifecycle methods (like initState, didUpdateWidget and dispose), resulting in complex and unmanageable code.
  4. Difficult to test: Since StatefulWidget has internal state, writing unit and integration tests becomes more difficult, potentially affecting the quality and reliability of your application.
  5. Poor reusability: The state of a StatefulWidget is often tightly coupled to a specific instance, reducing the reusability of the component.

2. Status management solution

In Flutter, there are other state management methods to choose from, the following are some common state management methods.

  1. InheritedWidget and InheritedModel: These are special types of components provided by Flutter that allow state to be passed down the component tree. They help you share state between different layers of your application. This approach is suitable for smaller applications or limited state sharing needs.
  2. Provider: A third-party library for dependency injection and state management. It is encapsulated on the basis of InheritedWidget. It has the capabilities of the above components, but it is simpler and easier to use. Providers can listen for state changes and rebuild associated components when needed. This approach is suitable for applications of all sizes and is scalable and flexible.
  3. Riverpod: A relatively new state management library, similar to Provider, but offering more features and improvements. Riverpod allows you to create immutable, composable, and testable state management solutions. This approach is suitable for applications that require a higher degree of controllability and testability.
  4. BLoC (Business Logic Component): A state management method based on reactive programming. BLoC separates business logic from UI, allowing you to easily test and reuse code. BLoC is often used with RxDart, a reactive programming library for Dart, to provide powerful data flow processing capabilities. This approach is suitable for applications that need to handle complex business logic and large data streams.
  5. Redux: A centralized state management library that stores the state of an application in a single state tree. Redux uses pure functions (called reducers) to handle state updates, allowing you to easily track and manage your application’s state changes. This approach is suitable for applications that require strict state management and predictability.

The specific state management method you choose depends on the needs, complexity, and personal preferences of your application. Different methods have different advantages and disadvantages, so when choosing a state management method, it is important to fully understand the characteristics of each method and weigh its applicability.

3. Why choose Riverpod

The reason is that some of the main features of Riverpod are more powerful, which fits our needs, and listen to me slowly…

  1. Immutability. The state in Riverpod is immutable, which means that when the state is updated, a new object is created instead of modifying the existing object. This helps reduce errors and makes the state easier to understand and track.
  2. Type safety. Riverpod provides stronger type safety at compile time, helping to reduce type errors and improve code quality.
  3. No BuildContext required. Unlike Providers, Riverpod does not rely on the BuildContext to access state. This makes it easier to access state in places outside of the component, such as functions or classes, while improving testability.
  4. Composable. Riverpod allows you to combine different Providers to create more complex state management solutions. This helps keep the code modular and maintainable.
  5. Easy to test. Since Riverpod’s state doesn’t depend on the BuildContext, you can write unit tests more easily. Additionally, Riverpod provides utilities for simulating state and testing.
  6. Family Features. Riverpod has a so-called “family” feature that allows you to create multiple Provider instances of the same type based on parameters. This allows for better state management when using multiple components with the same logic but different parameters.
  7. Very flexible. Riverpod is highly flexible and adapts well to different application structures and needs. You can use Riverpod to build simple local state management, or complex global state management solutions.

In conclusion, Riverpod is a powerful state management library for Flutter applications of all sizes. It offers immutability, type safety, BuildContext-free access, composability, ease of testing, and family capabilities. If you’re looking for a modern, flexible, and easy-to-use state management solution, Riverpod is an option worth considering.

Riverpod Official Documentation

https://docs-v2.riverpod.dev/zh-hans/

Several Providers provided by Riverpod

image-20230522142246669

2. Official example

1. Installation

flutter pub add flutter_riverpod dev:custom_lint dev:riverpod_lint riverpod_annotation dev:build_runner dev:riverpod_generator

2. Official example

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

part 'main.g.dart';

// We create a "provider" that will hold a value (here "Hello world").
// By using a provider, we can mock or override exposed values.
@riverpod
String helloWorld(HelloWorldRef ref) {<!-- -->
  return 'Hello world';
}

void main() {<!-- -->
  runApp(
    // In order for the component to read the provider, we need to pass the entire
    // Applications are wrapped in a "ProviderScope" component.
    // This is where all our provider state is stored.
    ProviderScope(
      child: MyApp(),
    ),
  );
}

// Extend HookConsumerWidget from Riverpod instead of HookWidget
class MyApp extends ConsumerWidget {<!-- -->
  @override
  Widget build(BuildContext context, WidgetRef ref) {<!-- -->
    final String value = ref. watch(helloWorldProvider);

    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: const Text('Example')),
        body: Center(
          // read the value of provider
          // Here, for easy viewing, a large font is set
          child: Text(value, style: const TextStyle(fontSize: 40),),
        ),
      ),
    );
  }
}

3. Code generation

# --delete-conflicting-outputs is optional, when generating code conflicts, delete the original code and regenerate
flutter pub run build_runner build --delete-conflicting-outputs

4. Official sample running results

image-20230522133110041

3. Basic usage

1. Transform main.dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:study/pages/HomePage.dart';

void main() {<!-- -->
  runApp(
    // In order for the component to read the provider, we need to pass the entire
    // Applications are wrapped in a "ProviderScope" component.
    // This is where all our provider state is stored.
    const ProviderScope(
      child: MyApp(),
    ),
  );
}

class MyApp extends StatelessWidget {<!-- -->
  const MyApp({<!-- -->super.key});

  @override
  Widget build(BuildContext context) {<!-- -->

    return const MaterialApp(
      home: HomePage(),
    );
  }
}

2. Create home_page.dart

/lib/pages/home_page.dart

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

import '../states/hello_state.dart';

class HomePage extends HookConsumerWidget {<!-- -->
  const HomePage({<!-- -->super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {<!-- -->
    final hello = ref. watch(helloStateProvider);
    return Scaffold(
      appBar: AppBar(
        title: const Text('Home'),
      ),
      body: Center(
          child: SizedBox(
        height: 400,
        child: Column(
          children: [
            // text
            Text(hello. hello, style: const TextStyle(fontSize: 40),),
            // update text
            ElevatedButton(
              style: ButtonStyle(minimumSize: MaterialStateProperty. all(const Size(200, 50))),
              onPressed: () {<!-- -->
                ref.read(helloStateProvider.notifier).setHello("The text has been updated!");
              },
              child: const Text('Update'),
            ),
          ],
        ),
      )),
    );
  }
}

3. Create hello_state.dart

lib/state/hello_state.dart

import 'package:flutter_riverpod/flutter_riverpod.dart';

class HelloState {<!-- -->
  final String hello;

  HelloState({<!-- -->
    this. hello = 'Hello World',
  });
}

class HelloStateProvider extends StateNotifier<HelloState> {<!-- -->
  HelloStateProvider() : super(HelloState());

  void setHello(String hello) {<!-- -->
    state = HelloState(
      hello: hello,
    );
  }
}

final helloStateProvider = StateNotifierProvider<HelloStateProvider, HelloState>(
  (ref) => HelloStateProvider(),
);

4. Running results

image-20230522140032127

4. Using code generation

Code generation refers to using tools to generate code for us.

In Dart, it has the disadvantage of requiring an extra step to “compile” the application. Although this problem may be fixed in the near future, the Dart team is researching and solving potential solutions to this problem.

Code generation is completely optional when using Riverpod. You can of course not use it at all.

In the meantime, Riverpod supports code generation and recommends that you use it.

1. Transform hello_state.dart

import 'package:riverpod_annotation/riverpod_annotation.dart';

part 'hello_state.g.dart';

@riverpod
class HelloList extends _$HelloList {<!-- -->
  @override
  List<String> build() {<!-- -->
    return ["hello world!"];
  }

  void addHello(String hello) {<!-- -->
    state = [...state, hello];
  }
}

2. Code generation

# --delete-conflicting-outputs is optional, when generating code conflicts, delete the original code and regenerate
flutter pub run build_runner build --delete-conflicting-outputs

3. Transform home_page.dart

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

import '../states/hello_state.dart';


class HomePage extends HookConsumerWidget {<!-- -->
  const HomePage({<!-- -->super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {<!-- -->
    final List<String> hellos = ref. watch(helloListProvider);
    return Scaffold(
      appBar: AppBar(
        title: const Text('Home'),
      ),
      body: Center(
          child: SizedBox(
        height: 400,
        child: Column(
          children: [
            // text: loop through the hellos list
            ...hellos. map((e) => Text(e, style: const TextStyle(fontSize: 40),)),
            // update text
            ElevatedButton(
              style: ButtonStyle(minimumSize: MaterialStateProperty. all(const Size(200, 50))),
              onPressed: () {<!-- -->
                // get the current time
                DateTime now = DateTime. now();
                ref.read(helloListProvider.notifier).addHello("hello ${now.second}");
              },
              child: const Text('Update'),
            ),
          ],
        ),
      )),
    );
  }
}

4. Running results

image-20230522141813454

5. Why use code generation in Riverpod

You might be thinking: “If code generation is optional in Riverpod, why use it?”

Make your code life easier.

This includes but is not limited to:

  • Better syntax, more readable and flexible, plus a reduced learning curve.
    • No need to worry about FutureProvider, Provider or other providers. Just write your logic, and Riverpod will choose the most suitable provider for you.
    • Passing parameters to providers is now unrestricted. No longer limited to using family and passing a single parameter, any form of parameter can now be passed. This includes named parameters, optional parameters and even default values.
  • Code written in Riverpod supports stateful hot reload.
  • Better debugging, by generating additional metadata and then debugging with a debugger.
  • Some features of Riverpod will only support code generation.

At the same time, code generation like Freezed or json_serializable is already used in many applications. In this case, your project is probably already configured for code generation, and using Riverpod should be straightforward.