• 【Android-Jetpack进阶】3、ViewModel 视图模型:使用、源码解析


    三、ViewModel 视图模型

    ViewModel 介于 View(视图) 和 Model(数据模型) 之间,可以解耦分层,架构如下:

    在这里插入图片描述

    因为 ViewModel 的生命周期比 Activity 长,所以当手机旋转屏幕时,可通过 ViewModel 处理数据的存储和恢复,其生命周期示例如下:

    在这里插入图片描述

    首先,新建 Jetpack3ViewModelTest 项目,在项目添加 implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' 依赖。

    然后,新建 TimerViewModel 类。当 ViewModel 不再被需要后,其 onCleared() 方法就会被调用,可在其中做一些释放资源的操作。而屏幕旋转并不会销毁 ViewModel,代码如下:

    package com.bignerdranch.android.jetpack3viewmodeltest
    
    import androidx.lifecycle.ViewModel
    
    class TimerViewModel : ViewModel() {
        override fun onCleared() {
            super.onCleared()
        }
    }
    

    接下来,为了验证生命周期,在 ViewModel 内每隔1秒,通过 startTiming 启动定时器,再通过 OnTimeChangeListener 通知其调用者,代码如下:

    package com.bignerdranch.android.jetpack3viewmodeltest
    
    import android.util.Log
    import androidx.lifecycle.ViewModel
    import java.util.*
    
    class TimerViewModel : ViewModel() {
        private var timer: Timer? = null
        private var currentSecond: Int = 0
        private val TAG = this.javaClass.name
    
        // 开始计时
        fun startTiming() {
            if (timer == null) {
                currentSecond = 0
                timer = Timer()
                val timerTask: TimerTask = object : TimerTask() {
                    override fun run() {
                        currentSecond++
                        if (onTimeChangeListener != null) {
                            onTimeChangeListener!!.onTimeChanged(currentSecond)
                        }
                    }
                }
                timer?.schedule(timerTask, 1000, 1000) //延迟1秒执行, 间隔1秒
            }
        }
    
        // 通过接口的方式,完成对调用者的通知,这种方式不是太好,更好的方式是通过LiveData组件来实现
        interface OnTimeChangeListener {
            fun onTimeChanged(second: Int)
        }
    
        private var onTimeChangeListener: OnTimeChangeListener? = null
    
        fun setOnTimeChangeListener(onTimeChangeListener: OnTimeChangeListener?) {
            this.onTimeChangeListener = onTimeChangeListener
        }
    
        // 由于屏幕旋转导致的Activity重建,该方法不会被调用
        // 只有ViewModel已经没有任何Activity与之有关联,系统则会调用该方法,你可以在此清理资源
        override fun onCleared() {
            super.onCleared()
            Log.d(TAG, "onCleared()");
            timer?.cancel()
        }
    }
    

    然后,设置 activity_main.xml 的布局如下:

    
    <RelativeLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
    
        <TextView
            android:id="@+id/tvTime"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerInParent="true"
            android:textSize="40sp"
            android:text="TIME"/>
    
    RelativeLayout>
    

    activity_main.xml 的布局如下:

    在这里插入图片描述

    然后,在 MainActivity 中通过 ViewModelProvider 创建 timeViewModel,并监听 timerViewModel 的 OnTimeChangeListener() 回调函数传来的 second 参数,并据此更新 UI 界面的 textView,代码如下:

    package com.bignerdranch.android.jetpack3viewmodeltest
    
    import android.os.Bundle
    import android.widget.TextView
    import androidx.appcompat.app.AppCompatActivity
    import androidx.lifecycle.ViewModelProvider
    import com.bignerdranch.android.jetpack3viewmodeltest.TimerViewModel.OnTimeChangeListener
    
    
    class MainActivity : AppCompatActivity() {
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
        }
    
        private fun iniComponent() {
            val tvTime = findViewById<TextView>(R.id.tvTime)
            // 通过ViewModelProviders得到ViewModel,如果ViewModel不存在就创建一个新的,如果已经存在就直接返回已经存在的
            val timerViewModel = ViewModelProvider(this).get(TimerViewModel::class.java)
            timerViewModel.setOnTimeChangeListener(object : OnTimeChangeListener {
                override fun onTimeChanged(second: Int) {
                    runOnUiThread { tvTime.text = "TIME:$second" } // 更新UI界面
                }
            })
            timerViewModel.startTiming()
        }
    }
    

    运行后,当按 Home键、查看预览屏、旋转屏幕时,ViewModel 的 Timer 均继续运行(即使 Activity 被销毁,而 ViewModel 却一直长存),效果如下:

    在这里插入图片描述

    项目代码github详见

    3.1 ViewModel 源码解析

    通过 val timerViewModel = ViewModelProvider(this).get(TimerViewModel::class.java) 可获得 viewModel 对象。

    源码的 androix.FragmentActivity 实现了 ViewModelStoreOwner 接口,

    public class FragmentActivity extends ComponentActivity implements
            ActivityCompat.OnRequestPermissionsResultCallback,
            ActivityCompat.RequestPermissionsRequestCodeValidator {
            
    	class HostCallbacks extends FragmentHostCallback<FragmentActivity> implements
                ViewModelStoreOwner,
                OnBackPressedDispatcherOwner,
                ActivityResultRegistryOwner,
                FragmentOnAttachListener {
    
            @Override
    		public ViewModelStore getViewModelStore() {
                return FragmentActivity.this.getViewModelStore();
            }
        }
    }
    

    源码中 androidx.lifecycle.ViewModelStore 源码如下,其以 HashMap 缓存了 ViewModel,当页面需要 ViewModel 时,其会向 ViewModelProvider 索要,若存在则返回,若不存在则实例化一个。Activity 的销毁重建并不影响 ViewModel。注意我们不要向 ViewModel 传入任何类型的 Context,这可能会导致页面无法被销毁,从而引发内存泄漏:

    /*
     * Copyright (C) 2017 The Android Open Source Project
     *
     * Licensed under the Apache License, Version 2.0 (the "License");
     * you may not use this file except in compliance with the License.
     * You may obtain a copy of the License at
     *
     *      http://www.apache.org/licenses/LICENSE-2.0
     *
     * Unless required by applicable law or agreed to in writing, software
     * distributed under the License is distributed on an "AS IS" BASIS,
     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     * See the License for the specific language governing permissions and
     * limitations under the License.
     */
    
    package androidx.lifecycle;
    
    import java.util.HashMap;
    import java.util.HashSet;
    import java.util.Set;
    
    /**
     * Class to store {@code ViewModels}.
     * 

    * An instance of {@code ViewModelStore} must be retained through configuration changes: * if an owner of this {@code ViewModelStore} is destroyed and recreated due to configuration * changes, new instance of an owner should still have the same old instance of * {@code ViewModelStore}. *

    * If an owner of this {@code ViewModelStore} is destroyed and is not going to be recreated, * then it should call {@link #clear()} on this {@code ViewModelStore}, so {@code ViewModels} would * be notified that they are no longer used. *

    * Use {@link ViewModelStoreOwner#getViewModelStore()} to retrieve a {@code ViewModelStore} for * activities and fragments. */ 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(); } }

    因为 androidx.lifecycle.ViewModelStoreOwner 接口,被 Fragment、FragmentActivity 等类实现,实现的类如下图所示,所以在这些类中均可使用 ViewModel:

    在这里插入图片描述

    3.2 ViewModel 和 AndroidViewModel

    • 不能把任何类型的 Context,或含 Context 引用的对象,传入 ViewModel,否则会导致内存泄漏
    • 但可将 Context 传入 AndroidViewModel 类,其继承自 ViewModel 类,其生命周期和 Application 一样,也就不存在内存泄漏的问题了

    3.3 ViewModel 和 onSaveInstanceState() 的区别

    二者都可以解决旋转屏幕带来的数据丢失问题,当各有特殊用途。

    • onSaveInstanceState() 只能保存少量的、能序列化的数据;当页面被完全销毁时仍可持久化数据。
    • ViewModel 可保存页面中所有的数据;当页面被完全销毁时,ViewModel 也会被销毁,故无法持久化数据。
  • 相关阅读:
    CentOS系统磁盘目录空间调整
    Android显示系统-GraphicBuffer和Gralloc分析
    机器人导航必备的栅格地图数学模型及使用
    深度学习案例分享 | 房价预测 - PyTorch 实现
    优思学院|单件流为什么是精益生产管理的理想状态?
    ansible——利用主机模式选择主机
    CentOS8.2 配置go开发环境
    hadoop的yarn部署
    Vue3-小兔鲜项目
    【多线程 - 10、线程同步3 ThreadLocal】
  • 原文地址:https://blog.csdn.net/jiaoyangwm/article/details/127062746