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