【Android Jetpack】理解ViewModel

文章目录

ViewModel

Android系统提供控件,比如ActivityFragment,这些控件都是具有生命周期方法,这些生命周期方法被系统调用。在数据使用上有两个问题:

  • 当这些控件被销毁或者被重建的时候,如果数据保存在这些对象中,那么数据就会丢失。比如在一个界面,保存了一些用户信息,当界面重新创建的时候,就需要重新去获取数据。当然了也可以使用控件自动再带的方法,在onSaveInstanceState方法中保存数据,在onCreate中重新获得数据,但这仅仅在数据量比较小的情况下。如果数据量很大,这种方法就不能适用了。

  • 另外一个问题就是,经常需要在Activity中加载数据,这些数据可能是异步的,因为获取数据需要花费很长的时间。那么Activity就需要管理这些数据调用,否则很有可能会产生内存泄露问题。最后需要做很多额外的操作,来保证程序的正常运行。

同时Activity不仅仅只是用来加载数据的,还要加载其他资源,做其他的操作,最后Activity类变大,就是我们常讲的上帝类。也有不少架构是把一些操作放到单独的类中,比如MVP就是这样,创建相同类似于生命周期的函数做代理,这样可以减少Activity的代码量,但是这样就会变得很复杂,同时也难以测试。

ViewModel是专门用于存放应用程序页面所需的数据。ViewModel将页面所需的数据从页面中剥离出来,页面只需要处理用户交互和展示数据。

Viewmodel解决两个问题:

  1. 数据的变更与view隔离,也就是解耦
  2. 开发者不需要额外去关心因为屏幕旋转带来的数据恢复问题

实现ViewModel

ViewModel是一个抽象类,其中只有一个onCleared()方法。当ViewModel不再被需要,即与之相关的Activity都被销毁时,onCleared()方法会被系统调用。我们可以在该方法中执行一些资源释放的相关操作。(注意,由于屏幕旋转而导致的Activity重建,并不会调用该方法)

kotlin 复制代码
class TimerViewModel : ViewModel() {
    private val timer: Timer = Timer() 	//定时器类
    private var timeChangeListener: OnTimeChangeLister? = null
    private var currentSecond: Int = 0
    fun start() {
        timer.schedule(timerTask {
            currentSecond++
            timeChangeListener?.onTimeChanged(currentSecond)
        }, 1000, 1000)
    }

    override fun onCleared() {
        super.onCleared()
        timer.cancel()
    }

    fun setOnTimeChangeListener(listener: OnTimeChangeLister) {
        timeChangeListener = listener
    }

    interface OnTimeChangeLister {
        fun onTimeChanged(second: Int)
    }
}
  • ViewModel的实例化过程,是通过ViewModelProvider来完成的。ViewModelProvider会判断ViewModel是否存在,若存在则直接返回,否则它会创建一个ViewModel。
kotlin 复制代码
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val tv = findViewById<TextView>(R.id.tv)
        val model = ViewModelProvider(this).get(TimerViewModel::class.java)
        //获得ViewModel实例
        model.setOnTimeChangeListener(object : TimerViewModel.OnTimeChangeLister {
            override fun onTimeChanged(second: Int) {
                runOnUiThread {
                    tv.text = second.toString()
                }
            }
        })
        model.start()
    }
}

运行程序并旋转屏幕。当旋转屏幕导致Activity重建时,计时器并没有停止。这意味着,横/竖屏状态下的Activity所对应的ViewModel是同一个,它并没有被销毁,它所持有的数据也一直都存在着。

ViewModel的生命周期

  • ViewModel在获取ViewModel对象时会通过ViewModelProvider的传递来绑定对应的声明周期。
  • ViewModel只有在Activity finish或者Fragment detach之后才会销毁。

在Fragments间分享数据

有时候一个Activity中的两个或多个Fragment需要分享数据或者相互通信,这样就会带来很多问题,比如数据获取,相互确定生命周期。

使用ViewModel可以很好的解决这个问题。假设有这样两个Fragment,一个Fragment提供一个列表,另一个Fragment提供点击每个item现实的详细信息。

java 复制代码
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);
        //以上官方文档写法,已被弃用,采取以下写法:ViewModel viewModel = new ViewModelProvider(getActivity()/this).get(*ViewModel.class);
        itemSelector.setOnClickListener(item -> {
            model.select(item);
        });
    }
}

public class DetailFragment extends LifecycleFragment {
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        SharedViewModel model = ViewModelProviders.of(getActivity()).get(SharedViewModel.class);
        //以上官方文档写法,已被弃用,采取以下写法:ViewModel viewModel = new ViewModelProvider(getActivity()/this).get(*ViewModel.class);
        ViewModel viewModel = new ViewModelProvider(getActivity()/this).get(*ViewModel.class);
        model.getSelected().observe(this, { item ->
           // update UI
        });
    }
}

两个Fragment都是通过getActivity()来获取ViewModelProvider。这意味着两个Activity都是获取的属于同一个Activity的同一个ShareViewModel实例。

这样做优点如下:

  • Activity不需要写任何额外的代码,也不需要关心Fragment之间的通信。
  • Fragment不需要处理除SharedViewModel以外其他的代码。这两个Fragment不需要知道对方是否存在。
  • Fragment的生命周期不会相互影响

ViewModel和SavedInstanceState对比

ViewModel使得在configuration change(旋转屏幕等)保存数据变的十分方便,但是这不能用于应用被系统杀死时持久化数据。举个简单的例子,有一个界面展示用户信息。

不应该把整个用户信息放到SavedInstanceState里,而是把单个用户对应的id放到SavedInstanceState,等到界面恢复时,再通过id去获取详细的信息。这些详细的信息应该被存放在数据库中。

++Jetpack ViewModel 并不等价于 MVVM ViewModel++

这二者没有在同一个层次,MVVM ViewModel是MVVM架构中的一个角色,看不见摸不着只是一种思想。 而Jetpack ViewModel是一个实实在在的框架用于做状态托管,有对应的作用域可跟随Activity/Fragment生命周期,但这种特性恰好可以充当MVVM ViewModel的角色,分隔数据层和视图层并做数据托管。

所以结论是Jetpack ViewModel可以充当MVVM ViewModel 但二者并不等价

但jetpack的组件是可以分别使用的,所以可以先熟悉各个组件,然后往自己的项目里套。

ViewModel原理

kotlin 复制代码
val model = ViewModelProvider(this).get(TimerViewModel::class.java)

首先看ViewModelProvider的源码:

java 复制代码
public class ViewModelProvider {
    private final Factory mFactory;
    private final ViewModelStore mViewModelStore;

    /**
     * Creates {@code ViewModelProvider}. This will create {@code ViewModels}
     * and retain them in a store of the given {@code ViewModelStoreOwner}.
     * <p>
     * This method will use the
     * {@link HasDefaultViewModelProviderFactory#getDefaultViewModelProviderFactory() default factory}
     * if the owner implements {@link HasDefaultViewModelProviderFactory}. Otherwise, a
     * {@link NewInstanceFactory} will be used.
     */
    public ViewModelProvider(@NonNull ViewModelStoreOwner owner) {
        this(owner.getViewModelStore(), owner instanceof HasDefaultViewModelProviderFactory
                ? ((HasDefaultViewModelProviderFactory) owner).getDefaultViewModelProviderFactory()
                : NewInstanceFactory.getInstance());
    }
    //比较常用的构造函数
    
    public ViewModelProvider(@NonNull ViewModelStore store, @NonNull Factory factory) {
        mFactory = factory;
        mViewModelStore = store;
    }
    
    public <T extends ViewModel> T get(@NonNull Class<T> modelClass) {
        String canonicalName = modelClass.getCanonicalName();
        if (canonicalName == null) {
            throw new IllegalArgumentException("Local and anonymous classes can not be ViewModels");
        }
        return get(DEFAULT_KEY + ":" + canonicalName, modelClass);
    }//获得ViewModel实例
    
     public <T extends ViewModel> T get(@NonNull String key, @NonNull Class<T> modelClass) {
        ViewModel viewModel = mViewModelStore.get(key);

        if (modelClass.isInstance(viewModel)) {
            if (mFactory instanceof OnRequeryFactory) {
                ((OnRequeryFactory) mFactory).onRequery(viewModel);
            }
            return (T) viewModel;
        } else {
            //noinspection StatementWithEmptyBody
            if (viewModel != null) {
                // TODO: log a warning.
            }
        }
        if (mFactory instanceof KeyedFactory) {
            viewModel = ((KeyedFactory) mFactory).create(key, modelClass);
        } else {
            viewModel = mFactory.create(modelClass);
        }
        mViewModelStore.put(key, viewModel);
        return (T) viewModel;
    }
    
}    

ViewModelProvider接收一个ViewModelStoreOwner对象作为参数。在以上示例代码中该参数是this,指代当前的Activity。这是因为我们的Activity继承自ComponentActivity,而在androidx依赖包中,ComponentActivity默认实现了ViewModelStoreOwner接口:

java 复制代码
public class ComponentActivity extends androidx.core.app.ComponentActivity implements
        LifecycleOwner,
        ViewModelStoreOwner,
        SavedStateRegistryOwner,
        OnBackPressedDispatcherOwner {
            
    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;
    }            
}
java 复制代码
public interface ViewModelStoreOwner {
    /**
     * Returns owned {@link ViewModelStore}
     *
     * @return a {@code ViewModelStore}
     */
    @NonNull
    ViewModelStore getViewModelStore();
}

ViewModelStore类的代码是:

java 复制代码
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();
    }
}

从ViewModelStore的源码可以看出,ViewModel实际上是以HashMap<String,ViewModel>的形式被缓存起来了。ViewModel与页面之间没有直接的关联,它们通过ViewModelProvider进行关联。

++当页面需要ViewModel时,会向ViewModelProvider索要,ViewModelProvider检查该ViewModel是否已经存在于缓存中,若存在,则直接返回,若不存在,则实例化一个++。

因此,Activity由于配置变化导致的销毁重建并不会影响ViewModel,ViewModel是独立于页面而存在的。也正因为此,我们在使用ViewModel时,需要特别注意,不要向ViewModel中传入任何类型的Context或带有Context引用的对象,这可能会导致页面无法被销毁,从而引发内存泄漏。

ViewModel与AndroidViewModel

在使用ViewModel时,不能将任何类型的Context或含有Context引用的对象传入ViewModel,因为这可能会导致内存泄漏。但如果你希望在ViewModel中使用Context,该怎么办呢?可以使用AndroidViewModel类,它继承自ViewModel,并接收Application作为Context。这意味着,AndroidViewModel的生命周期和Application是一样的,那么这就不算是一个内存泄漏了。

在上面的示例中,我们通过自定义接口OnTimeChangeListener来完成ViewModel与页面之间的通信。这不是一个好的方案。Jetpack为我们提供了LiveData组件来解决这个问题。通过LiveData,当ViewModel中的数据发生变化时,页面能自动收到通知,进而更新UI。

相关推荐
似霰3 小时前
安卓adb shell串口基础指令
android·adb
fatiaozhang95275 小时前
中兴云电脑W102D_晶晨S905X2_2+16G_mt7661无线_安卓9.0_线刷固件包
android·adb·电视盒子·魔百盒刷机·魔百盒固件
CYRUS_STUDIO6 小时前
Android APP 热修复原理
android·app·hotfix
鸿蒙布道师7 小时前
鸿蒙NEXT开发通知工具类(ArkTs)
android·ios·华为·harmonyos·arkts·鸿蒙系统·huawei
鸿蒙布道师7 小时前
鸿蒙NEXT开发网络相关工具类(ArkTs)
android·ios·华为·harmonyos·arkts·鸿蒙系统·huawei
大耳猫7 小时前
【解决】Android Gradle Sync 报错 Could not read workspace metadata
android·gradle·android studio
ta叫我小白7 小时前
实现 Android 图片信息获取和 EXIF 坐标解析
android·exif·经纬度
dpxiaolong9 小时前
RK3588平台用v4l工具调试USB摄像头实践(亮度,饱和度,对比度,色相等)
android·windows
tangweiguo0305198710 小时前
Android 混合开发实战:统一 View 与 Compose 的浅色/深色主题方案
android
老狼孩1112210 小时前
2025新版懒人精灵零基础及各板块核心系统视频教程-全分辨率免ROOT自动化开发
android·机器人·自动化·lua·脚本开发·懒人精灵·免root开发