Android - LiveData 随机不触发更新值(深度问题)
Android - LiveData randomly doesn't trigger with the updated value (in depth question)
错误的视频演示
首先观看视频以了解错误是什么:https://imgur.com/a/X9OVraS
我模拟了自动切换到 SmistamentoGiroCercaGiroFragment 的代码,返回到 SmistamentoGiroTabFragment 并向数据库添加一条新记录。在第 5 次插入中,列表被正确更新,但是 header 计数器没有更新并保持 4 的值。发生这种情况是因为它的特定 LiveData 没有正确触发。
建筑
我将 MVVM 方法与 Navigator 一起用于片段导航。
Activity
activity 不是问题的一部分。它只是初始化导航图及其生命周期方法 onPause、onDestroy... 永远不会被触发,正如您可以从视频中推断的那样(显然 onCreate 和 onResume 被称为单击“SMISTAMENTO A GIRO”的时刻)
片段
此演示中使用了两个片段,SmistamentoGiroTabFragment(已将 child 片段作为选项卡,但对于此问题的目的而言并不重要)和 SmistamentoGiroCercaGiroFragment。
要从一切换到一,使用两个操作:
- findNavController().navigate(SmistamentoGiroTabFragmentDirections.actionDashboardToCercaGiro...
- findNavController().navigate(SmistamentoGiroCartellaCarrelloFragmentDirections.actionGiroToDashboard...
所以每次我从一个切换到一个时,整个生命周期方法都会被调用:
// Removed old fragment I am leaving
SmistamentoGiroCercaGiroFragment - onPause
SmistamentoGiroCercaGiroFragment - onDestroy
// New fragment just added
SmistamentoGiroTabFragment - onAttach
SmistamentoGiroTabFragment - onCreate
SmistamentoGiroTabFragment - onViewCreated
SmistamentoGiroTabFragment - onResume
反之亦然。
代码
SmistamentoGiroTabFragment
在 onCreateView 中观察到用于更新 header 计数器的 LiveDate(出于调试目的,它用于布局):
smistamentoGiroTabViewModel.searchLVCount.observe(viewLifecycleOwner, Observer {
Logger.debug("TestGrafico", "searchLVCount $it")
})
SmistamentoGiroTabFragment xml 布局
app:bindInt 只是一个绑定适配器,它接受一个 Int 并转换为 String。对于此目的没有什么重要的。
<TextView
android:id="@+id/oggetti"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textStyle="bold"
app:bindInt="@{viewModel.searchLVCount}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="18" />
SmistamentoGiroTabViewModel
val searchLVCount = smistamentoGiroRepository.searchLVCount()
直接在Dao中搜索LVCount
@Query("SELECT COUNT(codiceLdV) FROM SmistamentoGiroLV")
fun searchLVCount(): LiveData<Int>
插入的工作原理
片段 SmistamentoGiroTabFragment 从 SmistamentoGiroCercaGiroFragment 接收到一个 object 以及要插入的信息。
所以在 SmistamentoGiroTabFragment.onCreateView 的末尾调用了一个方法:
smistamentoGiroTabViewModel.handleArgs(args, activity().currentView)
这导致:
return smistamentoGiroLVDao.insert(smistamentoGiroLVEntity)
在片段观察到的table中插入一条记录:
@Insert(onConflict = OnConflictStrategy.REPLACE)
Long insert(SmistamentoGiroLVEntity element);
调试日志:
您在视频中观看的测试对应的日志如下:
“TabWM init”是SmistamentoGiroTabViewModel中的如下日志:
init {
Logger.debug("TestGrafico", "TabWM init")
}
日志:
// First callback when opening the activity
TestGrafico - TabWM init
TestGrafico -
TestGrafico - searchLVCount 1
// First automatic insertion
TestGrafico - TabWM init
TestGrafico -
TestGrafico - searchLVCount 1
TestGrafico - searchLVCount 2
// Second automatic insertion
TestGrafico - TabWM init
TestGrafico -
TestGrafico - searchLVCount 2
TestGrafico - searchLVCount 3
// Third automatic insertion
TestGrafico - TabWM init
TestGrafico -
TestGrafico - searchLVCount 3
TestGrafico - searchLVCount 4
// Fourth automatic insertion
TestGrafico - TabWM init
TestGrafico -
TestGrafico - searchLVCount 4
如你所见,我预料到了
TestGrafico - searchLVCount 5
但这并没有触发。
结论
我已经阅读了相关问题 和每个答案,但这没有帮助,因为他们谈论的是被触发两次的原因,而不是为什么有时,它不会被触发第二次。
更新
SmistamentoGiroTabFragment 片段生命周期所有者初始化:
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
var currentFragment: Fragment? = null
val binding = FragmentSmistamentoGiroMainBinding.inflate(
inflater, container, false
).apply {
lifecycleOwner = viewLifecycleOwner
ViewModel 的初始化方式:
SmistamentoGiroTabFragment
@Inject
lateinit var viewModelFactory: ViewModelProvider.Factory
private val smistamentoGiroTabViewModel: SmistamentoGiroTabViewModel by viewModels {
viewModelFactory
}
ViewModelModule(用于 Dagger 注入)
@Binds
abstract fun bindViewModelFactory(factory: MagazzinoViewModelFactory): ViewModelProvider.Factory
@Binds
@IntoMap
@ViewModelKey(SmistamentoGiroTabViewModel::class)
abstract fun bindSmistamentoGiroTabViewModel(smistamentoGiroTabViewModel: SmistamentoGiroTabViewModel): ViewModel
ViewModelKey
@MustBeDocumented
@Target(
AnnotationTarget.FUNCTION,
AnnotationTarget.PROPERTY_GETTER,
AnnotationTarget.PROPERTY_SETTER
)
@Retention(AnnotationRetention.RUNTIME)
@MapKey
annotation class ViewModelKey(val value: KClass<out ViewModel>)
MagazzinoViewModelFactory
@Singleton
class MagazzinoViewModelFactory @Inject constructor(
private val creators: Map<Class<out ViewModel>, @JvmSuppressWildcards Provider<ViewModel>>
) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
val creator = creators[modelClass] ?: creators.entries.firstOrNull {
modelClass.isAssignableFrom(it.key)
}?.value ?: throw IllegalArgumentException("unknown model class $modelClass")
@Suppress("UNCHECKED_CAST")
return creator.get() as T
}
}
更新 2
这是 SmistamentoGiroTabViewModel 地址的错误日志
TestGrafico - TabWM init SmistamentoGiroTabViewModel@467a7a4
TestGrafico - searchLVCount 18
TestGrafico - searchLVCount 19
TestGrafico - TabWM init SmistamentoGiroTabViewModel@3585336
TestGrafico - searchLVCount 19
TestGrafico - searchLVCount 20
TestGrafico - TabWM init SmistamentoGiroTabViewModel@3351174
TestGrafico - searchLVCount 20
TestGrafico - searchLVCount 21
TestGrafico - TabWM init SmistamentoGiroTabViewModel@c6c785f
// Bug occoured
TestGrafico - searchLVCount 21
更新 3
有两个值由观察触发是正确的,因为旧值是 fragment/VM 初始化,新值是 handleArgs 方法调用的结果(导致在 db 中插入记录) onCreateView 的结尾,如上文所述 in-depth。
在对项目的架构元素进行了整整一个月的测试和分析之后,我找到了解决方案。在我使用 RoomDatabase.JournalMode.TRUNCATE.
的 Room 数据库的初始化中
Google LiveData 系统与 Room 数据库日志模式 TRUNCATE 不完全兼容。
这是模式的参考 link:
https://developer.android.com/reference/android/arch/persistence/room/RoomDatabase.JournalMode
必须使用的是WRITE_AHEAD_LOGGING(默认使用)。通过使用它,所有随机图形错误都消失了。
错误的视频演示
首先观看视频以了解错误是什么:https://imgur.com/a/X9OVraS
我模拟了自动切换到 SmistamentoGiroCercaGiroFragment 的代码,返回到 SmistamentoGiroTabFragment 并向数据库添加一条新记录。在第 5 次插入中,列表被正确更新,但是 header 计数器没有更新并保持 4 的值。发生这种情况是因为它的特定 LiveData 没有正确触发。
建筑
我将 MVVM 方法与 Navigator 一起用于片段导航。
Activity
activity 不是问题的一部分。它只是初始化导航图及其生命周期方法 onPause、onDestroy... 永远不会被触发,正如您可以从视频中推断的那样(显然 onCreate 和 onResume 被称为单击“SMISTAMENTO A GIRO”的时刻)
片段
此演示中使用了两个片段,SmistamentoGiroTabFragment(已将 child 片段作为选项卡,但对于此问题的目的而言并不重要)和 SmistamentoGiroCercaGiroFragment。 要从一切换到一,使用两个操作:
- findNavController().navigate(SmistamentoGiroTabFragmentDirections.actionDashboardToCercaGiro...
- findNavController().navigate(SmistamentoGiroCartellaCarrelloFragmentDirections.actionGiroToDashboard...
所以每次我从一个切换到一个时,整个生命周期方法都会被调用:
// Removed old fragment I am leaving
SmistamentoGiroCercaGiroFragment - onPause
SmistamentoGiroCercaGiroFragment - onDestroy
// New fragment just added
SmistamentoGiroTabFragment - onAttach
SmistamentoGiroTabFragment - onCreate
SmistamentoGiroTabFragment - onViewCreated
SmistamentoGiroTabFragment - onResume
反之亦然。
代码
SmistamentoGiroTabFragment
在 onCreateView 中观察到用于更新 header 计数器的 LiveDate(出于调试目的,它用于布局):
smistamentoGiroTabViewModel.searchLVCount.observe(viewLifecycleOwner, Observer {
Logger.debug("TestGrafico", "searchLVCount $it")
})
SmistamentoGiroTabFragment xml 布局
app:bindInt 只是一个绑定适配器,它接受一个 Int 并转换为 String。对于此目的没有什么重要的。
<TextView
android:id="@+id/oggetti"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textStyle="bold"
app:bindInt="@{viewModel.searchLVCount}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="18" />
SmistamentoGiroTabViewModel
val searchLVCount = smistamentoGiroRepository.searchLVCount()
直接在Dao中搜索LVCount
@Query("SELECT COUNT(codiceLdV) FROM SmistamentoGiroLV")
fun searchLVCount(): LiveData<Int>
插入的工作原理
片段 SmistamentoGiroTabFragment 从 SmistamentoGiroCercaGiroFragment 接收到一个 object 以及要插入的信息。 所以在 SmistamentoGiroTabFragment.onCreateView 的末尾调用了一个方法:
smistamentoGiroTabViewModel.handleArgs(args, activity().currentView)
这导致:
return smistamentoGiroLVDao.insert(smistamentoGiroLVEntity)
在片段观察到的table中插入一条记录:
@Insert(onConflict = OnConflictStrategy.REPLACE)
Long insert(SmistamentoGiroLVEntity element);
调试日志:
您在视频中观看的测试对应的日志如下: “TabWM init”是SmistamentoGiroTabViewModel中的如下日志:
init {
Logger.debug("TestGrafico", "TabWM init")
}
日志:
// First callback when opening the activity
TestGrafico - TabWM init
TestGrafico -
TestGrafico - searchLVCount 1
// First automatic insertion
TestGrafico - TabWM init
TestGrafico -
TestGrafico - searchLVCount 1
TestGrafico - searchLVCount 2
// Second automatic insertion
TestGrafico - TabWM init
TestGrafico -
TestGrafico - searchLVCount 2
TestGrafico - searchLVCount 3
// Third automatic insertion
TestGrafico - TabWM init
TestGrafico -
TestGrafico - searchLVCount 3
TestGrafico - searchLVCount 4
// Fourth automatic insertion
TestGrafico - TabWM init
TestGrafico -
TestGrafico - searchLVCount 4
如你所见,我预料到了
TestGrafico - searchLVCount 5
但这并没有触发。
结论
我已经阅读了相关问题
更新
SmistamentoGiroTabFragment 片段生命周期所有者初始化:
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
var currentFragment: Fragment? = null
val binding = FragmentSmistamentoGiroMainBinding.inflate(
inflater, container, false
).apply {
lifecycleOwner = viewLifecycleOwner
ViewModel 的初始化方式:
SmistamentoGiroTabFragment
@Inject
lateinit var viewModelFactory: ViewModelProvider.Factory
private val smistamentoGiroTabViewModel: SmistamentoGiroTabViewModel by viewModels {
viewModelFactory
}
ViewModelModule(用于 Dagger 注入)
@Binds
abstract fun bindViewModelFactory(factory: MagazzinoViewModelFactory): ViewModelProvider.Factory
@Binds
@IntoMap
@ViewModelKey(SmistamentoGiroTabViewModel::class)
abstract fun bindSmistamentoGiroTabViewModel(smistamentoGiroTabViewModel: SmistamentoGiroTabViewModel): ViewModel
ViewModelKey
@MustBeDocumented
@Target(
AnnotationTarget.FUNCTION,
AnnotationTarget.PROPERTY_GETTER,
AnnotationTarget.PROPERTY_SETTER
)
@Retention(AnnotationRetention.RUNTIME)
@MapKey
annotation class ViewModelKey(val value: KClass<out ViewModel>)
MagazzinoViewModelFactory
@Singleton
class MagazzinoViewModelFactory @Inject constructor(
private val creators: Map<Class<out ViewModel>, @JvmSuppressWildcards Provider<ViewModel>>
) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
val creator = creators[modelClass] ?: creators.entries.firstOrNull {
modelClass.isAssignableFrom(it.key)
}?.value ?: throw IllegalArgumentException("unknown model class $modelClass")
@Suppress("UNCHECKED_CAST")
return creator.get() as T
}
}
更新 2
这是 SmistamentoGiroTabViewModel 地址的错误日志
TestGrafico - TabWM init SmistamentoGiroTabViewModel@467a7a4
TestGrafico - searchLVCount 18
TestGrafico - searchLVCount 19
TestGrafico - TabWM init SmistamentoGiroTabViewModel@3585336
TestGrafico - searchLVCount 19
TestGrafico - searchLVCount 20
TestGrafico - TabWM init SmistamentoGiroTabViewModel@3351174
TestGrafico - searchLVCount 20
TestGrafico - searchLVCount 21
TestGrafico - TabWM init SmistamentoGiroTabViewModel@c6c785f
// Bug occoured
TestGrafico - searchLVCount 21
更新 3
有两个值由观察触发是正确的,因为旧值是 fragment/VM 初始化,新值是 handleArgs 方法调用的结果(导致在 db 中插入记录) onCreateView 的结尾,如上文所述 in-depth。
在对项目的架构元素进行了整整一个月的测试和分析之后,我找到了解决方案。在我使用 RoomDatabase.JournalMode.TRUNCATE.
的 Room 数据库的初始化中Google LiveData 系统与 Room 数据库日志模式 TRUNCATE 不完全兼容。
这是模式的参考 link: https://developer.android.com/reference/android/arch/persistence/room/RoomDatabase.JournalMode
必须使用的是WRITE_AHEAD_LOGGING(默认使用)。通过使用它,所有随机图形错误都消失了。