1

The code below takes user input, and prints it in the upper case after a delay of 1s.

Minimal reproducible code:

class FooPage extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final asyncValue = ref.watch(resultProvider);
    print('loading: ${asyncValue.isLoading}');
    return Scaffold(
      body: Column(
        children: [
          TextField(onChanged: (s) => ref.read(queryProvider.notifier).state = s),
          asyncValue.when(
            data: Text.new,
            error: (e, s) => Text('Error = $e'),
            loading: () => Text('Loading...'),
          ),
        ],
      ),
    );
  }
}

final stringProvider = FutureProvider.family<String, String>((ref, query) async {
  await Future.delayed(Duration(seconds: 1));
  return query.toUpperCase();
});

final queryProvider = StateProvider<String>((ref) => '');

final resultProvider = FutureProvider<String>((ref) async {
  final query = ref.watch(queryProvider);
  return ref.watch(stringProvider(query).future);
});

After running the code,

  1. Enter any text (say a) and wait for the output (the upper case)
  2. Enter another text (say b), and wait for the output (the upper case)
  3. Press the backspace (i.e. delete the character b) and now the console will print loading: true and because of this the loading widget builds for a fraction of seconds. This causes a very poor UX.

This issue is happening on the latest 2.0.2 version. So, how can I get the previous value so I can consistently show data once a data is fetched?


Update:

This is how I'm using the git:

dependencies:
  flutter:
    sdk: flutter

  flutter_riverpod:
    git:
      url: https://github.com/rrousselGit/riverpod/tree/master/packages/flutter_riverpod
      ref: master 
iDecode
  • 22,623
  • 19
  • 99
  • 186
  • try adding cache property to your resultProvider – john Oct 18 '22 at 07:06
  • @john cache is not available in `2.0.2` and if was available, I don't really need to cache anything because I'm not using `autoDispose` – iDecode Oct 18 '22 at 07:07
  • how about watching the state of your queryProvider since that is a StateProvider so it should be `final query = ref.watch(queryProvider.notifier).state;` – john Oct 18 '22 at 07:13
  • i ran your code and not seeing any loading or even a fraction of delay so maybe the problem is somewhere else that's affecting your code. – john Oct 18 '22 at 07:21
  • @john Add `print(asyncResult.valueOrNull)` line in the `build()` method and you'll see the state goes in loading state for a fraction of seconds. – iDecode Oct 18 '22 at 07:23
  • I tried it but it's working fine. can't see any weird behavior. – john Oct 18 '22 at 07:30
  • @john When you use `print(asyncResult)` in the `build()` method and It clearly prints `AsyncLoading()` after pressing the delete button. – iDecode Oct 18 '22 at 07:58

3 Answers3

5

The 2.1.0 version contains extra utilities to obtain the previous value/error during loading.

Note At the time of posting, 2.1.0 is not currently released. If you want to use it, you can use a git dependency in your pubspec to do:

# Using dependency_overrides is necessary due to flutter_riverpod depending on riverpod
dependency_overrides:
  flutter_riverpod:
    git:
      url: https://github.com/rrousselGit/riverpod/
      ref: master
      path: packages/flutter_riverpod
  riverpod:
    git:
      url: https://github.com/rrousselGit/riverpod/
      ref: master
      path: packages/riverpod

It is now possible to obtain the previous value/error during loading state.

Let's say you define a FutureProvider as followed:

final counterProvider = StateProvider<int>((ref) => 0);

final futureProvider = FutureProvider<String>((ref) async {
  await Future.delayed(Duration(seconds: 2));
  return "Hello world ${ref.watch(counterProivder)}';
});

where changing counterProvider causes futureProvider to go back to loading state.

With this snippet, in version 2.1.0 and above, if futureProvider goes back to loading, you can still access value/error:

AsyncValue<String> state = ref.watch(futureProvider);

if (state.isLoading) {
  // during loading state, value/errors are still available
  print(state.value);
  print(state.error);
}

Of course, writing if (state.isLoading) is a bit inconvenient compared to the nice when syntax.

By default, the when syntax correctly already invokes data/error instead of loading if the provider rebuilds because of a Ref.refresh/Ref.invalidate.

On the other hand, it doesn't do so if the rebuild is triggered by Ref.watch on purpose (as the parameters of your provider have changed).
But when exposes flags to change this.

More specifically, you could write:

AsyncValue<String> state = ref.watch(futureProvider);

state.when(
  skipLoadingOnReload: true,
  loading: () => print('loading'),
  data: (value) => print('value: $value'),
  error: (err, stack) => print('error: $err');
);

With this code, whether a provider rebuilds by Ref.watch or Ref.refresh, the when method will skip loading and instead invoke data/error.

Rémi Rousselet
  • 256,336
  • 79
  • 519
  • 432
0

after upgrading my riverpod to 2.0.2, i now can see the behavior. The main reason for that is because of the 1 second delay. And there is no way you can eliminate that loading unless you remove the delay because everytime there is a change in your query, the future provider is being called, i tried it and its working fine. Now for your main question that how to access the previous state of a provider, you can do that using ref.listen(). here below it the demo on how you can access it.

class FooPage extends ConsumerWidget {
  const FooPage({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    
    ref.listen<AsyncValue<String>>(resultProvider, (previous, next) {
      //* you can access the previous state here
      if (previous?.value != '' && previous?.value != null) {
        ref.read(upperCaseQuery.notifier).state = previous?.value ?? '';
      }
    });
    final resultValue = ref.watch(resultProvider);
    final upperCased = ref.watch(upperCaseQuery.state).state;
    return Scaffold(
      body: Center(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            TextField(onChanged: (s) {
              ref.read(queryProvider.notifier).state = s;
            }),
            //previous value of the the provider
            Text(upperCased),

            resultValue.when(
              data: (value) =>  Text(value),
              error: (e, s) => Text('Error = $e'),
              loading: () => const Text('Loading...'),
            ),
          ],
        ),
      ),
    );
  }
}


final queryProvider = StateProvider<String>((ref) => 'default');

final resultProvider = FutureProvider.autoDispose<String>((ref) async {
  final query = ref.watch(queryProvider);
  await Future.delayed(const Duration(seconds: 1));
  final upperCased = query.toUpperCase();
  return upperCased;
}, name: 'result provider');

final upperCaseQuery = StateProvider<String>((ref) => '', name: 'upper cased');
iDecode
  • 22,623
  • 19
  • 99
  • 186
john
  • 1,438
  • 8
  • 18
  • There are two problems. 1. The delay only works when you're writing a new query but deleting a query won't cause any delay. There's no 1s delay after you press the back button. 2. I know how to use `ref.listen` in general but the question was how to use it inside the widget, where I see a fraction of seconds `loading` text – iDecode Oct 18 '22 at 09:34
  • you are right, there should be a way to preserve the previous state unless you refresh it manually. but as of now, there are issues on dart sdk that are affecting riverpod 2.0.2 that's why it's recommended to use the previous release until the issue is addressed. – john Oct 18 '22 at 11:10
  • Can you point me out where it says to use the previous release on Riverpod page? I know there are some issues on web but it doesn't effect the behavior of Riverpod core concepts. Riverpod docs mention that you can't access the previous value like you used to. – iDecode Oct 18 '22 at 11:21
  • it is just my opinion though. but if you are not using flutter web then there is no problem. – john Oct 18 '22 at 11:35
0

If you move await Future.delayed(const Duration(seconds: 1)); from stringProvider to resultProvider everything will work as you would expect.

Also, let me offer you a variant of code modernization:

/// Instead `stringProvider`.
Future<String> stringConvert(String query) async {
  await Future.delayed(const Duration(seconds: 1));

  return query.toUpperCase();
}

final queryProvider = StateProvider<String>((ref) {
  return '';
});

final resultProvider = FutureProvider<String>((ref) async {
  final query = ref.watch(queryProvider);
  return stringConvert(query);
});
Ruble
  • 2,589
  • 3
  • 6
  • 29
  • Basically you removed the provider and created a function out if, sorry this isn't what I am looking for. The code I shared was working fine before Riverpod `2.0.2`, that means the code is absolutely fine (till that version), so I'm looking for a correct code for the latest version. Removing a provider is definitely not the right thing because for simplicity I created a small provider which can be converted into a function as you did but in real life application, it can't be converted to a function. – iDecode Oct 18 '22 at 15:25
  • And the option of moving `Future` is also not possible? – Ruble Oct 19 '22 at 04:30