前言

登录作为运用程序的核心功用之一。其代码是否易于了解、保护和测验于运用的稳定性至关重要,为了达到这个目标,开发人员需求挑选恰当的架构和形式来安排和办理登录流程。

Android Jetpack供给了一套强大的东西和组件,其间包含MVVM(Model-View-ViewModel)架构形式,为咱们供给了一种高雅的办法来构建可保护、可扩展且易于测验的运用程序。MVVM架构经过将业务逻辑与界面逻辑别离,以及经过数据绑定机制完结视图和ViewModel的交互,使得开发进程愈加简化和高效。

本文将介绍如何运用Android Jetpack的MVVM架构形式来完结登录功用。咱们将运用ViewModel作为衔接视图和数据的中间层,并结合LiveData和Repository来办理数据和进行异步操作。经过这种办法,咱们能够完结呼应式的UI更新,防止内存走漏问题,并使得代码更易于了解、扩展和保护。

在本文中,咱们将逐渐引导您完结一个登录功用的完结,涵盖以下关键方面:

  • 创立ViewModel类并界说LiveData以办理登录状况和结果。
  • 完结Repository类来处理登录数据和操作。
  • 构建与ViewModel相关的UI界面,经过数据绑定机制完结双向绑定和呼应式更新。
  • 异步处理登录恳求,并经过LiveData将结果回来给UI层。
  • 针对不同的登录状况,展示相应的用户界面和反应。

经过本文的学习,您将把握如何运用Android Jetpack的MVVM架构形式来完结登录功用,从而加深对该架构的了解和运用才能,并能够在实践项目中灵活运用MVVM形式构建愈加高雅和可保护的运用程序。

Jetpack架构组件

Jetpack供给了多个强大的组件,其间Lifecycle、LiveData和ViewModel是构建MVVM架构的关键组件。在本文中,咱们将运用这些组件与Kotlin协程协同工作,以完结更高效的MVVM架构。

  1. Lifecycle

Lifecycle是一个用于办理Android组件(如Activity和Fragment)生命周期的库。它供给了一种方便的办法来保证在组件的生命周期发生变化时,相关代码能够主动发动或停止。Lifecycle库经过将组件的生命周期状况与组件的相关操作(如发动和停止服务)进行相关,从而防止了内存走漏和其他相关问题。

  1. LiveData

LiveData是一个具有生命周期感知才能的可调查数据存储类。它供给了一种方便的办法来完结数据驱动的UI,以及保证UI组件和数据存储之间的一致性。LiveData能够感知组件的生命周期状况,并在组件处于激活状况时告诉调查者,从而防止了不必要的数据更新和内存走漏。

  1. ViewModel

ViewModel是一个用于办理UI相关数据的类。它供给了一种方便的办法来防止数据丢失和内存走漏,并保证在组件的生命周期发生变化时,数据能够主动保存和恢复。ViewModel一般与LiveData结合运用,以保证UI组件和数据存储之间的一致性。

4.Data Binding(数据绑定):Data Binding 答应将布局文件中的视图与运用程序的数据模型进行绑定,从而完结数据驱动的用户界面。经过 Data Binding,开发人员能够经过在布局文件中直接引证变量和表达式来削减手动的视图操作和数据更新。这样能够削减样板代码的数量,进步代码的可读性和保护性。Data Binding 还能够与 LiveData 和 ViewModel 紧密集成,使数据的更新和界面的刷新变得愈加简略和一致。

5.Kotlin协程

Kotlin 协程是Kotlin语言中的一种轻量级线程库,旨在简化异步编程和并发编程。Kotlin协程是一种十分有用和强大的异步编程和并发编程库,能够帮助开发者简化异步使命的处理和协调,并进步运用程序的功能和稳定性。在MVVM架构中,Kotlin协程一般与ViewModel和LiveData结合运用,以完结更高效、更健壮的数据存储和UI更新。

完结MVVM架构

MVVM 架构

Android Jetpack MVVM 实战:登录功能的优雅实现

登录界面效果如下面动图所示:

Android Jetpack MVVM 实战:登录功能的优雅实现

界说Model层,包含 LoginModel,LoginState,LoginDataSource,LoginRepository

登录数据模型

LoginModel.kt

data class LoginModel(
    var userId: String,
    var password: String
){
    /**
     * 是否合规
     * 不为空而且密码>= 6
     */
    fun isValid():Boolean{
        return !TextUtils.isEmpty(userId) && !TextUtils.isEmpty(password) && password.length >=6
    }
}

其间isValid用于验证数据的合规性。userId 不为空并且password的长度大于等于6,登录按钮才Enable。

界说登录状况

LoginState.kt

data class LoginState(
   var loginState: Int = NOT_LOGIN
){
   companion object{
       const val NOT_LOGIN = 0
       const val LOGIN_VALID = 1
       const val LOGIN_ING = 2
       const val LOGIN_SUCCESS = 3
       const val LOGIN_FAIL = 4
   }
}

登录共5种状况

1,未登录状况: 0 这是初始状况,用户没有进行登录操作或登录凭证已过期。在这种状况下,用户只能拜访运用的有限功用或许需求登录才能拜访的功用将被约束。

2,登录Valid状况: 1 当用户输入满意userId 不为空并且password的长度大于等于6,登录按钮才Enable,这个状况登录按钮点击,称为登录Valid 状况。

3,登录中状况: 2 当用户点击登录按钮后,运用会进入登录中状况,此时或许显现一个加载动画或进度条来指示登录进程正在进行中。在这个状况下,用户需求等待登录进程完结。

4,登录成功状况 :3 假如用户供给的登录凭证验证经过,运用将进入登录成功状况。在这个状况下,用户能够拜访登录后的功用,并且运用一般会跳转到主页面或其他授权拜访的页面。

5,登录失利状况:4 假如用户供给的登录凭证验证失利,运用将进入登录失利状况。在这个状况下,运用或许会显现错误消息或供给其他办法来帮助用户处理登录问题。

界说登录回来LoginResult

LoginResult.kt

data class LoginResult(
    val success: UserInfo? = null,
    val error: String?=null
)

登录成功回来UserInfo,失利回来error 信息

界说登录回来UserInfo,包含 token

UserInfo.kt

data class UserInfo(
    val displayName: String,
    val userId :String,
    val token:String
)

界说登录LoginDataSource,模仿登录,第一次失利,第2次成功。

class LoginDataSource {
    var count = 0
    //模仿登录
   suspend fun login(username: String, password: String): Result<LoginResult> {
        return try {
            count++
            delay(100)
            if(count %2 ==0){
                val userInfo = UserInfo(username,"1233333",java.util.UUID.randomUUID().toString())
                val result = LoginResult( userInfo,"")
                Result.Success(result)
            }else{
                val result = LoginResult( null,"登录失利")
                Result.Fail(result)
            }
        } catch (e: Throwable) {
            val result = LoginResult( null,"Error logging in")
            Result.Fail(result)
        }
    }
}

LoginRepository:ViewModel 拜访数据的桥梁。

class LoginRepository(val dataSource: LoginDataSource) {
    val user: LoginModel by lazy {
        LoginModel("","")
    }
    suspend fun login(username: String, password: String): Result<LoginResult> {
        return dataSource.login(username, password)
    }
}

界说View层,包含 Activity,Fragment,Databinding

Databinding

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">
    <data>
        <variable
            name="viewModel"
            type="com.dhl.loginmvvm.ui.login.LoginViewModel" />
        <import type="android.view.View" />
        <import type="com.dhl.loginmvvm.ui.login.LoginState" />
    </data>
    <androidx.core.widget.NestedScrollView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        >
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="@drawable/loginbkg"
            android:gravity="center"
            android:orientation="vertical">
            <androidx.cardview.widget.CardView
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_margin="30dp"
                android:background="@drawable/custom_edittext"
                app:cardCornerRadius="20dp"
                app:cardElevation="20dp">
                <LinearLayout
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:layout_gravity="center_horizontal"
                    android:focusable="false"
                    android:orientation="vertical"
                    android:padding="24dp">
                    <TextView
                        android:id="@+id/loginText"
                        android:layout_width="match_parent"
                        android:layout_height="wrap_content"
                        android:text="Login"
                        android:textAlignment="center"
                        android:textColor="@color/purple"
                        android:textSize="36sp"
                        android:textStyle="bold" />
                    <EditText
                        android:id="@+id/username"
                        android:layout_width="match_parent"
                        android:layout_height="50dp"
                        android:layout_marginTop="40dp"
                        android:afterTextChanged="@{(text) -> viewModel.onUserTextChanged(text)}"
                        android:background="@drawable/custom_edittext"
                        android:drawableLeft="@drawable/person"
                        android:drawablePadding="8dp"
                        android:hint="Username"
                        android:padding="8dp"
                        android:textColor="@color/black"
                        android:textColorHighlight="@color/cardview_dark_background" />
                    <EditText
                        android:id="@+id/password"
                        android:layout_width="match_parent"
                        android:layout_height="50dp"
                        android:layout_marginTop="20dp"
                        android:afterTextChanged="@{(text) -> viewModel.onPasswordTextChanged(text)}"
                        android:background="@drawable/custom_edittext"
                        android:drawableLeft="@drawable/password"
                        android:drawablePadding="8dp"
                        android:hint="Password"
                        android:inputType="textPassword"
                        android:padding="8dp"
                        android:textColor="@color/black"
                        android:textColorHighlight="@color/cardview_dark_background" />
                    <Button
                        android:id="@+id/loginButton"
                        android:layout_width="match_parent"
                        android:layout_height="60dp"
                        android:layout_marginTop="30dp"
                        android:enabled="@{viewModel.loginStateLivedata.loginState == LoginState.LOGIN_VALID }"
                        android:onClick="@{()->viewModel.loginOnClick()}"
                        android:text="Login"
                        android:textSize="18sp"
                        app:cornerRadius="20dp" />
                </LinearLayout>
            </androidx.cardview.widget.CardView>
            <TextView
                android:id="@+id/signupText"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginBottom="20dp"
                android:padding="8dp"
                android:text="Not yet registered? SignUp Now"
                android:textAlignment="center"
                android:textColor="@color/purple"
                android:textSize="14sp" />
        </LinearLayout>
    </androidx.core.widget.NestedScrollView>
</layout>

Activity

class LoginActivity : AppCompatActivity() {
    private lateinit var loginViewModel: LoginViewModel
    private val dialog: MaterialDialog by lazy {
        MaterialDialog.Builder(this)
            .content("登录中...")
            .progress(true, 10)
            .cancelable(false)
            .build()
    }
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val binding = DataBindingUtil.setContentView<ActivityLoginBinding>(
            this,
            R.layout.activity_login
        )
        loginViewModel = ViewModelProvider(this, LoginViewModelFactory())
            .get(LoginViewModel::class.java)
        binding.lifecycleOwner = this
        binding.viewModel = loginViewModel
        loginViewModel.loginStateLivedata.observe(this, {
            when (it.loginState) {
                LOGIN_ING -> {
                    showLoginDialog()
                }
                LoginState.LOGIN_FAIL, LoginState.LOGIN_SUCCESS -> {
                    disLoginDialog()
                }
            }
            loginViewModel.loginIsValid()
        })
        loginViewModel.loginResult.observe(this@LoginActivity, Observer {
            if (it.error != null) {
                showToast(it.error)
            }
            if (it.success != null) {
                showToast("user:${it.success.displayName}")
                goMain()
            }
        })
    }
    private fun showLoginDialog() {
        dialog.show()
    }
    private fun disLoginDialog() {
        dialog.dismiss()
    }
    /**
     * goMain
     */
    private fun goMain() {
        val intent = Intent(this, MainActivity::class.java)
        startActivity(intent)
        finish()
    }
}
fun Activity.showToast(str: String) {
    Toast.makeText(this, str, Toast.LENGTH_SHORT).show()
}

经过调查者形式接纳ViewModel的数据,用来提示或许进入下一个页面。

界说ViewModel

class LoginViewModel(val loginRepository: LoginRepository) : ViewModel() {
    private val _loginStateLiveData = MutableLiveData<LoginState>()
    val loginStateLivedata: LiveData<LoginState> = _loginStateLiveData
    private val _loginResult = MutableLiveData<LoginResult>()
    val loginResult: LiveData<LoginResult> = _loginResult
    /**
     * 默许没有登录
     */
    private val loginState = LoginState(loginState = LoginState.NOT_LOGIN)
    /**
     * login
     */
    fun login(username: String, password: String) {
        viewModelScope.launch {
            val result = loginRepository.login(username, password)
            delay(1000)
            if (result is Result.Success) {
                _loginResult.value = result.data
                _loginStateLiveData.value = LoginState(loginState = LoginState.LOGIN_SUCCESS)
            } else if(result is Result.Fail){
                _loginResult.value = result.data
                _loginStateLiveData.value = LoginState(loginState = LoginState.LOGIN_FAIL)
            }
        }
    }
    fun onUserTextChanged(text: Editable) {
        loginRepository.user.userId = text.toString()
        loginIsValid()
    }
    fun onPasswordTextChanged(text: Editable) {
        loginRepository.user.password = text.toString()
        loginIsValid()
    }
    /**
     * 判断 账号密码是否有效
     * 这儿应该用postValue 而不是setVlaue
     *
     */
    fun loginIsValid() {
        viewModelScope.launch {
            withContext(Dispatchers.Default){
                if (loginRepository.user.isValid()) {
                    loginState.loginState = LoginState.LOGIN_VALID
                } else {
                    loginState.loginState = LoginState.NOT_LOGIN
                }
                _loginStateLiveData.postValue(loginState)
            }
        }
    }
    /**
     * btn for login
     */
    fun loginOnClick() {
        _loginStateLiveData.value = LoginState(loginState = LoginState.LOGIN_ING)
        login(loginRepository.user.userId, loginRepository.user.password)
    }
}

这儿账号和密码输入框验证是否Valid的进程中(loginIsValid),我运用LiveData的postValue办法而不是setValue办法。

在 LiveData 中,postValue() 和 setValue() 是用于更新 LiveData 数据的两种办法。它们在本质上有以下区别:

线程安全性:postValue() 办法是线程安全的,能够在任何线程中调用。它会将数据更新操作投递到主线程的消息行列中,在主线程空闲时进行实践的数据更新操作。而 setValue() 办法必须在主线程中调用,否则会抛出异常。

数据更新时机:postValue() 办法会延迟履行数据更新操作,直到主线程空闲时才会进行实践的更新。这样能够防止在短时间内连续进行屡次数据更新导致的频繁界面刷新。而 setValue() 办法会立即履行数据更新操作,并触发相应的调查者告诉。

屡次更新兼并:postValue() 办法能够处理屡次数据更新,并将它们兼并成一次更新。假如在屡次 postValue() 调用之间存在较短的时间距离,只会触发一次数据更新和调查者告诉。而 setValue() 办法每次调用都会立即触发数据更新和告诉。

综上所述,postValue() 办法合适在后台线程中进行数据更新操作,能够防止线程安全问题,并兼并屡次更新以进步功能。而 setValue() 办法应在主线程中运用,用于需求立即更新数据并告诉调查者的场景。

源码:

github.com/ThirdPrince…

总结

该代码完结了一个运用Jetpack MVVM架构的Android登录界面。它经过Jetpack架构组件(如Lifecycle、LiveData和ViewModel和Databinding)与Kotlin协程,以完结更高效的MVVM架构。