I am making first steps with Riverpod and just want to check if my understanding of handling changes of some data class properties using Riverpod is correct.
Imagine, I have a data class like that:
class MyClass {
final String name;
final int id;
const MyClass(this.name, this.id);
}
Then I create a StateNotifier:
class MyClassStateNotifier extends StateNotifier<MyClass> {
MyClassStateNotifier(MyClass state) : super(state);
void setName(String name) {
state.name = name;
}
}
And this won't work - UI will not be rebuilt after calling setName this way.
So I need to modify classes in the following way:
class MyClass {
final String name;
final int id;
const MyClass(this.name, this.id);
MyClass copyWith({name, id}) {
return MyClass(name ?? this.name, id ?? this.id);
}
}
and the StateNotifier as following:
class MyClassStateNotifier extends StateNotifier<MyClass> {
MyClassStateNotifier(MyClass state) : super(state);
void setName(String name) {
state = state.copyWith(name: name);
}
}
This pair will work and the UI will be rebuilt.
So, my question: does one always need to reinstantiate the object in this way?..
From my perspective, this is a bit strange (simple datatypes like String / int do not require this) and the boilerplate for copyWith method might become pretty huge if I have a dozen of object's properties.
Is there any better solution available for Riverpod or is it the only one and correct?..
Thanks in advance! :)
To trigger a state change you have to use the state setter. The implementation looks like this:
#protected
set state(T value) {
assert(_debugIsMounted(), '');
final previousState = _state;
_state = value;
/// only notify listeners when should
if (!updateShouldNotify(previousState, value)) {
return;
}
_controller?.add(value);
// ...
The internal StreamController<T> _controller needs to be triggered (add), to notify listeners (in this case riverpod) about updates.
By using state.name = something you're not informing the StateNotifier about a new state (not calling the state setter). Only your object holds the new value but nobody was notified.
Your state is mutable and that very often leads to such misbehavior. By using an immutable object you can prevent such errors in the first place. Write it yourself or use freezed.
Learn more about immutability in my talk
Related
Let's say I have few properties which describe a single context device (keepEmail is bool, email is String, deviceToken is String, themeMode is ThemeMode enumeration.
As they are belong to single domain object I think I need to create a DeviceState class (because I would like to save this object in web local storage as {"keepEmail": true, "email": "email#email.com", ...}
class DeviceState {
final bool keepEmail;
final String email;
final String deviceToken;
final ThemeMode themeMode;
// other constructors and methods
}
which in its turn is a part of ApplicationState:
class ApplicationState extends ChangeNotifier {
DeviceState get deviceState => localStorage['deviceState'];
set deviceState(DeviceState state) {
if(localStorage['deviceState'] != state) {
localStorage['deviceState'] = state;
notifyListeners();
}
}
// other states can be here
}
But here the chip is coming. Some of properties must be observables to refresh the navigator (I use package GoRouter which can listen Listenable to refresh its state) or user interface.
Namely change of deviceToken must launch router redirect (guard) system which checks if device token is set do something, and change of themeMode must refresh current theme.
So the question is if it is wise to combine all application states (which certainly can be persisted) like themes, languages, scroll positions, selected tabs etc to a single ApplicationState which is ChangeNotifier or create ChangeNotifier for all pieces of changed data and then aggregate them into single class:
class DeviceState extends ChangeNotifier {
String get deviceToken;
}
class LoginState extends ChangeNotifier {
String get accessToken;
}
class GoogleMapState extends ChangeNotifier {
MapStyle get mapStyle;
}
//... and many many other similar classes
class ApplicationState {
DeviceState deviceState;
AccessState accessState;
GoogleMapState mapState;
//...
}
and then use required Listenable in a specific place?
So I am a little lost how to implement corectly and I am ready to hear some ideas and advices.
e.g. I have class ProfileModel with bunch of fields
many of them don't have default values unless they're initialising when I get user info from backend
with riverpod I need to write something like
final profileProvider = StateNotifierProvider((ref) => ProfileState());
class ProfileState extends StateNotifier<ProfileModel> {
ProfileState() : super(null);
}
I understand I need to pass something like ProfileState.empty() into super() method instead passing null
but in this case I have to invent default values for every ProfileModels fields
this sounds weird for me, I don't want to break my head to care about empty or default state of EVERY model in project
in my example there are no default values for user name, age etc
this is pure immutable class
what I'm doing wrong or missing?
or I can declare model as nullable extends StateNotifier<ProfileModel?>
but I'm not sure is this a good way
It is fine to use the StateNotifier with a nullable model. If you semantically want to indicate the value can be actually absent, I would say that that having null is alright.
However, what I usually do and what I think is better, is create a state model that contains the model, but also properties that relate to the different states the app could be in.
For example, while fetching the data for the model from an API, you might want to have a loading state to show a spinner in the UI while waiting for the data to be fetched. I wrote an article about the architecture that I apply using Riverpod.
A simple example of the state model would be:
class ProfileState {
final ProfileModel? profileData;
final bool isLoading;
ProfileState({
this.profileData,
this.isLoading = false,
});
factory ProfileState.loading() => ProfileState(isLoading: true);
ProfileState copyWith({
ProfileModel? profileData,
bool? isLoading,
}) {
return ProfileState(
profileData: profileData ?? this.profileData,
isLoading: isLoading ?? this.isLoading,
);
}
#override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is ProfileState &&
other.profileData == profileData &&
other.isLoading == isLoading;
}
#override
int get hashCode => profileData.hashCode ^ isLoading.hashCode;
}
Lets say a User in my App can note his weight. Now I want to use this weight in many other Widgets all over my App for example to calculate some data, which depends on the user weight.
Is it a good practise to use a static variable like this:
class UserManager {
static double weight;
}
So now I have access to the User weight in every Class and can make calculations for example:
double value = UserManager.weight * 0.4;
Is this a good practise or are there some better solutions?
You can use GetStorage()
final appData = GetStorage();
appData.writeIfNull("data", false);
bool yourVar = appData.read("data");
or SharedPreferences
var yourData;
final Future<SharedPreferences> _prefs = SharedPreferences.getInstance();
void _getSomeTh() async {
final prefs = await _prefs;
final result = prefs.getBool("data");
yourData = result;
}
It would be better not to work with static variables, but to have an object that you distribute to all widgets in the widget tree. You can achieve this, for example, with an InheritedWidget or with packages such as BLoC or Provider. Making such data static seems to me to be an anti-pattern.
With the BLoC pattern:
class User {
final double weight;
User({required this.weight});
}
class UserManager extends Cubit<User>{
...
}
This way you have a state managing system with which you could very easily rebuild all the widgets concerned when the weight changes.
For a simple requirement as this, the static variable approach might be fine. However, for a more complex object that you wish to share in a hierarchy of widgets, you should use packages like provider
For example, you have a User object that you wish to share app-wide, then
class User with ChangeNotifier {
late String uuid;
late String name;
// Our one and only one static instance on which we operate
static late User? instance;
// There is a good chance that the init functions like below are async too
static void initUser(String uuid, String name) {
instance = User._init(uuid, name);
}
// It is usually the 'instance' that calls the below function
void changeFunction(/*params*/){
// Change the user instance, say, assign it to null if a user logs out
// More importanly, notify the listeners about the change
notifyListeners();
}
// Note this constructor is not exposed to the public.
User._init(this.uuid,this.name);
}
Then propagate the User object using a ChangeNotifierProvider using the guidelines mentioned here
I yield same state but with different object in my bloc but BlocBuilder not called again.
How I can do this scenario ?
My mapEventToState is
if (event is EditUserProfileImageChanged) {
UserProfile newUserProfile = state.userProfile;
newUserProfile.avatar = event.imgSrc;
yield EditUserProfileTotalState(userProfile: newUserProfile);
}
When we yield a state in the private mapEventToState handlers, we are always yielding a new state instead of mutating the state. This is because every time we yield, bloc will compare the state to the nextState and will only trigger a state change (transition) if the two states are not equal. If we just mutate and yield the same instance of state, then state == nextState would evaluate to true and no state change would occur.
If you want to change the value of the state, make a copyWith function for your model class.
class UserProfile extends Equatable {
final String name;
final Image avatar;
const UserProfile({this.name, this.avatar});
UserProfile copyWith({String name, Image avatar,}) {
return UserProfile(
name: name ?? this.name,
avatar: avatar?? this.avatar,
);
}
#override
List<Object> get props => [name, avatar];
}
if (event is EditUserProfileImageChanged) {
var newState = state.userProfile.copyWith(avatar: event.imgSrc);
yield EditUserProfileTotalState(userProfile: newState);
}
Removing Equatable solve the problem.
Removing equatbale rebuilds every time even values of the properties are not changed. Instead create new state instance every time.
I'm new to flutter, and i bumped into a problem.
I have a Feed model in my app that looks like this:
import 'package:uuid/uuid.dart';
class Feed {
// Static Members
var uuid = new Uuid();
// Members
String id;
bool isScheduled;
DateTime createdTime;
DateTime feedingTime;
String deviceId;
// Constructors
Feed({this.feedingTime, this.deviceId, this.isScheduled}) {
id = uuid.v4();
createdTime = DateTime.now();
}
Feed.fromDevice(deviceId) {
Feed(deviceId: deviceId, feedingTime: DateTime.now(), isScheduled: false);
}
}
Now i have my AddFeedForm that i'm trying to initialize with default values, in the InitState:
class _AddFeedFormState extends State<AddFeedForm> {
// Final Members
final _formKey = GlobalKey<FormState>();
final List<Machine> _devices = machinesFromServer;
// Members
Feed _feed;
#override
void initState() {
_feed = Feed.fromDevice(_devices.first.id);
super.initState();
}
But somehow after the initState the _feed parameter stays null!
Any ideas?
But somehow after the initState the _feed parameter stays null!
Are you sure this is the case, and not that you're getting a Feed instance that has null fields?
It looks like your named constructor is incorrect:
Feed.fromDevice(deviceId) {
Feed(deviceId: deviceId, feedingTime: DateTime.now(), isScheduled: false);
}
Here you're calling the default Feed constructor inside a named constructor, but not doing anything with the result - this is creating another Feed and then throwing it away. The one returned by the named constructor has not been initialised.
What you probably wanted was this:
Feed.fromDevice(deviceId):
this(deviceId: deviceId, feedingTime: DateTime.now(), isScheduled: false);
This makes the fromDevice constructor call the default constructor for initialisation of the instance, rather than creating another copy that goes unused.
Another option would be to make it a static method:
static fromDevice(deviceId) {
return Feed(deviceId: deviceId, feedingTime: DateTime.now(), isScheduled: false);
}
There wouldn't be much difference in this case.. Constructors seem nicer, but sometimes you might find that you want to a) make initialisation async (static methods can return a Future<Feed> but constructors cannot or b) do more processing of the arguments before they're passed to the real constructor that might not fit nicely in the initialiser call.