如何应对 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 就讲到这里吧~

欢迎三连


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

相关推荐
未来猫咪花9 小时前
LiveData "数据倒灌":一个流行的错误概念
android·android jetpack
alexhilton2 天前
借助RemoteCompose开发动态化页面
android·kotlin·android jetpack
QING6182 天前
Jetpack Compose Brush API 简单使用实战 —— 新手指南
android·kotlin·android jetpack
QING6182 天前
Jetpack Compose Brush API 详解 —— 新手指南
android·kotlin·android jetpack
QING6184 天前
Jetpack Compose 中 Flow 收集详解 —— 新手指南
android·kotlin·android jetpack
ljt27249606614 天前
Compose笔记(五十七)--snapshotFlow
android·笔记·android jetpack
QING6184 天前
kotlin 协程: GlobalScope 和 Application Scope 选择和使用 —— 新手指南
android·kotlin·android jetpack
QING6184 天前
Kotlin 协程中Job和SupervisorJob —— 新手指南
android·kotlin·android jetpack
天花板之恋5 天前
Compose中的协程:rememberCoroutineScope 和 LaunchedEffect
android jetpack
我命由我123455 天前
Android 开发问题:布局文件中的文本,在预览时有显示出来,但是,在应用中没有显示出来
android·java·java-ee·android studio·android jetpack·android-studio·android runtime