在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