0

I'm completely stuck with the task below. So, the idea is to solve these steps using Riverpod

  1. Fetch data from db with some kind of Future async while pausing the app (display SomeLoadingPage() etc.)

  2. Once the data has loaded:

    2.1 initialize multiple global StateNotifierProviders which utilize the data in their constructors and can further be used throughout the app with methods to update their states.

    2.2 then show MainScreen() and the rest of UI

So far I've tried something like this:

class UserData extends StateNotifier<AsyncValue<Map>> { // just <Map> for now, for simplicity
  UserData() : super(const AsyncValue.loading()) {
    init();
  }

  Future<void> init() async {
    state = const AsyncValue.loading();
    try {
      final HttpsCallableResult response =
      await FirebaseFunctions.instance.httpsCallable('getUserData').call();
      state = AsyncValue.data(response.data as Map<String, dynamic>);
    } catch (e) {
      state = AsyncValue.error(e);
    }}}
final userDataProvider = StateNotifierProvider<UserData, AsyncValue<Map>>((ref) => UserData());

final loadingAppDataProvider = FutureProvider<bool>((ref) async {
  final userData = await ref.watch(userDataProvider.future);
  return userData.isNotEmpty;
});
class LoadingPage extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return FutureBuilder(
      future: ref.watch(loadingAppDataProvider.future),
      builder: (ctx, AsyncSnapshot snap) {
        // everything here is simplified for the sake of a question
        final Widget toReturn;
        if (snap.connectionState == ConnectionState.waiting) {
          toReturn = const SomeLoadingPage();
        } else {
          snap.error != null
          ? toReturn = Text(snap.error.toString())
          : toReturn = const SafeArea(child: MainPage());
        }
        return toReturn;},);}}

I intentionally use FutureBuilder and not .when() because in future i may intend to use Future.wait([]) with multiple futures

This works so far, but the troubles come when I want to implement some kind of update() methods inside UserData and listen to its variables through the entire app. Something like

  late Map userData = state.value ?? {};
  late Map<String, dynamic> settings = userData['settings'] as Map<String, dynamic>;

  void changeLang(String lang) {
    print('change');
    for (final key in settings.keys) {
      if (key == 'lang') settings[key] = lang;
      state = state.whenData((data) => {...data});
    }
  }

SomeLoadingPage() appears on each changeLang() method call.

In short: I really want to have several StateNotifierProviders with the ability to modify their state from the inside and listen to it from outside. But fetch the initial state from database and make the intire app wait for this data to be fetched and these providers to be initilized.

Bohdan
  • 128
  • 6

1 Answers1

0

So, I guess I figured how to solve this:

final futureExampleProvider = FutureProvider<Map>((ref) async {
  final HttpsCallableResult response =
  await FirebaseFunctions.instance.httpsCallable('getUserData').call();

  return response.data as Map;
});

final exampleProvider = StateNotifierProvider<Example, Map>((ref) {

  // we get AsyncValue from FutureNotifier
  final data = ref.read(futureExampleProvider);
  // and wait for it to load
  return data.when(
    // in fact we never get loading state because of FutureBuilder in UI
    loading: () => Example({'loading': 'yes'}),
    error: (e, st) => Example({'error': 'yes'}),
    data: (data) => Example(data),
  );
});
class LoadingPage extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return FutureBuilder(
      // future: ref.watch(userDataProvider.future),
      future: ref.watch(futureExampleProvider.future),
      builder: (ctx, AsyncSnapshot snap) {
        final Widget toReturn;
        if (snap.data != null) {
          snap.error != null
          ? toReturn = Text(snap.error.toString())
          : toReturn = const SafeArea(child: MainPage());
        } else {
          // this is the only 'Loading' UI the user see before everything get loaded
          toReturn = const Text('loading');
        }
        return toReturn;
      },
    );
  }
}
class Example extends StateNotifier<Map> {
  Example(this.initData) : super({}) {
    // here comes initial data loaded from FutureProvider
    state = initData;
  }
  // it can be used further to refer to the initial data, kinda like cache
  Map initData;

  // this way we can extract any parts of initData
  late Map aaa = state['bbb'] as Map

  // this method can be called from UI
  void ccc() {
    
    // modify and update data
    aaa = {'someKey':'someValue'};
    // trigger update
    state = {...state};

  }
}

This works for me, at least on this level of complexity. I'll leave question unsolved in case there are some better suggestions.

Bohdan
  • 128
  • 6