8

I want to implement a Login Activity in my android app and I used the Android Studio template for login activities. According to google's documentation here: https://developer.android.com/studio/projects/templates#LoginActivity it should include an AsyncTask that I can use for my purposes however it seems that the current version of the Android Studio no longer provides this.

Current android studio version I'm using is 3.4 (April 10th 2019) and my sdk are:

        minSdkVersion 23
        targetSdkVersion 26

I have a LoginDataSource.java class with the following code that I assume will be used for the authentication work. However when I call my HTTP methods just below the TODO line, it gives me an exception about not being able to do async in the main thread:

/**
 * Class that handles authentication w/ login credentials and retrieves user information.
 */
public class LoginDataSource {

    public Result<LoggedInUser> login(String username, String password) {

        try {
            // TODO: handle loggedInUser authentication
            LoggedInUser fakeUser =
                    new LoggedInUser(
                            java.util.UUID.randomUUID().toString(),
                            "Jane Doe");
            return new Result.Success<>(fakeUser);
        } catch (Exception e) {
            return new Result.Error(new IOException("Error logging in", e));
        }
    }

    public void logout() {
        // TODO: revoke authentication
    }
}

I implemented in a previous project a HTTP call using asynctask but it was in the same activity that used it and not using this template. Where it should be the best place to place the asynctask and call it?

Thanks!

Other classes in the template:

LoginRepository.java

/**
 * Class that requests authentication and user information from the remote data source and
 * maintains an in-memory cache of login status and user credentials information.
 */
public class LoginRepository {

    private static volatile LoginRepository instance;

    private LoginDataSource dataSource;

    // If user credentials will be cached in local storage, it is recommended it be encrypted
    // @see https://developer.android.com/training/articles/keystore
    private LoggedInUser user = null;

    // private constructor : singleton access
    private LoginRepository(LoginDataSource dataSource) {
        this.dataSource = dataSource;
    }

    public static LoginRepository getInstance(LoginDataSource dataSource) {
        if (instance == null) {
            instance = new LoginRepository(dataSource);
        }
        return instance;
    }

    public boolean isLoggedIn() {
        return user != null;
    }

    public void logout() {
        user = null;
        dataSource.logout();
    }

    private void setLoggedInUser(LoggedInUser user) {
        this.user = user;
        // If user credentials will be cached in local storage, it is recommended it be encrypted
        // @see https://developer.android.com/training/articles/keystore
    }

    public Result<LoggedInUser> login(String username, String password) {
        // handle login
        Result<LoggedInUser> result = dataSource.login(username, password);
        if (result instanceof Result.Success) {
            setLoggedInUser(((Result.Success<LoggedInUser>) result).getData());
        }
        return result;
    }
}

LoggedInUserView.java

/**
 * Class exposing authenticated user details to the UI.
 */
class LoggedInUserView implements Serializable {
    private String displayName;
    //... other data fields that may be accessible to the UI

    LoggedInUserView(String displayName) {
        this.displayName = displayName;
    }

    String getDisplayName() {
        return displayName;
    }
}

LoginViewModel.java

public class LoginViewModel extends ViewModel {

    private MutableLiveData<LoginFormState> loginFormState = new MutableLiveData<>();
    private MutableLiveData<LoginResult> loginResult = new MutableLiveData<>();
    private LoginRepository loginRepository;

    LoginViewModel(LoginRepository loginRepository) {
        this.loginRepository = loginRepository;
    }

    LiveData<LoginFormState> getLoginFormState() {
        return loginFormState;
    }

    LiveData<LoginResult> getLoginResult() {
        return loginResult;
    }

    public void login(String username, String password) {
        // can be launched in a separate asynchronous job
        Result<LoggedInUser> result = loginRepository.login(username, password);

        if (result instanceof Result.Success) {
            LoggedInUser data = ((Result.Success<LoggedInUser>) result).getData();
            loginResult.setValue(new LoginResult(new LoggedInUserView(data.getDisplayName())));
        } else {
            loginResult.setValue(new LoginResult(R.string.login_failed));
        }
    }

    public void loginDataChanged(String username, String password) {
        if (!isUserNameValid(username)) {
            loginFormState.setValue(new LoginFormState(R.string.invalid_username, null));
        } else if (!isPasswordValid(password)) {
            loginFormState.setValue(new LoginFormState(null, R.string.invalid_password));
        } else {
            loginFormState.setValue(new LoginFormState(true));
        }
    }

    // A placeholder username validation check
    private boolean isUserNameValid(String username) {
        if (username == null) {
            return false;
        }
        if (username.contains("@")) {
            return Patterns.EMAIL_ADDRESS.matcher(username).matches();
        } else {
            return !username.trim().isEmpty();
        }
    }

    // A placeholder password validation check
    private boolean isPasswordValid(String password) {
        return password != null && password.trim().length() > 5;
    }
}

LoginActivity.java

public class LoginActivity extends AppCompatActivity {

    private LoginViewModel loginViewModel;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_login);
        loginViewModel = ViewModelProviders.of(this, new LoginViewModelFactory())
                .get(LoginViewModel.class);

        final EditText usernameEditText = findViewById(R.id.username);
        final EditText passwordEditText = findViewById(R.id.password);
        final Button loginButton = findViewById(R.id.login);
        final ProgressBar loadingProgressBar = findViewById(R.id.loading);

        loginViewModel.getLoginFormState().observe(this, new Observer<LoginFormState>() {
            @Override
            public void onChanged(@Nullable LoginFormState loginFormState) {
                if (loginFormState == null) {
                    return;
                }
                loginButton.setEnabled(loginFormState.isDataValid());
                if (loginFormState.getUsernameError() != null) {
                    usernameEditText.setError(getString(loginFormState.getUsernameError()));
                }
                if (loginFormState.getPasswordError() != null) {
                    passwordEditText.setError(getString(loginFormState.getPasswordError()));
                }
            }
        });

        loginViewModel.getLoginResult().observe(this, new Observer<LoginResult>() {
            @Override
            public void onChanged(@Nullable LoginResult loginResult) {
                if (loginResult == null) {
                    return;
                }
                loadingProgressBar.setVisibility(View.GONE);
                if (loginResult.getError() != null) {
                    showLoginFailed(loginResult.getError());
                }
                if (loginResult.getSuccess() != null) {
                    updateUiWithUser(loginResult.getSuccess());
                }
                setResult(Activity.RESULT_OK);

                //Complete and destroy login activity once successful
                finish();
            }
        });

        TextWatcher afterTextChangedListener = new TextWatcher() {
            @Override
            public void beforeTextChanged(CharSequence s, int start, int count, int after) {
                // ignore
            }

            @Override
            public void onTextChanged(CharSequence s, int start, int before, int count) {
                // ignore
            }

            @Override
            public void afterTextChanged(Editable s) {
                loginViewModel.loginDataChanged(usernameEditText.getText().toString(),
                        passwordEditText.getText().toString());
            }
        };
        usernameEditText.addTextChangedListener(afterTextChangedListener);
        passwordEditText.addTextChangedListener(afterTextChangedListener);
        passwordEditText.setOnEditorActionListener(new TextView.OnEditorActionListener() {

            @Override
            public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
                if (actionId == EditorInfo.IME_ACTION_DONE) {
                    loginViewModel.login(usernameEditText.getText().toString(),
                            passwordEditText.getText().toString());
                }
                return false;
            }
        });

        loginButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                loadingProgressBar.setVisibility(View.VISIBLE);
                loginViewModel.login(usernameEditText.getText().toString(),
                        passwordEditText.getText().toString());
            }
        });
    }

    private void updateUiWithUser(LoggedInUserView model) {
        String welcome = getString(R.string.welcome) + model.getDisplayName();
        // TODO : initiate successful logged in experience
        Toast.makeText(getApplicationContext(), welcome, Toast.LENGTH_LONG).show();
    }

    private void showLoginFailed(@StringRes Integer errorString) {
        Toast.makeText(getApplicationContext(), errorString, Toast.LENGTH_SHORT).show();
    }
}
Zoe
  • 27,060
  • 21
  • 118
  • 148
Narshe
  • 427
  • 8
  • 22
  • I think you need to look at some Async examples. Here's one. https://stackoverflow.com/a/9671602/10936389 – Adeel Zafar Apr 21 '19 at 22:46
  • I know how to implement an Async task. My question would be more about *where* to implement it inside this template. As I've been unable to make it work by calling .execute() inside the // TODO: handle loggedInUser authentication – Narshe Apr 22 '19 at 00:13
  • 1
    Your question is still about the code, making it irrelevant to the IDE. Where you get the code doesn't matter - as an example, if you found it on GH, that doesn't mean you can tag the question `github` - it's an irrelevant tag. – Zoe Apr 22 '19 at 10:04
  • Did you fixed this? I'm in the same position, a little bit lost with MVVM pattern. – Jon Zangitu Sep 12 '19 at 15:05
  • I am also waiting for an answer. – Rohit Kumar Jul 13 '20 at 05:45

5 Answers5

2

I'm also still searching for a good solution. I ended up implementing 2 Listeners interfaces for the moment.

public interface OnLoginListener {
    void onLoginResult(Result result);
}
public interface OnRegistrationListener {
    void onRegistrationResult(Result result);
}

I made my ViewModel implement these interfaces and change the UI accordantly, I'm not sure this follows the best practices of MVVM Pattern. The LoginDataSource gets passed a reference to the listeners

private OnLoginListener onLoginListener;
private OnRegistrationListener onRegistrationListener;

and calls the onRegistrationResult method from the onPostExecute, or like in my case from the onComplete method of Firebase authentication.

@Override
        public void onComplete(@NonNull Task<AuthResult> task) {
            if (task.isSuccessful()) {
                // Sign in success, update UI with the signed-in user's information
                Log.d(TAG, "createUserWithEmail:success");
                FirebaseUser user = mAuth.getCurrentUser();
                onRegistrationListener.onRegistrationResult(new Result.Success<>(user));
            } else {
                // If sign in fails, display a message to the user.
                Log.w(TAG, "createUserWithEmail:failure", task.getException());
                onRegistrationListener.onRegistrationResult(new Result.Error(task.getException()));
            }
        }

I hope this can be helpful to someone ;).

GMouaad
  • 69
  • 1
  • 6
1

Consider add

new LoginTask().execute();

inside sentence of try, then outside of login method just add the inner class

public class LoginTask extends AsyncTask<Void, String, Void>{
 RestTemplate restTemplate = new RestTemplate();
 ...
}
TuGordoBello
  • 4,350
  • 9
  • 52
  • 78
1

Use kotlinx.coroutines in LoginViewModel:

import kotlinx.coroutines.*

fun login(username: String, password: String) {
    // launched in a separate asynchronous job
    MainScope().launch {
        val result = withContext(Dispatchers.IO) {
            loginRepository.login(username, password)
        }
        if (result is Result.Success) {
            _loginResult.value =
                LoginResult(success = LoggedInUserView(displayName = result.data.displayName))
        } else {
            _loginResult.value = LoginResult(error = R.string.login_failed)
        }
    }
}

Add keyword suspend to fun login() methods in LoginRepository and LoginDataSource.

Handle kotlin coroutine in LoginDataSource e.g. with suspendCoroutine

Add build.gradle dependencies:

implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_version"
User Rebo
  • 3,056
  • 25
  • 30
Yue
  • 19
  • 5
0

be sure everything works.

The best place is LoginViewModel.kt

Here is the example code.


    package com.example.gosoft.ui.login

import android.content.Context
import android.util.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import android.util.Patterns
import com.android.volley.Request
import com.android.volley.Response
import com.android.volley.toolbox.JsonObjectRequest
import com.android.volley.toolbox.Volley
import com.example.gosoft.data.LoginRepository
import com.example.gosoft.data.Result

import com.example.gosoft.R
import org.json.JSONObject

class LoginViewModel(private val loginRepository: LoginRepository) : ViewModel() {

    private val _loginForm = MutableLiveData<LoginFormState>()
    val loginFormState: LiveData<LoginFormState> = _loginForm

    private val _loginResult = MutableLiveData<LoginResult>()
    val loginResult: LiveData<LoginResult> = _loginResult

    fun login(username: String, password: String, context: Context) {
        // can be launched in a separate asynchronous job

        //http request start

        val queue = Volley.newRequestQueue(context)



        val url = "Your URL"
        var name = "test"
        Log.d("name_init",name)

// Request a string response from the provided URL.
        val JsonObjectRequest =  JsonObjectRequest(
            Request.Method.GET, url, null,
            Response.Listener<JSONObject> { response ->
                // Display the first 500 characters of the response string.
                //textView.text = "Response is: ${response.substring(0, 500)}"
                name = "Aram"
                Log.d("name",name)
                Log.d("Response", response.toString())
                val result = loginRepository.login(username, password, name)
                if (result is Result.Success) {
                    _loginResult.value = LoginResult(success = LoggedInUserView(displayName = result.data.displayName))
                } else {
                    _loginResult.value = LoginResult(error = R.string.login_failed)
                }

            },
            Response.ErrorListener { error ->
                name = "No name"
                Log.d("name_error",name)
                Log.d("volley_error", error.toString())
            })

// Add the request to the RequestQueue.
        Log.d("queue_add",name)
        queue.add(JsonObjectRequest)

        //http request end



//        val result = loginRepository.login(username, password)

//        if (result is Result.Success) {
//            _loginResult.value = LoginResult(success = LoggedInUserView(displayName = result.data.displayName))
//        } else {
//            _loginResult.value = LoginResult(error = R.string.login_failed)
//        }
    }

    fun loginDataChanged(username: String, password: String) {
        if (!isUserNameValid(username)) {
            _loginForm.value = LoginFormState(usernameError = R.string.invalid_username)
        } else if (!isPasswordValid(password)) {
            _loginForm.value = LoginFormState(passwordError = R.string.invalid_password)
        } else {
            _loginForm.value = LoginFormState(isDataValid = true)
        }
    }

    // A placeholder username validation check
    private fun isUserNameValid(username: String): Boolean {
        return if (username.contains('@')) {
            Patterns.EMAIL_ADDRESS.matcher(username).matches()
        } else {
            username.isNotBlank()
        }
    }

    // A placeholder password validation check
    private fun isPasswordValid(password: String): Boolean {
        return password.length > 5;
    }
}

0

My answer is a little late, but I was just in this situation. I achieve the async task inside the LoginDataSource object:

/**
 * Class that handles authentication w/ login credentials and retrieves user information.
 */
public class LoginDataSource {
    LoggedInUser fakeUser;

    public Result<LoggedInUser> login(String username, String password) {

        try {
            LoginTask loginTask = new LoginTask();
            synchronized (loginTask) {
                loginTask.execute(map).notify();
            }
            if (fakeUser != null) {
                return new Result.Success<>(fakeUser);
            } else {
                return new Result.Error(new IOException("Usuario o contraseña incorrectos")); // closed hanging quotation
            }
        } catch (Exception e) {
            return new Result.Error(new IOException("Error logging in", e));
        }
    }

    public void logout() {
        // TODO: revoke authentication
    }

    private class LoginTask extends AsyncTask<HashMap<String, String>, Void,  Void> {

        /**
         * Override this method to perform a computation on a background thread. The
         * specified parameters are the parameters passed to {@link #execute}
         * by the caller of this task.
         * <p>
         * This will normally run on a background thread. But to better
         * support testing frameworks, it is recommended that this also tolerates
         * direct execution on the foreground thread, as part of the {@link #execute} call.
         * <p>
         * This method can call {@link #publishProgress} to publish updates
         * on the UI thread.
         *
         * @param hashMaps The parameters of the task.
         * @return A result, defined by the subclass of this task.
         * @see #onPreExecute()
         * @see #onPostExecute
         * @see #publishProgress
         */
        @Override
        protected Void doInBackground(HashMap<String, String>... hashMaps) {
            
            // Use retrofit2 or whatever to get it
            fakeUser =
                    new LoggedInUser(
                            java.util.UUID.randomUUID().toString(),
                            "Jane Doe");
            return null;
        }
    }
}
Community
  • 1
  • 1
Néstor
  • 570
  • 2
  • 8
  • 22