3

Video demostration of the bug

At first watch the video to understand what is the bug: https://i.stack.imgur.com/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.

Link 88
  • 553
  • 8
  • 27
  • 1
    Can you show how do you init viewmodel? i.e. who is the owner lifecycle of the viewmodel? – Y.Kakdas Apr 01 '21 at 10:36
  • I'll edit the question right now. – Link 88 Apr 01 '21 at 10:43
  • @Y.Kakdas Updated the question with the information you have requested. – Link 88 Apr 01 '21 at 11:00
  • Do your fragments being hold by viewpager or such that cachable data structures? And did you check the addresses of viewmodel by debugging? I think when same values are written, there are alive viewmodels from previous fragments. You may start looking that if the producer of duplicate values caused from same instance of viewmodel or not. – Y.Kakdas Apr 01 '21 at 11:21
  • 1
    Also, for not updating duplicate values, you may use Transformations.distinctUntilChange() – Y.Kakdas Apr 01 '21 at 11:23
  • @Y.Kakdas No the children fragments are disposed just like the tabfragment. The moment the tabfragment is disposed (onPause -> onDestroy), the children fragments are disposed too. Anyway this bug occours in other section of the app there there are no viewpager, it's just one fragment to one fragment approach. Any problem releated to viewpager can be excluded. For the second question I'm updating the question. – Link 88 Apr 01 '21 at 11:26
  • Now the duplicate values is not happening to me in the log i'm about to add, so it's not a problem (maybe was just a double log I added in my last test). With the new test with ViewModel address print it call only once the new value. – Link 88 Apr 01 '21 at 11:31
  • Can you provide a MWE (https://stackoverflow.com/help/minimal-reproducible-example)? I'd guess that the cause of your problem are issues with timing regarding the lifecycle and database operations. However, without a MWE there is no way (for me at least) to get to the root of it all... – greyhairredbear Apr 09 '21 at 22:13
  • @LPeteR90 ok, I'll create a MWE and share it here. Keep observing this question pls. – Link 88 Apr 12 '21 at 07:37
  • Great, will do :) – greyhairredbear Apr 12 '21 at 09:15
  • Please check if the database entry count is as expected after calling ```Long insert``` with an imperative approach to narrow down the problem. It means adding ```suspend fun insert()``` and then calling it, then calling ```suspend fun searchLVCount()```; to be sure the problem is with live observation, not the databasse. – mrahimygk Apr 13 '21 at 11:46
  • @LPeteR90 I'm sorry that more than 1 week has passed. I've been busy and now I have the go for creating the MWE. It'll arrive today! – Link 88 Apr 26 '21 at 08:03
  • @LPeteR90 I'm posting the solution as answer if you are interested ;) – Link 88 May 12 '21 at 07:23

1 Answers1

2

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.

Link 88
  • 553
  • 8
  • 27