Jetpack组件之ViewModel

Jetpack组件之ViewModel

Android Jetpack组件系列文章:
Android Jetpack组件(一)LifeCycle
Android Jetpack组件(二)Navigation
Android Jetpack组件(三)ViewModel
Android Jetpack组件(四)LiveData
Android Jetpack组件(五)Room
Android JetPack组件(六)DataBinding
Android Jetpack组件(七)Paging
Android Jetpack组件(八)WorkManager

首语

对于支持横竖屏切换的应用程序,我们切换横竖屏时,Activity会被重新创建,我们需要考虑数据的存储和恢复。Jetpack为我们提供了ViewModel组件帮我们解决这个问题,ViewModel以注重生命周期的方式存储和管理界面相关的数据。ViewModel独立于配置变化,就算Activity重建,也不会影响ViewModel的生命周期。
ViewModel生命周期
在应用开发中,通常将UI交互、数据获取等业务逻辑全部写在页面中,当项目需求不断增加,页面功能复杂时,页面类会显得尤为臃肿,且不宜维护。这样做也违背了“单一功能原则”,页面只应该负责处理用户与UI控件的交互及数据展示,获取数据的业务逻辑应该单独处理。
Android提供了ViewModel类专门用于存放应用程序页面所需的数据,它可以理解为视图与数据模型的桥梁,使视图与数据分离开同时也保持通信。

ViewModel与onSaveInstanceState()

通常我们使用onSaveInstanceState()来解决屏幕旋转带来的数据丢失问题,但是它只能保存少量的支持序列化的数据,Viewmodel支持页面中的所有数据。需要注意的是,ViewModel不支持数据的持久化,当界面彻底销毁时,ViewModel及所持有的数据就不存在了,onSaveInstanceState()没有这个限制,可以持久化页面的数据,两者用途不一。

依赖

    //包含了 viewmodel 和 livedata,lifecycle-extensions 中的 API 已弃用
    //implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
    //或者指明使用viewmodel
    implementation "androidx.lifecycle:lifecycle-viewmodel:2.2.0"

使用

首先自定义一个ViewModel,继承ViewModel类。

public class HomeViewModel extends ViewModel {

    @Override
    protected void onCleared() {
        super.onCleared();
    }
}

ViewModel是一个抽象类,只有一个onClear(),当ViewModel与之关联的Activity都被销毁时,该方法就会被调用,在方法里可以执行一些资源释放相关的工作,注意屏幕旋转导致的Activity重建不会调用该方法。
我们通过一个计数器的例子来展示ViewModel的使用,将视图与数据分离。

	private int value;

    public String gtValue() {
        return String.valueOf(value);
    }

    public void addValue() {
        value+=1;
        if (onChangedListener != null) {
            onChangedListener.onChanged(String.valueOf(value));
        }
    }

    /**
     * 通过接口的形式将数据传递出去,更好的方式是通过LiveData,
     */
    public interface onChangedListener {
        void onChanged(String value);
    }

    private onChangedListener onChangedListener;

    public void setOnChangeListener(onChangedListener onChangeListener) {
        this.onChangedListener = onChangeListener;
    }

然后在Activity中,每点击一次,计数器+1,ViewModel的实例化是通过ViewModelProvider来完成的,它会判断ViewModel是否存在,若存在直接返回,不存在则创建。

		HomeViewModel homeViewModel = new ViewModelProvider(this).get(HomeViewModel.class);
        textView.setText(homeViewModel.gtValue());

        button.setOnClickListener(view -> homeViewModel.addValue());
        homeViewModel.setOnChangeListener(textView::setText);

运行代码可以发现点击计数器都会+1,同时旋转屏幕导致Activity重建时,数据不会消失,代表ViewModel并没有被销毁,持有的数据一直存在。

Fragment间共享数据

Activity中的两个或更多 Fragment 经常需要相互通信,这种情况处理比较复杂,ViewModel能将数据从Activity中剥离处理,只要Activity不销毁,ViewModel就一直存在,基于这些特性,多个Fragment 可以使用其 Activity 范围共享 ViewModel 来处理此类通信。

 //ViewModelProvider的范围必须是所在activity
 HomeViewModel homeViewModel = new ViewModelProvider(getActivity()).get(HomeViewModel.class);

在另一个Fragment中同样实例化ViewModel。当切换Fragment时,会提示计数器当前值,达到了Fragment之间的通信。

 homeViewModel = new ViewModelProvider(getActivity()).get(HomeViewModel.class);
 @Override
    public void onHiddenChanged(boolean hidden) {
        super.onHiddenChanged(hidden);
        Toast.makeText(getActivity(), homeViewModel.gtValue(),Toast.LENGTH_SHORT).show();
    }

Fragment中使用ViewModel和Activity中使用类似。

ViewModel的原理

ViewModel接收一个ViewModelStoreOwner对象作为参数,我们传递this,这是因为Activity继承ComponentActivity,它默认实现了ViewModelStoreOwner接口。源码如下。

public interface ViewModelStoreOwner {
    /**
     * Returns owned {@link ViewModelStore}
     *
     * @return a {@code ViewModelStore}
     */
    @NonNull
    ViewModelStore getViewModelStore();
}

实现此接口的职责是在配置更改期间保留拥有的ViewModelStore ,并在该作用域将要被销毁时调用。

public class ComponentActivity extends androidx.core.app.ComponentActivity implements
        LifecycleOwner,
        ViewModelStoreOwner,
        HasDefaultViewModelProviderFactory,
        SavedStateRegistryOwner,
        OnBackPressedDispatcherOwner {
        
 	public ComponentActivity() {
  		getLifecycle().addObserver(new LifecycleEventObserver() {
            @Override
            public void onStateChanged(@NonNull LifecycleOwner source,
                    @NonNull Lifecycle.Event event) {
                if (event == Lifecycle.Event.ON_DESTROY) {
                    if (!isChangingConfigurations()) {
                        getViewModelStore().clear();
                    }
                }
            }
        });
 	}
 	
    @NonNull
    @Override
    public ViewModelStore getViewModelStore() {
        if (getApplication() == null) {
            throw new IllegalStateException("Your activity is not yet attached to the "
                    + "Application instance. You can't request ViewModel before onCreate call.");
        }
        if (mViewModelStore == null) {
            NonConfigurationInstances nc =
                    (NonConfigurationInstances) getLastNonConfigurationInstance();
            if (nc != null) {
                // Restore the ViewModelStore from NonConfigurationInstances
                mViewModelStore = nc.viewModelStore;
            }
            if (mViewModelStore == null) {
                mViewModelStore = new ViewModelStore();
            }
        }
        return mViewModelStore;
    }
}

getViewModelStore()返回的类型是ViewModelStore,从源码可以看出,ViewModel是以HashMap<String,ViewModel>的形式缓存起来了,就像之前说的,页面需要ViewModel时,先判断缓存中是否存在,若存在,直接返回,不存在则创建。
Fragment也默认实现了ViewModelStoreOwner接口,原理和Activity类似。
需要注意的是,实例化ViewModel时不要传入任何类型的Context或带有Context引用的对象,将引发内存泄漏。

public class ViewModelStore {

    private final HashMap<String, ViewModel> mMap = new HashMap<>();

    final void put(String key, ViewModel viewModel) {
        ViewModel oldViewModel = mMap.put(key, viewModel);
        if (oldViewModel != null) {
            oldViewModel.onCleared();
        }
    }

    final ViewModel get(String key) {
        return mMap.get(key);
    }

    Set<String> keys() {
        return new HashSet<>(mMap.keySet());
    }

    /**
     *  Clears internal storage and notifies ViewModels that they are no longer used.
     */
    public final void clear() {
        for (ViewModel vm : mMap.values()) {
            vm.clear();
        }
        mMap.clear();
    }
}

AndroidViewModel

如果实例化ViewModel要传递Context对象,可以使用AndroidViewModel类,它继承自ViewModel,并接收Application作为Context,因此它的生命周期和应用Application的生命周期一样,不会导致内存泄漏,同时可以处理特定场景的数据问题。

Copyright: 采用 知识共享署名4.0 国际许可协议进行许可

Links: https://www.yanghujun.com/archives/viewmodel

Buy me a cup of coffee ☕.