简介
当前大前端主流的架构为 MVVM,即 Model-View-ViewModel,Model 表示数据模型,View 表示视图模型,视图与数据之间采用 ViewModel 层进行隔离,解耦视图与数据。
Android 官方推荐的 MVVM 架构如下图所示:
可以看到,ViewModel 层充当数据持有者,它专门用于存放页面所需数据(ViewModel 层可以通过相应业务逻辑获取得到数据)。通常来说,一个页面所有数据都交由一个 ViewModel 进行维护,当 View 层需要进行渲染时,就可以通过 ViewModel 获取数据(View 层持有 ViewModel 的引用),最终将数据渲染到页面上。
在 Android Jetpack 中,已经为我们提供了一个 ViewModel 层相应类型:ViewModel
,其源码如下所示:
package androidx.lifecycle;
...
public abstract class ViewModel {
...
@SuppressWarnings("WeakerAccess")
protected void onCleared() {
}
}
可以看到,ViewModel
是一个抽象类,其内部有一个protected
方法:onCleared()
,该方法在ViewModel
被销毁时会自动被调用,子类可以覆写该方法,进行一些资源释放操作。
依赖引入
项目中要使用ViewModel
,只需在 app 的 build.gradle 中引入:
dependencies {
def lifecycle_version = '2.3.0'
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
}
如果项目已引入appcompat
包,则无需再独立引入ViewModel
:
implementation 'androidx.appcompat:appcompat:1.2.0'
生命周期
Android 官网对ViewModel
生命周期描述如下图所示:
这里有一个很有趣的特性:ViewModel
与其宿主Activity
的生命周期一致,在Activity
正常退出后,ViewModel
就会触发回调其onCleared()
方法,进行一些资源释放操作,然后退出,而当Activity
非正常退出时(比如屏幕翻转导致Activity
重新创建),ViewModel
保持不变,因此,ViewModel
的生命周期大于或等于宿主Activity
生命周期。
注:由于ViewModel
生命周期可能大于Activity
,因此,不要将宿主上下文Context
传递给ViewModel
,否则可能导致异常情况下,宿主Activity
退出后却无法被回收,造成内存泄漏。如果ViewModel
内部确实需要使用Context
,可考虑使用AndroidViewModel
,其内部维护一个Application
上下文对象:
package androidx.lifecycle;
...
public class AndroidViewModel extends ViewModel {
@SuppressLint("StaticFieldLeak")
private Application mApplication;
public AndroidViewModel(@NonNull Application application) {
mApplication = application;
}
@SuppressWarnings("TypeParameterUnusedInFormals")
@NonNull
public <T extends Application> T getApplication() {
//noinspection unchecked
return (T) mApplication;
}
}
Activity 中获取 ViewModel
下面通过一个小例子来阐述下ViewModel
的使用:假设点击主页面MainActivity
中的一个按钮,实现上网获取一个字符串,然后显示到当前页面上,具体操作步骤如下:
-
首先创建一个
MyViewModel
,获取页面所需数据:class MyViewModel : ViewModel() { // 数据 var data: String = "" override fun onCleared() { super.onCleared() } // 模拟获取数据业务逻辑 fun fetchData() { // skip detailed fetching process this.data = "fetchData: ${Math.random()}" } }
-
页面持有对应
ViewModel
,获取数据进行渲染到页面上,其具体步骤如下:-
首先为
Activity
引入相关依赖,方便创建ViewModel
:dependencies { def activity_version = "1.2.1" implementation "androidx.activity:activity-ktx:$activity_version" }
注:引入上述依赖,才能在
Activity
中使用ViewModelProvider(ViewModelStoreOwner)
该函数创建ViewModel
。 -
为页面创建相应布局:
<!-- layout/activity_main.xml --> <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center_vertical" android:orientation="vertical"> <TextView android:id="@+id/tvData" android:layout_width="match_parent" android:layout_height="wrap_content" android:gravity="center" android:text="Hello World!" /> <Button android:id="@+id/btn" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="fetch data" /> </LinearLayout>
-
创建主页面,并实例化相应
ViewModel
:class MainActivity : AppCompatActivity() { private lateinit var viewModel: MyViewModel override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) // 获取 ViewModel this.viewModel = ViewModelProvider(this).get(MyViewModel::class.java) this.showData(this.viewModel.data) this.btn.setOnClickListener { // 获取数据 this.viewModel.fetchData() // 显示数据 this.showData(this.viewModel.data) } } // 将数据渲染到页面 private fun showData(data: String) { this.tvData.text = data } }
到此,我们就成功将
MainActivity
与MyViewModel
绑定到一起了,MainActivity
就可以从MyViewModel
中获取数据并渲染到页面上,且即使翻转手机,让MainActivity
重新创建,也不会导致数据丢失,因为新创建的MainActivity
实例获取到的还是先前那个MyViewModel
实例,故数据仍存在。 -
上面内容我们在Activity
中创建的是不带参数的ViewModel
,如果需要传递参数给ViewModel
,只需实现一个ViewModelProvider.Factory
即可,具体如下所示:
-
修改
MyViewModel
,提供带参数构造函数:class MyViewModel(private val prefix: String) : ViewModel() { var data: String = "" ... // 模拟获取数据业务逻辑 fun fetchData() { // skip detailed fetching process this.data = "${this.prefix}: ${Math.random()}" } }
-
自定义
ViewModelProvider.Factory
,提供参数注入:class MyViewModelFactory(private val prefix: String) : ViewModelProvider.Factory { override fun <T : ViewModel?> create(modelClass: Class<T>): T { return MyViewModel(this.prefix) as T } }
-
Activity
通过ViewModelProvider.Factory
获取ViewModel
实例:class MainActivity : AppCompatActivity() { private val viewModel: MyViewModel by lazy { ViewModelProvider(this, MyViewModelFactory("retriveData")).get(MyViewModel::class.java) } ... }
以上,我们就完成了在
Activity
中传递参数给ViewModel
。
注:在Activity
中获取ViewModel
除了以上方法外,其实 Androidx 还提供了一些更简便的方法让我们获取Activity
所需的ViewModel
,具体方法如下:
-
在
Acvitity
中更加方便获取ViewModel
,需要使用activity-ktx
模块:dependencies { implementation "androidx.activity:activity-ktx:1.2.1" }
-
然后在
Acvitity
中就可使用如下方式获取所需ViewModel
:class MainActivity : AppCompatActivity() { // 通过扩展函数 viewModels() 获取 Activity 所需 ViewModel private val viewModel by viewModels<MyViewModel>() ... }
注:如果扩展函数
viewModels()
使用时报错,则在 app Module 的 build.gralde 进行如下配置:android { ... compileOptions { sourceCompatibility = 1.8 targetCompatibility = 1.8 } kotlinOptions { jvmTarget = "1.8" } }
-
如果需要传递参数给
ViewModel
,只需如下调用:class MainActivity : AppCompatActivity() { ... private val viewModel by viewModels<MyViewModel> { MyViewModelFactory("retriveData") } }
Fragment 中获取 ViewModel
Fragment
中获取ViewModel
的方式与Activity
基本一致,大致如下所述:
通过
ViewModelProvider
获取:上文在MainActivity
中使用ViewModelProvider(ViewModelStoreOwner)
来获取页面所需ViewModel
实例,刚好Fragment
也实现了ViewModelStoreOwner
接口,因此Fragment
中使用ViewModel
的方式与Activity
基本一致,此处不再赘述。-
通过扩展函数获取:
activity-ktx
模块为Acvitity
提供了相应扩展函数方便Acvitity
获取ViewModel
,对于Fragment
而言,它也有相应的扩展函数方便其获取所需ViewModel
实例,这个模块为fragment-ktx
,具体操作如下所示:-
引入相关依赖:
dependencies { implementation "androidx.fragment:fragment-ktx:1.3.1" }
-
获取
Fragment
所需ViewModel
,可采用如下方式:class MyFragment : Fragment() { // Get a reference to the ViewModel scoped to this Fragment val viewModel by viewModels<MyViewModel>() ... }
-
获取
Fragment
宿主Acvitity
的ViewModel
,可采用如下方式:class MyFragment : Fragment() { // Get a reference to the ViewModel scoped to its Activity val viewModel by activityViewModels<MyViewModel>() }
-
最后,事实上ViewModel
经常跟LiveData
一起使用,具体使用方法可参考:Jetpack Architecture - LiveData
附录
- Androidx 各组件最新版本查询:参考官网:Library Release