2

I am working in Riverpod Auth flow boilerplate application.

I want to use common loading screen for all async function even login and logout. Currently I have AppState provider if Appstate loading i show loading screen. it's working fine for login but i wonder it’s good way or bad way.

Can i use this loading screen for all async task in the App?

AuthWidget:

class AuthWidget extends ConsumerWidget {
  const AuthWidget({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    AppState appState = ref.watch(appStateProvider);

    if(appState.isLoading){
      return const Center(child: CircularProgressIndicator(color: Colors.red),);
    }
    return appState.isAuthenticated ? const HomePage() : const SignIn();
  }
}

AppState:

class AppState {
  User? user;
  bool isLoading;
  bool isAuthenticated;

  AppState(this.user, this.isLoading, this.isAuthenticated);

}

AuthRepository:

class AuthRepository extends StateNotifier<AppState>{

  AuthRepository() : super(AppState(null,false,false));

  Future<void> signIn()async {
    state = AppState(null,true,false);

    await Future.delayed(const Duration(seconds: 3));
    User user = User(userName: 'FakeUser', email: 'user@gmail.com');
    AppState appState = AppState(user, false, true);
    state = appState;
  }

}

final appStateProvider = StateNotifierProvider<AuthRepository,AppState>((ref){
  return AuthRepository();
});
yathavan
  • 2,051
  • 2
  • 18
  • 25
  • https://stackoverflow.com/questions/65172046/best-way-to-create-a-global-loading-screen-with-bloc ? – Martin Zeitler May 23 '22 at 04:54
  • Can you? Yes Should you use for every async operation? Probably not. For UX reasons and you'd be rebuilding everything after calling it. Better off having dedicated loading states per feature/or widget. – atruby May 23 '22 at 10:54

3 Answers3

3

To answer your question : Yes you can.

The only thing I'd change here is the content of your AppState : I'd use a LoadingState dedicated to trigger your Loader instead.

Here is how I like to manage screens with a common loader in my apps.

1 - Create a LoadingState and provide it

final loadingStateProvider = ChangeNotifierProvider((ref) => LoadingState());

class LoadingState extends ChangeNotifier {
  bool isLoading = false;

  void startLoader() {
    if (!isLoading) {
      isLoading = true;
      notifyListeners();
    }
  }

  void stopLoader() {
    if (isLoading) {
      isLoading = false;
      notifyListeners();
    }
  }
}

2 - Define a base page with the "common" loader

class LoadingContainer extends ConsumerWidget {
  const LoadingContainer({
    Key? key,
    required this.child,
  }) : super(key: key);

  final Widget child;

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final state = ref.watch(loadingStateProvider);
    return Stack(
      children: [
        child,
        if (state.isLoading)
          const Center(child: CircularProgressIndicator())
        else
          const SizedBox(),
      ],
    );
  }
}

3 - Implement this widget whenever I need to handle loading datas.

return Scaffold(
      backgroundColor: AppColor.blue,
      body: LoadingContainer(
        child: ...

And then I simply have to update my loadingStateProvider and it's isLoading value from a Controller or the Widget directly

FDuhen
  • 4,097
  • 1
  • 15
  • 26
1

If you want a centralized/common async calls, the InheritedWidget is ideal for that, you can just add a method and call it from anywhere down stream and because the call is offloaded with async, you can attach extra arguments and add usefull functionality such as a live update instead of relying on stuff like .then(). This example might not be as simple as FDuhen's but you can mix them together if you want to not use keys

AppState now is a widget and contains trigers that rely on global keys to rebuild the correct components, here i assumed that you actualy want to have an common overlay and not a loading screen widget, if not using a Navigator would be batter

Using keys is specially good if you end up implementing something this line, <token> been just a number that references a group of widgets

key: AppState.of(ctx).rebuild_on_triger(<token>)
class App_State_Data {
  GlobalKey? page_key;
  bool is_logged = false;

  bool loading_overlay = false;
  String loading_message = '';
}

class AppState extends InheritedWidget {
  final App_State_Data _state;
  bool get is_logged => _state.is_logged;
  bool get should_overlay => _state.loading_overlay;
  String get loading_message => _state.loading_message;

  void page_rebuild() {
    (_state.page_key!.currentState as _Page_Base).rebuild();
  }

  GlobalKey get page_key {
    if (_state.page_key == null) {
      _state.page_key = GlobalKey();
    }
    return _state.page_key!;
  }

  void place_overlay(String msg) {
    _state.loading_message = msg;
    _state.loading_overlay = true;
    page_rebuild();
  }

  void clear_overlay() {
    _state.loading_message = '';
    _state.loading_overlay = false;
    page_rebuild();
  }

  Future<void> triger_login(String message) async {
    place_overlay(message);
    await Future.delayed(const Duration(seconds: 2));
    _state.is_logged = true;
    clear_overlay();
  }

  Future<void> triger_logout(String message) async {
    place_overlay(message);
    await Future.delayed(const Duration(seconds: 1));
    _state.is_logged = false;
    clear_overlay();
  }

  AppState({Key? key, required Widget child})
      : this._state = App_State_Data(),
        super(key: key, child: child);

  static AppState of(BuildContext ctx) {
    final AppState? ret = ctx.dependOnInheritedWidgetOfExactType<AppState>();
    assert(ret != null, 'No AppState found!');
    return ret!;
  }

  @override
  bool updateShouldNotify(AppState old) => true;
}

Here i added it as the topmost element making it like a global data class with is not necessary, you can split the state content and add just the necessary to where its needed

void main() => runApp(AppState(child: App()));

class App extends StatelessWidget {
  const App({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext ctx) {
    return MaterialApp(
      home: Scaffold(
        body: Page_Base(
          key: AppState.of(ctx).page_key,
        ),
      ),
    );
  }
}

class Page_Base extends StatefulWidget {
  final GlobalKey key;
  const Page_Base({
    required this.key,
  }) : super(key: key);

  @override
  _Page_Base createState() => _Page_Base();
}

class _Page_Base extends State<Page_Base> {
  Widget build_overlay(BuildContext ctx) {
    return Center(
      child: Container(
        width: double.infinity,
        height: double.infinity,
        color: Color(0xC09E9E9E),
        child: Center(
          child: Text(AppState.of(ctx).loading_message),
        ),
      ),
    );
  }

  @override
  Widget build(BuildContext ctx) {
    return Stack(
      children: [
        AppState.of(ctx).is_logged ? Page_Home() : Page_Login(),
        AppState.of(ctx).should_overlay ? build_overlay(ctx) : Material(),
      ],
    );
  }

  void rebuild() {
    // setState() is protected and can not be called
    // from outside of the this. scope
    setState(() => null);
  }
}

Using AppState is the best part, just because the widget does not have to call more than 1 function and it will rebuild with the correct data on complition

class Page_Login extends StatelessWidget {
  const Page_Login({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext ctx) {
    return Center(
      child: InkWell(
        onTap: () => AppState.of(ctx).triger_login('Login'),
        child: Container(
          width: 200,
          height: 200,
          color: Colors.greenAccent,
          child: Text('Page_Login'),
        ),
      ),
    );
  }
}

class Page_Home extends StatelessWidget {
  const Page_Home({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext ctx) {
    return Center(
      child: InkWell(
        onTap: () => AppState.of(ctx).triger_logout('Logout'),
        child: Container(
          width: 200,
          height: 200,
          color: Colors.blueAccent,
          child: Text('Page_Home'),
        ),
      ),
    );
  }
}
SrPanda
  • 854
  • 1
  • 5
  • 9
0

Global loading indicator

If you want a centralized loading indicator to use in your whole app you could take advantage of Overlay's, which flutter already uses for dialogs, popups, bottom sheets etc. This way we don't introduce new widget in the widget tree.

If you only want to toggle between loading states you can use a StateProvider to handle the simple boolean value, else you could create a State/Change Notifier. This way you decouple your loading state from your AppState

final loadingProvider = StateProvider<bool>((ref) => false);

void main() => runApp(const ProviderScope(child: MaterialApp(home: GlobalLoadingIndicator(child: Home()))));


// This widget should wrap your entire app, but be below MaterialApp in order to have access to the Overlay 
class GlobalLoadingIndicator extends ConsumerStatefulWidget {
  final Widget child;

  const GlobalLoadingIndicator({required this.child, Key? key}) : super(key: key);

  @override
  ConsumerState createState() => _GlobalLoadingIndicatorState();
}

class _GlobalLoadingIndicatorState extends ConsumerState<GlobalLoadingIndicator> {
  //We need to cache the overlay entries we are showing as part of the indicator in order to remove them when the indicator is hidden.
 final List<OverlayEntry> _entries = [];

  @override
  Widget build(BuildContext context) {
    ref.listen<bool>(loadingProvider, (previous, next) {
      // We just want to make changes if the states are different
      if (previous == next) return;
      if (next) {
        // Add a modal barrier so the user cannot interact with the app while the loading indicator is visible
        _entries.add(OverlayEntry(builder: (_) => ModalBarrier(color: Colors.black12.withOpacity(.5))));
        _entries.add(OverlayEntry(
            builder: (_) =>const Center(
                child: Card(child: Padding(padding:  EdgeInsets.all(16.0), child: CircularProgressIndicator())))));
      // Insert the overlay entries into the overlay to actually show the loading indicator
        Overlay.of(context)?.insertAll(_entries);
      } else {
        // Remove the overlay entries from the overlay to hide the loading indicator
        _entries.forEach((e) => e.remove());
        // Remove the cached overlay entries from the widget state
        _entries.clear();
      }
    });
    return widget.child;
  }
}

We insert the GlobalLoadingIndicator high up in the widget tree although anywhere below the MaterialApp is fine (as long as it can access the Overlay via context).

The GlobalLoadingIndicator wont create extra widgets in the widget tree, and will only manage the overlays, here I add two overlays, one is a ModalBarrier which the user from interacting with widgets behind itself. And the other the actual LoadingIndicator. You are free to not add the ModalBarrier, or make it dismissible (or even if you decide to create a more complex loadingProvider, customize it in case you need to cater different use cases).

A sample usage after you have this set up is just switching the state of the loadingProvider, most of the times you would do this programatically, but for interactiveness I'll use a Switch :

class Home extends ConsumerWidget {
  const Home({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, ref) {
    final isLoading = ref.watch(loadingProvider);
    return Scaffold(
        appBar: AppBar(),
        body: Center(
          child: SwitchListTile(
            value: isLoading,
            onChanged: (value) {
              ref.read(loadingProvider.notifier).state = value;
              Future.delayed(const Duration(seconds: 4)).then((value) {
                ref.read(loadingProvider.notifier).state = false;
              });
            },
            title: const FlutterLogo(),
          ),
        ));
  }
}

You can fiddle with this snippet in dartpad

Result:

Result

Per Screen/Section loading indicator

As a side note when displaying loading states inside components of the app I recommend you to use an AnimatedSwitcher , as it fades between the widgets , super handy when dealing with screens which can change content abruptly.

final loadingProvider = StateProvider<bool>((ref) => false);

void main() => runApp(ProviderScope(child: MaterialApp(home: Home())));

class Home extends ConsumerWidget {
  const Home({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, ref) {
    final isLoading = ref.watch(loadingProvider);
    return Scaffold(
        appBar: AppBar(),
        body: Center(
          child: SwitchListTile(
            value: isLoading,
            onChanged: (value) {
              ref.read(loadingProvider.notifier).state = value;
            },
            title: AnimatedSwitcher(
              duration: Duration(milliseconds: 400),
              child: isLoading?CircularProgressIndicator():FlutterLogo()
            ),
          ),
        ));
  }
}

Per section loading switcher

croxx5f
  • 5,163
  • 2
  • 15
  • 36