At first watch the video to understand what is the bug: https://i.sstatic.net/7mkuF.jpg
I have mocked the code to automatically switch to the SmistamentoGiroCercaGiroFragment, go back to SmistamentoGiroTabFragment and add a new record to the database. In the 5th insertion the list is correctely updated, but the header counter is not updated and keep the value of 4. That happened because its specific LiveData is not correctely triggered.
I'm using the MVVM approach along with Navigator for fragment navigation.
The activity is not a part of the problem. it just initializes the navigation graph and its lifecycle methods onPause, onDestroy... are never triggered as you can deduce from the video (obviously onCreate and onResume are called the moment in click on "SMISTAMENTO A GIRO")
Two fragments are used in this demostration, SmistamentoGiroTabFragment (that has got child fragments as tabs but it's not important for the purpose of this question) and SmistamentoGiroCercaGiroFragment. To switch from one to one, two actions are used:
So everytime I switch from one to one the entire lifecyle methods are called:
// Removed old fragment I am leaving
SmistamentoGiroCercaGiroFragment - onPause
SmistamentoGiroCercaGiroFragment - onDestroy
// New fragment just added
SmistamentoGiroTabFragment - onAttach
SmistamentoGiroTabFragment - onCreate
SmistamentoGiroTabFragment - onViewCreated
SmistamentoGiroTabFragment - onResume
And the other way.
SmistamentoGiroTabFragment
In the onCreateView the LiveDate used to update the header counter is observed (for debug purpose, it is used in the layout):
smistamentoGiroTabViewModel.searchLVCount.observe(viewLifecycleOwner, Observer {
Logger.debug("TestGrafico", "searchLVCount $it")
})
SmistamentoGiroTabFragment xml layout
app:bindInt is just a binding adapter that takes an Int and convert to String. Nothing important for this purpose.
<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()
searchLVCount directely in Dao
@Query("SELECT COUNT(codiceLdV) FROM SmistamentoGiroLV")
fun searchLVCount(): LiveData<Int>
The fragment SmistamentoGiroTabFragment recieves from SmistamentoGiroCercaGiroFragment an object with the information to insert. So at the end of the SmistamentoGiroTabFragment.onCreateView a method is called:
smistamentoGiroTabViewModel.handleArgs(args, activity().currentView)
Which leads to:
return smistamentoGiroLVDao.insert(smistamentoGiroLVEntity)
That inserts a record in the table observed by the fragment:
@Insert(onConflict = OnConflictStrategy.REPLACE)
Long insert(SmistamentoGiroLVEntity element);
The corresponding log of the test you have watched in the video is the following: "TabWM init" is the following log in SmistamentoGiroTabViewModel:
init {
Logger.debug("TestGrafico", "TabWM init")
}
Log:
// 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
As you can see I expected
TestGrafico - searchLVCount 5
But that's not triggered.
I have read the releated question Why LiveData observer is being triggered twice for a newly attached observer and every answer, but that is not helpful since they talk about the reason is being triggered twice and not why sometimes, it is not triggered the second time.
SmistamentoGiroTabFragment fragment lifecycleOwner initialization:
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
var currentFragment: Fragment? = null
val binding = FragmentSmistamentoGiroMainBinding.inflate(
inflater, container, false
).apply {
lifecycleOwner = viewLifecycleOwner
how ViewModel is initialized:
@Inject
lateinit var viewModelFactory: ViewModelProvider.Factory
private val smistamentoGiroTabViewModel: SmistamentoGiroTabViewModel by viewModels {
viewModelFactory
}
@Binds
abstract fun bindViewModelFactory(factory: MagazzinoViewModelFactory): ViewModelProvider.Factory
@Binds
@IntoMap
@ViewModelKey(SmistamentoGiroTabViewModel::class)
abstract fun bindSmistamentoGiroTabViewModel(smistamentoGiroTabViewModel: SmistamentoGiroTabViewModel): ViewModel
@MustBeDocumented
@Target(
AnnotationTarget.FUNCTION,
AnnotationTarget.PROPERTY_GETTER,
AnnotationTarget.PROPERTY_SETTER
)
@Retention(AnnotationRetention.RUNTIME)
@MapKey
annotation class ViewModelKey(val value: KClass<out ViewModel>)
@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
}
}
Here is the log of the bug with SmistamentoGiroTabViewModel addresses
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
It's correct to have two values triggered by the observe, because the old value is the fragment/VM initialization, the new value is the result of the handleArgs method called (that leads to record insertion in db) at the end of the onCreateView as in-depth explainted above.
After one entire month of testing and analysis of the architectural elements of the project I have found the solution. In the initialization of the Room database I was using RoomDatabase.JournalMode.TRUNCATE.
Here is the ref link of the modes: https://developer.android.com/reference/android/arch/persistence/room/RoomDatabase.JournalMode
What has to be used is WRITE_AHEAD_LOGGING (used by default). By using it, every random graphic errors disappeared.