Android Jetpack实战追踪从一个简单的登录页开始

2022-08-05 08:16:50

在Android开发的设计模式上,大体上经历了MVC、MVP以及如今“甚嚣尘上”的MVVM,而Jetpack的横空出世无疑给MVVM添了一把柴火。

------ 老朽

那么对于一个初学者甚至刚听过这个名词的开发者,该如何入门呢?接下来就让老朽带领。。噢不。。是跟大家一起去深入Jetpack单词的拼写,J-E-T-P-A-C-K,来read after me: 借特派克。

好了,废话不多说,进入正题。

在进入正题前,有必要把我的开发环境列一下:
win10 + Android Studio 4.2 Beta5 + sdk 30 + gradle 6.7.1 + kotlin 1.4.31

今天这篇文章准备刚以下几点:

  • 1、ViewModel简单使用
  • 2、数据绑定
  • 3、双向数据绑定

1、ViewModel简单使用

首先我们引入ViewModel,ViewModel位于androidx.lifecycle包中。
不知道是不是Android Studio或androidx版本原因,我这里建完项目后自动引入lifecycle包,但是查看gradle也没有这个包。
无妨,如果没有的话,大家按照官网提示依赖就可以了。

def lifecycle_version="2.3.0"def arch_version="2.1.0"// ViewModel
    implementation"androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"// LiveData
    implementation"androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"// Lifecycles only (without ViewModel or LiveData)
    implementation"androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version"

环境准备好了,我们画个登录页先,既然不是讲UI,我们就用IDE简单地拖一个界面:一个用户名输入框,一个密码输入框,一个登录按钮,很快就好了。
在这里插入图片描述
功能有两个:1、用户名自动填充,如果之前输入过的话;2、登录并根据返回结果显示对应信息。
画完了页面,我们定义一个ViewModel,其UML类图如下:
在这里插入图片描述
简单地写点逻辑。、

class LoginViewModel:ViewModel(){companionobject{privateconstval TAG="LoginViewModel"}/**
     * Simply define a string field to express Model.
     */var name= MutableLiveData<String>()/**
     * For caller to observe the login result.
     */val loginResult= MutableLiveData<Boolean>()funloadUser(){// Just set a value directly, it may a complicated process in fact, that is, retrieve data from DB or the backend.
        name.value="Jetpack"}funlogin(name: String, password: String){
        Log.d(TAG,"login() name =$name password =$password")// Simulate Login Executing..., return a random result.
        loginResult.value= Random.nextBoolean()}}

VM写好了,怎么去使用呢?
我们再在Activity里注册观察者。第一个观察者来观察用户名的变化 ,完成第1个功能;第二个观察者来观察登录的结果,完成第二个功能。
代码很简单:

/**
     * ViewModel to provide data for Views.
     */privatelateinitvar mLoginViewModel: LoginViewModeloverridefunonCreate(savedInstanceState: Bundle?){super.onCreate(savedInstanceState)setContentView(R.layout.activity_login)// instantiate ViewModel with its no-args constructor.
        mLoginViewModel=ViewModelProvider(this, ViewModelProvider.NewInstanceFactory()).get(LoginViewModel::class.java)val nameView= findViewById<TextView>(R.id.name_et)val passwordView= findViewById<TextView>(R.id.password_et)// Observe name changes and display the changed value to View.
        mLoginViewModel.name.observe(this,{ name->
            nameView?.text= name})// Load user for retrieving the latest changes which will change the name field.
        mLoginViewModel.loadUser()
        findViewById<View>(R.id.login_btn)?.setOnClickListener{// Simple checking.if(TextUtils.isEmpty(nameView.text)|| TextUtils.isEmpty(passwordView.text)){
                Toast.makeText(this,"Invalid name or password(name =${nameView.text}, password =${passwordView.text}", Toast.LENGTH_LONG).show()return@setOnClickListener}// Do Login Executing.
            mLoginViewModel.login(nameView.text.toString(), passwordView.text.toString())}// Observe the login result and show corresponding message.
        mLoginViewModel.loginResult.observe(this,{ success->if(success){
                Toast.makeText(applicationContext,"login success", Toast.LENGTH_LONG).show()}else{
                Toast.makeText(applicationContext,"login failed", Toast.LENGTH_LONG).show()}})}

逻辑很好理解,就是通常说的观察者模式,也懒得啰嗦了,一张图(别在乎两个小圆点,就是表示一对眼睛在观察):
在这里插入图片描述
我们来跑一下……

在这里插入图片描述
这便是一个简单的MVVM了,M就是LoginViewModel里的name或loginResult,这里只是图方便,写在VM里面了而已,V就是UI了,VM便是LoginViewModel。

2、数据绑定

LoginActivity:我TM算什么呢?
是啊,这到底是MVVM还是MVCVM啊。就是说你既然用了Activity却不给个名份,换谁心里也难受不是。

那我们就想了,能不能把Activity再简化一下,你就加载个资源和实例化一些对象,至于什么观察、什么绑定赋值啥的你不用管。

于是数据绑定就呼之欲出了……

其实databinding并不是什么新鲜事,只不过之前都是在MVC中把View与Model进行静态绑定,为什么说是静态呢,因为绑定后就完事了,Model值再怎么改变,View是无法感知的,只能苦巴巴地再次调用executePendingBindings()。

那动态的又是怎样的呢?其实就是利用观察者模式,让View盯着Model的变化,一旦改变,View就实时显示更新的数据,那么这类Model我们称为具有生命周期感知能力的对象,英文叫做Lifecycle-aware。

那么想实现动态绑定,安卓中提供了两种方法。

第一种是Android Studio3.1之前的做法,就是把ObservableField把原来的类型包起来,下面我们分别看下这两种方法。
当然了,我们第一步还是在项目中开启databinding,具体做就是修改app的gradle文件。

    dataBinding{
        enabled=true}

如果使用kotlin的话,gradle的plugins加上kapt在编译期生成必要的类文件。

id'kotlin-kapt'//以前好像是plugin XXX,具体方法以gradle版本为准

接下来我们修改布局文件并引入ViewModel,在用户名处绑定自动填充的用户名。

<?xml version="1.0" encoding="utf-8"?><layout><data><variablename="viewModel"type="com.codersth.jetpackpractice.viewmodel.LoginViewModel"/></data><androidx.constraintlayout.widget.ConstraintLayoutxmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"xmlns:tools="http://schemas.android.com/tools"android:layout_width="match_parent"android:layout_height="match_parent"tools:context=".view.ui.LoginActivity"><Buttonandroid:id="@+id/login_btn"android:layout_width="0dp"android:layout_height="wrap_content"android:layout_marginStart="16dp"android:layout_marginLeft="16dp"android:layout_marginEnd="16dp"android:layout_marginRight="16dp"android:layout_marginBottom="180dp"android:padding="16dp"android:text="@string/login"app:layout_constraintBottom_toBottomOf="parent"app:layout_constraintEnd_toEndOf="parent"app:layout_constraintStart_toStartOf="parent"/><com.google.android.material.textfield.TextInputLayoutandroid:id="@+id/textInputLayout"android:layout_width="0dp"android:layout_height="wrap_content"android:layout_marginStart="16dp"android:layout_marginLeft="16dp"android:layout_marginTop="180dp"android:layout_marginEnd="16dp"android:layout_marginRight="16dp"app:layout_constraintEnd_toEndOf="parent"app:layout_constraintStart_toStartOf="parent"app:layout_constraintTop_toTopOf="parent"><com.google.android.material.textfield.TextInputEditTextandroid:id="@+id/name_et"android:layout_width="match_parent"android:layout_height="wrap_content"android:ellipsize="end"android:hint="@string/please_input_name"android:maxLength="16"android:singleLine="true"android:text="@{viewModel.name}"/></com.google.android.material.textfield.TextInputLayout><com.google.android.material.textfield.TextInputLayoutandroid:layout_width="0dp"android:layout_height="wrap_content"android:layout_marginStart="16dp"android:layout_marginLeft="16dp"android:layout_marginTop="16dp"android:layout_marginEnd="16dp"android:layout_marginRight="16dp"app:layout_constraintEnd_toEndOf="parent"app:layout_constraintStart_toStartOf="parent"app:layout_constraintTop_toBottomOf="@+id/textInputLayout"><com.google.android.material.textfield.TextInputEditTextandroid:id="@+id/password_et"android:layout_width="match_parent"android:layout_height="wrap_content"android:ellipsize="end"android:hint="@string/please_input_password"android:inputType="textPassword"android:maxLength="16"android:singleLine="true"/></com.google.android.material.textfield.TextInputLayout></androidx.constraintlayout.widget.ConstraintLayout></layout>

改完后ide就自动生成了ActivityLoginBinding(没有就手动make一下)。
然后我们把上面的LoginViewModel简单地改下:

/**
     * Simply define a string field to express Model.
     */var name= ObservableField<String>()
funloadUser(){// Just set a value directly, it may a complicated process in fact, that is, retrieve data from DB or the backend.
        name.set("Jetpack")}

最后,在LoginActivity中完成View与ViewModel的数据绑定。

overridefunonCreate(savedInstanceState: Bundle?){super.onCreate(savedInstanceState)// Retrieve layout binding.val binding: ActivityLoginBinding= DataBindingUtil.setContentView(this, R.layout.activity_login)// Bind binding's lifecycle to the current component.
        binding.lifecycleOwner=this// instantiate ViewModel with its no-args constructor.val loginViewModel=ViewModelProvider(this, ViewModelProvider.NewInstanceFactory()).get(LoginViewModel::class.java)
        binding.viewModel= loginViewModel// Load user for retrieving the latest changes which will change the name field.Handler(Looper.getMainLooper()).postDelayed(Runnable{
            loginViewModel.loadUser()}, DURATION_SIMULATE_REQUEST)}

可以看到,当我们在若干秒后调用loadUser并改变name字段后,用户名被重新赋值。

至于其它数据类型、集合、对象等,都是一个道理,大家可以去官网逛逛.

第二个绑定方法就是利用LiveData,我们把上面LoginViewModel代码稍微改变如下:

/**
     * For caller to observe the login result.
     */val loginResult= MutableLiveData<Boolean>()funloadUser(){// Just set a value directly, it may a complicated process in fact, that is, retrieve data from DB or the backend.
        name.value="Jetpack"}

效果是一样的,Android Studio3.1后基本上都在大量使用LiveData。

3、双向数据绑定

MD,码字是真累,你们看到这里的一定要记得给老朽点个赞。

虽然说数据绑定很好用,但大家注意到了没,上面这种绑定虽然是动态的,但却是单向的,也就是说M的修改可以影响V,但反过来却不行。

我们都知道在layout中不仅可以绑定属性,还可绑定方法,我们不妨先把ViewModel中登录方法改下:

/**
     * Binding for layout and called by login clicked.
     */funlogin(){}

然后在登录按钮中绑定下。

<Buttonandroid:id="@+id/login_btn"android:layout_width="0dp"android:layout_height="wrap_content"android:layout_marginStart="16dp"android:layout_marginLeft="16dp"android:layout_marginEnd="16dp"android:layout_marginRight="16dp"android:layout_marginBottom="180dp"android:padding="16dp"android:text="@string/login"app:layout_constraintBottom_toBottomOf="parent"app:layout_constraintEnd_toEndOf="parent"app:layout_constraintStart_toStartOf="parent"android:onClick="@{() -> viewModel.login()}"/>

但是……用户名和密码怎么传进去呢?
如果说我在LoginViewModel定义两个字段,然后分别绑定layout中的用户名和密码,再使用双向绑定,这样当用户输入后,LoginViewModel中的两个字段总能取到最新值,拿出来做为参数做逻辑处理不就完事了?

然而,根据官网的说法,我大ViewModel要实现Observable接口,为了省事,我就把官网demo中的ObservableViewModel偷来了。
这样LoginViewModel就变成:
在这里插入图片描述
其中新增两个方法getName和getPassword并加注解@Bindable,表示V通过这个方法取值。

/**
     * Annotation as [Bindable] for two-way binding with view.
     */@BindablefungetName(): String{return nameData}@BindablefungetPassword(): String{return passwordData}

对于set方法则无要求,需要注意的是判断值确实有更新才通知V,因为你不判断的话,就进入“我更新你更新我更新你……”的无限纠缠之中。

funsetName(newValue: String){if(nameData!= newValue){
            nameData= newValuenotifyPropertyChanged(BR.name)}}

其中notifyPropertyChanged就是通知V更新值,BR.name中的"name"需要与getName()方法中的"name"保持一致(别忘了make一下)。

经过这样一番折腾,login方法中的逻辑就可以全部写到VM中了。

/**
     * Binding for layout and called by login clicked.
     */funlogin(){
        Log.d(TAG,"login name =$nameData and password =$passwordData")// Simple checking.if(TextUtils.isEmpty(nameData)|| TextUtils.isEmpty(passwordData)){//            Toast.makeText(this, "Invalid name or password(name = ${nameView.text}, password = ${passwordView.text}", Toast.LENGTH_LONG).show()
            Log.d(TAG,"Invalid name or password(name =${nameData}, password =${passwordData}")return}// Do Login Executing.
        loginResult.value= Random.nextBoolean()}

在Activity中我们几乎只做了两件事:1)初始化;2)处理结果,某些场景这个都可以在VM中做掉。

overridefunonCreate(savedInstanceState: Bundle?){super.onCreate(savedInstanceState)// Retrieve layout binding.val binding: ActivityLoginBinding= DataBindingUtil.setContentView(this, R.layout.activity_login)// Bind binding's lifecycle to the current component.
        binding.lifecycleOwner=this// instantiate ViewModel with its no-args constructor.val loginViewModel=ViewModelProvider(this, ViewModelProvider.NewInstanceFactory()).get(LoginViewModel::class.java)
        binding.viewModel= loginViewModel// Load user for retrieving the latest changes which will change the name field.Handler(Looper.getMainLooper()).postDelayed(Runnable{
            loginViewModel.loadUser()}, DURATION_SIMULATE_REQUEST)// Observe the login result and show corresponding message.
        mLoginViewModel.loginResult.observe(this,{ success->if(success){
                Toast.makeText(applicationContext,"login success", Toast.LENGTH_LONG).show()}else{
                Toast.makeText(applicationContext,"login failed", Toast.LENGTH_LONG).show()}})
        mLoginViewModel= loginViewModel}

这样一个简单的MVVM的雏形便出来了!

好了,今天就学习这么多,有时间再继续追踪~

以上案例请分别参见以下commit:
在这里插入图片描述
GIT:https://github.com/codersth/JetpackPractice.git

  • 作者:APP亿哥章磊
  • 原文链接:https://blog.csdn.net/m0_48179608/article/details/114628010
    更新时间:2022-08-05 08:16:50