Android-CleanArchitecture icon indicating copy to clipboard operation
Android-CleanArchitecture copied to clipboard

How to deal with login?

Open ghost opened this issue 9 years ago • 8 comments

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?

ghost avatar Jun 19 '16 21:06 ghost

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.

Weava avatar Jun 29 '16 14:06 Weava

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));
    }
}

ghost avatar Jun 29 '16 17:06 ghost

@Try4W Hi, if you can provide some more code or demo to deal with the login API then it will be helpful to me.

Dharmendra10 avatar Jun 30 '16 10:06 Dharmendra10

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.

android10 avatar Jul 01 '16 12:07 android10

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

android10 avatar Jul 01 '16 12:07 android10

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 avatar Apr 12 '17 06:04 tigerteamdc2

@tigerteamdc2 I would encapsulate everything in a UseCase class and inject it wherever you need.

android10 avatar Apr 12 '17 08:04 android10

@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!

tigerteamdc2 avatar Apr 12 '17 10:04 tigerteamdc2