如何在 Android Studio 中立即获取 LivaData<String> 的值?
How can I get the value of a LivaData<String> at once in Android Studio?
savedRecordFileName
是LivaData<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)
}
savedRecordFileName
是LivaData<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)
}