草庐IT

ViewModel 你应该知道的知识点

Drew_MyINTYRE 2023-03-28 原文

ViewModel 的 Saved State

在屏幕旋转时,ViewModel 可以保存数据。但是当应用在后台进程被系统杀死,当重新打开页面时,ViewModel 的数据并不会恢复。这种情况就需要与SavedStateHandle 结合,在后台进程回收时保存数据。

第一步:添加依赖

def lifecycle_version = "2.2.0"
implementation "androidx.lifecycle:lifecycle-viewmodel-savedstate:$lifecycle_version

第二步:在 Activity 或 Fragment 的 onCreate 方法中,将 ViewModelProvider 的调用修改为:

//androidx.fragment:fragment-ktx:x.y.z 或
//androidx.activity:activity-ktx:x.y.z 

val viewModel by viewModels { SavedStateViewModelFactory(application, this) }

// 或者不使用 ktx
val viewModel = ViewModelProvider(this, SavedStateViewModelFactory(application, this))
            .get(MyViewModel::class.java)

第三步:使用 SaveStateHandle

class WithSavedStateViewModel(private val state: SavedStateHandle) : ViewModel() {
    private val key = "key"
    fun setValue(value: String) = state.set(key, value)
    fun getValue(): LiveData<String> = state.getLiveData(key)
}

class ViewModelWithSavedStateFragment :
    BaseFragment<FragmentViewmodelBinding>(R.layout.fragment_viewmodel) {

    private val mViewModel by viewModels<WithSavedStateViewModel>()

    override fun initBinding(view: View): FragmentViewmodelBinding =
        FragmentViewmodelBinding.bind(view)

    override fun init(savedInstanceState: Bundle?) {
        binding.save.setOnClickListener { mViewModel.setValue(binding.edit.text.toString()) }
        mViewModel.getValue().observe(viewLifecycleOwner) {
            binding.text.text = getString(R.string.saved_in_viewmodel, it)
        }
    }
}

ViewMode 与 Kotlin 协程:viewModelScope

真实的使用环境下很容易创建出许多协程,这就难免会导致有些协程的状态无法被跟踪。如果这些协程中刚好有您想要停止的任务时,就会导致任务泄漏(work leak)。为了防止任务泄漏,您需要将协程加入到一个 CoroutineScope中。CoroutineScope 可以持续跟踪协程的执行,它可以被取消。当 CoroutineScope 被取消时,它所跟踪的所有协程都会被取消。正如我们不推荐随意使用全局变量一样,GlobalScope 这种方式通常不推荐使用。所以,如果想要使用协程,您要么限定一个作用域 (scope),要么获得一个作用域的访问权限。而在 ViewModel 中,我们可以使用 viewModelScope 来管理协程的作用域。假设您正在准备将一个位图 (bitmap) 显示到屏幕上。这种操作就符合我们前面提到的一些特征:既不能在执行时阻塞主线程,又要求在用户退出相关界面时停止执行。使用协程进行此类操作时,就应当使用 viewModelScope

class MyViewModel() : ViewModel() {

    fun doSth() {
        viewModelScope.launch {
            processBitmap()
        }
    }

    suspend fun processBitmap() = withContext(Dispatchers.Default) {
        // TODO: handle logic
    }
}

几种数据恢复方式的总结:

1,当您的 Activity 开始停止时,系统会调用 onSaveInstanceState() 方法,以便您的 Activity 可以将状态信息保存到实例状态 Bundle 中。重建先前被销毁的 Activity 后,您可以从系统传递给 Activity 的 Bundle 中恢复保存的实例状态。onCreate()onRestoreInstanceState() 回调方法均会收到包含实例状态信息的相同 Bundle

2,当配置发生改变时,Fragment 会随着宿主 Activity 销毁与重建,当我们调用 Fragment 中的 setRetainInstance(true) 方法时,系统允许 Fragment 绕开销毁-重建 的过程。使用该方法,将会发送信号给系统,让 Activity 重建时,保留 Fragment 的实例。需要注意的是:

  • 使用该方法后,不会调用 Fragment 的 onDestory() 方法,但仍然会调用 onDetach() 方法;

  • 使用该方法后,不会调用 Fragment 的 onCreate(Bundle) 方法。因为 Fragment 没有被重建;

  • 使用该方法后,Fragment 的 onAttach(Activity)onActivityCreated(Bundle) 方法仍然会被调用。

3,在 Activity 中提供了 onRetainNonConfigurationInstance() 方法,用于处理配置发生改变时数据的保存。随后在重新创建的 Activity 中调用 getLastNonConfigurationInstance() 获取上次保存的数据。我们不能直接重写上述方法,如果想在 Activity 中自定义想要恢复的数据,需要我们调用上述两个方法的内部方法:

注意:onRetainNonConfigurationInstance() 方法系统调用时机介于 onStop() - onDestory() 之间,getLastNonConfigurationInstance() 方法可在 onCreate() 与 onStart() 方法中调用。

  • onRetainCustomNonConfigurationInstance()

  • getLastCustomNonConfigurationInstance()

为什么旋转手机时,ViewModel 可以保存数据?

Activity 因旋转发生改变时,系统会重新创建一个新的 Activity 。那老的 Activity 中的 ViewModel 是如何传递给新的 Activity 的呢?

ViewModel 在官方设计之初就倾向于在配置改变时进行数据的恢复。考虑到数据恢复时的效率,官方最终采用了 onRetainNonConfigurationInstance() 的方式来恢复 ViewModel

在 Androidx 中的 Activity 的最新代码中,官方重写了 onRetainNonConfigurationInstance() 方法,在该方法中保存了 ViewModelStore (ViweModelStore 中存储了 ViewModel ),进而也保存了 ViewModel,具体代码如下所示:

public final Object onRetainNonConfigurationInstance() {
    Object custom = onRetainCustomNonConfigurationInstance();

    ViewModelStore viewModelStore = mViewModelStore;
    if (viewModelStore == null) {
        NonConfigurationInstances nc =
                (NonConfigurationInstances) getLastNonConfigurationInstance();
        if (nc != null) {
            viewModelStore = nc.viewModelStore;
        }
    }

    if (viewModelStore == null && custom == null) {
        return null;
    }

    //将ViewModel存储在 NonConfigurationInstances 对象中
    NonConfigurationInstances nci = new NonConfigurationInstances();
    nci.custom = custom;
    nci.viewModelStore = viewModelStore;
    return nci;
}

于是我们知道,在屏幕旋转后,当新的 Activity 重新创建,并调用 ViewModelProviders.of(this).get(xxxModel.class) 时,又会在 getViewModelStore() 方法中获取老 Activity 保存的 ViewModelStore

public ViewModelStore getViewModelStore() {
    if (getApplication() == null) {
        throw new IllegalStateException("Your activity is not yet attached to the "
                + "Application instance. You can't request ViewModel before onCreate call.");
    }
    if (mViewModelStore == null) {
        //?获取保存的NonConfigurationInstances,
        NonConfigurationInstances nc =
                (NonConfigurationInstances) getLastNonConfigurationInstance();
        if (nc != null) {
            //?从该对象中获取ViewModelStore
            mViewModelStore = nc.viewModelStore;
        }
        if (mViewModelStore == null) {
            mViewModelStore = new ViewModelStore();
        }
    }
    return mViewModelStore;
}

ViewModel 实际是存储在 ViewModelStore 中的,ViewModelStore 还原后,那么也就拿到了 ViewModel。具体代码如下所示: 从 ViewModelStroe 中获取 ViewModel 的相关代码:

@NonNull
@MainThread
public <T extends ViewModel> T get(@NonNull String key, @NonNull Class<T> modelClass) {
    ViewModel viewModel = mViewModelStore.get(key);

    if (modelClass.isInstance(viewModel)) {
        //noinspection unchecked
        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);
    //noinspection unchecked
    return (T) viewModel;
}

这就是为什么屏幕旋转后,ViewModel 可以保存数据的原因。

为什么 Fragment 中的数据屏幕旋转后可以保存?

如果我们在 Fragment 中调用如下代码:

val model: MyViewModel = ViewModelProviders.of(this).get(MyViewModel::class.java)

追踪他们的调用,可以发现获取 ViewModel 是通过 mNonConfig 存储的:

@NonNull
ViewModelStore getViewModelStore(@NonNull Fragment f) {
    return mNonConfig.getViewModelStore(f);
}

那么 mNonConfig 又是什么时候创建的呢?又存储在哪里?

在将 Fragment 添加到 FragmentManager 中时会调用到下面的函数,
因为传入的 parent = null,且 Activity 默认实现了 ViewModelStoreOwner 接口,所以会获取 Activity 中的 ViewModelStore,接着调用 FragmentManagerViewModel 的 getInstance() 方法:

void attachController(@NonNull FragmentHostCallback<?> host,
        @NonNull FragmentContainer container, @Nullable final Fragment parent) {
    //省略更多...
    if (parent != null) {
        mNonConfig = parent.mFragmentManager.getChildNonConfig(parent);
    } else if (host instanceof ViewModelStoreOwner) {
        //?走这里
        ViewModelStore viewModelStore = ((ViewModelStoreOwner) host).getViewModelStore();
        mNonConfig = FragmentManagerViewModel.getInstance(viewModelStore);
    } else {
        mNonConfig = new FragmentManagerViewModel(false);
    }
}

// 创建 FragmentManagerViewModel,并将其添加到 Activity 中的 ViewModelStore 中
static FragmentManagerViewModel getInstance(ViewModelStore viewModelStore) {
    ViewModelProvider viewModelProvider = new ViewModelProvider(viewModelStore,
            FACTORY);
    return viewModelProvider.get(FragmentManagerViewModel.class);
}

调用流程如下:

Activity #onCreate() -> mFragments #attachHost(null) -> FragmentManager #attachController -> (创建 FragmentManagerViewModel,并将其添加到 Activity 中的 ViewModelStore 中。)

ViewModel 在 Fragment 中不会因配置改变而销毁的原理

根据上面的分析,ViewModel 在 Fragment 中不会因配置改变而销毁的原因其实是因为其声明的 ViewModel 是存储在 FragmentManagerViewModel 中的,而 FragmentManagerViewModel 是存储在宿主 Activity 中的 ViewModelStore 中,又因 Activity 中 ViewModelStore 不会因配置改变而销毁,故 Fragment 中 ViewModel 也不会因配置改变而销毁。

ViewModel 能在 Fragment 中共享的原理

ViewModel 的另一大特性就是能在 Fragment 中共享数据。假如我们想 Fragment D 获取 Fragment A 中的数据,那么我们只有在 Activity 中的 ViewModelStore 下添加 ViewModel。只有这样,我们才能在不同 Fragment 中获取相同的数据。这也是为什么在 Fragment 中使用共享的 ViewModel 时,我们要在调用ViewModelProvider.of() 创建 ViewModel 时需要传入 getActivity() 的原因。

public class SharedViewModel extends ViewModel {
    private final MutableLiveData<Item> selected = new MutableLiveData<Item>();

    public void select(Item item) {
        selected.setValue(item);
    }

    public LiveData<Item> getSelected() {
        return selected;
    }
}

public class FragmentA extends Fragment {
    private SharedViewModel model;
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //?传入的是宿主 Activity
        model = ViewModelProviders.of(getActivity()).get(SharedViewModel.class);
        itemSelector.setOnClickListener(item -> {
            model.select(item);
        });
    }
}

public class FragmentD extends Fragment {
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
         //?传入的是宿主Activity
        SharedViewModel model = ViewModelProviders.of(getActivity()).get(SharedViewModel.class);
        model.getSelected().observe(this, { item ->
           // Update the UI.
        });
    }
}

感谢:

https://flywith24.gitee.io/2020/03/23/Jetpack-ViewModel/

有关ViewModel 你应该知道的知识点的更多相关文章

  1. ruby - 检查 "command"的输出应该包含 NilClass 的意外崩溃 - 2

    为了将Cucumber用于命令行脚本,我按照提供的说明安装了arubagem。它在我的Gemfile中,我可以验证是否安装了正确的版本并且我已经包含了require'aruba/cucumber'在'features/env.rb'中为了确保它能正常工作,我写了以下场景:@announceScenario:Testingcucumber/arubaGivenablankslateThentheoutputfrom"ls-la"shouldcontain"drw"假设事情应该失败。它确实失败了,但失败的原因是错误的:@announceScenario:Testingcucumber/ar

  2. java - 我的模型类或其他类中应该有逻辑吗 - 2

    我只想对我一直在思考的这个问题有其他意见,例如我有classuser_controller和classuserclassUserattr_accessor:name,:usernameendclassUserController//dosomethingaboutanythingaboutusersend问题是我的User类中是否应该有逻辑user=User.newuser.do_something(user1)oritshouldbeuser_controller=UserController.newuser_controller.do_something(user1,user2)我

  3. ruby-on-rails - 带有 Zeus 的 RSpec 3.1,我应该在 spec_helper 中要求 'rspec/rails' 吗? - 2

    使用rspec-rails3.0+,测试设置分为spec_helper和rails_helper我注意到生成的spec_helper不需要'rspec/rails'。这会导致zeus崩溃:spec_helper.rb:5:in`':undefinedmethod`configure'forRSpec:Module(NoMethodError)对thisissue最常见的回应是需要'rspec/rails'。但这是否会破坏仅使用spec_helper拆分rails规范和PORO规范的全部目的?或者这无关紧要,因为Zeus无论如何都会预加载Rails?我应该在我的spec_helper中做

  4. ruby - EventMachine - 你怎么知道你是否落后了? - 2

    我正在研究使用EventMachine支持的twitter-streamruby​​gem来跟踪和捕获推文。我对整个事件编程有点陌生。我如何判断我在事件循环中所做的任何处理是否导致我落后?有没有简单的检查方法? 最佳答案 您可以通过使用周期性计时器并打印出耗时来确定延迟。如果您使用的是1秒的计时器,您应该已经过了大约1秒,如果它更长,您就知道您正在减慢react器的速度。@last=Time.now.to_fEM.add_periodic_timer(1)doputs"LATENCY:#{Time.now.to_f-@last}"@

  5. ruby - 我正在学习编程并选择了 Ruby。我应该升级到 Ruby 1.9 吗? - 2

    我完全不是程序员,正在学习使用Ruby和Rails框架进行编程。我目前正在使用Ruby1.8.7和Rails3.0.3,但我想知道我是否应该升级到Ruby1.9,因为我真的没有任何升级的“遗留”成本。缺点是什么?我是否会遇到与普通gem的兼容性问题,或者甚至其他我不太了解甚至无法预料的问题? 最佳答案 你应该升级。不要坚持从1.8.7开始。如果您发现不支持1.9.2的gem,请避免使用它们(因为它们很可能不被维护)。如果您对gem是否兼容1.9.2有任何疑问,您可以在以下位置查看:http://www.railsplugins.or

  6. ruby-on-rails - 我现在(2010 年 1 月)应该使用哪个版本的 Ruby? - 2

    我有1.8.6附带的VanillaMacOSXLeopard。我是RoR的新手,所以会学习网上的教程。在使用更高版本的Ruby时,我是否可能会发现遵循它们的问题?我目前正在查看提到1.8.6和1.8.7的这个-http://www.railstutorial.org/book 最佳答案 RoR教程对两者都适用,但如果您正在学习Ruby,则应该学习1.9。Rails3将不支持1.8.6,所以我会选择1.8.7或1.9。我还推荐使用RVM在Ruby版本之间切换。 关于ruby-on-rail

  7. ruby-on-rails - Rails 中的类实例变量应该在互斥体中设置吗? - 2

    假设我的Rails项目中有一个设置实例变量的Ruby类。classSomethingdefself.objects@objects||=begin#somelogicthatbuildsanarray,whichisultimatelystoredin@objectsendendend是否可以多次设置@objects?是否有可能在一个请求期间,在上面的begin/end之间执行代码时,可以在第二个请求期间调用此方法?我想这实际上归结为Rails服务器实例如何fork的问题。我应该改用Mutex还是线程同步?例如:classSomethingdefself.objectsreturn@o

  8. ruby-on-rails - Ruby 如何知道在哪里可以找到所需的文件? - 2

    这里还有一个新手问题:require'tasks/rails'我在每个Rails项目的根路径中的Rakefile中看到了这一行。我猜这行用于要求vendor/rails/railties/lib/tasks/rails.rb加载所有rake任务:$VERBOSE=nil#LoadRailsrakefileextensionsDir["#{File.dirname(__FILE__)}/*.rake"].each{|ext|loadext}#LoadanycustomrakefileextensionsDir["#{RAILS_ROOT}/lib/tasks/**/*.rake"].so

  9. ruby - 我怎样才能更好地了解/了解更多关于 Ruby 的知识? - 2

    按照目前的情况,这个问题不适合我们的问答形式。我们希望答案得到事实、引用或专业知识的支持,但这个问题可能会引发辩论、争论、投票或扩展讨论。如果您觉得这个问题可以改进并可能重新打开,visitthehelpcenter指导。关闭9年前。我最近开始学习Ruby,这是我的第一门编程语言。我对语法感到满意,并且我已经完成了许多只教授相同基础知识的教程。我已经写了一些小程序(包括我自己的数组排序方法,在有人告诉我谷歌“冒泡排序”之前我认为它非常聪明),但我觉得我需要尝试更大更难的东西来理解更多关于Ruby.关于如何执行此操作的任何想法?

  10. ruby - 构建网络蜘蛛时,应该使用递归吗? - 2

    构建一个深度优先的网络蜘蛛,这意味着它将访问第一页上的所有链接,然后转到每个链接,并访问所有第二页上的链接...你应该使用递归吗?我发现这是CPU密集型的。defrecursion()linkz_on_first_page.eachdo|link|recursion(link)endendrecursion(firstpage) 最佳答案 绝对不是,由于万维网的实际性质,您很快就会遇到问题。当您访问带有主导航部分的网站时,每个页面都链接到其他页面,您就进入了一个无限循环。您可以跟踪您处理了哪些链接,但即便如此,递归循环并不真正适合万

随机推荐