In this blog we are going to create an app with a login feature following best practices in android. If you are not familiar with best practices checkout the blog on android best practices Android best practice.
Things we are going to do and learn in this blog?
- Create Login activity and its layout with email, password input field and a button to login.
- Integrate jetpack architecture to use viewmodels, data binding and view binding.
- Handle orientation changes.
- Use hilt for dependency injection.
- Write unit and instrumented test cases
Let’s start with creating LoginActivty.kt and activity_login.xml layout file.
activity_login.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
android:padding="16dp"
tools:context=".ui.login.LoginActivity">
<EditText
android:id="@+id/email_et"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/enter_email_address"
android:inputType="textEmailAddress" />
<EditText
android:id="@+id/password_et"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:hint="@string/enter_password"
android:inputType="textPassword" />
<Button
android:id="@+id/login_btn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/login" />
</LinearLayout>
LoginActivity.kt
class LoginActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_login)
}
}
Next step let’s enable Data binding and view binding in our application.
Add these lines in the build.gradle file to enable viewBinding and data binding.
Checkout the MVVM architecture blog to know more about viewBinding and DataBinding.
plugins {
id 'kotlin-kapt'
}
android {
buildFeatures {
viewBinding true
dataBinding true
}
}
Click sync and rebuild the project.
View binding – will provide a Binding class for each XML layout file. This will reduce the use of findViewById to access the views in the layout.
Data binding – Will provide a way to bind the view model with the view.
To know more about data binding and view binding check out the doc – https://developer.android.com/topic/libraries/view-binding
After view binding is added ActivityLoginBinding file will be generated. We can use this binding file to access the views.
class LoginActivity : AppCompatActivity() {
private lateinit var mBinding: ActivityLoginBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mBinding = ActivityLoginBinding.inflate(layoutInflater)
setContentView(mBinding.root)
}
}
Next we need to save the input the user enters. This data should be saved when the user rotates the device.To survive orientation change there are multiple ways to save the data. In this blog we are going to use viewModels to save and bind data with the view.
To know more about viewModels checkout the documentation – https://developer.android.com/topic/libraries/architecture/viewmodel
To bind data with the layout we need to create LoginActivityViewmodel.kt file to save email and password.
class LoginActivityViewModel : ViewModel() {
val emailMLD = MutableLiveData<String>()
val passwordMLD = MutableLiveData<String>()
val isLoginEnabled = MediatorLiveData<Boolean>()
init {
isLoginEnabled.addSource(emailMLD) {
isLoginEnabled.postValue(isValidEmailAndPasswords())
}
isLoginEnabled.addSource(passwordMLD) {
isLoginEnabled.postValue(isValidEmailAndPasswords())
}
}
private fun isValidEmailAndPasswords(): Boolean {
return EmailValidator.isValidEmail(emailMLD.value) && passwordMLD.value != null && passwordMLD.value?.length!! > 4
}
}
Login button will be enabled only when the user enters a valid email id and password. To validate the email address let’s create an EmailValidator.kt file to validate the given email.
object EmailValidator {
fun isValidEmail(email: String?): Boolean {
if (email.isNullOrEmpty()) {
return false
}
return PatternsCompat.EMAIL_ADDRESS.matcher(email).matches()
}
}
Now let’s bind the view model with the view.
In the layout xml file we need to declare a viewmodel that needs to be binded.
To do that add the following code as below.
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="viewModel"
type="com.aswin.androidpatterns.ui.login.LoginActivityViewModel" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
android:padding="16dp"
tools:context=".ui.login.LoginActivity">
<EditText
android:id="@+id/email_et"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/enter_email_address"
android:inputType="textEmailAddress"
android:text="@={viewModel.emailMLD}" />
<EditText
android:id="@+id/password_et"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:hint="@string/enter_password"
android:inputType="textPassword"
android:text="@={viewModel.passwordMLD}" />
<Button
android:id="@+id/login_btn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:enabled="@{viewModel.isLoginEnabled}"
android:onClick="@{()-> viewModel.login()}"
android:text="@string/login" />
</LinearLayout>
</layout>
We have used the viewmodel in the xml file to bind the data. Now we just need to initialize the viewmodel and bind it with the view binder.
mViewModel = ViewModelProvider(this).get(LoginActivityViewModel::class.java)
Adding this line of code will create an instance of the view model.
There is another way to get the view model provided by the fragment dependency.
implementation 'androidx.fragment:fragment-ktx:1.3.6'
Add this line of code in the LoginActivity.kt file.
private val mViewModel: LoginActivityViewModel by viewModels()
The viewModels() will provide the required viewModel instance.
Set the lifecycle of the binding and assign the created viewModel to the viewModel variable we declared in the xml file.
class LoginActivity : AppCompatActivity() {
private lateinit var mBinding: ActivityLoginBinding
private val mViewModel: LoginActivityViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mBinding = ActivityLoginBinding.inflate(layoutInflater)
setContentView(mBinding.root)
initViewModel()
}
private fun initViewModel() {
mBinding.lifecycleOwner = this
mBinding.viewModel = mViewModel
}
}
Now the inputs given by the user will survive orientation change.
Next step is to handle the login button click action.
When a user clicks on the login button we need to make the login api call with the email and password. Repositories are responsible to provide the required api services.
Let’s create LoginRepository.kt in a new directory repository.login .
And we need to create an interface to implement the methods for calling api services. This interface can also be used in creating fake repositories for testing.
interface LoginRepositoryInterface {
suspend fun loginUser(email: String, password: String): Boolean
}
Implement this interface in the LoginRepository.kt
class LoginRepository : LoginRepositoryInterface {
override suspend fun loginUser(email: String, password: String): Boolean {
return email == "user@mail.com" && password == "password"
}
}
For login, let’s return true if the email id equals “user@mail.com” and password equals “password”.
To know more about Kotlin coroutines check out this blog Kotlin coroutines.
We have created the repository that provides the login service. Now we need to use this repository inside the viewmodel.
We cannot create an instance of the LoginRepository.kt inside the viewmodel as it will create dependency between the viewmodel and repository.
To avoid dependency we can pass an instance of the LoginRepository inside the viewmodel. This is called dependency injection. Dependency injection is nothing but making a class loosely coupled with the dependencies it requires.
Passing a LoginRepository instance to the viewModel will make it hard for testing, as we don’t test the repository or viewmodel with actual rest api call.
To make it simpler we pass the instance of the LoginRepositoryInterface to the viewModel. This way we can create another repository which implements this interface and pass it to the viewmodel.
class LoginActivityViewModel(private val loginRepository: LoginRepositoryInterface) : ViewModel() {
...
}
This way is traditionally called as manual dependency injection.
Now we need to use the repository to make the api call and handle the callback.
In the LoginActivityViewModel.kt let’s add two mutable live data to observe the api error and login status.
val errorMLD = MutableLiveData<String>()
val loginSuccessMLD = MutableLiveData<Boolean>()
errorMLD will be observed in the activity and it will show a toast if any error is thrown in the api call.
If loginSuccessMLD is set to true it will navigate to the product screen.
Call this method inside the initViewModel() to start observing.
private fun attachObservers() {
mViewModel.errorMLD.observe(this) {
if (it != null) {
Toast.makeText(this, it, Toast.LENGTH_SHORT).show()
mViewModel.errorMLD.value = null
}
}
mViewModel.loginSuccessMLD.observe(this) {
if (it) {
startActivity(Intent(this, ProductActivity::class.java))
finish()
}
}
}
Now let’s move back inside the LoginActivityViewMode.kt. When a user clicks on the login button we need to make the api call and handle the error and response.
Since loginRepository.loginUser(…) is a suspended call we need to call this function from a coroutine scope.
To know more about kotlin coroutines check out the blog on Kotlin coroutines
fun login() {
if (emailMLD.value != null && passwordMLD.value != null) {
viewModelScope.launch {
val result = loginRepository.loginUser(emailMLD.value!!, passwordMLD.value!!)
if (!result) {
errorMLD.postValue("user not found")
}
loginSuccessMLD.postValue(result)
}
}
}
Call this method from the layout file, in the button click action.
<Button
android:id="@+id/login_btn"
...
android:onClick="@{()-> viewModel.login()}"
android:text="@string/login" />
We have manually injected the repository to our viewmodel. Now let’s see how we can use Hilt for dependency injection to reduce the boiler plate codes needed while doing manual dependency injection.
To know more about dependency injection(DI) checkout this Dependency Injection
Add the following dependencies in the gradle file to integrate Hilt.
https://developer.android.com/training/dependency-injection/hilt-android check out the documentation to set up hilt.
Project level build.gradle
buildscript {
...
dependencies {
...
classpath 'com.google.dagger:hilt-android-gradle-plugin:2.28-alpha'
}
}
App level build.gradle
...
apply plugin: 'kotlin-kapt'
apply plugin: 'dagger.hilt.android.plugin'
android {
...
}
dependencies {
implementation "com.google.dagger:hilt-android:$hilt_version"
kapt "com.google.dagger:hilt-compiler:$hilt_version"
}
Click sync.
Create an application class and annotate with @HiltAndoridApp
@HiltAndroidApp
class AndroidPatternsApplication : Application()
Add this application to the manifest.
<application
android:name=".AndroidPatternsApplication" … </application>
To notify hilt to inject the dependencies we need to annotate the activity/fragments with @AndroidEntryPoint
@AndroidEntryPoint
class LoginActivity : AppCompatActivity() { ... }
And in the view model we need to add the following annotations.
@HiltViewModel
class LoginActivityViewModel @Inject constructor(private val loginRepository: LoginRepositoryInterface) :
ViewModel() { ... }
Since we passed the LoginRepositoryInterface, hilt doesn’t know how to create the instance of the login repository. We need to create and provide the required instance of the repository.
Create an AppModule object inside ‘di’ directory and annotate the class as below.
@Module
@InstallIn(SingletonComponent::class)
object AppModule {
@Singleton
@Provides
fun loginRepository(): LoginRepositoryInterface {
return LoginRepository()
}
}
That’s it we have completed the setup of hilt and injected the repository inside the viewmode. Now the view model can use the repository to make api calls.
And that’s it. We have successfully created a login flow with best practices.
Next step let’s see how we can write test cases for the login flow. To know more about test check out this blog – Testing
Hope this blog helps you in understanding and implementing some of the best practices like MVVM architecture, Dependency injection and writing unit / instrumented test cases.
Check out the other blogs Android best practice to know more about writing clean and testable code in the upcoming features.
Happy learning
Team Appmetry