Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Provider a way to merge AsyncValue together #67

Open
stargazing-dino opened this issue Jul 31, 2020 · 72 comments
Open

Provider a way to merge AsyncValue together #67

stargazing-dino opened this issue Jul 31, 2020 · 72 comments
Assignees
Labels
enhancement New feature or request riverpod_generator

Comments

@stargazing-dino
Copy link

Is your feature request related to a problem? Please describe.
I'm refactoring a small app I have to use river_pods but one thing I keep wanting is a way to combine providers in a when or maybeWhen so they both get loaded at the same time in the case they're asynchronous.

Describe the solution you'd like

// Both userProvider and profileProvider are `StreamProvider`s
return useProvider2(userProvider, profileProvider).when(
  data: (user, profile) {
    // Logged in
  },
  loading: () {},
  error: (_, __) {}
);

Or, another example, all possible badges (pulled from a json file) and the user's current badges.

// userBadgesProvider is a StreamProvider and badgesProvider is a FutureProvider
return useProvider2(userBadgesProvider, badgesProvider).when(
  data: (userBadges, allBadges) {
    // Show list of a user's current badges in list of all possible badges
  },
  loading: () {},
  error: (_, __) {}
);

Describe alternatives you've considered
I can do what I want by checking both the .data values on the providers to check if they're null but then I lose out on the error case of when:

final user = useProvider(userProvider).data;
final profile = useProvider(profileProvider).data;
final isLoggedIn = user != null && profile != null;

Or for the other example, I currently do this:

useProvider(userBadgesProvider).when(
  data: (userBadges) {
    final badges = useProvider(badgesProvider).maybeMap<List<Badge>>(
      data: (asyncData) => asyncData.data.value,
      orElse: () => [],
    );
  },
  loading: () {},
  error: (_, __) {},
),

Additional context
hooks_riverpod: 0.6.0-dev

[✓] Flutter (Channel dev, 1.21.0-1.0.pre, on Mac OS X 10.15.5 19F101, locale en-US)
 
[✓] Android toolchain - develop for Android devices (Android SDK version 29.0.3)
[✓] Xcode - develop for iOS and macOS (Xcode 11.6)
[✓] Chrome - develop for the web
[✓] Android Studio (version 3.6)
[✓] Connected device (3 available)
@stargazing-dino stargazing-dino added the enhancement New feature or request label Jul 31, 2020
@rrousselGit
Copy link
Owner

Once thing you can do is use is:

AsyncValue<int> first;
AsyncValue<int> second;

if (first is AsyncError || second is AsyncError) {
  return Text('error');
} else if (first is AsyncLoading || second is AsyncLoading) {
  return Text('loading');
}

return Text('${first.data.value} ${seconc.data.value}');

@stargazing-dino
Copy link
Author

Thank you for the example code!

This might be my own laziness for not separating them based on their immediate location in the UI but in one place in my code I call down three separate asynchronous resources. This is just what I had previously from my Provider implementation that felt natural. With three or more the code you posted still works but there would be even more checks and nested values.

What would be the reason against something like useProviderN?

@rrousselGit
Copy link
Owner

What would be the reason against something like useProviderN?

It wouldn't to what you want. You're looking at combining multiple AsyncValue into one. useProvider is completely unrelated to AsyncValue, it's just returning whatever the provider exposes.

What you're probably looking for is something like:

final AsyncValue<First> user = useProvider(firstProvider);
final AsyncValue<Second> profile = useProvider(secondProvider);

return AsyncValue.merge(
  data: (read) {
     return Text('${read(user).name} ${read(profile).name}');
  },
  loading: () => CircularProgressIndicator(),
  error: (err, stack) => Text('error'),
);

@stargazing-dino
Copy link
Author

Sorry for the misunderstanding about useProvider.

But yes, having that merge ability on the AsyncValue would be very nice to have

@rrousselGit rrousselGit changed the title useProvider, useProvider2, useProviderN Provider a way to merge AsyncValue together Aug 16, 2020
@zgramming
Copy link

@rrousselGit i have same case to merge multiple AsyncValue to one , if i implement your advice i don't have ability to get error information.

class WelcomeScreen extends StatelessWidget {
  static const routeNamed = '/welcome-screen';
  @override
  Widget build(BuildContext context) {
    return Consumer(
      (ctx, watch) {
        final tugas = watch(showAllTugas);
        final pelajaran = watch(showAllPelajaran);
        final dosen = watch(showAllDosen);
        if (tugas is AsyncError || pelajaran is AsyncError || dosen is AsyncError) {
          return Scaffold(body: Center(child: Text('Error Found')));
        } else if (tugas is AsyncLoading || pelajaran is AsyncLoading || dosen is AsyncLoading) {
          return Scaffold(body: Center(child: CircularProgressIndicator()));
        }
        return Scaffold(body: Center(child: Text('Success ')));
      },
    );
  }
}

Another solution is using AsyncValue.merge , but i not see merge method.

tanya riverpod

Version Package

flutter_riverpod: ^0.6.1

@rrousselGit
Copy link
Owner

AsyncValue.merge does not exist yet.

@zgramming
Copy link

So for now i can only get error information from multiple AsyncValue using nested when ?


class WelcomeScreen extends StatelessWidget {
  static const routeNamed = '/welcome-screen';
  @override
  Widget build(BuildContext context) {
    return Consumer(
      (ctx, watch) {
        final tugas = watch(showAllTugas);
        final pelajaran = watch(showAllPelajaran);
        final dosen = watch(showAllDosen);
        return tugas.when(
          data: (valueTugas) {
            return pelajaran.when(
              data: (valuePelajaran) {
                return dosen.when(
                  data: (valueDosen) {
                    return Text('Hore');
                  },
                  loading: null,
                  error: null,
                );
              },
              loading: null,
              error: null,
            );
          },
          loading: null,
          error: null,
        );
      },
    );
  }
}

@rrousselGit
Copy link
Owner

is works:

AsyncValue<int> value;

if (value is AsyncError<int>) {
  print(value.error);
  print(value.stack);
}

@zgramming
Copy link

@rrousselGit i don't why error and stack method not showing . When i try your example , i can see those method.

tanya riverpod

But when i implement it to my AsynValue<List<TugasModel>> not showing those method.

tanya riverpod 2

I mistake somewhere ?

@rrousselGit
Copy link
Owner

Because your if contains path where your value may not be an AsyncError

Remove the || or change them into &&

@zgramming
Copy link

replace all || with &&

tanya riverpod

Only use 1 param

tanya riverpod only one

@rrousselGit
Copy link
Owner

Eh
Anyway it's not something I have control over. That's Dart, nor Riverpod

If you need to, you can make multiple ifs or you can use as to cast the variables.

@zgramming
Copy link

i see , thank's for your clarification. For now i think i can't get error and stack information without nested when

@rrousselGit
Copy link
Owner

rrousselGit commented Sep 6, 2020

About this issue:

I'm a bit mixed about #67 (comment), as this wouldn't be very performant and it could cause some confusion with hooks.

We could also have an AsyncValue.merge2 / AsyncValue.merge3 / AsyncValue.merge4 / ... which fuses AsyncValue<A> + AsyncValue<B> into AsyncValue<Tuple2<A, B>>.
But we are losing the names and it could be confusing too, on the top of being a bit tedious to write.

A solution I am considering is to instead continue my previous experiment: https://github.com/rrousselGit/boundary

We would then write:

class Example extends ConsumerWidget {
  @override
  Widget build(context, watch) {
    AsyncValue<A> first;
    AsyncValue<B> second;

    A a = unwrap(first);
    B b = unwrap(second);

    return Text('$a $b');
  }
}

typically used this way:

Widget build(context) {
  return Scaffold(
    body: Boundary(
      loading: (context) => const Center(child: CircularProgressIndicator()),
      error: (err, stack) => Center(child: Text('Error $err'),
      child: Example(),
    ),
  );
}

@stargazing-dino
Copy link
Author

Considering Provider has something already similar to AsyncValue.mergeN I don't think it'd be too confusing for newcomers (all likely to come from Provider). That said I like the newer syntax using Boundary a lot.

@rrousselGit
Copy link
Owner

Considering Provider has something already similar to AsyncValue.mergeN

What are you referring to?
Provider does not have AsyncValue. It has ProxyProviderN, but the equivalent is Riverpod's ref parameter

My main concern with AsyncValue.mergeN is the tuple. There is no standard Tuple in Dart, and this leads to quite an ugly syntax:

AsyncValue<Tuple3<int, String, double>> value;

value.when(
  data: (tuple) {
    return Text('${tuple.item1} ${tuple.item2} ${tuple.item3}');
  });
)

That itemN isn't very readable. We would need destructuring in Dart to make this syntax more reasonable.

@stargazing-dino
Copy link
Author

stargazing-dino commented Sep 9, 2020

I don't think waiting for the Dart team to introduce native touples and destructuting would be the best option considering this proposal is a year old. The package Tuple on the other hand is at least maintained by google.dev so it's a fairly reliable a dependancy if you were to rely on it.

In any case, I'm all in favour of any solution you decide on

rrousselGit pushed a commit that referenced this issue Dec 7, 2020
@erf
Copy link

erf commented Jan 9, 2021

I'm having the same problem now - what about a MultiAsyncProvider, which would take a list of either FutureProvider or StreamProvider or any type of provider which returns a type of AsyncValue, which would then produce either an AsyncError state if any items have error, a AsyncLoading state if any items are loading (and don't have error), and when all items are ready, would produce an List of AsyncData. Or would this be too specific? I would not want to clutter up the API's too much either so maybe this could be an optional third party package.

@stargazing-dino
Copy link
Author

I've just been using this for the meanwhile. Seems to be cover most use cases:

AsyncValue<Tuple2<T, R>> combineAsync2<T, R>(
  AsyncValue<T> asyncOne,
  AsyncValue<R> asyncTwo,
) {
  if (asyncOne is AsyncError) {
    final error = asyncOne as AsyncError<T>;
    return AsyncError(error.error, error.stackTrace);
  } else if (asyncTwo is AsyncError) {
    final error = asyncTwo as AsyncError<R>;
    return AsyncError(error.error, error.stackTrace);
  } else if (asyncOne is AsyncLoading || asyncTwo is AsyncLoading) {
    return AsyncLoading();
  } else if (asyncOne is AsyncData && asyncTwo is AsyncData) {
    return AsyncData(Tuple2<T, R>(asyncOne.data.value, asyncTwo.data.value));
  } else {
    throw 'Unsupported case';
  }
}

I've only needed to go up to 4 async values at one time so that's what I left it at. Here's the gist. Note this requires tuple dep

@erf
Copy link

erf commented Jan 9, 2021

That only covers the case of two AsyncValue's, I was looking for a generic case of an array of values, but it should not be too hard to make.

@erf
Copy link

erf commented Jan 9, 2021

Would it make sense to make a common base class for FutureProvider and StreamProvider called AsyncProvider? Which both return an AsyncValue so it would be easier to listen to a set of these?

@erf
Copy link

erf commented Jan 10, 2021

I made a proof of concept in this gist to combine values as i suggested in a previous comment.

I also made a method in order to check if all providers are loaded.

@stargazing-dino
Copy link
Author

stargazing-dino commented Jan 10, 2021

The trouble with a list is that you lose out on type safety. You have to know the position of each value to properly type it once you get the values out. Sorry too, I only showed the combineAsync2 but the gist I linked has up to combineAsync4. I'm not implying my code is a solution either but just a functional placeholder until something better comes along

@erf
Copy link

erf commented Jan 10, 2021

I was thinking something like rxdart's combineLatest or Future.wait - it's the same order out (result) as you put in. Type is dynamic so you just set it to the correct type. But not sure how you should handle errors if more than one, maybe just the first one you encounter. Also not sure how to get data for all the various states. I just thought i add some thoughts so the maintainer might get some ideas.

@erf
Copy link

erf commented Jan 11, 2021

I made a repositiory as a package combined_provider if you want to check out.

@stargazing-dino
Copy link
Author

stargazing-dino commented Jan 11, 2021

@erf I see where you're coming from now and I disagree with the need for the combineProviders you suggest. If you want a value from a Provider that isn't a StreamProvider or a FutureProvider you don't gain anything by having it inside a combineProvider. In fact, you lose type safety and considering riverpod promises to be Compile safe that's a big deal.

If you want a value from a Provider, StateProvider or ChangeNotifierProvider you can get them in build without doing anything special unlike async Providers

class Example extends ConsumerWidget {
  @override
  Widget build(BuildContext context, reader) {
    final myChangeNotifier = reader(myChangeNotifierProvider);
    final myState = reader(myStateProvider);

    // 
  }
}

If you need to filter rebuilds, then you can move all of your watch's to inside a Consumer:

class Example extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Consumer(
      builder: (context, watch, child) {
        final myChangeNotifier = watch(myChangeNotifierProvider);
        final myState = watch(myStateProvider);

        //
      }
    );
  }
}

The reason async providers are special cases is the different states their values can be from loading, error and data but you can't easily bunch them up without (possibly deeply) nesting them to account for all the states.

class Example extends ConsumerWidget {
  @override
  Widget build(BuildContext context, reader) {
    return reader(myStream).when(
      data: (data1) {
        return reader(mySecondStream).when(
          data: (data2) => Text('$data1, $data2'),
          loading: loading,
          error: error,
        );
      },
      loading: loading,
      error: error,
    );
  }
}

@rrousselGit
Copy link
Owner

In these instances I can't see how that proposal would work. I could split the file into multiple widgets, that might add complexity here.

You can keep using when in those situations. The goal isn't to completely remove when, but only to simplify common use-cases

@rrousselGit
Copy link
Owner

This issue will likely not be fixed before metaprogramming.

I really don't want to promote tuples because of their bad variable name, therefore we need structures/records.
Once we have metaprogramming, Riverpod will be able to generate pseudo records to help with that.

@rddewan
Copy link

rddewan commented Jul 26, 2022

Hi @rrousselGit any update on merging the AsyncValue ? This would reduce lot of code in UI

@rrousselGit rrousselGit added this to the 2.0.0 milestone Jul 31, 2022
@rrousselGit
Copy link
Owner

I don't think the loading/errorBuilder approach mentioned before is a good idea.
We'd lose the compile safety. People will forget to handle errors.
It's also not very flexible. Folks may want to do some sequential loading. So stopping at the first loading would prevent that

I think we'll need code generation.

I'm currently considering:

final userProvider = FutureProvider<User>();
final familyExampleProvider = FutureProvider.family<Foo, Param>();

@AsyncValue.all({userProvider, familyExampleProvider})
final groupProvider = _$group;

Widget build(context, ref) {
  final group = ref.watch(
    groupProvider(
      // for families, we need to pass in the provider with its arguments
      familyExampleProvider: familyExampleProvider(Param()),
    ),
  );

  return group.when(
    loading: () => Loading(),
    error: (err, stack) => Text('...'),
    data: (Group group) {
      return Text('a: ${group.user}, b: ${group.familyExample}');
    },
  );
}

@rrousselGit
Copy link
Owner

I'll implement this code-generation proposal shortly after the 2.0

@rrousselGit rrousselGit modified the milestones: 2.0.0, 2.1.0 Sep 21, 2022
@Thelm76
Copy link

Thelm76 commented Oct 21, 2022

I personally implemented an extension like this :

typedef MultiAsync<T> = Iterable<AsyncValue<T>>;

extension MultiAsyncExtension<T> on MultiAsync<T> {
  /// Performs an action based on the state of the [AsyncValue].
  ///
  /// All cases are required, which allows returning a non-nullable value.
  R when<R>({
    required R Function(Iterable<T> data) data,
    required R Function(
      Iterable<Object> error,
      Iterable<StackTrace?> stackTrace,
    )
        error,
    required R Function() loading,
  }) {
    if (any((element) => element is AsyncError)) {
      final err = where(
        (element) => element is AsyncError,
      ).map((e) => e as AsyncError);

      return error(
        err.map((e) => e.error),
        err.map((e) => e.stackTrace),
      );
    } else if (any((element) => element is AsyncLoading)) {
      return loading();
    } else if (every((element) => element is AsyncData)) {
      return data(map((e) => e.value as T));
    } else {
      return error([], []);
    }
  }
}

it's not perfect but it does the job quite well IMO

to use it you can use it as a regular when but decomposing the data iterable :

like

[async1, async2, async3].when(
    data: (data) {
        data1 = data.elementAt(0) as Type1?;
        data2 = data.elementAt(1) as Type2?;
    }

and then check for null values and if needed return a AsyncError.

@rifkyputra
Copy link

rifkyputra commented Jan 19, 2023

another way to "merging" AsyncValue together, is to create a new provider.

final isLoggedIn = FutureProvider<bool>((ref) async {
  final user = await ref.watch(userProvider);
  final profile = await ref.watch(profileProvider);

  return user != null && profile != null;
});


@PollyGlot
Copy link

another way to "merging" AsyncValue together, is to create a new provider.

final isLoggedIn = FutureProvider<bool>((ref) async {
  final user = await ref.watch(userProvider);
  final profile = await ref.watch(profileProvider);

  return user != null && profile != null;
});

That's what I do for now

@ykaito21
Copy link

another way to "merging" AsyncValue together, is to create a new provider.

final isLoggedIn = FutureProvider<bool>((ref) async {
  final user = await ref.watch(userProvider);
  final profile = await ref.watch(profileProvider);

  return user != null && profile != null;
});

I think this await ref.watch(userProvider); need to be await ref.watch(userProvider.future); now, and this way doesn't work well with cache. So, if cache is needed, I did the following.

final isLoggedIn = Provider<AsyncValue<bool>>((ref) async {
  final user = ref.watch(userProvider);
  final profile = ref.watch(profileProvider);
  return user.when(
    data: (user) => profile.when(
      data: (profile) => AsyncData(true),
      loading: () => AsyncData(false),
      error: (e, st) => AsyncData(false),
    ),
    loading: () => AsyncData(false),
    error: (e, st) => AsyncData(false),
  );
});

@kwang87
Copy link

kwang87 commented Jun 28, 2023

so what's the conclusion? ❓

@rrousselGit
Copy link
Owner

Well for starters, "when" is now discouraged in favor of pattern matching.

And pattern matching doesn't quite suffer from this issue as much. Because pattern-matching can have the same "switch" handle loading/error of multiple AsyncValues

@ekerik220
Copy link

How would you handle this with pattern matching in one switch? I haven't played with Dart 3 much yet, so maybe it's an obvious question. I think we could use a record, like switch ((provider1, provider2)) { case (AsyncLoading(), AsyncLoading()): ... } but defining the cases would be a lot of work, especially if we are trying to combine 3+ providers (using a default case helps, but removes the benefits of exhaustive case checking).

@kyeshmz
Copy link

kyeshmz commented Aug 9, 2023

It would also be great to somehow manage an async value that is dependent on another asyncvalue resolving

@rrousselGit rrousselGit removed this from the Planned but not sure when milestone Oct 15, 2023
@jan-Kulpas
Copy link

Could you show an example of having using a switch expression for multiple AsyncValues?

@NeKoFu
Copy link

NeKoFu commented Nov 22, 2023

Could you show an example of having using a switch expression for multiple AsyncValues?

I use a function that takes a tuple as its input.

final serviceAsync = ref.watch(serviceProvider);
final otherServiceAsync = ref.watch(otherServiceProvider);

final widget = switch((serviceAsync, otherServiceAsync)) {
  ( AsyncValue(value: final service, hasValue: true), 
    AsyncValue(value: final otherService, hasValue: true)) 
    => _bodyBuilder(service, otherService),
  (AsyncError(:final error), _) || (_, AsyncError(:final error)) 
    => Text("Error: $error"),
  _ => const LoadingAnimation(),
};

But I'm not satisfied with the error handling. 🤔

@feronetick
Copy link

It seems to me that this new patterns are hard to read and look creepy

I'm trying this one now:

final profileAsync = ref.watch(profileProvider);
final productAsync = ref.watch(productProvider);

return AsyncBuilder(
  values: [profileAsync, productAsync],
  builder: (context) {
    final profile = profileAsync.requireValue;
    final product = productAsync.requireValue;

    return ChildWidget(...);
  },

  // optional, if null default builder used
  loadingBuilder: (context) { 
    return LoadingIndicator();
  },

  // optional, if null default builder used
  errorBuilder: (context, error, stackTrace) {
    return ErrorWidget(error, stackTrace);
  },
);

@NeKoFu
Copy link

NeKoFu commented Nov 23, 2023

For my part, I simplify with the code below

final service = ref.watch(serviceProvider).value;
final other = ref.watch(otherServiceProvider).value;

return switch((service, other)) {
  (var s?, var o?) => _bodyBuilder(s, o),
  _ => const LoadingAnimation(),
};

In this case, the variables 's' and 'o' are non-nullable, and obviously, you should catch errors in a try-catch block at the level you desire.
You will found more informations on pattern types here:
https://dart.dev/language/pattern-types

@VinhNgT
Copy link

VinhNgT commented Apr 7, 2024

This is my approach

class Async2ValueWidget<T, U> extends StatelessWidget {
  final ({AsyncValue<T> value1, AsyncValue<U> value2}) values;
  final bool showLoadingIndicator;
  final Widget? loadingWidget;
  final Widget Function(T data1, U data2) builder;

  const Async2ValueWidget({
    super.key,
    required this.values,
    this.showLoadingIndicator = false,
    this.loadingWidget,
    required this.builder,
  });

  @override
  Widget build(BuildContext context) {
    // When both values are data
    if (values
        case (
          value1: AsyncData<T>(value: final data1),
          value2: AsyncData<U>(value: final data2)
        )) {
      return builder(data1, data2);
    }

    // When one of the values is loading
    if (values
        case (
          value1: AsyncValue<T>(isLoading: final isLoading1),
          value2: AsyncValue<U>(isLoading: final isLoading2)
        )) {
      if (isLoading1 || isLoading2) {
        return loadingWidget ??
            _LoadingIndicator(hidden: !showLoadingIndicator);
      }
    }

    // When one of the values is an error
    if (values
        case (
          value1: AsyncValue<T>(error: final error1, stackTrace: final _),
          value2: AsyncValue<U>(error: final error2, stackTrace: final _)
        )) {
      if (error1 != null) {
        return Center(child: ErrorMessageWidget(error1.toString()));
      }

      if (error2 != null) {
        return Center(child: ErrorMessageWidget(error2.toString()));
      }
    }

    // This should never happen
    throw UnimplementedError();
  }
}

Very easy to read, the only problem with this is you need to specify T and U manually because for some reason 'builder' function can't infer the correct type

Example:

Async2ValueWidget<int?, bool>(
      values: (
        value1: selectedAnswerIndex,
        value2: isExamMode,
      ),
      // selectedAnswerIndexValue is int?, isExamModeValue is bool
      builder: (selectedAnswerIndexValue, isExamModeValue) =>
          ListView.separated(
...

@bannzai
Copy link

bannzai commented May 1, 2024

Hello, everyone.

My approach with async_value_group.

Example:

class TweetsPage extends HookConsumerWidget {
  const TweetsPage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return AsyncValueGroup.group2(
      ref.watch(tweetsProvider),
      ref.watch(userProvider),
    ).when(
      data: (t) => t.$1.isEmpty ? const TweetsEmpty() : TweetsBody(tweets: t.$1, user: t.$2),
      error: (error, st) => ErrorPage(error: error),
      loading: () => const Loading(),
    );
  }
}

While reading through this issue, I also came across examples of pattern matching available from Dart 3, which looks fantastic. For those who still want to use .when, my async_value_group could also be a useful reference.

@Guang1234567
Copy link

Guang1234567 commented May 28, 2024

@rrousselGit 

I merge AsyncValue together by Record(other language named Tuple) in Dart3.

Seems everything is ok as below code:

typedef DependencyServices = ({
  GenerateAppTitle onGenerateAppTitle,
  Iterable<LocalizationsDelegate<dynamic>> delegates,
  Iterable<Locale> supportedLocales,
  LocaleListResolutionCallback localeListResolutionCallback,
  Locale? localeUserPreferred,
});

@Riverpod()
Future<DependencyServices> someServices(SomeServicesRef ref) async {

  return (
    onGenerateAppTitle:
        await ref.watch(onGenerateAppTitleControllerProvider.future),
    delegates: await ref.watch(delegatesControllerProvider.future),
    supportedLocales:
        await ref.watch(supportedLocalesControllerProvider.future),
    localeListResolutionCallback:
        await ref.watch(localeListResolutionCallbackControllerProvider.future),
     localeUserPreferred:
         await ref.watch(localeUserPreferredControllerProvider.future),
  );
}

class MaterialLocalizationApp extends HookConsumerWidget {
  final ThemeData? theme;

  final Widget? home;

  const MaterialLocalizationApp({
    super.key,
    this.theme,
    this.home,
  });

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final AsyncValue<DependencyServices> someServicesAsync =
        ref.watch(someServicesProvider);

    return someServicesAsync.when(
      data: (DependencyServices s) {
        final (
          :onGenerateAppTitle,
          :delegates,
          :supportedLocales,
          :localeListResolutionCallback,
          :localeUserPreferred,
        ) = s;

        /*final AsyncValue<Locale?> localeUserPreferredAsync =
            ref.watch(localeUserPreferredControllerProvider);

        return localeUserPreferredAsync.when(
          data: (Locale? localeUserPreferred) { */
            debugPrint(
                'MaterialLocalizationApp # build  localeUserPreferred = $localeUserPreferred  s(${s.hashCode}) = $s');
            return MaterialApp(
              onGenerateTitle: onGenerateAppTitle,
              localizationsDelegates: delegates,
              supportedLocales: supportedLocales,
              localeListResolutionCallback: localeListResolutionCallback,
              locale: localeUserPreferred,
              theme: theme,
              home: home,
            );
          /*},
          loading: () =>
              const Center(child: CircularProgressIndicator(color: Colors.yellow)),
          error: (err, _) => Center(
            child: Text(
              err.toString(),
              style: const TextStyle(
                color: Colors.yellow,
              ),
            ),
          ),
        );*/
      },
      loading: () =>
          const Center(child: CircularProgressIndicator(color: Colors.red)),
      error: (err, _) => Center(
          child: Text(
        err.toString(),
        style: const TextStyle(
          color: Colors.red,
        ),
      )),
    );
  }
}

But has a bug, it will cause the HomePage's createState() function recalled that losing the counter state.

Could you give me some help? Thanks.

Tip
All providers are keepAlive: true

@nateshmbhat
Copy link

is there a plan to solve this limitation/boilerplateness in riverpod v3 ? any alternative best plan for now ?

@dickermoshe
Copy link

For combining streams, rxdart has this
The implementation is not pretty, but it's works fairly well.

This looks kinda neat:

// Combine the AsyncValue into a single callback
// Each AsyncValue has its own Consumer widget, so rebuilts no trigger all the `watch`s again.
return authProvider.merge(launchStatsProvider).build(
  (context, ref, auth, launchStats) {
    return Text(
        'LoggedIn: ${auth.isAuthenticated} - Total Launches: ${launchStats.totalLaunches}');
  },
  (context, ref, error, stackTrace, erroredProvider) {
    return const Text('Oops, something unexpected happened');
  },
  (context, ref, provider) {
    return const CircularProgressIndicator();
  },
);

@rrousselGit Would you be interested in a PR that add something like this?

Implementation

import 'package:flutter/widgets.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';

extension AsyncValueProviderCombinerExt<T> on ProviderBase<AsyncValue<T>> {
  AsyncValueProviderCombiner2<T, P> merge<P>(ProviderBase<AsyncValue<P>> b) {
    return AsyncValueProviderCombiner2(this, b);
  }
}

typedef OnErrorCombine = Widget Function(
    BuildContext context,
    WidgetRef ref,
    Object error,
    StackTrace stackTrace,
    ProviderBase<AsyncValue<dynamic>> provider);
typedef OnLoadingCombine = Widget Function(BuildContext context, WidgetRef ref,
    ProviderBase<AsyncValue<dynamic>> provider);

class BaseAsyncValueProviderCombiner {
  const BaseAsyncValueProviderCombiner();
  Consumer _makeConsumer(
      Iterable<ProviderBase<AsyncValue<dynamic>>> providers,
      List results,
      Function data,
      OnErrorCombine error,
      OnLoadingCombine loading,
      int providerCount) {
    return Consumer(
      builder: (context, ref, child) {
        if (providers.isEmpty) {
          return data(
              context, ref, results[0], results[1], results[2], results[3]);
        } else {
          return ref.watch(providers.first).when(
              data: (data) => _makeConsumer(providers.skip(1),
                  results..add(data), data, error, loading, providerCount),
              error: (err, st) => error(context, ref, err, st, providers.first),
              loading: () => loading(context, ref, providers.first));
        }
      },
    );
  }
}

class AsyncValueProviderCombiner2<A, B> extends BaseAsyncValueProviderCombiner {
  final ProviderBase<AsyncValue<A>> a;
  final ProviderBase<AsyncValue<B>> b;
  const AsyncValueProviderCombiner2(this.a, this.b);
  AsyncValueProviderCombiner3<A, B, C> merge<C>(ProviderBase<AsyncValue<C>> c) {
    return AsyncValueProviderCombiner3(a, b, c);
  }

  Widget build(Widget Function(BuildContext context, WidgetRef ref, A, B) data,
          OnErrorCombine error, OnLoadingCombine loading) =>
      _makeConsumer([a, b], [], data, error, loading, 2);
}

class AsyncValueProviderCombiner3<A, B, C>
    extends BaseAsyncValueProviderCombiner {
  final ProviderBase<AsyncValue<A>> a;
  final ProviderBase<AsyncValue<B>> b;
  final ProviderBase<AsyncValue<C>> c;
  const AsyncValueProviderCombiner3(this.a, this.b, this.c);
  AsyncValueProviderCombiner4<A, B, C, D> merge<D>(
      ProviderBase<AsyncValue<D>> d) {
    return AsyncValueProviderCombiner4(a, b, c, d);
  }

  Widget build(
          Widget Function(BuildContext context, WidgetRef ref, A, B, C) data,
          OnErrorCombine error,
          OnLoadingCombine loading) =>
      _makeConsumer([a, b, c], [], data, error, loading, 3);
}

class AsyncValueProviderCombiner4<A, B, C, D>
    extends BaseAsyncValueProviderCombiner {
  final ProviderBase<AsyncValue<A>> a;
  final ProviderBase<AsyncValue<B>> b;
  final ProviderBase<AsyncValue<C>> c;
  final ProviderBase<AsyncValue<D>> d;
  const AsyncValueProviderCombiner4(this.a, this.b, this.c, this.d);

  AsyncValueProviderCombiner5<A, B, C, D, E> merge<E>(
      ProviderBase<AsyncValue<E>> e) {
    return AsyncValueProviderCombiner5(a, b, c, d, e);
  }

  Widget build(
          Widget Function(BuildContext context, WidgetRef ref, A, B, C, D) data,
          OnErrorCombine error,
          OnLoadingCombine loading) =>
      _makeConsumer([a, b, c, d], [], data, error, loading, 4);
}

class AsyncValueProviderCombiner5<A, B, C, D, E>
    extends BaseAsyncValueProviderCombiner {
  final ProviderBase<AsyncValue<A>> a;
  final ProviderBase<AsyncValue<B>> b;
  final ProviderBase<AsyncValue<C>> c;
  final ProviderBase<AsyncValue<D>> d;
  final ProviderBase<AsyncValue<E>> e;
  const AsyncValueProviderCombiner5(this.a, this.b, this.c, this.d, this.e);

  AsyncValueProviderCombiner6<A, B, C, D, E, F> merge<F>(
      ProviderBase<AsyncValue<F>> f) {
    return AsyncValueProviderCombiner6(a, b, c, d, e, f);
  }

  Widget build(
          Widget Function(BuildContext context, WidgetRef ref, A, B, C, D, E)
              data,
          OnErrorCombine error,
          OnLoadingCombine loading) =>
      _makeConsumer([a, b, c, d, e], [], data, error, loading, 5);
}

class AsyncValueProviderCombiner6<A, B, C, D, E, F>
    extends BaseAsyncValueProviderCombiner {
  final ProviderBase<AsyncValue<A>> a;
  final ProviderBase<AsyncValue<B>> b;
  final ProviderBase<AsyncValue<C>> c;
  final ProviderBase<AsyncValue<D>> d;
  final ProviderBase<AsyncValue<E>> e;
  final ProviderBase<AsyncValue<F>> f;
  const AsyncValueProviderCombiner6(
      this.a, this.b, this.c, this.d, this.e, this.f);

  AsyncValueProviderCombiner7<A, B, C, D, E, F, G> merge<G>(
      ProviderBase<AsyncValue<G>> g) {
    return AsyncValueProviderCombiner7(a, b, c, d, e, f, g);
  }

  Widget build(
          Widget Function(BuildContext context, WidgetRef ref, A, B, C, D, E, F)
              data,
          OnErrorCombine error,
          OnLoadingCombine loading) =>
      _makeConsumer([a, b, c, d, e, f], [], data, error, loading, 6);
}

class AsyncValueProviderCombiner7<A, B, C, D, E, F, G>
    extends BaseAsyncValueProviderCombiner {
  final ProviderBase<AsyncValue<A>> a;
  final ProviderBase<AsyncValue<B>> b;
  final ProviderBase<AsyncValue<C>> c;
  final ProviderBase<AsyncValue<D>> d;
  final ProviderBase<AsyncValue<E>> e;
  final ProviderBase<AsyncValue<F>> f;
  final ProviderBase<AsyncValue<G>> g;
  const AsyncValueProviderCombiner7(
      this.a, this.b, this.c, this.d, this.e, this.f, this.g);

  AsyncValueProviderCombiner8<A, B, C, D, E, F, G, H> merge<H>(
      ProviderBase<AsyncValue<H>> h) {
    return AsyncValueProviderCombiner8(a, b, c, d, e, f, g, h);
  }

  Widget build(
          Widget Function(
                  BuildContext context, WidgetRef ref, A, B, C, D, E, F, G)
              data,
          OnErrorCombine error,
          OnLoadingCombine loading) =>
      _makeConsumer([a, b, c, d, e, f, g], [], data, error, loading, 7);
}

class AsyncValueProviderCombiner8<A, B, C, D, E, F, G, H>
    extends BaseAsyncValueProviderCombiner {
  final ProviderBase<AsyncValue<A>> a;
  final ProviderBase<AsyncValue<B>> b;
  final ProviderBase<AsyncValue<C>> c;
  final ProviderBase<AsyncValue<D>> d;
  final ProviderBase<AsyncValue<E>> e;
  final ProviderBase<AsyncValue<F>> f;
  final ProviderBase<AsyncValue<G>> g;
  final ProviderBase<AsyncValue<H>> h;
  const AsyncValueProviderCombiner8(
      this.a, this.b, this.c, this.d, this.e, this.f, this.g, this.h);

  AsyncValueProviderCombiner9<A, B, C, D, E, F, G, H, I> merge<I>(
      ProviderBase<AsyncValue<I>> i) {
    return AsyncValueProviderCombiner9(a, b, c, d, e, f, g, h, i);
  }

  Widget build(
          Widget Function(
                  BuildContext context, WidgetRef ref, A, B, C, D, E, F, G, H)
              data,
          OnErrorCombine error,
          OnLoadingCombine loading) =>
      _makeConsumer([a, b, c, d, e, f, g, h], [], data, error, loading, 8);
}

class AsyncValueProviderCombiner9<A, B, C, D, E, F, G, H, I>
    extends BaseAsyncValueProviderCombiner {
  final ProviderBase<AsyncValue<A>> a;
  final ProviderBase<AsyncValue<B>> b;
  final ProviderBase<AsyncValue<C>> c;
  final ProviderBase<AsyncValue<D>> d;
  final ProviderBase<AsyncValue<E>> e;
  final ProviderBase<AsyncValue<F>> f;
  final ProviderBase<AsyncValue<G>> g;
  final ProviderBase<AsyncValue<H>> h;
  final ProviderBase<AsyncValue<I>> i;
  const AsyncValueProviderCombiner9(
      this.a, this.b, this.c, this.d, this.e, this.f, this.g, this.h, this.i);

  AsyncValueProviderCombiner10<A, B, C, D, E, F, G, H, I, J> merge<J>(
      ProviderBase<AsyncValue<J>> j) {
    return AsyncValueProviderCombiner10(a, b, c, d, e, f, g, h, i, j);
  }

  Widget build(
          Widget Function(BuildContext context, WidgetRef ref, A, B, C, D, E, F,
                  G, H, I)
              data,
          OnErrorCombine error,
          OnLoadingCombine loading) =>
      _makeConsumer([a, b, c, d, e, f, g, h, i], [], data, error, loading, 9);
}

class AsyncValueProviderCombiner10<A, B, C, D, E, F, G, H, I, J>
    extends BaseAsyncValueProviderCombiner {
  final ProviderBase<AsyncValue<A>> a;
  final ProviderBase<AsyncValue<B>> b;
  final ProviderBase<AsyncValue<C>> c;
  final ProviderBase<AsyncValue<D>> d;
  final ProviderBase<AsyncValue<E>> e;
  final ProviderBase<AsyncValue<F>> f;
  final ProviderBase<AsyncValue<G>> g;
  final ProviderBase<AsyncValue<H>> h;
  final ProviderBase<AsyncValue<I>> i;
  final ProviderBase<AsyncValue<J>> j;
  const AsyncValueProviderCombiner10(this.a, this.b, this.c, this.d, this.e,
      this.f, this.g, this.h, this.i, this.j);

  Widget build(
          Widget Function(BuildContext context, WidgetRef ref, A, B, C, D, E, F,
                  G, H, I, J)
              data,
          OnErrorCombine error,
          OnLoadingCombine loading) =>
      _makeConsumer(
          [a, b, c, d, e, f, g, h, i, j], [], data, error, loading, 10);
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request riverpod_generator
Projects
None yet
Development

No branches or pull requests