How to deal with login?
Introduction:
At the data layer I have retrofit-interface. For example:
public interface SomeApi {
@POST(...)
Observable login(LoginRequest body);
}
At the domain layer I have AccountManger:
public class AccountManager {
private SomeApi api;
@Inject
public AccountManager(SomeApi api) { // errot: SomeApi out of classpath
this.api = api;
}
public void login(String login, String password, Subscriber<Void> subscriber) {
api.login(...) ...
}
}
And then I would use AccountManagerin presentation layer.
But. Nope. Compile time error.
SomeApi is in data module classpath, because domain module has no gradle dependency on data module as data module has android dependency while domain module hasn't.
There is my solution:
Create LoginApi interface:
public interface LoginApi {
Observable login(LoginRequest body);
}
Use it in AccountManager:
public class AccountManager {
private LoginApi api;
@Inject
public AccountManager(LoginApi api) {
this.api = api;
}
...
}
Create implementation of LoginApi on data-layer that use SomeApi:
public class LoginApiImpl implement LoginApi {
private SomeApi api;
@Inject
public LoginApiImpl(SomeApi api) {
this.api = api;
}
Observable login(LoginRequest body) {
return api.login(body);
}
}
And inject it via Dagger 2 in presentation layer.
Any other ideas?
It seems like your original solution had a data module dependency trying to be used in the domain module. In this architecture, that is not possible.
To keep in-line with what @android10 has created, you may have to create a LoginRepository (or just add a login function to the UserRepository in the existing implementation) like so:
public interface LoginRepository {
Observable logUserIn(String email, String password)
}
Still in the domain layer, have a LoginUseCase that uses the LoginRepository as a component, like so:
public class LoginUseCase extends UseCase {
private final int userId;
private final LoginRepository loginRepository;
private final String email;
private final String password;
@Inject
public LoginUseCase(int userId, LoginRepository loginRepository,
ThreadExecutor threadExecutor, PostExecutionThread postExecutionThread,
String email, String password) {
super(threadExecutor, postExecutionThread);
this.userId = userId;
this.loginRepository = loginRepository;
this.email = email;
this.password = password;
}
@Override protected Observable buildUseCaseObservable() {
return this.loginRepository.logUserIn(email, password);
}
}
The data layer process might be a bit more involved. This is where you would actually implement your repository pattern for the login case. If you want some sort of login persistence, you will need to implement some form of SharedPreferences or SQLite for storing the data on a successful login, as well as an API to handle the initial request. I won't get too much into that here, as it is quite involved depending on your implementation.
In the presentation layer, you would inject your LoginRepository from the domain layer to use wherever that dependency is needed. It would be advantageous to check if a user is logged in, and have signOut and other important methods in the repository to maintain persistence.
This isn't a great implementation by any means, and may even be too complex for your needs. I'm still learning how this architecture works myself, and this is just how I see that a login implementation may work.
Hope this helps. Let me know if you have any questions.
Yep, thanks, but I already came up with the same solution :)
(it described under There is my solution:).
But Repository mustn't do things like authentification. It must just to provide data. In my case LoginRepository is named as LoginApiMediator.
As I think, UseCases is too complex and I came up with Managers. For example, AccountManager does functions of LoginUseCase, LogoutUseCase, ChangePasswordUseCase etc
Some code as bonus:
@Module
public class AccountManagerModule {
@PerApplication
@Provides
public AccountApiMediator provideAccountApiMediator(AccountApi accountApi) {
return new AccountApiMediatorImpl(accountApi);
}
@PerApplication
@Provides
public AccountManager provideAccountManager(
@IoScheduler Scheduler ioScheduler,
@UiScheduler Scheduler uiScheduler,
AccountApiMediator accountApiMediator,
AccountManagerMetaDataStorage metaDataStorage
) {
return new AccountManager(ioScheduler, uiScheduler, accountApiMediator, metaDataStorage);
}
}
public class AccountApiMediatorImpl implements AccountApiMediator {
private AccountApi accountApi;
public AccountApiMediatorImpl(AccountApi accountApi) {
this.accountApi = accountApi;
}
private LoginResponseMapper loginResponseMapper = new LoginResponseMapper();
@Override
public Observable<LoginData> login(String login, String password) {
return accountApi.login(new LoginRequest(login, password));
}
@Override
public Observable<Void> logout() {
return accountApi.logoff();
}
@Override
public Observable<Void> changePassword(String oldPassword, String newPassword) {
// newPassword and newPassword2 are equals as it already checked at presentation level and there is no needs to check it on server
return accountApi.changePassword(new ChangePasswordRequest(oldPassword, newPassword, newPassword));
}
}
@Try4W Hi, if you can provide some more code or demo to deal with the login API then it will be helpful to me.
Retrofit or any other http library is an implementation detail. Basically from my perspective you should try to follow up what is done with other use cases, to keep consistency across the codebase.
LoginView -> LoginPresenter -> LoginUseCase -> UserRepository
I would reuse the UserRepository since it makes sense in this case:
userRepository.loginUser(user);
At a data source level you would get the user data from the local storage based on your business rules, otherwise you might try to connect to the api. And of course afterwards go all the way up back to the presentation layer where you would reflect the result at UI level.
I would also quote this from @Trikke:
Well, the first question should always be, do i really need a context here and why? Since i don't know how your SessionManager works, i can only guess. So the first thing to do is see if you can modify your Managers to work without.
You shouldn't be creating layers for any type of generalised action. Stuff like Security is actually a cross-cutting concern. This means that it is a topic that doesn't belong in a specific layer.
So your Session logic and Authorization logic belongs in many layers. For example :
- Part of user authorisation is a login screen, and this is presentation logic
- The bit where you check the details and pass them on to the data layer, or infrastructure layer belongs > in the business layer.
- If you store your Session/User locally, this belongs in the data layer.
The hard part for you seems to be that Authorization seems tightly coupled with your REST service. So it would seem logical to have the Session Manager (preferably without a Context) in the same layer as your REST service. This can be data, or infrastructure.
And if you wanna follow up the discussion, here it is: https://github.com/android10/Android-CleanArchitecture/issues/151
Hi @android10 Suppose that I have SessionManager on the data layer to control user state on the application. Somewhere on presenter I would like to check whether the user logged in or not. How can I handle that case?
@tigerteamdc2 I would encapsulate everything in a UseCase class and inject it wherever you need.
@android10 How could I do using UseCase? Could I just do something like
if (sessionManager.isLoggedIn()) {
// Do something
} else {
// Go to login page
}
Please give some advices on this!
