美文网首页
Android 之 ViewModel 的正确用法

Android 之 ViewModel 的正确用法

作者: 雁过留声_泪落无痕 | 来源:发表于2021-09-09 11:05 被阅读0次

放一张官图


viewmodel-lifecycle.png
  1. 先上代码
/**
 * 提供自定义 Factory 和 viewModel 扩展,使得定义 ViewModel 时可以传参
 *
 * <code>
 * private val model by viewModel { XxxViewModel(1, 2, 3) }
 * </code>
 */
class ParamViewModelFactory<VM : ViewModel>(
    private val factory: () -> VM,
) : ViewModelProvider.Factory {
    override fun <T : ViewModel> create(modelClass: Class<T>): T = factory() as T
}

inline fun <reified VM : ViewModel> AppCompatActivity.viewModel(
    noinline factory: () -> VM,
): Lazy<VM> = viewModels { ParamViewModelFactory(factory) }

class ViewModelActivity : BaseActivity() {

    companion object {
        const val TAG = "hehe"
    }

    // 1. 使用 viewModels() 扩展实例化 ViewModel,
    // 在 implementation "androidx.activity:activity-ktx:1.3.1" 该库中
    private val model by viewModels<MyViewModel>()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // 2. 打印 Activity 实例,旋转屏幕后实例发生变化
        Log.d(TAG, "" + this)
        // 3. 打印 ViewModel 实例,旋转屏幕后实例未发生变化
        Log.d(TAG, "" + model)

        // 4. 使用另一种方式实例化 ViewModel
        val model = ViewModelProvider(this).get(MyViewModel::class.java)
        model.text.observe(this) {
            // 打印并显示 ViewModel 中的数据
            Log.d(TAG, "text: $it")
            findViewById<TextView>(R.id.hello_world).text = it
        }
        // 5. 再次打印 ViewModel 实例,表明两种方式得到的是同一个实例,细节参看 ViewModelProvider 具体实现
        Log.d(TAG, "" + model)

        // 6. 开始请求数据
        model.update()
    }
}

class MyViewModel : ViewModel() {
    val text = MutableLiveData("No book")

    fun update() {
        // 7. 模拟延时返回数据
        thread {
            Thread.sleep(1000L * 5)
            text.postValue("This is book " + (Random(System.currentTimeMillis()).nextInt(10)))
        }
    }

    override fun onCleared() {
        // 8. 需要清理资源的回调,Activity#onDestroy() 时会调过来(旋转屏幕导致的 onDestroy 不会)
        super.onCleared()
        Log.d(ViewModelActivity.TAG, "MyViewModel#onCleared()")
    }
}
  1. 所使用的依赖
implementation 'androidx.core:core-ktx:1.3.2'
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'com.google.android.material:material:1.3.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'

implementation "androidx.activity:activity-ktx:1.3.1"
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.3.1'
  1. 需要注意的点

a. 不能使用 new XxxViewModel() 的方式实例化 ViewModel。因为这样实例化出来的 ViewModel 不能再屏幕旋转后仍然保持有效;同时也不会使得 onCleared() 方法被回调;再次,如果使用了 viewModelScope 发起协程的话,也不会使得协程被取消。

1. viewModelScope 的实现如下:

public val ViewModel.viewModelScope: CoroutineScope
    get() {
        val scope: CoroutineScope? = this.getTag(JOB_KEY)
        if (scope != null) {
            return scope
        }
        return setTagIfAbsent(
            JOB_KEY,
            CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
        )
    }

internal class CloseableCoroutineScope(context: CoroutineContext) : Closeable, CoroutineScope {
    override val coroutineContext: CoroutineContext = context

    override fun close() {
        coroutineContext.cancel()
    }
}

2. 可以看到在 close() 方法中取消了协程,那么 close() 方法在哪儿被调用了呢?

3. 看 ViewModel#clear() 方法和 ViewModel#closeWithRuntimeException() 方法

final void clear() {
    mCleared = true;
    // Since clear() is final, this method is still called on mock objects
    // and in those cases, mBagOfTags is null. It'll always be empty though
    // because setTagIfAbsent and getTag are not final so we can skip
    // clearing it
    if (mBagOfTags != null) {
        synchronized (mBagOfTags) {
            for (Object value : mBagOfTags.values()) {
                // see comment for the similar call in setTagIfAbsent
                closeWithRuntimeException(value);
            }
        }
    }
    onCleared();
}

private static void closeWithRuntimeException(Object obj) {
    if (obj instanceof Closeable) {
        try {
            ((Closeable) obj).close();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

4. 可以看到在 closeWithRuntimeException() 方法调用了 CloseableCoroutineScope#close() 方法

5. ViewModelStore#clear() 会调用 ViewModel#clear() 方法,再往上请参考源码

b. 初始值最好放在 MutableLiveData 的构造方法里,不要放在 Activity 里,这样屏幕旋转时界面不会跳变(否则每次都会先闪现一下 Activity 里的初始值)

错误用法:

package top.gangshanghua.xiaobo.helloworld

import android.os.Bundle
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import kotlin.concurrent.thread
import kotlin.random.Random

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        findViewById<TextView>(R.id.hello_world).text = "No book"

        val model = ViewModelProvider(this).get(MyViewModel::class.java)
        model.text.observe(this) {
            // 为了效果更加明显,假设渲染任务较重,延时模拟
            findViewById<TextView>(R.id.hello_world).post {
                Thread.sleep(1000L)
                findViewById<TextView>(R.id.hello_world).text = it
            }
        }

        model.update()
    }
}

class MyViewModel : ViewModel() {
    val text = MutableLiveData<String>()

    fun update() {
        thread {
            Thread.sleep(1000L * 5)
            text.postValue("This is book " + (Random(System.currentTimeMillis()).nextInt(10)))
        }
    }
}
旋转屏幕后界面跳变.gif

c. 请您补充~

相关文章

网友评论

      本文标题:Android 之 ViewModel 的正确用法

      本文链接:https://www.haomeiwen.com/subject/mljiwltx.html