Flutter Riverpod v1.0 - state not updating with StateNotifier List - flutter

I migrated from RiverPod 0.4x to 1.0 stable and now this StateNotifier no longer updates state even though the move() function is being called (debugPrint shows the call) when animation ends. This was working in 0.4x, but obviously in the improved 1.0 RiverPod I've not fully migrated.
What am I missing here for RiverPod 1.0 to update state when the state is a List?
final animateCoversStateNotifierProvider = StateNotifierProvider.autoDispose<
AnimateCoversStateNotifier,
List<CoverAlignment>>((ref) => AnimateCoversStateNotifier());
class AnimateCoversStateNotifier extends StateNotifier<List<CoverAlignment>> {
AnimateCoversStateNotifier() : super([]);
void addCover({
required CoverAlignment alignment,
}) =>
state.add(alignment);
void move({
required int cover,
bool? readyToOpen,
bool? speechBalloonVisible,
Duration? animatedOpacityDuration,
bool? removeSpeechBalloonPadding,
}) {
debugPrint('move cover called');
Future.delayed(const Duration(milliseconds: 200), () {
if (mounted) {
state[cover].removeSpeechPadding = speechBalloonVisible != true;
state[cover].speechBalloonOpacity =
speechBalloonVisible == true ? kMaxSpeechBalloonOpacity : 0.0;
state[cover].x = CoverAlignment.randomDouble();
state[cover].y = CoverAlignment.randomDouble();
state[cover].curve = CoverAlignment.getCurve();
state[cover].seconds = CoverAlignment.randomSeconds();
state[cover].degrees = CoverAlignment.randomIntInRangeWithMinimum(
min: 0,
max: 45,
);
/// This was required to update state using RiverPod 0.4x, but no longer works in
/// RiverPod 1.0.
state = state;
}
});
}
}
In my build screen body I am use watch to react to the notifier's changes.
/// Display covers
final List coverAlignment =
ref.watch(animateCoversStateNotifierProvider);
EDIT: Creating a Freezed class as Remi in comments suggests
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:flutter/foundation.dart';
import 'package:myapp/app/corral/models/cover_alignment_model.dart';
part 'animate_covers.freezed.dart';
class AnimateCovers extends StateNotifier<List<CoverAlignment>>
with _$AnimateCovers {
factory AnimateCovers() = _AnimateCovers;
void addCover({
required int cover,
required CoverAlignment alignment,
}) {
state.insert(cover, alignment);
}
void move({
required int cover,
bool? readyToOpen,
bool? speechBalloonVisible,
Duration? animatedOpacityDuration,
bool? removeSpeechBalloonPadding,
}) {
/// What do I do here?
}
}

Doing:
state = state
was never supposed to work.
You're not supposed to mutate the existing state. You're supposed to clone the state
Instead do something like:
state[cover] = state[cover].copyWith(
removeSpeechPadding: speechBalloonVisible != true,
...
),
You can generate this copyWith method using Freezed

Related

Update state of an object in Riverpod

I want to update a state of an object and notify to update the UI accordingly.
For example
import 'package:riverpod_annotation/riverpod_annotation.dart';
class CounterState {
int value;
bool isCompleted;
CounterState({this.value = 0, this.isCompleted = false});
}
#riverpod
class CounterProvider extends _$CounterProvider {
#override
CounterState build() {
return CounterState;
}
void setValue(int newValue) {
state.value = newValue;
if (newValue == 1) {
state.isCompleted = true;
}
}
}
I use CounterState to contain all the necessary state of for 1 page. As this method doesn't work, and only works if I use this
if (newValue == 1) {
state = CounterState(value: newValue, isCompleted: true);
}
As if the state object become more and more complex, I want to keep other variable in the state remain the same, and only update the one I need, for the example above I can use something like this
if (___) {
state = CounterState(value: state.value, isCompleted: true);
}
But it is going to be difficult to update the state if CounterState have 10 variables or more.
Is there a way to overcome this?
Try implementing the copyWith method
class CounterState {
int value;
bool isCompleted;
CounterState({
this.value = 0,
this.isCompleted = false
});
CounterState copyWith({ int? value, bool? isCompleted }) => CounterState(
value: value ?? this.value,
isCompleted: isCompleted ?? this.isCompleted
);
}
With that done, you can just do:
state = state.copyWith(value: newValue, isCompleted: true, ...);
And just specify the fields you would like to update.

Riverpod trigger rebuild when update state with freezed copywith even if nothing changed

I thought Riverpod will only trigger rebuild if the state value is different but turn out it rebuild every time when state is set although the value is the same. Is that true?
The case is as below
#Freezed(genericArgumentFactories: true)
class Model with _$Model {
const factory Model({required int id}) = _Model;
}
class Manager {
static StateProvider<Model> modelProvider =
StateProvider<Model>((ref) => Model(id: 1));
Manager() {
Stream.periodic(Duration(seconds: 1)).take(1000).listen((event) {
ref.read(modelProvider.notifier).update((state) {
var cloneState = state.copyWith();
print("${state == cloneState}"); //This print true
return cloneState;
});
});
}
}
class TestWidget extends ConsumerWidget {
const TestWidget();
#override
Widget build(BuildContext context, WidgetRef ref) {
var model = ref.watch(Manager.modelProvider);
print("model change......................"); //print every second
return Text(model.id.toString());
}
}
It showed that the TestWidget was rebuilt every seconds but I thought it shouldn't as the state is the same although I set it again.
Am I missing something? Thanks.
Riverpod by default doesn't rely on == but identical to filter updates.
The reasoning is that == can be quite inefficient if your model becomes large.
But this causes the behavior you described: If two objects have the same content but use a different instance, listeners will be notified.
It's not considered a problem though, as there is no value in doing:
state = state.copyWith();
It's all about using identical(old, current) under the hood to compare states. identical presents for itself the following:
/// Check whether two references are to the same object.
///
/// Example:
/// ```dart
/// var o = new Object();
/// var isIdentical = identical(o, new Object()); // false, different objects.
/// isIdentical = identical(o, o); // true, same object
/// isIdentical = identical(const Object(), const Object()); // true, const canonicalizes
/// isIdentical = identical([1], [1]); // false
/// isIdentical = identical(const [1], const [1]); // true
/// isIdentical = identical(const [1], const [2]); // false
/// isIdentical = identical(2, 1 + 1); // true, integers canonicalizes
/// ```
external bool identical(Object? a, Object? b);
Here is a complete copy-run example:
void main() => runApp(const ProviderScope(child: MyApp()));
#Freezed(genericArgumentFactories: true)
class Model with _$Model {
const factory Model({required int id}) = _Model;
}
class Manager {
static StateProvider<Model> modelProvider = StateProvider<Model>((ref) {
Stream.periodic(const Duration(seconds: 1)).take(1000).listen((event) {
ref.read(modelProvider.notifier).update((state) {
final cloneState = state.copyWith();
// const cloneState = Model(id: 1); //The print true in both cases
print("${state == cloneState}"); //This print true
print("identical: ${identical(state, cloneState)}"); //This print false
return cloneState;
});
});
return const Model(id: 1);
});
}
class MyApp extends ConsumerWidget {
const MyApp();
#override
Widget build(BuildContext context, WidgetRef ref) {
var model = ref.watch(Manager.modelProvider);
print("model change......................"); //print every second
return MaterialApp(home: Text(model.id.toString()));
}
}
I modified the example a little, but kept the essence the same. Only by applying `const' can we achieve the absence of rebuilds.

How do I update a Map with BLoC and equatable?

I am still a beginner with BLoC architecture. So far the UI updates when using int, bool, and other basic data types. But when it comes to Maps it really confuses me. My code basically looks like this:
my state
enum TalentStatus { initial, loading, loaded, error }
class TalentState extends Equatable {
const TalentState({
required this.talentStatus,
this.selectedService = const {},
required this.talents,
this.test = 0,
});
final TalentStatus talentStatus;
final Talents talents;
final Map<String, Service> selectedService;
final int test;
TalentState copyWith({
TalentStatus? talentStatus,
Talents? talents,
Map<String, Service>? selectedService,
int? test,
}) =>
TalentState(
selectedService: selectedService ?? this.selectedService,
talentStatus: talentStatus ?? this.talentStatus,
talents: talents ?? this.talents,
test: test ?? this.test,
);
#override
List<Object> get props => [talentStatus, talents, selectedService, test];
}
my event
abstract class TalentEvent extends Equatable {
const TalentEvent();
#override
List<Object> get props => [];
}
class TalentStarted extends TalentEvent {}
class TalentSelectService extends TalentEvent {
const TalentSelectService(
this.service,
this.talentName,
);
final Service service;
final String talentName;
}
and my bloc
class TalentBloc extends Bloc<TalentEvent, TalentState> {
TalentBloc(this._talentRepository)
: super(TalentState(
talentStatus: TalentStatus.initial, talents: Talents())) {
on<TalentSelectService>(_selectService);
}
final TalentRepository _talentRepository;
Future<void> _selectService(
TalentSelectService event,
Emitter<TalentState> emit,
) async {
state.selectedService[event.talentName] = event.service;
final selectedService = Map<String, Service>.of(state.selectedService);
emit(
state.copyWith(
selectedService: selectedService,
),
);
}
}
whenever an event TalentSelectService is called BlocBuilder doesn't trigger, what's wrong with my code?
Your Service object must be comparable. One suggestion is that it extends Equatable. Either way it have to implement (override) the == operator and hashCode
The reason your BlocBuilder doesn't trigger is (probably) that it doesn't recognize that there has been a change in the Map.

Best Practice for storing temp data across States in Bloc

I have a simple counter application made by Flutter using Bloc. The idea is after the user presses the increase button, it will delay for 2 seconds, show loading, then increase/decrease the value.
The counter bloc contains 2 states, CounterValue and CounterLoading. However, whenever I increase, the bloc starts creating a new CounterLoading object with a value of 0. To deal with this, I have to pass the current value at the CounterValue state to CounterLoading state, and after 2 seconds, I have to pass again the current value of Loading state to CounterValue state to increase the value. Hence this seems to be pretty redundant and confused when it comes to real situations where we have multiple states in the middle which don't need data while the first and last state emitted are dependent.
What is the best practice to store temp data across states using bloc?
counter_state.dart
class CounterState {
int value = 0;
}
class CounterLoading extends CounterState {}
class CounterValue extends CounterState {}
counter_bloc.dart
class CounterBloc extends Bloc<CounterEvent, CounterState> {
CounterBloc() : super(CounterValue()) {
on<IncrementCounter>(
(event, emit) async {
emit(CounterLoading()..value = state.value);
await Future.delayed(const Duration(seconds: 2));
emit(CounterValue()..value = state.value + 1);
},
);
}
I would recommend investigating immutable states, which is very convenient while using BLoC. It means that instead of directly changing property values in the state, you are rather creating a new state object and replacing the previous state.
For this specific problem, I would recommend you to have a more complex state instead of having two separate states for Loading/Value. For instance:
State:
class CounterState {
const CounterState({
this.value = 0,
this.isLoading = false
});
final int value;
final bool isLoading;
}
BLoC:
class CounterBloc extends Bloc<CounterEvent, CounterState> {
CounterBloc() : super(CounterValue()) {
on<IncrementCounter>(
(event, emit) async {
final cur = state.value;
emit(CounterState(value: cur, isLoading: true));
await Future.delayed(const Duration(seconds: 2));
emit(CounterState(value: cur + 1, isLoading: false));
},
);
}
When state is more complicated than just 2 values, you should combine the use of Equatable package and copyWith method in your state.
Equatable makes it easy to tell that stateA != StateB. It's a replacement for manually overriding the == operator in your state class.
copyWith just returns a new state with zero or more of its values changed. It should be used basically every time you want to emit a new state.
State:
class CounterState extends Equatable {
const CounterState({
this.value = 0,
this.isLoading = false
});
final int value;
final bool isLoading;
#override
List<Object> get props => [value, isLoading];
CounterState copyWith({
int? value,
bool? isLoading,
}) {
return CounterState(
value: value ?? this.value,
isLoading: isLoading ?? this.isLoading,
);
}
}
Cubit:
class CounterCubit extends Cubit<CounterState> {
CounterCubit() : super(const CounterState());
Future<void> increment() async {
emit(state.copyWith(isLoading: true));
await Future<void>.delayed(const Duration(seconds: 2));
emit(state.copyWith(value: state.value + 1, isLoading: false));
}
}
And for even larger apps where you might use custom objects, not just integers, strings and booleans, I recommend you take a look at freezed for creating models to include in your state with their own copyWith methods, etc. automatically generated.
Emits would look something like this for an AuthState with a User model:
void updateUserName(String newName) {
emit(state.copyWith(user: state.user.copyWith(name: newName)));
}

Dart variable not final warning

So I have written this code below to make an Icon Stateful inside a Stateless widget.
class IconState extends StatefulWidget {
final bool isSelected;
IconState({
this.isSelected,
});
_IconState state; // this is not final because I need to assign it below
void toggle() {
state.change();
}
#override
_IconState createState() => state = new _IconState(
isSelected: this.isSelected,
);
}
class _IconState extends State<IconState> {
_IconState({
this.isSelected,
});
bool isSelected = false;
Widget _unSelected = Icon(
null,
);
Widget _selected = Icon(
Icons.check_outlined,
color: Colors.red,
);
void change() {
setState(() {
this.isSelected = this.isSelected == true ? false : true;
});
}
Icon evaluate() {
if (isSelected) {
return _selected;
}
return _unSelected;
}
#override
Widget build(BuildContext context) {
return evaluate();
}
}
To update the state of the Icon, I call the toggle() method from my Stateless widget.
Dart is giving me a non-final instance warning inside an #immutable class, but I am unable to find a workaround for this.
I have tried following:
final _IconState state = new _IconState(
isSelected: this.isSelected, // throws an error => Invalid reference to 'this' expression.
);
also this, but doesn't work either
final _IconState state;
IconState({this.isSelected}) {
this.state = new _IconState(
isSelected: this.isSelected,
);
};
Is there a workaround?
I would put the isSelected boolean inside an external state management class, then you can return 2 separate widgets in response to the change. Otherwise you would have to change the state inside of the widget where the icon will be displayed. Something like this:
class IconState extends ChangeNotifier{
bool _isSelected;
//any other needed state
bool get isSelected => _isSelected;
void changeIsSelected(bool selected) {
_isSelected = selected;
notifyListeners();
}
}
Then use ChangeNotifierProvider to to inject the state and call the change method.
final iconStateProvider = ChangeNotifierProvider((ref) => IconState());
Now, you can use iconStateProvider to access the state and methods. You will need a Builder or Consumer widget to listen for changes to the state.
Consumer( builder: (context, watch, child) {
final iconState = watch(iconStateProvider);
if (iconState.isSelected) {
return Icon();
} else {
return null;
}
This is using the Riverpod library, which is only 1 of many external state management libraries. I recommend watching tutorials on YouTube on different libraries and pick one that best suits you.