felangel / bloc

A predictable state management library that helps implement the BLoC design pattern
https://bloclibrary.dev
MIT License
11.79k stars 3.39k forks source link

[Login and Firebase Login] Understand User Storage #2033

Closed dmayle closed 3 years ago

dmayle commented 3 years ago

Is your feature request related to a problem? Please describe. I'm trying to understand the management of storage and caching in a bloc-based application on Flutter, specifically for the login use case. Both the Login and Firebase Login fail to address this in the examples, and so I'm still trying to understand how to properly use Blocs.

In the Login example, there is no connection between the auth repository and the user repository, so following an auth state change in the repository, the auth bloc queries the user repository, which magically generates the User object out of thin air. I fail to see how an actual auth repository would make user details available to the app. Does the auth repository only provide some sort of auth token, and then the user repository use that to query the user? If so, how is the token passed between the two, push to user repository, or pull from auth repository? If the auth repository generates the user, than how is that user passed?

For the firebase login example, the single auth repository avoids the question by relegating all storage of the User object to the firebase package. The user thus ends up being stored in the state of the auth bloc and the firebase package. It's not clear if that means that I should be caching the user object in the auth repository as well as in the auth bloc state, or if that's just an artifact of how the firebase package works.

I think the idea of having state machines in blocs is wonderful, but it seems like the state management is actually being hidden inside of an external package, and not actually handled by the auth bloc in this case. It's just not clear to me yet how to properly handle this.

Describe the solution you'd like I would like some clarity as to how to store this information when actually implementing something similar. In the example would be idea, because it would help all users, but just a response on this issue is enough to provide clarity.

felangel commented 3 years ago

Hi @dmayle ๐Ÿ‘‹ Thanks for opening an issue and sorry for the delayed response. Generally, when I'm not using firebase_auth, I have been gravitating toward just having a UserRepository which exposes methods such as signIn, signOut, and a user getter like:

Stream<User> get user;

I would recommend using something like fresh_dio or fresh_graphql to manage the auth tokens and refresh and would try to keep my blocs as feature-driven as possible. This usually means rather than having a UserBloc which manages the current user, I would have feature-blocs which require user data request the user data from the UserRepository. The caching of the user can be done at the UserRepository and in my opinion this simplifies/standardizes the data flow and dependency graph because you don't end up having many interbloc-dependencies. Hope that helps!

Closing for now but feel free to comment with additional questions and I'm happy to continue the conversation ๐Ÿ‘

dmayle commented 3 years ago

@felangel no worries on the timing... especially since it's the holidays. Thanks for getting back to me!

I'm actually working with GRPC, where auth interceptors are attached at the level of the client, which is typed per-service. This means that I have to choose between either storing the interceptor (and token) in a data package which understands all of the GRPC services I use, or I have to have dependency between two repositories.

Also important, I think, is that I have a logged out user experience, so the app changes (for example, navigation, user menu, features). If I don't have a UserBloc, than each of my feature blocs have to maintain the user state in order to adapt to the user being logged in or not.

What I've started building so far is a UserBloc with UserRepository and a MessagesRepository (example feature repository). When MessagesRepository wants to create a new MessagesClient, it asks UserRepository for the UserInterceptor, and attaches it to the client. The two downsides to this approach so far are the strict dependency, and the difficulty of getting a message to the UserBloc from the feature repositories when an auth error is returned. Ideally, the UserRepository would allow the UserBloc to attach a callback to the UserInterceptor so that I wouldn't need to have an out-of-band response, something like chaining it to the ResponseFuture, though I haven't tried that yet.

dmayle commented 3 years ago

@felangel I'm trying to get my head wrapper around your statement:

This usually means rather than having a UserBloc which manages the current user, I would have feature-blocs which require user data request the user data from the UserRepository.

Would it make sense to than have, for example, a LoginBloc for the login menu, and then a MessagesBloc without having the explicit UserBloc?

felangel commented 3 years ago

Would it make sense to than have, for example, a LoginBloc for the login menu, and then a MessagesBloc without having the explicit UserBloc?

Yup ๐Ÿ‘

dmayle commented 3 years ago

Further along in my code... @felangel How do you handle logout then? By piping through the UserRepository to any feature that needs logout UI? Or by making any feature that needs logout UI include logout in the feature bloc?

cnpog commented 3 years ago

@dmayle I am working on something similar currently but I do not know how I can do the following:

  1. Saving the jwt I receive from my Login grpc service (currently I do not know how to save it at all but I want to save it in storage so when the app opens I do not need to login again)
  2. Saving a User in the repository (The idea is that after I receive the jwt, I can use the token to retrieve User information and store that in the repository in this example)

I am struggling with that since a few days already, started over a few times but could not find a solution yet. Maybe you could give me a hint?

dmayle commented 3 years ago

@cnpog if you store your jwt in a Bloc, than you can switch to HydratedBloc, which will automatically persist the state for you, you just have to define fromJson and toJson methods. In the architecture of bloc, 'repository' is a model-specific library, so you wouldn't store data persistently in the repository, though I suppose you could provide methods to load it to or from hive (a dart/flutter in-app database library).

In terms of persistence, I'm going back and forth between @felangel 's suggestion to eliminate the UserBloc, which would require me to manage persistence at the repository layer (e.g. having the user getter Stream load first from hive, and then persist on each update), or having an actual UserBloc based on HydratedBloc which would handle all of that for me, but then require piping the User into all of my feature repositories so that I can instantiate grpc clients with the auth interceptor.

felangel commented 3 years ago

For managing tokens, I would highly recommend taking a look at fresh.

dmayle commented 3 years ago

@felangel My reading of fresh is that it handles OAuth2 token when using dio or graphql, which makes it mostly useful for third-party authentication. @cnpog may or may not be exposing OAuth2 tokens over GRPC, and I definitely am not using OAuth2, mine is a first-party auth only.

felangel commented 3 years ago

@dmayle fresh is not limited to OAuth2 and can support custom auth solutions as well ๐Ÿ‘

dmayle commented 3 years ago

@cnpog I just added hive to my UserRepository and it took something like 13 lines of code to persist, retrieve and update the token/user.

felangel commented 3 years ago

The storage is fairly simple however I'm guessing you'll also need to send the token with each outbound request.