Majid Hajian
Majid Hajian is a passionate software developer with years of developing and architecting complex web and mobile applications. He is passionate about web platform especially flutter, IoT, PWAs, and performance.
Majid Hajian
mhadaily
mhadaily
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
}));
Find me on the internet by
Head of DevRel at Invertase
* Said by the author (Remi)
mhadaily
mhadaily
An object that encapsulates a piece of state and allows listening to that state.
mhadaily
mhadaily
mhadaily
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
You should not use StateProvider
if:
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
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.
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
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
package:provider
when using its ChangeNotifierProvider
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
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
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
final myProvider = Provider((ref) {
// Bad practice to call `read` here
final value = ref.read(anotherProvider);
});
mhadaily
// 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
// 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
// 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
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
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 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
mhadaily
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
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
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
mhadaily
mhadaily
Majid Hajian
find me on internet
mhadaily
Slides and link to source code
slides.com/mhadaily
mhadaily
By Majid Hajian
Riverpod is more just a state management package it is a framework by itself that does caching, data binding, DI, and more.
Majid Hajian is a passionate software developer with years of developing and architecting complex web and mobile applications. He is passionate about web platform especially flutter, IoT, PWAs, and performance.