如何在 Android Studio 中立即获取 LivaData<String> 的值?

How can I get the value of a LivaData<String> at once in Android Studio?

savedRecordFileNameLivaData<String>的一个变量,希望在代码A中一下子得到savedRecordFileName的值

你知道LiveData变量是惰性的,可能savedRecordFileName的值在binding.btnStop.setOnClickListener { }中为null,所以binding.btnStop.setOnClickListener { }中的代码不会在[=的值时触发15=] 为空。

我希望binding.btnStop.setOnClickListener { }中的代码可以一直触发,我该怎么做?

顺便说一句,我认为代码 B 不合适,因为 savedRecordFileName 的值可能会被其他函数更改。

代码B

binding.btnStop.setOnClickListener {
   mHomeViewModel.savedRecordFileName.observe(viewLifecycleOwner){
        val aMVoice = getDefaultMVoice(mContext,it)             
        mHomeViewModel.add(aMVoice)  
   }
}

代码A

class FragmentHome : Fragment() {

    private val mHomeViewModel by lazy {
        getViewModel {
            HomeViewModel(mActivity.application, provideRepository(mContext))
        }
    }

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {        
       ... 
       binding.btnStop.setOnClickListener {
            mHomeViewModel.savedRecordFileName.value?.let{
                val aMVoice = getDefaultMVoice(mContext,it)             
                mHomeViewModel.add(aMVoice)                
            }
        }
        ...
        return binding.root
    }

}


class HomeViewModel(val mApplication: Application, private val mDBVoiceRepository: DBVoiceRepository) : AndroidViewModel(mApplication) {
    val savedRecordFileName: LiveData<String> = mDBVoiceRepository.getTotalOfVoice().map {
        mApplication.getString(R.string.defaultName, (it+1).toString())
    }
}

class DBVoiceRepository private constructor(private val mDBVoiceDao: DBVoiceDao){
    fun getTotalOfVoice() = mDBVoiceDao.getTotalOfVoice()
}

@Dao
interface DBVoiceDao{
   @Query("SELECT count(id) FROM voice_table")
   fun getTotalOfVoice(): LiveData<Long>
}

添加内容

致 Ridcully:谢谢!

我认为您“将所有内容移至视图模型 class”的方法很好!

我觉得你Code C里filename是livedata也行吧?

代码C

viewModelScope.launch(Dispatchers.IO) {
    filename = dao.getFilename() // without livedata.  I think it will be OK even if the filename is livedata
    voice = getDefaultVoice(...) // also do this in background
    add(voice)
    result.postValue(true)
}

TL;DR - 代码 A 更好,有额外的补充。

如您所知,LiveData 不能保证在实例化后立即具有 non-null 值。理想情况下,就最佳实践而言,最好始终将 LiveData 值视为不断变化的(即 - 假设两次直接值访问将产生不同的值)。这是预期的行为,因为 LiveData 被设计为可观察的,而不是你处理即时值。

因此,请收听 LiveData 并相应地更新您的 UI。例如,当里面没有数据时,禁用或隐藏 停止按钮

mHomeViewModel.savedRecordFileName.observe(viewLifecycleOwner) {
    binding.btnStop.isEnabled = it != null
}
binding.btnStop.setOnClickListener {
    mHomeViewModel.savedRecordFileName.value?.also {
        val aMVoice = getDefaultMVoice(mContext,it)             
        mHomeViewModel.add(aMVoice)                
    }
}

解决方案 1

simple/tricky/dirty 解决方案将是 onViewCreated 中的空观察,例如:

mHomeViewModel.savedRecordFileName.observe(viewLifecycleOwner){ }

这会导致观察到 LiveData,因此 Room 会查询它,因为它是活动的,希望当您单击按钮时,LiveData 会被填充。



解决方案 2

但我最喜欢的方法是被动方法。我不知道你是否想对你的代码采取被动的态度,但你可以使用 MediatorLiveData(或利用像 this 这样的库)来被动地解决这种情况。 让您的 VM 中有另一个名为 btnStopClicked 的 LiveData,每次单击都会更改该 LiveData 的值。因此,您的代码可能如下所示:

class FragmentHome : Fragment() {

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {        
       ... 
       binding.btnStop.setOnClickListener {
            mHomeViewModel.btnStopClicked.value = null //we don't care about the value
        }
        mHomeViewModel.addVoiceEvent.observe(viewLifecycleOwner){
            val aMVoice = getDefaultMVoice(mContext,it)             
            mHomeViewModel.add(aMVoice)   
        }
        ...
        return binding.root
    }

}


class HomeViewModel(val mApplication: Application, private val mDBVoiceRepository: DBVoiceRepository) : AndroidViewModel(mApplication) {
 
    val savedRecordFileName: ... //whatever it was
    val btnStopClicked = MutableLiveData<Any>()

    val addVoiceEvent = savedRecordFileName
        .combineLatestWith(btnStopClicked){srfn, _ -> srfn}
        .toSingleEvent()
}

也不要忘记 toSingleEvent 因为如果你不把它变成一个事件,它会在某些情况下被无意中调用(看看 this)。

如您所说,您应该在后台执行 Room 查询以避免阻塞 ui 线程。但你不应该止步于此。在后台线程中做尽可能多的工作。在您的情况下,单击按钮时,启动 运行 一个完成所有工作的后台线程:从数据库中获取名称(使用直接查询,现在可以 运行 在后台),获取默认语音,并将其添加到您要添加的任何内容中。

您还应该将所有这些移动到视图模型中 class。

btnStop.setOnClickListener {
    viewmodel.stop()
}

在视图模型中是这样的(java/伪代码):

void stop() {
    runinbackgroundthread {
        filename = dao.getFilename() // without livedata
        voice = getDefaultVoice(...) // also do this in background
        add(voice)
        // If you want the action to have a reault, observable by ui
        // use a MutableLiveData and set it here, via postValue()
        // as we are still in background thread.
        result.postValue(true)
    }
}

关于如何实际实现我的伪代码 runinbackgroundthread 请在此处查看一些官方 guide:https://developer.android.com/guide/background/threading

简而言之,对于 Kotlin 协同程序,它应该是这样的:

viewModelScope.launch(Dispatchers.IO) {
    filename = dao.getFilename() // without livedata
    voice = getDefaultVoice(...) // also do this in background
    add(voice)
    result.postValue(true)
}