【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 的布局如下:

<?xml version="1.0" encoding="utf-8"?>
<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<String, ViewModel> 缓存了 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}.
 * <p>
 * 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}.
 * <p>
 * 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.
 * <p>
 * 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 也会被销毁,故无法持久化数据。

猜你喜欢

转载自blog.csdn.net/jiaoyangwm/article/details/127062746