Jetpack —— ViewModel,在生命周期中管理 UI 数据

ViewModel 类旨在以生命周期意识的方式存储和管理 UI 数据。ViewModel 类允许数据在配置更改(例如屏幕旋转)后继续存在。

想要在项目中引入 ViewModel,需要在 build.gradle 中添加如下依赖:

dependencies {
    def lifecycle_version = "2.0.0"

    // ViewModel and LiveData
    implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version"
    // alternatively - just ViewModel
    implementation "androidx.lifecycle:lifecycle-viewmodel:$lifecycle_version" // For Kotlin use lifecycle-viewmodel-ktx
    // alternatively - just LiveData
    implementation "androidx.lifecycle:lifecycle-livedata:$lifecycle_version"
    // alternatively - Lifecycles only (no ViewModel or LiveData). Some UI
    //     AndroidX libraries use this lightweight import for Lifecycle
    implementation "androidx.lifecycle:lifecycle-runtime:$lifecycle_version"

    annotationProcessor "androidx.lifecycle:lifecycle-compiler:$lifecycle_version" // For Kotlin use kapt instead of annotationProcessor
    // alternately - if using Java8, use the following instead of lifecycle-compiler
    implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"

    // optional - ReactiveStreams support for LiveData
    implementation "androidx.lifecycle:lifecycle-reactivestreams:$lifecycle_version" // For Kotlin use lifecycle-reactivestreams-ktx

    // optional - Test helpers for LiveData
    testImplementation "androidx.arch.core:core-testing:$lifecycle_version"
}

Android Framework 管理 UI 控制器的生命周期,例如 Activity 和 Fragment。Framework 可以决定销毁或者重建 UI 控制器以相应某些完全不受控制的用户操作或者设备事件。

如果系统销毁或重新创建 UI 控制器,则存储在其中的任何临时 UI 相关的数据都将丢失。例如,您的应用可能包含其中一项活动中的用户列表。当为配置更改重新创建活动时,新活动必须重新获取用户列表。
对于简单的数据,Activity 可以使用 onSaveInstanceState() 方法并从 onCreate() 中的 bundle 中恢复其数据,但此方法仅适用于可以序列化然后反序列化的少量数据,可能不适合像用户或位图的列表这样的大量数据。

另一个问题是 UI 控制器经常需要进行异步调用,这可能需要一些时间才能返回。UI 控制器需要管理这些调用,并确保系统在销毁后清理它们以避免潜在的内存泄漏。这种管理需要大量的维护,并且在为配置更改而重新创建对象的情况下,由于对象可能不得不重新发出已经做出的请求,所以浪费资源。

UI 控制器(如Activity和Fragment)主要用于显示 UI 数据,对用户操作做出反应或处理操作系统通信(如权限请求)。如果要求 UI 控制器也负责从数据库或网络加载数据,就会使改类变得臃肿。为 UI 控制器分配过多的责任可能会导致一个类尝试单独处理应用程序的所有工作,而不是将工作委托给其他类。通过这种方式给 UI 控制器分配过多的责任也使测试变得更加困难。

将视图数据所有权从 UI 控制器逻辑中分离出来更简单,更高效。

实现一个ViewModel

架构组件为UI控制器提供 ViewModel 助手类。ViewModel 对象在配置更改期间会自动保留,以便它们保存的数据立即可用于下一个 Activity 或 fragment 实例。例如,如果您需要在应用中显示用户列表,请明确分配职责来获取数据并将用户列表保存到 ViewModel,而不是 Activity 或 fragment,如以下示例代码所示:

public class MyViewModel extends ViewModel {
    private MutableLiveData<List<User>> users;
    public LiveData<List<User>> getUsers() {
        if (users == null) {
            users = new MutableLiveData<List<User>>();
            loadUsers();
        }
        return users;
    }

    private void loadUsers() {
        // Do an asynchronous operation to fetch users.
    }
}

你可以从一个 Activity 中访问列表,如下所示:

public class MyActivity extends AppCompatActivity {
    public void onCreate(Bundle savedInstanceState) {
        // Create a ViewModel the first time the system calls an activity's onCreate() method.
        // Re-created activities receive the same MyViewModel instance created by the first activity.

        MyViewModel model = ViewModelProviders.of(this).get(MyViewModel.class);
        model.getUsers().observe(this, users -> {
            // update UI
        });
    }
}

如果 Activity 重新创建,它将接收由第一个 Activity 创建的相同的 MyViewModel 实例。当持有 ViewModel 的 Activity finish 后,框架将调用 ViewModel 对象的 onCleared() 方法,以便它可以清理资源。

ViewModel 绝不能引用视图,生命周期或可能持有对活动上下文的引用的任何类。

ViewModel 对象被设计为脱离视图或 LifecycleOwners 的特定实例。这种设计还意味着您可以更轻松地编写测试来覆盖 ViewModel,因为它不知道视图和生命周期对象。ViewModel 对象可以包含 LifecycleObservers,例如 LiveData 对象。但是,ViewModel 对象绝不能观察对生命周期感知的可观察对象(如 LiveData 对象)的更改。如果 ViewModel 需要应用程序上下文(例如查找系统服务),那么它可以扩展 AndroidViewModel 类并具有构造函数,该构造函数在构造函数中接收 Application,因为 Application 类扩展了 Context。

ViewModel 的生命周期

ViewModel 对象的范围是在获取 ViewModel 时传递给 ViewModelProvider 的生命周期。ViewModel 保留在内存中,直到生命周期的范围永久消失:在一个 Activity 的情况下,finish() 时,在一个 Fragment 的情况下,当它被 detached(分离)时。

下图说明了一个 Activity 在进行一次旋转然后 finish 后的各种生命周期状态。该图还显示了相关 Activity 生命周期旁边 ViewModel 的生命周期。这个特定的图表说明了一个 Activity 的状态。这些相同的基本状态同样适用于 Fragment 的生命周期。

系统首次调用 Activity 对象的 onCreate() 方法时,通常会请求 ViewModel。系统可能会在整个 Activity 的生命周期中多次调用 onCreate(),例如当设备屏幕旋转时。ViewModel 从第一次请求 ViewModel 直到 Activity finished 和销毁时一直存在。

在 Fragment 之间共享数据

Activity 中的两个或更多 fragment 需要彼此进行通信是很常见的。想象一下,主-从关系的 F 让给 met 的一种常见情况,其中有一个 Fragment,用户从列表中选择一个项目,另一个 fragment 显示所选项目的内容。这种情况有些麻烦,因为这两个片段都需要定义一些接口描述,并且所有者 Activity 必须将两者绑定在一起。此外,这两个 fragment 必须处理其他 fragment 尚未创建或可见的场景。

可以使用 ViewModel 对象解决这个常见的痛点。这些 fragment 可以使用其 Activity 范围共享 ViewModel 来处理此通信,如以下示例代码所示:

public class SharedViewModel extends ViewModel {
    private final MutableLiveData<Item> selected = new MutableLiveData<Item>();

    public void select(Item item) {
        selected.setValue(item);
    }

    public LiveData<Item> getSelected() {
        return selected;
    }
}


public class MasterFragment extends Fragment {
    private SharedViewModel model;
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        model = ViewModelProviders.of(getActivity()).get(SharedViewModel.class);
        itemSelector.setOnClickListener(item -> {
            model.select(item);
        });
    }
}

public class DetailFragment extends Fragment {
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        SharedViewModel model = ViewModelProviders.of(getActivity()).get(SharedViewModel.class);
        model.getSelected().observe(this, { item ->
           // Update the UI.
        });
    }
}

请注意,在获取 ViewModelProvider 时,这两个 Fragment 都使用 getActivity()。因此,两个 Fragment 都接收相同的 SharedViewModel 实例,该实例的范围限定为 Activity。
这种方法具有以下优点:

  • Activity 不需要做任何事情,也不需要了解这种沟通。
  • 除了 SharedViewModel 约定之外,fragment 不需要彼此了解。如果其中一个 fragment 消失,另一个 fragment 继续照常工作。
  • 每个片 fragment 都有其自己的生命周期,并且不受其他生命周期的影响。如果一个 fragment 替换另一个 fragment,UI 将继续工作而不会出现任何问题。

用 ViewModel 替换 Loaders

CursorLoader 这样的 Loader 类经常用于保持应用程序UI中的数据与数据库同步。您可以使用 ViewModel 和其他几个类来替换 Loaders。使用 ViewModel 将您的UI控制器与数据加载操作分开,这意味着您在类之间的强引用减少了。

在使用 loaders 的一种常见方法中,应用程序可能使用 CursorLoader 来观察数据库的内容。当数据库中的值发生更改时,加载程序会自动触发重新加载数据并更新UI:

ViewModel 与 Room 和 LiveData 一起使用来替换 Loaders。ViewModel 可确保数据在设备配置更改后仍然存在。当数据库发生更改时,Room 会通知您的 LiveData,而 LiveData 则会用修改的数据更新您的 UI。