【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。

相关推荐
诸神黄昏EX1 小时前
Android 分区相关介绍
android
大白要努力!2 小时前
android 使用SQLiteOpenHelper 如何优化数据库的性能
android·数据库·oracle
Estar.Lee2 小时前
时间操作[取当前北京时间]免费API接口教程
android·网络·后端·网络协议·tcp/ip
Winston Wood2 小时前
Perfetto学习大全
android·性能优化·perfetto
Dnelic-5 小时前
【单元测试】【Android】JUnit 4 和 JUnit 5 的差异记录
android·junit·单元测试·android studio·自学笔记
Eastsea.Chen8 小时前
MTK Android12 user版本MtkLogger
android·framework
长亭外的少年15 小时前
Kotlin 编译失败问题及解决方案:从守护进程到 Gradle 配置
android·开发语言·kotlin
建群新人小猿17 小时前
会员等级经验问题
android·开发语言·前端·javascript·php
1024小神18 小时前
tauri2.0版本开发苹果ios和安卓android应用,环境搭建和最后编译为apk
android·ios·tauri
兰琛19 小时前
20241121 android中树结构列表(使用recyclerView实现)
android·gitee