rrousselGit / riverpod

A reactive caching and data-binding framework. Riverpod makes working with asynchronous code a breeze.
https://riverpod.dev
MIT License
6.17k stars 943 forks source link

How to force a re-render even if provider value hasn't changed #169

Closed lukaspili closed 3 years ago

lukaspili commented 3 years ago

Just started experimenting with riverpod + statenotifer + freezed so I'm not sure where the question belongs. I'm looking to manually re-render a ConsumerWidget even if its watched value hasn't changed.

I have a widget with a textfield driven by a viewstate, generated by freezed and contained in a statenotifier:

class _StateNotifier extends StateNotifier<SomeViewState> {
  _StateNotifier() : super(SomeViewState());

  void updateDoubleValue(String value) {
    state = state.copyWith(doubleValue: double.tryParse(value));
  }
}

final _stateProvider = StateNotifierProvider.autoDispose((ref) => _StateNotifier());

class _SomeScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // ListenableTextField is a hook widget that uses a combo of useProvider, useFocusNode, useTextEditingController and useEffect 
    // to always show the value from ProviderListenable when user finished typing (textfield is not focused anymore) 
    return ListenableTextField(
      value: _stateProvider.state.select((s) => s.doubleValue?.toString() ?? ''),
      onChanged: context.read(_stateProvider).updateDoubleValue,
      keyboardType: TextInputType.numberWithOptions(decimal: true),
      // Allow only double input with 0 or 1 decimal
      inputFormatters: <TextInputFormatter>[
        FilteringTextInputFormatter.allow(RegExp(r'^\d+(\.|\,)?\d{0,1}')),
      ],
    );
  }
}

The following example shows the issue and what I want to achieve:

  1. Textfield is empty because doubleValue is null
  2. User enters 12. in the textfield (not catched by inputformatter) and finishes editing
  3. onChanged -> updateDoubleValue -> new state with doubleValue = 12
  4. ListenableTextField re-renders with new value and shows 12 (textfield value changed from 12. to 12)
  5. User enters 12. again and finishes editing
  6. onChanged -> updateDoubleValue -> new state with doubleValue = 12
  7. ListenableTextField doesn't re-render

The reason is that at step 6, the new state is the same as the previous one, so _stateProvider.state.select((s) => s.doubleValue?.toString() ?? '') doesn't trigger a new render. While the textfield still shows 12..

Is there a way to force a re-render for this kind of scenario?

It could also be solved by changing doubleValue to string and continuously updating it while user writes in the field. Like this when editing ends, the format would change the state value if necessary and always trigger a render. However that's not great because it implies the textfield will be re-rendered at every char type.

rrousselGit commented 3 years ago

Hello!

Why do you want to force a rerender? What's the purpose?

lukaspili commented 3 years ago

I want the textfield to always reflect the value from the state notifier provider, except when it's in focus and user is typing inside (in order to avoid re-renders). The purpose is to be able to change/reformat the entered input once editing is finished and have the textfield to show the updated value.

The request for re-render is to handle the edge case:

  1. User enters foobar123
  2. Triggers onChange('foobar123') => update state with 'foobar' (let's say we don't want any numbers and inputformatters don't exist, so we manually strip digits in the update method from state notifier)
  3. Textfield is updated with foobar
  4. User enters foobar456
  5. Triggers onChange('foobar456') => update state with 'foobar' (stripping numbers again)
  6. Consumer doesn't rebuild textfield because previous value 'foobar' == 'foobar' (new value from step 5). And textfield still shows the raw input from the user foobar456

Thus, forcing re-render at step 5 would avoid the bug described at step 6.

But maybe it's the wrong way to looking at the problem. I don't like having the view state (driven by state notifier provider) to not always be in sync with the view.

The reason it's not in sync is to avoid rebuilding the textfield every time a character is typed. And only update the state value once editing has finished (textfield lost the focus).

From my past experiences with this kind of architecture (view state = single source of truth and unidirectional data flows), it's useful to be able to update the state without triggering a render. For instance, exposing a silent param on context.read:

ListenableTextField(
      value: _stateProvider.state.select((s) => s.someValue),
      onChanged: context.read(_stateProvider, silent: true).updateSomeValue, // this time onChanged is called every time a new char is typed,
);

Calling state notifier mutation methods when silent == true would not trigger re-render, even if state has changed. Which makes sense because we update the state as a reaction from a view change, in order to keep the state in sync.

Again, I'm not familiar at all with riverpod, so I'm not sure if any of it makes sense. What do you think?

rrousselGit commented 3 years ago

You probably should drive your textfield by a "value" then.

Forcing rerenders is very hacky. You should probably refractor your code such that the UI and the state aren't out of sync

lukaspili commented 3 years ago

This is the case: ListenableTextField is driven by _stateProvider.state.select((s) => s.someValue) and gets rebuild whenever someValue changes (it's a HookWidget and it's using useProvider()

But I would also like to avoid rebuilding the textfield when someValue is updated from a keyboard input event, while still updating the value in the state notifier.

rrousselGit commented 3 years ago

Why make the widget rebuild when the state changes then?

lukaspili commented 3 years ago

Because I still want to be able to rebuild the widget when I change the state from some other action. For instance, refresh some API and update/override the field with a new value. Or the example explained above -> modify the value after user finished typing.

rrousselGit commented 3 years ago

For instance, refresh some API and update/override the field with a new value.

But why does the TextField needs to rebuild for this? The textfield isn't the one triggering the requests, right?

modify the value after user finished typing.

That can be done on other textfields life-cycles, like on unfocus/submit, with TextEditingController and/or FocusNode Why use a declarative API here?

rrousselGit commented 3 years ago

Closing as the issue appears to be stale.