ViewModel 库一发布,便成为了 Jetpack 中的核心组件之一。咱们在 2019 年作的一份开发者问卷显示,超过 40% 的 Android 开发者已经在本身的应用中使用了 ViewModel。ViewModel 能够将数据层与 UI 分离,而这种架构不只能够简化 UI 的生命周期的控制,也能让代码得到更好的可测试性。若是想了解更多,能够参考 ViewModel: 简单介绍视频和 官方文档。html
因为 ViewModel 是许多功能实现的基础,咱们在过去的几年里作了许多工做来改进 ViewModel 的易用性,也让它可以更加简便地与其余组件库相结合。下面的文章中,我将介绍 ViewModel 的四种集成方式:java
onSaveInstanceState 带来的挑战android
ViewModel 一发布,执行 onSaveInstanceState 的相关的逻辑时要如何操做 ViewModel,便成为了一个使人困惑的问题。Activity 和 Fragment 一般会在下面三种状况下被销毁:git
在后两种状况中,咱们一般都但愿重建 Activity。ViewModel 会帮您处理第二种状况,由于在这种状况下 ViewModel 没有被销毁;而在第三种状况下, ViewModel 被销毁了。因此一旦出现了第三种状况,便须要在 Activity 的 onSaveInstanceState 相关回调中保存和恢复 ViewModel 中的数据。我在 ViewModels: 持久化、onSaveInstanceState()、恢复 UI 状态与加载器 一文中更加详细地描述了这两种状况的区别。github
Saved State 模块编程
如今,ViewModel Saved State 模块将会帮您在应用进程被杀死时恢复 ViewModel 的数据。在免除了与 Activity 繁琐的数据交换后,ViewModel 也真正意义上的作到了管理和持有全部本身的数据。c#
ViewModel 的这一新功能是经过 SavedStateHandle 实现的。SavedStateHandle 和 Bundle 同样,以键值对形式存储数据,它包含在 ViewModel 中,而且能够在应用处于后台时进程被杀死的状况下幸存下来。诸如用户 id 等须要在 onSaveInstanceState 时获得保存下来的数据,如今均可以存在 SavedStateHandle 中。架构
设置 Save State 模块app
如今让咱们看看如何使用 SaveState 组件。注意接下来的代码会和 Lifecycles Codelab 第六步中的一段代码十分类似。那段是 Java 代码,而接下来的是 Kotlin 代码:框架
第一步: 添加依赖
SaveStateHandle 目前在一个独立的模块中,您须要在依赖中添加:
def lifecycle_version = "2.2.0"
implementation "androidx.lifecycle:lifecycle-viewmodel-savedstate:$lifecycle_version"
注意,本文发布时 lifecycle 组件的最新稳定版为 2.2.0,若是您但愿持续关注相关组件库的进展,能够查看 lifecycle 版本发布文档。
第二步: 修改调用 ViewModelProvider 的方式
接下来,您须要建立一个持有 SaveStateHandle 的 ViewModel。在 Activity 或 Fragment 的 onCreate 方法中,将 ViewModelProvider 的调用修改成:
//下面的 Kotlin 扩展须要依赖如下或更新新版本的 ktx 库: //androidx.fragment:fragment-ktx:1.0.0(最新版本 1.2.4) 或 //androidx.activity:activity-ktx:1.0.0 (最新版本 1.1.0) val viewModel by viewModels { SavedStateViewModelFactory(application, this) } // 或者不使用 ktx val viewModel = ViewModelProvider(this, SavedStateViewModelFactory(application, this)) .get(MyViewModel::class.java)
建立 ViewModel 的类是 ViewModel 工厂 (ViewModel factory),而建立包含 SaveStateHandle 的 View Model 的工厂类是 SavedStateViewModelFactory。经过此工厂建立的 ViewModel 将持有一个基于传入 Activity 或 Fragment 的 SaveStateHandle。
第三步: 使用 SaveStateHandle
当前面的步骤准备完成时,您就能够在 ViewModel 中使用 SavedStateHandle 了。下面是一个保存用户 ID 的示例:
class MyViewModel(state :SavedStateHandle) :ViewModel() { // 将Key声明为常量 companion object { private val USER_KEY = "userId" } private val savedStateHandle = state fun saveCurrentUser(userId: String) { // 存储 userId 对应的数据 savedStateHandle.set(USER_KEY, userId) } fun getCurrentUser(): String { // 从 saveStateHandle 中取出当前 userId return savedStateHandle.get(USER_KEY)?: "" } }
如今,不管是第二仍是第三种状况下,SavedStateHandle 均可以帮您恢复界面数据了。
若是您想要在 ViewModel 中使用 LiveData,能够调用 SavedStateHandle.getLiveData(),示例以下:
// getLiveData 方法会取得一个与 key 相关联的 MutableLiveData // 当与 key 相对应的 value 改变时 MutableLiveData 也会更新。 private val _userId : MutableLiveData<String> = savedStateHandle.getLiveData(USER_KEY) // 只暴露一个不可变 LiveData 的状况 val userId : LiveData<String> = _userId
如需了解更多,请移步至 Lifecycles Codelab 第六步 和 官方文档。
共享 ViewModel 数据所带来的挑战
Jetpack 导航组件 (Navigation) 十分适用于那些只有少许或一个 Activity,可是 Activity 中会包含多个 Fragment 的应用。Ian Lake 在他的演讲: 单 Activity 架构: 为何、什么状况下以及如何使用中介绍了一些咱们选择单一 Activity 架构的缘由,而与本文相关的一点,是这种架构容许在多个界面 (destination) 间共享 ActivityViewModel。您能够用 Activity 建立一个 ViewModel 实例,而后从这个 Activity 中的任一个 Fragment 中得到 ViewModel 的引用:
// 在Fragment的 onCreate 或 onActivityCreated 方法中执行 // 这个Kotlin扩展须要依赖最KTX库:androidx.fragment:fragment-ktx:1.1.0 val sharedViewModel: ActivityViewModel by activityViewModels()
假设咱们有这样一个单 Activity 应用,它包含了八个 Fragment,其中四个 Fragment 是购买支付流程:
△ 包含一些购买支付流程的导航图 (Navigation Graph)
这四个页面须要共享一些诸如收货地址、是否使用了优惠券等信息。按照前面所讲的作法,须要共享的数据会放在一个 ActivityViewModel 中,但这同时也意味着全部八个页面都会共享这些数据。支付流程外的界面并不须要关心这些数据,这么作显然并不合适。
ViewModel 与 NavGraph 集成
Navigation 2.1.0 中引入了依托一个导航图 (navigation graph) 建立 ViewModel 的功能。在使用时,您须要先把一个界面集合 (例如: 登陆流程、支付流程的相关界面),放到一个 嵌套导航图 (nested navigation graph) 中。此时再经过嵌套导航图建立出 ViewModel,即可以在相关界面中共享数据了。
想要建立嵌套导航图,您须要选中对应流程相关的界面,点击鼠标右键,并选择 Nested Graph → New Graph:
△ 建立嵌套导航图的截图
注意嵌套导航图在 XML 文件中的 id,在这里是 checkout_graph:
<navigation app:startDestination="@id/homeFragment" ...> <fragment android:id="@+id/homeFragment" .../> <fragment android:id="@+id/productListFragment" .../> <fragment android:id="@+id/productFragment" .../> <fragment android:id="@+id/bargainFragment" .../> <navigation android:id="@+id/checkout_graph" app:startDestination="@id/cartFragment"> <fragment android:id="@+id/orderSummaryFragment".../> <fragment android:id="@+id/addressFragment" .../> <fragment android:id="@+id/paymentFragment" .../> <fragment android:id="@+id/cartFragment" .../> </navigation> </navigation>
以上工做完成时,即可以使用 by navGraphViewModels 获取到对应的 ViewModel:
val viewModel: CheckoutViewModel by navGraphViewModels(R.id.checkout_graph)
Java 中一样适用,代码以下:
public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); // 设置其余 fragment NavController navController = NavHostFragment.findNavController(this); ViewModelProvider viewModelProvider = new ViewModelProvider(this, navController.getViewModelStore(R.id.checkout_graph)); CheckoutViewModel viewModel = viewModelProvider.get(CheckoutViewModel.class); // 使用 Checkout ViewModel }
须要注意的是,嵌套导航图相对于导航图的其余部分是一个独立的总体。您没法导航至嵌套导航图中包含的某个特定界面;当您导航至一个嵌套导航图时,打开的只会是其中的开始界面 (startDestination)。这种特性使得嵌套导航图适合用于封装特定流程的界面组合,好比前面提到过的登陆和支付流程。
ViewModel 与 NavGraph 的集成,是 2019 年 I/O 大会所发布的关于 Navigation 框架的新特性之一。
详细了解更多,请参阅:
)
移除 LiveData 相关的模板代码
ViewModel、LiveData 与 Data Binding 的集成方式并非什么新功能,但它始终很是好用。ViewModel 一般都包含一些 LiveData,而 LiveData 意味着能够被监听。因此最多见的使用场景是在 Fragment 中给 LiveData 添加一个观察者:
override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) myViewModel.name.observe(this, { newName -> // 更新UI,这里是一个TextView nameTextView.text = newName }) }
Data Binding 是一个经过观察数据变化来更新 UI 的组件库。经过 ViewModel、LiveData 和 Data Binding 的组合,您能够移除以往给 LiveData 添加观察者的作法,改成直接在 XML 中绑定 View Model 和 LiveData。
使用 Data Binding、ViewModel 和 LiveData
假设您但愿在 XML 布局文件中引用 ViewModel:
<layout xmlns:android="http://schemas.android.com/apk/res/android"> <data> <variable name="viewmodel" type="com.android.MyViewModel"/> </data> <... Rest of your layout ...> </layout>
调用 binding.setLifecycleOwner(this) 方法,而后将 ViewModel 传递给 binding 对象,就能够将 LiveData 与 Data Binding 结合起来:
class MainActivity : AppCompatActivity() { // 这个ktx扩展须要依赖 androidx.activity:activity-ktx:1.0.0 // 或更新版本 private val myViewModel: MyViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) //填充视图并建立 Data Binding 对象 val binding: MainActivityBinding = DataBindingUtil.setContentView(this, R.layout.main_activity) //声明这个 Activity 为 Data Binding 的 lifecycleOwner binding.lifecycleOwner = this // 将 ViewModel 传递给 binding binding.viewmodel = myViewModel } }
如今,您能够像下面这样使用 ViewModel:
<layout xmlns:android="http://schemas.android.com/apk/res/android"> <data> <variable name="viewmodel" type="com.android.MyViewModel"/> </data> <TextView android:id="@+id/name" android:text="@{viewmodel.name}" android:layout_height="wrap_content" android:layout_width="wrap_content"/> </layout>
注意,这里的 viewmodel.name 既能够是 String 类型,也能够是 LiveData。若是它是 LiveData,那么 UI 将根据 LiveData 值的改变自动刷新。
Android 平台上的协程
一般状况下,咱们使用回调 (Callback) 处理异步调用,这种方式在逻辑比较复杂时,会致使回调层层嵌套,代码也变得难以理解。Kotlin 协程 (Coroutines) 一样适用于处理异步调用,它让逻辑变得简单的同时,也确保了操做不会阻塞主线程。若是您不了解协程,这里有一系列很棒的博客 《在 Android 开发中使用协程》 以及 codelab: 在 Android 应用中使用 Kotlin 协程 以供参考。
一段简单的协程代码:
// 下面是示例代码,真实情景下不要使用 GlobalScope GlobalScope.launch { longRunningFunction() anotherLongRunningFunction() }
这段示例代码只启动了一个协程,但咱们在真实的使用环境下很容易建立出许多协程,这就不免会致使有些协程的状态没法被跟踪。若是这些协程中恰好有您想要中止的任务时,就会致使任务泄漏 (work leak)。
为了防止任务泄漏,您须要将协程加入到一个 CoroutineScope 中。CoroutineScope 能够持续跟踪协程的执行,它能够被取消。当 CoroutineScope 被取消时,它所跟踪的全部协程都会被取消。上面的代码中,我使用了 GlobalScope,正如咱们不推荐随意使用全局变量同样,这种方式一般不推荐使用。因此,若是想要使用协程,您要么限定一个做用域 (scope),要么得到一个做用域的访问权限。而在 ViewModel 中,咱们可使用 viewModelScope 来管理协程的做用域。
viewModelScope
当 ViewModel 被销毁时,一般都会有一些与其相关的操做也应当被中止。
例如,假设您正在准备将一个位图 (bitmap) 显示到屏幕上。这种操做就符合咱们前面提到的一些特征: 既不能在执行时阻塞主线程,又要求在用户退出相关界面时中止执行。使用协程进行此类操做时,就应当使用 viewModelScope.viewModelScope:kotlinx.coroutines.CoroutineScope)。
viewModelScope 是一个 ViewModel 的 Kotlin 扩展属性。正如前面所说,它能在 ViewModel 销毁时 (onCleared()) 方法调用时) 退出。这样一来,只要您使用了 ViewModel,您就可使用 viewModelScope 在 ViewModel 中启动各类协程,而不用担忧任务泄漏。
示例以下:
class MyViewModel() : ViewModel() { fun initialize() { viewModelScope.launch { processBitmap() } } suspend fun processBitmap() = withContext(Dispatchers.Default) { // 在这里作耗时操做 } }
详细了解更多,请参阅:
本文中,咱们讲了:
以上这些功能不少都来自社区提交的请求和反馈,若是您正在寻找 ViewModel 相关的功能,能够留意 功能需求列表 或者考虑 提交本身的需求。
若是您想了解架构组件和 Android Jetpack 的最新进展,请关注 Android 开发者博客,并留意 AndroidX 发布文档。
若是您对这些功能仍有疑问,能够在下方留言。感谢阅读!