Jetpack AAC完整解析(3)—-ViewModel

1. ViewModel 介绍

ViewModel是Jetpack AAC的重要组件,同时也有一个同名抽象类。

ViewModel,意为 视图模型,即 为界面准备数据的模型。简单理解就是,ViewModel为UI层提供数据。 官方文档定义如下:

ViewModel 以注重生命周期的方式存储和管理界面相关的数据。(作用)

ViewModel 类让数据可在发生屏幕旋转等配置更改后继续留存。(特点)

1.1 出场背景

  1. Activity可能会在某些场景(例如屏幕旋转)销毁和重新创建界面,那么存储在其中的界面相关数据都会丢失。例如,界面含用户信息列表,因配置更改而重新创建 Activity 后,新 Activity 必须重新请求用户列表,这会造成资源的浪费。能否直接恢复之前的数据呢?对于简单的数据,Activity 可以使用 onSaveInstanceState() 方法保存 然后从 onCreate() 中的Bundle恢复数据,但此方法仅适合可以序列化再反序列化的少量数据(IPC对Bundle有1M的限制),而不适合数量可能较大的数据,如用户信息列表或位图。 那么如何做到 因配置更改而新建Activity后的数据恢复呢?
  2. UI层(如 Activity 和 Fragment)经常需要通过逻辑层(如MVP中的Presenter)进行异步请求,可能需要一些时间才能返回结果,如果逻辑层持有UI层应用(如context),那么UI层需要管理这些请求,确保界面销毁后清理这些调用以避免潜在的内存泄露,但此项管理需要大量的维护工作。 那么如何更好的避免因异步请求带来的内存泄漏呢?

这时候ViewModel就闪亮出场了——ViewModel用于代替MVP中的Presenter,为UI层准备数据,用于解决上面两个问题。

1.2 特点

具体地,相比于Presenter,ViewModel有以下特点:

1.2.1 生命周期长于Activity

ViewModel最重要的特点是 生命周期长于Activity在页面onDestroy的时候ViewModel还没销毁结束,所以不要在ViewModel持有Context的引用或者持有Context类。来看下官网的一张图:

4.png

看到在因屏幕旋转而重新创建Activity后,ViewModel对象依然会保留。 只有Activity真正Finish的时ViewModel才会被清除。

也就是说,因系统配置变更Activity销毁重建,ViewModel对象会保留并关联到新的Activity。而Activity的正常销毁(系统不会重建Activity)时,ViewModel对象是会清除的。

那么很自然的,因系统配置变更Activity销毁重建,ViewModel内部存储的数据 就可供重新创建的Activity实例使用了。这就解决了第一个问题。

1.2.2 不持有UI层引用

我们知道,在MVP的Presenter中需要持有IView接口来回调结果给界面。

而ViewModel是不需要持有UI层引用的,那结果怎么给到UI层呢?答案就是使用上一篇中介绍的基于观察者模式的LiveData。 并且,ViewModel也不能持有UI层引用,因为ViewModel的生命周期更长。

所以,ViewModel不需要也不能 持有UI层引用,那么就避免了可能的内存泄漏,同时实现了解耦。这就解决了第二个问题。

2. ViewModel使用

2.1 基本使用

了解了ViewModel作用解特点,下面来看看如何结合LivaData使用的。

步骤:

  1. 继承ViewModel自定义MyViewModel
  2. 在MyViewModel中编写获取UI数据的逻辑
  3. 使用LiveData将获取到的UI数据抛出
  4. 在Activity/Fragment中使用ViewModelProvider获取MyViewModel实例
  5. 观察MyViewModel中的LiveData数据,进行对应的UI更新。

举个例子,如果您需要在Activity中显示用户信息,那么需要将获取用户信息的操作分放到ViewModel中,代码如下:

public class UserViewModel extends ViewModel {

    private MutableLiveData<String> userLiveData ;
    private MutableLiveData<Boolean> loadingLiveData;

    public UserViewModel() {
        userLiveData = new MutableLiveData<>();
        loadingLiveData = new MutableLiveData<>();
    }

    //获取用户信息,假装网络请求 2s后 返回用户信息
    public void getUserInfo() {

        loadingLiveData.setValue(true);

        new AsyncTask<Void, Void, String>() {
            @Override
            protected void onPostExecute(String s) {
                loadingLiveData.setValue(false);
                userLiveData.setValue(s);//抛出用户信息
            }
            @Override
            protected String doInBackground(Void... voids) {
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                String userName = "UserName";
                return userName;
            }
        }.execute();
    }

    public LiveData<String> getUserLiveData() {
        return userLiveData;
    }
    public LiveData<Boolean> getLoadingLiveData() {
        return loadingLiveData;
    }
}
复制代码

UserViewModel继承ViewModel,然后逻辑很简单:假装网络请求 2s后 返回用户信息,其中userLiveData用于抛出用户信息,loadingLiveData用于控制进度条显示。

再看UI层:

public class UserActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ...
        Log.i(TAG, "onCreate: ");

        TextView tvUserName = findViewById(R.id.textView);
        ProgressBar pbLoading = findViewById(R.id.pb_loading);
    	//获取ViewModel实例 --- 一定要通过ViewModelProvider创建ViewModel
        ViewModelProvider viewModelProvider = new ViewModelProvider(this);
        UserViewModel userViewModel = viewModelProvider.get(UserViewModel.class);
        //观察 用户信息 --- 每一个LiveData都得单独写一个observe方法添加观察者
        userViewModel.getUserLiveData().observe(this, new Observer<String>() {
            @Override
            public void onChanged(String s) {
                // update ui.
                tvUserName.setText(s);
            }
        });

        userViewModel.getLoadingLiveData().observe(this, new Observer<Boolean>() {
            @Override
            public void onChanged(Boolean aBoolean) {
                pbLoading.setVisibility(aBoolean?View.VISIBLE:View.GONE);
            }
        });
        //点击按钮获取用户信息
        findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                userViewModel.getUserInfo();
            }
        });
    }

    @Override
    protected void onStop() {
        super.onStop();
        Log.i(TAG, "onStop: ");
    }
    @Override
    protected void onDestroy() {
        super.onDestroy();
        Log.i(TAG, "onDestroy: ");
    }
}
复制代码

页面有个按钮用于点击获取用户信息,有个TextView展示用户信息。 在onCreate()中先 创建ViewModelProvider实例,传入的参数是ViewModelStoreOwner,Activity和Fragment都是其实现。然后通过ViewModelProvider的get方法 获取ViewModel实例,然后就是 观察ViewModel中的LiveData

运行后,点击按钮 会弹出进度条,2s后展示用户信息。接着旋转手机,我们发现用户信息依然存在。
旋转手机后确实是重建了Activity的,日志打印如下:

2021-01-06 20:35:44.984 28269-28269/com.hfy.androidlearning I/UserActivity: onStop: 
2021-01-06 20:35:44.986 28269-28269/com.hfy.androidlearning I/UserActivity: onDestroy: 
2021-01-06 20:35:45.025 28269-28269/com.hfy.androidlearning I/UserActivity: onCreate: 
复制代码

总结下:

  1. ViewModel的使用很简单,作用和原来的Presenter一致。只是要结合LiveData,UI层观察即可。
  2. ViewModel的创建必须通过ViewModelProvider
  3. 注意到ViewModel中没有持有任何UI相关的引用。
  4. 旋转手机重建Activity后,数据确实恢复了。

2.2 Fragment 间数据共享

Activity 中的多个Fragment需要相互通信是一种很常见的情况。假设有一个ListFragment,用户从列表中选择一项,会有另一个DetailFragment显示选定项的详情内容。在之前 你可能会定义接口或者使用EventBus来实现数据的传递共享。

现在就可以使用 ViewModel 来实现。这两个 Fragment 可以使用其 Activity 范围共享 ViewModel 来处理此类通信,如以下示例代码所示:

//ViewModel
public class SharedViewModel extends ViewModel {
//被选中的Item
    private final MutableLiveData<UserContent.UserItem> selected = new MutableLiveData<UserContent.UserItem>();

    public void select(UserContent.UserItem user) {
        selected.setValue(user);
    }
    public LiveData<UserContent.UserItem> getSelected() {
        return selected;
    }
}

//ListFragment
public class MyListFragment extends Fragment {
   ...
    private SharedViewModel model;
   ...
    public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        //获取ViewModel,注意ViewModelProvider实例传入的是宿主Activity
        model = new ViewModelProvider(requireActivity()).get(SharedViewModel.class);
        adapter.setListner(new MyItemRecyclerViewAdapter.ItemCLickListner(){
            @Override
            public void onClickItem(UserContent.UserItem userItem) {
                model.select(userItem);
            }
        });
    }
}

//DetailFragment
public class DetailFragment extends Fragment {

    public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        TextView detail = view.findViewById(R.id.tv_detail);
        //获取ViewModel,观察被选中的Item
        SharedViewModel model = new ViewModelProvider(requireActivity()).get(SharedViewModel.class);
        model.getSelected().observe(getViewLifecycleOwner(), new Observer<UserContent.UserItem>() {
            @Override
            public void onChanged(UserContent.UserItem userItem) {
                //展示详情
                detail.setText(userItem.toString());
            }
        });
    }
}
复制代码

代码很简单,ListFragment中在点击Item时更新ViewModel的LiveData数据,然后DetailFragment监听这个LiveData数据即可。

要注意的是,这两个 Fragment 通过ViewModelProvider获取ViewModel时 传入的都是它们宿主Activity。这样,当这两个 Fragment 各自获取 ViewModelProvider 时,它们会收到相同的 SharedViewModel 实例(其范围限定为该 Activity)。

此方法具有以下 优势

  1. Activity 不需要执行任何操作,也不需要对此通信有任何了解。
  2. 除了 SharedViewModel 约定之外,Fragment 不需要相互了解。如果其中一个 Fragment 消失,另一个 Fragment 将继续照常工作。
  3. 每个 Fragment 都有自己的生命周期,而不受另一个 Fragment 的生命周期的影响。如果一个 Fragment 替换另一个 Fragment,界面将继续工作而没有任何问题。

最后来看下效果:

Fragment共享.gif

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享