Search code examples
androidkotlinandroid-fragmentsandroid-livedataandroid-architecture-navigation

Android - LiveData randomly doesn't trigger with the updated value (in depth question)


Video demostration of the bug

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.

Architecture

I'm using the MVVM approach along with Navigator for fragment navigation.

Activity

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")

Fragment

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:

  • findNavController().navigate(SmistamentoGiroTabFragmentDirections.actionDashboardToCercaGiro...
  • findNavController().navigate(SmistamentoGiroCartellaCarrelloFragmentDirections.actionGiroToDashboard...

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.

Code

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>

How the insertion works

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);

Debug log:

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.

Conclusion

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.

Update

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:

SmistamentoGiroTabFragment

@Inject
lateinit var viewModelFactory: ViewModelProvider.Factory

private val smistamentoGiroTabViewModel: SmistamentoGiroTabViewModel by viewModels {
    viewModelFactory
}

ViewModelModule (for Dagger injection)

@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
    }
}

Update 2

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

Update 3

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.


Solution

  • 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.

    The Google LiveData system is not completely compatible with the Room database journal mode 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.