A Reactive Caching and Data-binding Framework

Majid Hajian

mhadaily

mhadaily

Agenda

  • Riverpod vs Provider
  • Providers
  • Best practices
  • Testing

ME.dart

import 'package:flutter/material.dart';
MaterialApp(
   ThemeData(
        name: "Majid Hajian",
        location: "Oslo, Norway",
        description: '''
                Google Developer Expert
        	Passionate Software engineer, 
	        Community Leader, Author and international Speaker
         ''',
        main: "Head of DevRel at Invertase.io",
        homepage: "https://www.majidhajian.com",
        socials: {
          twitter: "https://www.twitter.com/mhadaily",
          github: "https://www.github.com/mhadaily"
        },
        author: {
          Pluralsight: "www.pluralsight.com/authors/majid-hajian",
          Apress: "Progressive Web App with Angular, Book",
          PacktPub: "PWA development",
          Udemy: "PWA development",
        }
        founder: "Softiware As (www.Softiware.com)"
        devDependencies: {
          tea: "Ginger", 
          mac: "10.14+",
        },
        community: {
          MobileEraConference: "Orginizer",
          FlutterVikings: "Orginizer", 
          FlutterDartOslo: "Orginizer",
          GDGOslo: "Co-Orginizer",
          DevFestNorway: "Orginizer",
          ...more
        }));

mhadaily

Find me on the internet by

Head of DevRel at Invertase

What it means for Provider

  • Prefer using Riverpod over Provider*
  • Provider won't receive new features
  • Will still receive bug fixes and be updated to support latest language features

* Said by the author (Remi)

mhadaily

What it means for

  • The API is stable and production-ready
  • More features are on the way
  • The focus will shift to developer tooling and documentation
  • Riverpod is officially Provider "2.0"

mhadaily

Providers

An object that encapsulates a piece of state and allows listening to that state.

mhadaily

1- Allows easily accessing that state in multiple locations

Why use providers?

2- Simplifies combining this state with others

3- Enables performance optimizations

4- Increases the testability of your application

5- Allows easy integration with advanced features

mhadaily

1- Provider

2- StateProvider

3- StateNotifierProvider

4- ChangeNotifierProvider

5- StreamProvider

Providers variants

5- FutureProvider

mhadaily

Providers variants

Provider Type Provider Create Function Example Use Case
Provider Returns any type A service class / computed property (filtered list)
StateProvider Returns any type A filter condition / simple state object
FutureProvider Returns a Future of any type A result from an API call
StreamProvider Returns a Stream of any type A stream of results from an API
StateNotifierProvider Returns a subclass of StateNotifier A complex state object that is immutable except through an interface
ChangeNotifierProvider Returns a subclass of ChangeNotifier A complex state object that requires mutability

mhadaily

# pubspec.yaml 


dependencies:
  flutter:
    sdk: flutter
  flutter_riverpod: ^2.0.2

mhadaily

// main.dart

void main() {
  runApp(
    /// A widget that stores 
    /// the state of providers.
    const ProviderScope(
      child: MyApp(),
    ),
  );
}

mhadaily

final myCompany = Provider<String>((ref) => "Invertase");

class MyHomePage extends StatelessWidget {
  const MyHomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return const Scaffold(
      body: Center(
        child: Text('Norway'),
      ),
    );
  }
}

mhadaily

final myCompany = Provider<String>((ref) => "Invertase");
class MyHomePage extends StatelessWidget {
  const MyHomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Consumer(
          builder: (context, WidgetRef ref, child) {
            final country = ref.watch(myCountry);
            return Text(country);
          },
        ),
      ),
    );
  }
}

mhadaily

final myCompany = Provider<String>((ref) => "Invertase");
class MyHomePage extends StatelessWidget {
  const MyHomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Consumer(
          builder: (context, WidgetRef ref, child) {
            final country = ref.watch(myCountry);
            return Text(country);
          },
        ),
      ),
    );
  }
}

mhadaily

final myCountry = Provider<String>((ref) => "Invertase");


class MyHomePage extends ConsumerWidget {
  const MyHomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final country = ref.watch(myCountry);
    return Scaffold(
      body: Text(country),
    );
  }
}

mhadaily

StateProvider

  • an enum, such as a filter type
  • a String, typically the raw content of a text field
  • a boolean, for checkboxes
  • a number, for pagination or age form fields

You should not use StateProvider if:

  • your state needs validation logic
  • your state is a complex object (such as a custom class, a list/map, ...)
  • the logic for modifying your state is more advanced than a simple count++.

 

mhadaily

// An enum representing the filter type
enum ProductSortType {
  name,
  price,
}

class Product {
  Product({required this.name, required this.price});

  final String name;
  final double price;
}

final _products = [
  Product(name: 'iPhone', price: 999),
  Product(name: 'cookie', price: 2),
  Product(name: 'ps5', price: 500),
];

mhadaily

final productSortTypeProvider = StateProvider<ProductSortType>(
  (ref) => ProductSortType.name,
);
//... 
 	appBar: AppBar(
        actions: [
          DropdownButton<ProductSortType>(
            value: ref.watch(productSortTypeProvider),
            onChanged: (value) {
              ref.read(productSortTypeProvider.notifier).state = value!;
            },
            items: const [
              DropdownMenuItem(
                value: ProductSortType.name,
                child: Icon(Icons.sort_by_alpha),
              ),
              DropdownMenuItem(
                value: ProductSortType.price,
                child: Icon(Icons.sort),
              ),
            ],
          ),
        ],
      )
//... 

mhadaily

final productSortTypeProvider = StateProvider<ProductSortType>(
  (ref) => ProductSortType.name,
);
//... 
 	appBar: AppBar(
        actions: [
          DropdownButton<ProductSortType>(
            value: ref.watch(productSortTypeProvider),
            onChanged: (value) {
              ref.read(productSortTypeProvider.notifier).state = value!;
            },
            items: const [
              DropdownMenuItem(
                value: ProductSortType.name,
                child: Icon(Icons.sort_by_alpha),
              ),
              DropdownMenuItem(
                value: ProductSortType.price,
                child: Icon(Icons.sort),
              ),
            ],
          ),
        ],
      )
//... 

mhadaily

final productSortTypeProvider = StateProvider<ProductSortType>(
  (ref) => ProductSortType.name,
);
//... 
 	appBar: AppBar(
        actions: [
          DropdownButton<ProductSortType>(
            value: ref.watch(productSortTypeProvider),
            onChanged: (value) {
              ref.read(productSortTypeProvider.notifier).state = value!;
            },
            items: const [
              DropdownMenuItem(
                value: ProductSortType.name,
                child: Icon(Icons.sort_by_alpha),
              ),
              DropdownMenuItem(
                value: ProductSortType.price,
                child: Icon(Icons.sort),
              ),
            ],
          ),
        ],
      )
//... 

mhadaily

The watch method should not be called asynchronously, like inside an onPressed of an ElevatedButton. Nor should it be used inside initState and other State life-cycles.

 

In those cases, consider using ref.read instead.

mhadaily

final productsProvider = Provider<List<Product>>((ref) {
  return _products;
});





final productsProvider = Provider<List<Product>>((ref) {
  final sortType = ref.watch(productSortTypeProvider);
  switch (sortType) {
    case ProductSortType.name:
      return _products.sorted((a, b) => a.name.compareTo(b.name));
    case ProductSortType.price:
      return _products.sorted((a, b) => a.price.compareTo(b.price));
  }
});

mhadaily

StateNotifierProvider

  •  is a provider that is used to listen to and expose a StateNotifier (from the package state_notifier, which Riverpod re-exports).
  • exposing an immutable state which can change over time after reacting to custom events.
  • centralizing the logic for modifying some state (aka "business logic") in a single place, improving maintainability over time.

mhadaily

@immutable
class Task {
  const Task({
    required this.id,
    required this.completed,
  });
  final String id;
  final bool completed;

  Task copyWith({String? id, bool? completed}) {
    return Task(
      id: id ?? this.id,
      completed: completed ?? this.completed,
    );
  }
}

mhadaily

class TasksNotifier extends StateNotifier<List<Task>> {
  TasksNotifier()
      : super(
          const [
            Task(completed: false, id: '1'),
            Task(completed: true, id: '2')
          ],
        );

  void toggle(String taskId) {
    // state in StateNotifier is immutable
    state = [
      for (final task in state)
        if (task.id == taskId)
          task.copyWith(completed: !todo.completed)
        else
          task,
    ];
  }
}

mhadaily

final tasksNotifierProvider = StateNotifierProvider<TasksNotifier, List<Task>>((ref) {
  return TasksNotifier();
});
class MyHomePage extends ConsumerWidget {
  const MyHomePage({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final List<Task> tasks = ref.watch(tasksNotifierProvider);
    return Scaffold(
      body: ListView(
        children: [
          for (final task in tasks)
            CheckboxListTile(
              value: task.completed,
              onChanged: (value) {
                ref.read(tasksNotifierProvider.notifier).toggle(task.id);
              },
              title: Text(task.id),
            ),
        ],
      ),
    );
  }
}

mhadaily

final tasksNotifierProvider = StateNotifierProvider<TasksNotifier, List<Task>>((ref) {
  return TasksNotifier();
});
class MyHomePage extends ConsumerWidget {
  const MyHomePage({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final List<Task> tasks = ref.watch(tasksNotifierProvider);
    return Scaffold(
      body: ListView(
        children: [
          for (final task in tasks)
            CheckboxListTile(
              value: task.completed,
              onChanged: (value) {
                ref.read(tasksNotifierProvider.notifier).toggle(task.id);
              },
              title: Text(task.id),
            ),
        ],
      ),
    );
  }
}

mhadaily

ChangeNotifierProvider

  • is used to listen to and expose a ChangeNotifier from Flutter itself.
  • an easy transition from package:provider when using its ChangeNotifierProvider
  • supporting mutable state, even though immutable state is preferred

mhadaily

class Task {
  Task({
    required this.id,
    required this.completed,
  });
  String id;
  bool completed;
}
class TasksNotifier extends ChangeNotifier {
  final tasks = [
    Task(completed: false, id: '1'),
    Task(completed: true, id: '2')
  ];

  void toggle(String taskId) {
    for (final task in tasks) {
      if (task.id == taskId) {
        task.completed = !task.completed;
        notifyListeners();
      }
    }
  }
}

mhadaily

final tasksNotifierProvider = ChangeNotifierProvider<TasksNotifier>((ref) {
  return TasksNotifier();
});
class TaskListView extends ConsumerWidget {
  const TaskListView({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final List<Task> tasks = ref.watch(tasksNotifierProvider).tasks;
    return Scaffold(
      body: ListView(
        children: [
          for (final task in tasks)
            CheckboxListTile(
              value: task.completed,
              onChanged: (value) {
                ref.read(tasksNotifierProvider.notifier).toggle(task.id);
              },
              title: Text(task.id),
            ),
        ],
      ),
    );
  }
}

mhadaily

FutureProvider

  • for asynchronous code
  • performing and caching asynchronous operations (such as network requests)
  • nicely handling error/loading states of asynchronous operations
  • combining multiple asynchronous values into another value
  • listening to Firebase or web-sockets
  • rebuilding another provider every few seconds
  • removes the need to differentiate broadcast streams from normal streams.
  • it caches the latest value emitted by the stream

StreamProvider

mhadaily

@freezed
class Configuration with _$Configuration {
  @JsonSerializable(fieldRename: FieldRename.snake)
  factory Configuration({
    required String publicKey,
    required String privateKey,
    required String host,
  }) = _Configuration;

  factory Configuration.fromJson(Map<String, Object?> json) =>
      _$ConfigurationFromJson(json);
}

mhadaily

final configProvider = FutureProvider<Configuration>((ref) async {
  final content = json.decode(
    await rootBundle.loadString('assets/configurations.json'),
  ) as Map<String, Object?>;

  return Configuration.fromJson(content);
});

//...
Widget build(BuildContext context, WidgetRef ref) {
  AsyncValue<Configuration> config = ref.watch(configProvider);

  return config.when(
    loading: () => const CircularProgressIndicator(),
    error: (err, stack) => Text('Error: $err'),
    data: (config) {
      return Text(config.host);
    },
  );
}

mhadaily

final configProvider = FutureProvider<Configuration>((ref) async {
  final content = json.decode(
    await rootBundle.loadString('assets/configurations.json'),
  ) as Map<String, Object?>;

  return Configuration.fromJson(content);
});

//...
Widget build(BuildContext context, WidgetRef ref) {
  AsyncValue<Configuration> config = ref.watch(configProvider);

  return config.when(
    loading: () => const CircularProgressIndicator(),
    error: (err, stack) => Text('Error: $err'),
    data: (config) {
      return Text(config.host);
    },
  );
}

mhadaily

Combining

mhadaily

final productSortTypeProvider = StateProvider<ProductSortType>(
  (ref) => ProductSortType.name,
);

final dioProvider = Provider((ref) => Dio());

final productsProvider = FutureProvider<List<Product>>((ref) async {
  final sortType = ref.watch(productSortTypeProvider);
  final http = ref.watch(dioProvider);
  final response = await http.get('$URL?sort=$sortType');
  return response.data.map((product) => Product.fromJson(product)).toList();
});

mhadaily

final productSortTypeProvider = StateProvider<ProductSortType>(
  (ref) => ProductSortType.name,
);

final dioProvider = Provider((ref) => Dio());

final repositoryProvider = Provider((ref) => Repository(ref.read));

class Repository {
  Repository(this.read);
  final Reader read;
  Future<List<Product>> fetchProducts() async {
    final String sortType = read(productSortTypeProvider);
    final String dio = read(dioProvider);
    final response = await dio.get('/path', queryParameters: {
      'sortType': sortType,
    });
  return response.data.map((product) => Product.fromJson(product)).toList();
  }
}

mhadaily

DON'T CALL READ INSIDE THE BODY OF A PROVIDER

final myProvider = Provider((ref) {
  // Bad practice to call `read` here
  final value = ref.read(anotherProvider);
});

mhadaily

Do not listen to all changes if it's not needed!

// AVOID


final configProvider = StreamProvider<Configuration>(...);

final productsProvider = FutureProvider<List<Product>>((ref) async {
  final configs = await ref.watch(configProvider.future);

  return dio.get('${configs.host}/products');
});

mhadaily

"select" is your friend!

// PREFER


final configProvider = StreamProvider<Configuration>(...);

final productsProvider = FutureProvider<List<Product>>((ref) async {
  // Listens only to the host. If something else in the configurations
  // changes, this will not pointlessly re-evaluate our provider.

   final host = await ref.watch(
		configProvider.select((config) => config.host)
               );

  return dio.get('$host/products');
});

mhadaily

selectAsync (in version 2)

// PREFER


final configProvider = StreamProvider<Configuration>(...);

final productsProvider = FutureProvider<List<Product>>((ref) async {
  // Listens only to the host. If something else in the configurations
  // changes, this will not pointlessly re-evaluate our provider.

   final host = await ref.watch(
		configProvider.selectAsync((config) => config.host)
               );

  return dio.get('$host/products');
});

mhadaily

abstract class User {
  String get name;
  int get age;
}

//...

class MyWidget extends ConsumerWidget{

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // User user = ref.watch(userProvider);
    // return Text(user.name);
    
    String name = ref.watch(userProvider.select((user) => user.name));
    return Text(name);
  }
}

mhadaily

Modifiers

mhadaily

final productSortTypeProvider = StateProvider<ProductSortType>(
  (ref) => ProductSortType.name,
);

final dioProvider = Provider((ref) => Dio());

// to destroy the state of a provider when it is no-longer used
final productsProvider = FutureProvider.autoDispose<List<Product>>((ref) async {
  final sortType = ref.watch(productSortTypeProvider);
  final http = ref.watch(dioProvider);
  final response = await http.get('$URL?sort=$sortType');
  
  // in version 1 
  ref.maintainState = true  
  // in version 2 
  // ref.keepAlive();

  return response.data.map((product) => Product.fromJson(product)).toList();
});

mhadaily

final productSortTypeProvider = StateProvider<ProductSortType>(
  (ref) => ProductSortType.name,
);

final dioProvider = Provider((ref) => Dio());


final productProvider = FutureProvider.family<Product, String>(
	(ref, id) async {
      final http = ref.watch(dioProvider);
      final response = await http.get('$URL/$id');
      return Product.fromJson(response.data);
    }
);
//... 
Widget build(BuildContext context, WidgetRef ref) {
  final response = ref.watch(productProvider('id'));
}

mhadaily

final productSortTypeProvider = StateProvider<ProductSortType>(
  (ref) => ProductSortType.name,
);

final dioProvider = Provider((ref) => Dio());

// to destroy the state of a provider when it is no-longer used
final productProvider = FutureProvider
	.autoDispose
    .when<Product, String>((ref, id) async {
      final http = ref.watch(dioProvider);
      final response = await http.get('$URL/$id');
      // in version 1 
      ref.maintainState = true  
      // in version 2 
      // ref.keepAlive();
      return Product.fromJson(response.data);
});

mhadaily

PREFER using autoDispose when the parameter is not constant


final characters = FutureProvider
  .autoDispose
  .family<List<Character>, String>(
    (
      ref,
      filter,
    ) async {
      return fetchCharacters(filter: filter);
    },
  );

mhadaily

the parameter should have a consistent hashCode and ==

// the parameter should either be a primitive (bool/int/double/String), 
// a constant (providers), 
// or an immutable object that overrides == and hashCode.

This includes:

1- A tuple from tuple

2- Objects generated with Freezed or built_value

3- Objects using equatable

  •  

mhadaily

@freezed
abstract class MyParameter with _$MyParameter {
  factory MyParameter({
    required int userId,
    required Locale locale,
  }) = _MyParameter;
}

final exampleProvider = Provider.autoDispose.family<Something, MyParameter>(
(ref, myParameter) {
  print(myParameter.userId);
  print(myParameter.locale);
  // Do something with userId/locale
});

@override
Widget build(BuildContext context, WidgetRef ref) {
  int userId; // Read the user ID from somewhere
  final locale = Localizations.localeOf(context);

  final something = ref.watch(
    exampleProvider(MyParameter(userId: userId, locale: locale)),
  );

  ...
}

mhadaily

ProviderObserver

mhadaily

didAddProvider is called every time a provider was initialized, and the value exposed is value.

didDisposeProvider is called every time a provider was disposed.

didUpdateProvider is called every time by providers when they emit a notification.

mhadaily

class Logger extends ProviderObserver {
  @override
  void didUpdateProvider(
    ProviderBase provider,
    Object? previousValue,
    Object? newValue,
    ProviderContainer container,
  ) {
    print('''
{
  "provider": "${provider.name ?? provider.runtimeType}",
  "newValue": "$newValue"
}''');
  }
}

mhadaily

void main() {
  runApp(
    ProviderScope(
    	observers: [Logger()], 
        child: const MyApp(),
    ),
  );
}

/*
I/flutter (16783): {
I/flutter (16783):   "provider": "counter",
I/flutter (16783):   "newValue": "1"
I/flutter (16783): }
*/

mhadaily

ref.listen

mhadaily

class HomeView extends ConsumerWidget {
  const HomeView({Key? key}): super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
   ref.listen<String>(
      userProvider.select((user) => user.name),
      (String? previousName, String newName) {
          print('The user name changed $newName');

          // show a modal or snackbar!
      }
    );

    return Container();
  }
}

mhadaily

testing

mhadaily

1- No state should be preserved between test/testWidgets. That means no global state in the application, or all global states should reset after each test.

2- Being able to force our providers to have a specific state, either through mocking or by manipulating them until we reach the desired state.

3- While providers are declared as globals, the state of a provider is not global

4- ProviderContainer object is implicitly created by ProviderScope,

5- two testWidgets using providers do not share any state. As such, there is no need for any setUp/tearDown at all.

mhadaily

final counterProvider = StateProvider((ref) => 0);


class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Consumer(builder: (context, ref, _) {
        final counter = ref.watch(counterProvider);
        return ElevatedButton(
          onPressed: () => ref.read(counterProvider.notifier).state++,
          child: Text('$counter'),
        );
      }),
    );
  }
}

mhadaily

void main() {
  testWidgets('update the UI when incrementing the state', 
    (tester) async {
      await tester.pumpWidget(ProviderScope(child: MyApp()));

      expect(find.text('0'), findsOneWidget);
      expect(find.text('1'), findsNothing);

      await tester.tap(find.byType(ElevatedButton));
      await tester.pump();

      expect(find.text('1'), findsOneWidget);
      expect(find.text('0'), findsNothing);
    }
    );
    testWidgets('the counter state is not shared between tests', 
      (tester) async {
      await tester.pumpWidget(ProviderScope(child: MyApp()));

      expect(find.text('0'), findsOneWidget);
      expect(find.text('1'), findsNothing);
    },
  );
}

mhadaily

final todoListProvider = FutureProvider((ref) async => <Todo>[]);
// ...
ProviderScope(
  overrides: [
    todoListProvider.overrideWithValue(
      AsyncValue.data(
      [Todo(id: '42', label: 'Hello', completed: true)]
      ),
    ),
  ],
  child: const MyApp(),
);

mhadaily

class Repository {
  Future<List<Product>> fetchProducts() async => [];
}

final repositoryProvider = Provider((ref) => Repository());

class FakeRepository implements Repository {
  @override
  Future<List<Product>> fetchProducts() async {
    return [
      Product(id: '42', label: 'Hello world', price: 200),
    ];
  }
}

class ProductUI extends StatelessWidget {
  const ProductUI({Key? key, required this.product}) : super(key: key);
  final Product product;
  @override
  Widget build(BuildContext context) {
    return Text(product.id);
  }
}

mhadaily

void main() {
  testWidgets('override repositoryProvider', (tester) async {
    await tester.pumpWidget(
      ProviderScope(
        overrides: [
          repositoryProvider.overrideWithValue(FakeRepository())
        ],

	// ...
    
    await tester.pump();
    
    /... 
        
    expect(tester.widgetList(find.byType(ProductUI)), [
      isA<ProductUI>()
          .having((s) => s.product.id, 'product.id', '42')
          .having((s) => s.product.label, 'product.label', 'Hello world')
          .having((s) => s.product.price, 'product.price', 200),
    ]);    
    
    
//...

mhadaily

Summary

mhadaily

mhadaily

  • Riverpod vs Provider
  • Providers
  • Best practices
  • Testing

Majid Hajian

find me on internet 

mhadaily

Slides and link to source code

slides.com/mhadaily

mhadaily

Riverpod, A Reactive Caching and Data-binding Framework

By Majid Hajian

Riverpod, A Reactive Caching and Data-binding Framework

Riverpod is more just a state management package it is a framework by itself that does caching, data binding, DI, and more.

  • 980