在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() {
companion object {
private const val 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>()
fun loadUser() {
// Just set a value directly, it may a complicated process in fact, that is, retrieve data from DB or the backend.
name.value = "Jetpack"
}
fun login(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.
*/
private lateinit var mLoginViewModel: LoginViewModel
override fun onCreate(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>
<variable
name="viewModel"
type="com.codersth.jetpackpractice.viewmodel.LoginViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
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"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".view.ui.LoginActivity">
<Button
android: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.TextInputLayout
android: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.TextInputEditText
android: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.TextInputLayout
android: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.TextInputEditText
android: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>()
fun loadUser() {
// 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的数据绑定。
override fun onCreate(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>()
fun loadUser() {
// 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.
*/
fun login() {
}
然后在登录按钮中绑定下。
<Button
android: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.
*/
@Bindable
fun getName(): String {
return nameData
}
@Bindable
fun getPassword(): String {
return passwordData
}
对于set方法则无要求,需要注意的是判断值确实有更新才通知V,因为你不判断的话,就进入“我更新你更新我更新你……”的无限纠缠之中。
fun setName(newValue: String) {
if(nameData != newValue) {
nameData = newValue
notifyPropertyChanged(BR.name)
}
}
其中notifyPropertyChanged就是通知V更新值,BR.name中的"name"需要与getName()方法中的"name"保持一致(别忘了make一下)。
经过这样一番折腾,login方法中的逻辑就可以全部写到VM中了。
/**
* Binding for layout and called by login clicked.
*/
fun login() {
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中做掉。
override fun onCreate(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