Closed ValeriusGC closed 2 years ago
Hi there! I'm still waiting for the answer. Is it bug or is it feature? If the last one please give me advice how do you solve these cases?
You should not persist Events. Keep in mind Events are hacks. They use the store state as a delivery mechanism, but they should not really be considered part of the state. They are mutable, while the store state should immutable. Events are mutated when you consume them. That's why they can only be used for one form at a time. If you want to have 2 forms you need to separate events. You should consume them in the didUpdateWidget
method. Not in build. Please see the provided example. If you are not willing to work with these limitations, you should avoid using Events.
To sum up:
didUpdateWidget
method. Not in build. Probably not applicable, but in case you are creating a timer or a countdown to change a widget you can use the TimeBuilder widget from https://pub.dev/packages/assorted_layout_widgets
You should not persist Events. Keep in mind Events are hacks. They use the store state as a delivery mechanism, but they should not really be considered part of the state. They are mutable, while the store state should immutable. Events are mutated when you consume them. That's why they can only be used for one form at a time. If you want to have 2 forms you need to separate events. You should consume them in the
didUpdateWidget
method. Not in build. Please see the provided example. If you are not willing to work with these limitations, you should avoid using Events.To sum up:
- Events should not be persisted.
- Events should not be considered part of the state. They are a hack.
- Events are mutable, and they are mutated when you consume them.
- You can't use events to mutate more than one widget.
- You should consume Events in the
didUpdateWidget
method. Not in build.
I got it. But it is very, very uncomfortable to make a bunch of a duplicated classes to realize events like in my case. Alas. It would be much more comfortable if one can specify some part of State not to be persisted, something like 'non-persisted flag'.
Probably not applicable, but in case you are creating a timer or a countdown to change a widget you can use the TimeBuilder widget from https://pub.dev/packages/assorted_layout_widgets
Thanks, very interesting. It seems there's something typo.
Fixed. Thanks!
@ValeriusGC
The whole point of the Event is that is gives the widget a "pulse" that you can use to change something. But your question got me thinking, and I think it would not be difficult to achieve what you want, with something like this:
@immutable
class EvtState<T> {
static final _random = Random.secure();
final T? value;
final int _rand;
EvtState([this.value]) : _rand = _random.nextInt(1 << 32);
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is EvtState &&
runtimeType == other.runtimeType &&
value == other.value &&
_rand == other._rand;
@override
int get hashCode => value.hashCode ^ _rand.hashCode;
}
Then:
@override
void didUpdateWidget(MyWidget oldWidget) {
super.didUpdateWidget(oldWidget);
if (evt != oldWidget.evt) doSomethingWith(evt.value);
}
Explanation: The random part of the event will make sure an event you create is always different from the previous one, even when the value (the payload) is the same. This will trigger a widget rebuild in the connector (don't forget to include the event in the view-model). Then, the didUpdateWidget
method will be called. Since evt
is now different from oldWidget.evt
, it will run the doSomething
method.
This event class is never consumed, which means you can use it with more than one widget.
I haven't tested it. In case you do try it out, could you please tell me if it worked or not?
Hi, Marcello.
@immutable class EvtState
{
This did not take off (If i got you right of course).
I replaced ordinary int
to EvtState<int>
in the Event:
class TimeIsOnAction extends AppAction {
TimeIsOnAction(this.timerCounter);
final Int timerCounter;
//
@override
Future<AppState?> reduce() async {
return state.copyWith(timerCounter: Event<Int>(timerCounter));
}
}
typedef Int = EvtState<int>;
@immutable
class EvtState<T> {
static final _random = Random.secure();
final T? value;
final int _rand;
EvtState([this.value]) : _rand = _random.nextInt(1 << 32);
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is EvtState &&
runtimeType == other.runtimeType &&
value == other.value &&
_rand == other._rand;
@override
int get hashCode => value.hashCode ^ _rand.hashCode;
@override
String toString() {
return 'EvtState{value: $value, _rand: $_rand}';
}
}
So Connector
and VM
were replaced to:
class TimerWidgetConnector extends StatelessWidget {
const TimerWidgetConnector({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return StoreConnector<AppState, _Vm>(
vm: () => _Factory(),
builder: (context, vm) {
return TimerWidget(
timerCounter: vm.timerCounter,
);
},
);
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////
class _Factory extends AppVmFactory {
@override
_Vm fromStore() {
return _Vm(
timerCounter: state.timerCounter,
);
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////
class _Vm extends Vm {
final Event<Int> timerCounter;
_Vm({
required this.timerCounter,
}) : super(equals: [timerCounter]);
}
and widget itself became:
class TimerWidget extends StatefulWidget {
const TimerWidget({Key? key, required this.timerCounter}) : super(key: key);
final Event<Int> timerCounter;
@override
State<TimerWidget> createState() => _TimerWidgetState();
}
class _TimerWidgetState extends State<TimerWidget> {
var counter = Int(0);
@override
void didUpdateWidget(covariant TimerWidget oldWidget) {
debugPrint('$now: _TimerWidgetState.didUpdateWidget: old=${oldWidget.timerCounter.state}, new=${widget.timerCounter.state}');
super.didUpdateWidget(oldWidget);
counter = widget.timerCounter.consume() ?? Int(0);
}
@override
Widget build(BuildContext context) {
debugPrint('$now: _TimerWidgetState.build: ${counter.value}');
return Center(child: Text('${counter.value}'));
}
}
==============================================================
But result is the same. If event is been consuming widget.timerCounter.consume()
it never triggers later in another copy of widget on another screen. If not - AppState marked as changed and AppStore stores event in DB.
I even played with hooks like that (one of variants)
class _Vm extends Vm {
final Event<Int> timerCounter;
_Vm({
required Event<Int> timerCounter2,
}) : timerCounter = Event<Int>(timerCounter2.state), super(equals: [Event<Int>(timerCounter2.state)]) {
debugPrint('$now: _Vm._Vm: ${timerCounter}');
timerCounter2.consume();
}
}
but alas - if basic event (those that registered in AppState) has been consumed, no data in the second form. If not - AppStore has been storing event )))
@ValeriusGC Sorry if I was not clear, but I think you did not understand my suggestion. I am saying that for multiple widgets you should try it out with the new EvtState
class I suggested above, not the Event
class. You should not use the Event
class at all anymore.
You are doing this:
return state.copyWith(timerCounter: Event<Int>(timerCounter));
But you should be doing this:
class TimeIsOnAction extends AppAction {
TimeIsOnAction(this.newCounter);
final int newCounter;
Future<AppState?> reduce() async {
return state.copyWith(timerCounter: EvtState<int>(newCounter));
}}
Please note the EvtState
class does not have a consume
method.
int counter = 0;
@override
void didUpdateWidget(MyWidget oldWidget) {
super.didUpdateWidget(oldWidget);
if (evt != oldWidget.evt)
counter = widget.timerCounter.value;
}
@override
Widget build(BuildContext context) {
return Center(child: Text('${counter.value}'));
}
Hi @marcglasberg .
But as soon it stopped being Event
it automatically became a part of AppState
and every timer tick now saves in store as before trying use Event. Where is the difference between int
and EvtState<int>
from the point of view of AppState
?
In any case I tried it of course ))). But alas - its state automatically persisted with every EvtState<int>
changing (as with simple int
).
The only difference between int
and EvtState<int>
is that an integer is only different if it changed, while the event is different whenever it's recreated:
30 == 30 // true
EvtState(30) == EvtState(30) // false
Another example:
true == true // true
EvtState(true) == EvtState(true) // false
Suppose you have a list of items, and when the user types a number n
into a TextField and presses a button you want to scroll the list to show the n-th item. When the button is pressed you can add EvtState(n)
to the state, and that will trigger the event even if the number hasn't changed from last time. If you just add the integer n
to the state, it will only trigger the event if last time it was a different number.
Hi, @marcglasberg.
30 == 30 // true EvtState(30) == EvtState(30) // false
Of course, it's obvious )) . Your case with scroll is interesting. But i meant that in my context (where every changes of state is been saving in AppStore) it is useless. I'd like to reuse one TimerWidget
in multiple cases without duplicating VM+Connector+Widget
for every place where it needs, but without saving it in Store. Event<T>
would be nice choice if it allows using more than once.
It would be interesting when AppState has possibility to adjust its "persist behavior", something like that )) :
in store.dart
in app_state.dart
Wow, it workd @marcglasberg )))
I placed identicalWith
now in right place
and now i can control what i want to persist
of course it's just raw code, and maybe not in your roadmap. But it is very handful for case like mine and with minimal code injection.
And there is problem with naming, should name PersistControllerMixin.ignoreWhere()
or something )))
There is a way with even less dramatic changes: Just define mixin somewhere in the code
mixin PersistorMixin<St> {
bool isIdenticalForPersist(St? oldState) => identical(this, oldState);
}
And change just one string in class ProcessPersistence<St>
(line 37)
if (isPaused || (newState is PersistorMixin && newState.isIdenticalForPersist(lastPersistedState))) return false;
// if (isPaused || identical(lastPersistedState, newState)) return false;
and voila:
@override
bool isIdenticalForPersist(AppState? oldState) {
if(oldState == null) return false;
final r = oldState.isDarkMode == isDarkMode && oldState.currentIndex == currentIndex;
return r;
}
Hi! This is a case.
So I created it as an Event, and realized like this.
But! If i
consume
event in TimerWidget.build after applying - it works only on one FormIf i don't consume - its state automatically persisted with every event changing.
How can I do that in right way?
PS. Maybe, if
events
supposed to be "transparent" for store (as described in doc), Store could "remove" them from saving by default?