如何应对 Android 面试官 -> 玩转 Jetpack ViewModel

前言


本章重点讲解 ViewModel 的基础使用和原理;

因为 ViewModel 的原理比较简单,本章结合 LiveData、DataBinding、Lifecycle 一起搞一个小 demo 来讲解 ViewModel 的原理和基础使用;

ViewModel 用来干什么?


  • 一个容器,如果数据打包在 ViewModel 中,当配置发生变化的时候,这些数据是不会丢失的,保证数据的稳定性
  • 作为和界面 Controller 直接交互的最上层数据中心
    • 和界面 Controller 直接交互、数据中心;Activity Fragment 获得 ViewModel 对象,并从中获取数据显示到界面;
    • 最上层 ViewModel 中的数据格式未必是最底层格式,而是针对各个界面定制后的数据格式,ViewModel会向下层真正的数据中心取数据,并整理成上层格式;
  • 在Activity被销毁(转屏、系统语言切换)的时候,ViewModel 并没有被销毁,因此重建的 Activity 可以直接拿到数据,无需重新初始化------相当于是修复了 Activity Fragment 重建时数据也会被重建的 bug(虽然不是bug)
  • 可以在多个 Controller (Activity 和 Fragment 或者 多个 Fragment) 之间共享同一个 ViewModel,达到共享数据的结果,实际上就是为 Activity 和 Fragment 加了一个无额外操作成本的抽象的数据层;
    • 只支持竖屏的App 还需要 ViewModel 吗?
      • 需要,它可以让你的Activity 和 Fragment 以及多个 Fragment 之间共享数据
    • 如果用 Compose 可以共享数据吗?
      • 可以在多个 Compose 组件间共享数据
    • 如果你的App只支持竖屏,没有使用 Fragment,也没有自定义View 也不用 Compose 还需要ViewModel吗?
      • 不需要了
  • 减轻 Activity 的负担,让它不要什么都干;

基础使用篇


使用很简单,就是继承 ViewModel

kotlin 复制代码
import androidx.lifecycle.ViewModel

class MyViewModel : ViewModel() {
    var number : Int = 0;
}

然后 Activity 中创建它的实例

kotlin 复制代码
class MainActivity : AppCompatActivity() {

    private lateinit var myViewModel: MyViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        
        // 不能直接实例化,因为如果能这样写,系统不可控了
        // myViewModel = MyViewModel() 
        
        myViewModel = ViewModelProvider(this, ViewModelProvider.NewInstanceFactory())
            .get(MyViewModel::class.java)
            
        text_number.text = "${myViewModel.number}"

        // 点击事件 lambda
        btn_plus.setOnClickListener {
            text_number.text = "${++myViewModel.number}"
        }    
    }
}

PS: 不能使用 new 的方式,这样写,系统就不可控了;应该使用 ViewModel 提供者 ViewModeProvider 来创建;

运行之后,我们改变数值之后,旋转屏幕,可以看到,界面上的数值并不会被重置为 0 ;

组合使用

我们来搞一个小 demo,组合使用 LiveData + DataBinding + ViewModel + Lifecycle

我们就来搞一个『拨号键盘』点击键盘显示对应的数字,以及调用系统的打电话能力;

我们先来创建 ViewModel,PhoneViewModel

typescript 复制代码
public class PhoneViewModel extends AndroidViewModel {
    
    // 结合 LiveData 实现感应能力;
    private MutableLiveData<String> phoneInfo;
    // 上下文环境
    private Context context;
    
    public MainViewModel(Application application) {
        super(application);
        context = application;
    }

    // 提供数据接口 给布局用
    public MutableLiveData<String> getPhoneInfo() {
        if (phoneInfo == null) {
            phoneInfo = new MutableLiveData<>();
            // 设置默认值
            phoneInfo.setValue("");
        }
        return phoneInfo;
    }
    
    /**
     * 输入
     *
     * @param number
     */
    public void appendNumber(String number) {
        phoneInfo.setValue(phoneInfo.getValue() + number);
    }

    /**
     * 删除
     */
    public void backspaceNumber() {
        int length = phoneInfo.getValue().length();
        if (length > 0) {
            phoneInfo.setValue(phoneInfo.getValue().substring(0, phoneInfo.getValue().length() - 1));
        }
    }

    /**
     * 清空
     */
    public void clear() {
        phoneInfo.setValue("");
    }

    /**
     * 拨打
     */
    public void callPhone() {
        Intent intent = new Intent();
        intent.setAction(Intent.ACTION_CALL);
        intent.setData(Uri.parse("tel:" + phoneInfo.getValue()));
        // 非 Activity 启动拨号 或者是 非 Activity 启动任何的 startActivity 都会奔溃
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        context.startActivity(intent);
    }
}

这里我们使用继承 AndroidViewModel,AndroidViewModel 需要我们复写构造方法,传入 Application,这样我们就可以在 PhoneViewModel 中拿到 Context;

scala 复制代码
public class AndroidViewModel extends ViewModel {
    @SuppressLint("StaticFieldLeak")
    private Application mApplication;

    public AndroidViewModel(@NonNull Application application) {
        mApplication = application;
    }

    /**
     * Return the application.
     */
    @SuppressWarnings({"TypeParameterUnusedInFormals", "unchecked"})
    @NonNull
    public <T extends Application> T getApplication() {
        return (T) mApplication;
    }
}

接下来我们来创建 xml 布局文件,并结合 DataBinding 关联 PhoneViewModel

ini 复制代码
<layout xmlns:android="http://schemas.android.com/apk/res/android">

    <data>
        <variable
            name="vm"
            type="com.llc.jetpack.PhoneViewModel" />
    </data>

    <!-- UI绘制区域 -->
    <LinearLayout
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:orientation="vertical"
        android:background="@drawable/phone2_bg">

        <LinearLayout
            android:layout_width="fill_parent"
            android:layout_height="0dip"
            android:layout_weight="1" />

        <!-- 电话号码 -->
        <TextView
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:textSize="@dimen/activity_phone_tv"
            android:gravity="center"
            android:text="@{vm.phoneInfo}"
            android:textStyle="bold" />

        <!-- 表格布局 -->
        <TableLayout
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:layout_alignParentBottom="true"
            android:layout_marginBottom="16dip">

            <!-- 第一列 -->
            <TableRow
                android:layout_width="fill_parent"
                android:layout_height="wrap_content">

                <Button
                    android:layout_width="fill_parent"
                    android:layout_height="wrap_content"
                    android:layout_weight="1"
                    android:text="@string/phone1"
                    android:onClick="@{()->vm.appendNumber(String.valueOf(1))}"
                    android:textSize="@dimen/activity_phone_bt"
                    android:background="@drawable/phone_selector_number" />

                <Button
                    android:layout_width="fill_parent"
                    android:layout_height="wrap_content"
                    android:layout_weight="1"
                    android:text="@string/phone2"
                    android:onClick="@{()->vm.appendNumber(String.valueOf(2))}"
                    android:textSize="@dimen/activity_phone_bt"
                    android:background="@drawable/phone_selector_number" />

                <Button
                    android:layout_width="fill_parent"
                    android:layout_height="wrap_content"
                    android:layout_weight="1"
                    android:text="@string/phone3"
                    android:onClick="@{()->vm.appendNumber(String.valueOf(3))}"
                    android:textSize="@dimen/activity_phone_bt"
                    android:background="@drawable/phone_selector_number" />
            </TableRow>

            <!-- 第二列 -->
            <TableRow
                android:layout_width="fill_parent"
                android:layout_height="wrap_content">

                <Button
                    android:layout_width="fill_parent"
                    android:layout_height="wrap_content"
                    android:layout_weight="1"
                    android:text="@string/phone4"
                    android:onClick="@{()->vm.appendNumber(String.valueOf(4))}"
                    android:textSize="@dimen/activity_phone_bt"
                    android:background="@drawable/phone_selector_number" />

                <Button
                    android:layout_width="fill_parent"
                    android:layout_height="wrap_content"
                    android:layout_weight="1"
                    android:text="@string/phone5"
                    android:onClick="@{()->vm.appendNumber(String.valueOf(5))}"
                    android:textSize="@dimen/activity_phone_bt"
                    android:background="@drawable/phone_selector_number" />

                <Button
                    android:layout_width="fill_parent"
                    android:layout_height="wrap_content"
                    android:layout_weight="1"
                    android:text="@string/phone6"
                    android:onClick="@{()->vm.appendNumber(String.valueOf(6))}"
                    android:textSize="@dimen/activity_phone_bt"
                    android:background="@drawable/phone_selector_number" />
            </TableRow>

            <!-- 第3列 -->
            <TableRow
                android:layout_width="fill_parent"
                android:layout_height="wrap_content">

                <Button
                    android:layout_width="fill_parent"
                    android:layout_height="wrap_content"
                    android:layout_weight="1"
                    android:text="@string/phone7"
                    android:onClick="@{()->vm.appendNumber(String.valueOf(7))}"
                    android:textSize="@dimen/activity_phone_bt"
                    android:background="@drawable/phone_selector_number" />

                <Button
                    android:layout_width="fill_parent"
                    android:layout_height="wrap_content"
                    android:layout_weight="1"
                    android:text="@string/phone8"
                    android:onClick="@{()->vm.appendNumber(String.valueOf(8))}"
                    android:textSize="@dimen/activity_phone_bt"
                    android:background="@drawable/phone_selector_number" />

                <Button
                    android:layout_width="fill_parent"
                    android:layout_height="wrap_content"
                    android:layout_weight="1"
                    android:text="@string/phone9"
                    android:onClick="@{()->vm.appendNumber(String.valueOf(9))}"
                    android:textSize="@dimen/activity_phone_bt"
                    android:background="@drawable/phone_selector_number" />
            </TableRow>

            <!-- 第4列 -->
            <TableRow
                android:layout_width="fill_parent"
                android:layout_height="wrap_content">

                <Button
                    android:layout_width="fill_parent"
                    android:layout_height="wrap_content"
                    android:layout_weight="1"
                    android:text="@string/phonexin"
                    android:onClick="@{()->vm.appendNumber(@string/phonexin)}"
                    android:textSize="@dimen/activity_phone_bt"
                    android:background="@drawable/phone_selector_number" />

                <Button
                    android:layout_width="fill_parent"
                    android:layout_height="wrap_content"
                    android:layout_weight="1"
                    android:text="@string/phone0"
                    android:onClick="@{()->vm.appendNumber(String.valueOf(0))}"
                    android:textSize="@dimen/activity_phone_bt"
                    android:background="@drawable/phone_selector_number" />

                <Button
                    android:layout_width="fill_parent"
                    android:layout_height="wrap_content"
                    android:layout_weight="1"
                    android:text="@string/phonejin"
                    android:onClick="@{()->vm.appendNumber(@string/phonejin)}"
                    android:textSize="@dimen/activity_phone_bt"
                    android:background="@drawable/phone_selector_number" />
            </TableRow>

            <!-- 第5列 -->
            <TableRow
                android:layout_width="fill_parent"
                android:layout_height="wrap_content"
                android:layout_marginTop="6dip">

                <LinearLayout
                    android:layout_width="fill_parent"
                    android:layout_height="wrap_content"
                    android:layout_weight="1"
                    android:orientation="vertical">

                    <!-- 清空 -->
                    <Button
                        android:layout_width="40dp"
                        android:layout_height="40dp"
                        android:textSize="@dimen/activity_phone_bt"
                        android:background="@drawable/phone_selector_min"
                        android:layout_gravity="center"
                        android:onClick="@{()->vm.clear()}"
                        android:layout_margin="6dip" />

                </LinearLayout>

                <LinearLayout
                    android:layout_width="fill_parent"
                    android:layout_height="wrap_content"
                    android:layout_weight="1"
                    android:orientation="vertical">

                    <!-- 拨打 -->
                    <ImageView
                        android:layout_width="46dip"
                        android:layout_height="46dip"
                        android:src="@drawable/phone_selector_call"
                        android:onClick="@{()->vm.callPhone()}"
                        android:layout_gravity="center" />

                </LinearLayout>

                <LinearLayout
                    android:layout_width="fill_parent"
                    android:layout_height="wrap_content"
                    android:layout_weight="1"
                    android:orientation="vertical">

                    <!-- 删除一个字符 -->
                    <Button
                        android:layout_width="60dp"
                        android:layout_height="wrap_content"
                        android:textSize="@dimen/activity_phone_bt"
                        android:background="@drawable/phone_selector_backspace"
                        android:layout_gravity="center"
                        android:onClick="@{()->vm.backspaceNumber()}"/>

                </LinearLayout>

            </TableRow>

        </TableLayout>

    </LinearLayout>
</layout>

可以看到一个小 tips,在 DataBinding 下,我们的每个控件其实是不需要声明 id 的;

接下来,我们在 MainActivity 中进行绑定;

scala 复制代码
public class MainActivity extends AppCompatActivity {

    private ActivityMainBinding dataBinding; // DataBinding 初始化
    private PhoneViewModel phoneViewModel; // PhoneViewModel 初始化

    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        dataBinding = DataBindingUtil.setContentView(this, R.layout.activity_main);
        
        // extends ViewModel
        // phoneViewModel = new ViewModelProvider(this, ViewModelProvider.NewInstanceFactory()).get(PhoneViewModel.class);

        // extends AndroidViewModel
        phoneViewModel = new ViewModelProvider(getViewModelStore(), new ViewModelProvider.AndroidViewModelFactory(getApplication())).get(PhoneViewModel.class);
        // 让 ViewModel 和 DataBinding 建立关联
        dataBinding.setVm(phoneViewModel);
        // 让 LiveData 和 DataBinding 建立关联
        dataBinding.setLifecycleOwner(this);
    }
}

因为 PhoneViewModel 继承了 AndroidViewModel,所以 PhoneViewModel 实例的创建要改成

csharp 复制代码
new ViewModelProvider(getViewModelStore(), new ViewModelProvider.AndroidViewModelFactory(getApplication()))
    .get(PhoneViewModel.class)

看到这里的时候,有人就要提出疑问了,为什么 xml 中的 TextView 的 android:text="@{vm.phoneInfo}" 就可以渲染数据,而不用在 Activity 中调用 phoneInfo.observe 方法,在 onChange 回调中调用 textView.setText 方法?

上一章的 DataBinding 源码分析的时候,我们可以猜测下,应该是 ActivityMainBindingImpl 中帮我们生成了 getValue 的调用,以及 setText 的逻辑;

核心代码是下面这两行,让 DataBinding 和 LiveData 以及 ViewModel 组合在一起,就想 ViewPage 和 TabLayout 组合一样;

kotlin 复制代码
// 让 ViewModel 和 DataBinding 建立关联
dataBinding.setVm(phoneViewModel);
// 让 LiveData 和 DataBinding 建立关联
dataBinding.setLifecycleOwner(this);

这样,我们就构建了一个数据驱动UI的典型小 demo;

源码篇


一共使用了三个参数 this、ViewModelProvider.NewInstanceFactory()、PhoneViewModel.class

this

csharp 复制代码
phoneViewModel = new ViewModelProvider(this, ViewModelProvider.NewInstanceFactory()).get(PhoneViewModel.class);

我们进入这个构造方法看下:

less 复制代码
public ViewModelProvider(@NonNull ViewModelStoreOwner owner, @NonNull Factory factory) {
    this(owner.getViewModelStore(), factory);
}

这里调用了 owner.getViewModelStore() 方法,owner 就是我们传入的 this 当前 Activity;我们进入这个 getViewModelStore 方法看下:

ini 复制代码
public ViewModelStore getViewModelStore() {
    if (mViewModelStore == null) {
        NonConfigurationInstances nc =
                (NonConfigurationInstances) getLastNonConfigurationInstance();
        if (nc != null) {
            mViewModelStore = nc.viewModelStore;
        }
        if (mViewModelStore == null) {
            mViewModelStore = new ViewModelStore();
        }
    }
    return mViewModelStore;
}

这里就是创建 new ViewModelStore() ViewModelStore

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

可以看到,这里面持有了一个 HashMap 用来存储 ViewModel 的具体实现;

本质上就是我们创建的 Activity 就是这个 ViewModelStore 用来存储和这个 Activity 关联的 ViewModel;

Factory

Factory 用来反射创建我们要创建的 ViewModel 实例;我们进入这个 NewInstanceFactory 看下:

typescript 复制代码
public static class NewInstanceFactory implements Factory {

    private static NewInstanceFactory sInstance;

    @NonNull
    static NewInstanceFactory getInstance() {
        if (sInstance == null) {
            sInstance = new NewInstanceFactory();
        }
        return sInstance;
    }

    @SuppressWarnings("ClassNewInstance")
    @NonNull
    @Override
    public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
        try {
            return modelClass.newInstance();
        } catch (InstantiationException e) {
            throw new RuntimeException("Cannot create an instance of " + modelClass, e);
        } catch (IllegalAccessException e) {
            throw new RuntimeException("Cannot create an instance of " + modelClass, e);
        }
    }
}

最终通过 create 方法调用到我们传入的 class 对象的 newInstance() 方法来创建实例;

ViewModel.class

我们进入这个 get 方法看下:

less 复制代码
public <T extends ViewModel> T get(@NonNull Class<T> modelClass) {
    String canonicalName = modelClass.getCanonicalName();
    return get(DEFAULT_KEY + ":" + canonicalName, modelClass);
}

用 class 对象的 CanonicalName 作为 key;

less 复制代码
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;
}

先判断 ViewModelStore 中是否有,没有则调用 Factory 的 create 方法进行创建,然后保存到 ViewModeStore 中;

为什么横竖屏切换的时候,数据不会销毁

由 AMS 可以知道,当发生横竖屏切换的时候,最终会回调到 Activity.java 中的 retainNonConfigurationInstances 方法;

我们进入这个方法看下:

ini 复制代码
NonConfigurationInstances retainNonConfigurationInstances() {
    // 核心逻辑 1
    Object activity = onRetainNonConfigurationInstance();
    HashMap<String, Object> children = onRetainNonConfigurationChildInstances();
    FragmentManagerNonConfig fragments = mFragments.retainNestedNonConfig();
    mFragments.doLoaderStart();
    mFragments.doLoaderStop(true);
    ArrayMap<String, LoaderManager> loaders = mFragments.retainLoaderNonConfig();

    if (activity == null && children == null && fragments == null && loaders == null
            && mVoiceInteractor == null) {
        return null;
    }

    NonConfigurationInstances nci = new NonConfigurationInstances();
    nci.activity = activity;
    nci.children = children;
    nci.fragments = fragments;
    nci.loaders = loaders;
    if (mVoiceInteractor != null) {
        mVoiceInteractor.retainInstance();
        nci.voiceInteractor = mVoiceInteractor;
    }
    return nci;
}

核心逻辑1:onRetainNonConfigurationInstance(); 我们进入这个方法看下:

typescript 复制代码
public Object onRetainNonConfigurationInstance() {
    return null;
}

Activity.java 中是一个空实现,说明是其子类进行了实现,我们进入子类看下,点击左边的向下剪头;

ini 复制代码
public final Object onRetainNonConfigurationInstance() {
    Object custom = onRetainCustomNonConfigurationInstance();
    
    ViewModelStore viewModelStore = mViewModelStore;
    if (viewModelStore == null) {
        // 核心逻辑1 获取上一次的 NonConfigurationInstances 并获取其中的 ViewModelStore
        NonConfigurationInstances nc =
                (NonConfigurationInstances) getLastNonConfigurationInstance();
        if (nc != null) {
            viewModelStore = nc.viewModelStore;
        }
    }

    if (viewModelStore == null && custom == null) {
        return null;
    }
    // 核心逻辑2 将 viewModelStore 保存到 NonConfigurationInstances 中
    NonConfigurationInstances nci = new NonConfigurationInstances();
    nci.custom = custom;
    nci.viewModelStore = viewModelStore;
    return nci;
}

核心逻辑1 获取上一次的 NonConfigurationInstances 并获取其中的 ViewModelStore,横屏切换竖屏,获取的是竖屏时候的 NonConfigurationInstances 并获取其中的 ViewModelStore 来恢复竖屏的数据;

核心逻辑2 这也就能对上,getViewModelStore 方法中,为什么会优先从 NonConfigurationInstances 中获取;

ViewModel 生命周期

可以看到,只有在 Activity 执行了 onDestory,ViewModel 的数据才会清空;

屏幕旋转 发生了 onDestroy 和 onCreate 为什么 ViewModel 的数据没有销毁

ComponentActivity 中也会通过 Lifecycle 监听生命周期;

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

这里有个 isChangingConfigurations() 的判断,如果没有发生屏幕旋转,则在 Activity 销毁的时候清空 ViewModelStore,如果发生了旋转,则不会清空;

好了,ViewModel 就讲到这里吧~

欢迎三连


来都来了,点个关注,点个赞吧,你的支持是我最大的动力~

相关推荐
alexhilton2 天前
为什么你的App总是忘记所有事情
android·kotlin·android jetpack
刘龙超2 天前
如何应对 Android 面试官 -> 玩转 Jetpack DataBinding
android jetpack
雨白2 天前
Jetpack系列(四):精通WorkManager,让后台任务不再失控
android·android jetpack
刘龙超3 天前
如何应对 Android 面试官 -> 玩转 JetPack ViewBinding
android jetpack
顾林海3 天前
ViewModel 销毁时机详解
android·面试·android jetpack
雨白4 天前
Jetpack系列(三):Room数据库——从增删改查到数据库平滑升级
android·android jetpack
雨白5 天前
Jetpack系列(二):Lifecycle与LiveData结合,打造响应式UI
android·android jetpack
刘龙超6 天前
如何应对 Android 面试官 -> 玩转 JetPack LiveData
android jetpack
Wgllss16 天前
Kotlin+协程+FLow+Channel+Compose 实现一个直播多个弹幕效果
android·架构·android jetpack