JonasLevin
JonasLevin

Reputation: 2109

Flutter_bloc: Is there a cleaner way to access the state of a flutter Cubit than a redundant if/else statement?

I'm using flutter_bloc and have created a sessionState that gets triggered when a user has successfully authenticated and the state saves the user object (did).
I use a Cubit to change the state which then results in showing different screens in the ui. But whenever I want to access the did object of the Verified sessionState which holds the user information, I have to create an if else statement in every file to check if the sessionState is Verified and if that is true I can access the did object with state.did.
I would be interested to know if it's possible to provide this did object to all underlying widgets without passing it down manually every widget.
I would like to be able to just access the did object from the context so that I can access it everywhere below where it is provided.
My SessionState:

abstract class SessionState {}

class UnkownSessionState extends SessionState {}

class Unverified extends SessionState {}

class Verified extends SessionState {
  Verified({required this.did});

  final Did did;
}

The SessionCubit launches states that I use to define the global state of the app and show different screens as a result.

class SessionCubit extends Cubit<SessionState> {
  SessionCubit(this.commonBackendRepo) : super(UnkownSessionState()) {
    attemptGettingDid();
  }

  final CommonBackendRepo commonBackendRepo;
  final SecureStorage secureStorage = SecureStorage();

  Future<void> attemptGettingDid() async {
    try {
        //more logic
        emit(Unverified());
    } catch (e) {
      emit(Unverified());
    }
  }

  void showUnverified() => emit(Unverified());
  void showSession(Did did) {
    emit(Verified(did: did));
  }
}

This is how I currently access the did object in every file:

BlocBuilder<SessionCubit, SessionState>(builder: (context, state) {
  if (state is Verified) {
    return CustomScrollView(
      slivers: <Widget>[
        SliverAppBar(
          elevation: 0.0,
          backgroundColor: Theme.of(context).scaffoldBackgroundColor,
          title: Text(
            state.did.username,
            style: Theme.of(context).textTheme.headline5,
          ),
        ),
        SliverToBoxAdapter(
            child: Padding(
          padding: const EdgeInsets.all(20.0),
          child: CredentialDetailsView(),
        ))
      ],
    );
  } else {
    return const Text("Unverified");
  }
});

Edit:
That's how I would imagine the optimal solution to my scenario:

  1. Create if/else to check if state is verfied in one parent widget.
  2. If state is verfied create a Provider as a child with the 'did' Object so that the children of this Provider don't have to access the SessionState but can just access the provided did object.

Response to Robert Sandberg's answer
I also have an AppNavigator that either start the authNavigator or sessionNavigator. So the sessionNavigator is the parent widget of all widgets that get shown only when the state is Verified. So I think this would be a great place to wrap the sessionNavigator with the Provider.value, as you explained it.
I wrapped my SessionNavigator with the Provider.value widget and provided the state object:

class AppNavigator extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocBuilder<SessionCubit, SessionState>(
      builder: (context, state) {
        return Navigator(
          pages: [
            if (state is UnkownSessionState)
              MaterialPage(child: StartupScreen()),
            //show auth flow
            if (state is Unverified)
              MaterialPage(
                  child: BlocProvider(
                create: (context) => AuthCubit(context.read<SessionCubit()),
                child: AuthNavigator(),
              )),
            //show session flow
            if (state is Verified)
              MaterialPage(
                  child: Provider.value(
                // here I can access the state.did object
                value: state,
                child: SessionNavigator(),
              ))
          ],
          onPopPage: (route, result) => route.didPop(result),
        );
      },
    );
  }
}

For testing I tried to watch the provided value inside of my Home screen which is a child of the SessionNavigator:

...
class _HomeState extends State<Home> {
  @override
  Widget build(BuildContext context) {
    //
    final sessionState = context.watch<SessionState>();
    return CustomScrollView(
      slivers: <Widget>[
        SliverAppBar(
          floating: true,
          expandedHeight: 60.0,
          backgroundColor: Colors.white,
          flexibleSpace: FlexibleSpaceBar(
              title: Text(
                // I can't access state.did because value is of type SessionState
                sessionState.did,
                style: Theme.of(context).textTheme.headline6,
              ),
              titlePadding:
                  const EdgeInsetsDirectional.only(start: 20, bottom: 16)),
        )
      ],
    );
  }
}

But because the type of my value is SessionState I can't access the did object of the underlying Verified state. I though about providing the state.did object but I don't know how I would watch for that type.
I also got the error that my home screen couldn't find the Provider in the build context. Could that happen because my Home screen has a Navigator as parent?
This is a visualization of my app architecture: enter image description here

Upvotes: 0

Views: 2205

Answers (2)

Robert Sandberg
Robert Sandberg

Reputation: 8645

I use this pattern sometimes, and it sounds like my solution very much fits your imagined optimal solution :)

In a parent:

BlocBuilder<MyBloc, MyBlocState>(
          builder: (context, state) {
            return state.when(
              stateA: () => SomeWidgetA(),
              stateB: () => SomeWidgetB(),
              stateC: (myValue) {
                return Provider.value(
                  value: myValue,
                  child: SomeWidgetC(),
                );
              }),
           }
          )

... and then in SomeWidgetC() and all it's children I do the following to get hold of myValue (where it is of type MyValueType)

final myValue = context.watch<MyValueType>();

If you want, you could also use regular InheritedWidget instead of using the Provider package but you get a lot of nifty features with Provider.

EDIT Response to your further questions.

In your case, if you provide the entire state as you do in AppNavigator, then you should look for Verified state in SessionNavigator as such:

final sessionState = context.watch<Verified>();

Or, if you only need the Did object, then just provide that, so in AppNavigator:

if (state is Verified)
  MaterialPage(
    child: Provider.value(
             value: state.did,
             child: SessionNavigator(),
           ))

And then in SessionNavigator:

final did = context.watch<Did>();

Edit 3: It might be that your problems arises with nested Navigators. Have a look at this SO answer for some guidance.

If you continue struggle, I suggest that you open new SO questions for the specific problems, as this is starting to get a bit wide :) I think that the original question is answered, even though you haven't reached a final solution.

Upvotes: 3

mkobuolys
mkobuolys

Reputation: 5353

You can use something like (state as Verified).did.username, but I would still recommend using the if/else pattern since you ensure that you are using the correct state.

In my personal projects, I use the Freezed package (https://pub.dev/packages/freezed) for such cases since there are some useful methods to cover this. For instance, by using freezed, your issue could be resolved as:

return state.maybeWhen(
  verified: (did) => CustomScrollView(...),
  orElse: () => const Text("Unverified"),
)

Upvotes: 0

Related Questions