I've been using Riverpod with StateNotifier to store my app's configurations / settings. It's easy and convenient as I can use methods to update the state's values. I can always use StateProvider but then I have updated the state directly, which is not convenient (for me) especially when I have a lot of setting parameters.
Using StateNotifier also works perfectly. Well, until recently.. then I found out that updating a setting value not only updates the UI that listens to that value, but also updates other parts of the UI that don't listen to it, but listen to other setting values in the same state. I hope you get what I mean from the last sentence.
To make it clear, let me show you a very simple demonstration.
Let's code! You can put all the code below into 1 single dart file.
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class Settings {
final bool settingA;
final bool settingB;
Settings({
this.settingA = false,
this.settingB = false,
});
Settings copy({
bool settingA,
bool settingB,
}) =>
Settings(
settingA: settingA ?? this.settingA,
settingB: settingB ?? this.settingB,
);
}
First, create the Settings class. The copy method will be used to update the State later.
final allSettingsProvider =
StateNotifierProvider<SettingsNotifier>((ref) => SettingsNotifier());
class SettingsNotifier extends StateNotifier<Settings> {
SettingsNotifier() : super(Settings());
void updateSettingA(bool settingA) {
final newState = state.copy(settingA: settingA);
state = newState;
}
void updateSettingB(bool settingB) {
final newState = state.copy(settingB: settingB);
state = newState;
}
}
Next, create the StateNotifier with 2 methods to update each Settings' values. You see here we use the copy method we created in the Settings class.
class TestPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Center(child: BuildBoxA()),
Center(child: BuildBoxB()),
],
),
),
);
}
}
class BuildBoxA extends ConsumerWidget {
@override
Widget build(BuildContext context, ScopedReader watch) {
print('Building Box A');
final bool settingA = watch(allSettingsProvider.state).settingA;
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Switch.adaptive(
value: settingA,
onChanged: (value) {
context.read(allSettingsProvider).updateSettingA(value);
},
),
],
);
}
}
class BuildBoxB extends ConsumerWidget {
@override
Widget build(BuildContext context, ScopedReader watch) {
print('Building Box B');
final bool settingB = watch(allSettingsProvider.state).settingB;
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Switch.adaptive(
value: settingB,
onChanged: (value) {
context.read(allSettingsProvider).updateSettingB(value);
},
),
],
);
}
}
Next is the UI. Like I said, it's a very simple demo. No need to think about refactoring here.
The result:
Not good, right? Turning on and off Switch A should only build Box A, but it also builds Box B. Why? It's because we replace the whole state with a new one. Every value in Settings gets updated.
Is there any way to update just a single value in the state? I couldn't find one. So I headed to Github and the nice people there pointed out that I don't need to worry about how I write to the state, but rather how I read it. There's an example in the official documentation here.
So, let's make a few changes to the code. Add 2 Providers and change BuildBoxA and BuildBoxB like this:
final settingAProvider =
Provider<bool>((ref) => ref.watch(allSettingsProvider.state).settingA);
final settingBProvider =
Provider<bool>((ref) => ref.watch(allSettingsProvider.state).settingB);
class BuildBoxA extends ConsumerWidget {
@override
Widget build(BuildContext context, ScopedReader watch) {
print('Building Box A');
final bool settingA = watch(settingAProvider);
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Switch.adaptive(
value: settingA,
onChanged: (value) {
context.read(allSettingsProvider).updateSettingA(value);
},
),
],
);
}
}
class BuildBoxB extends ConsumerWidget {
@override
Widget build(BuildContext context, ScopedReader watch) {
print('Building Box B');
final bool settingB = watch(settingBProvider);
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Switch.adaptive(
value: settingB,
onChanged: (value) {
context.read(allSettingsProvider).updateSettingB(value);
},
),
],
);
}
}
The result:
Exactly what I (we) wanted!