Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Kotlin Flow returned from Room does not update when an insert is performed from another Fragment/ViewModel

I have a Room database that returns a Flow of objects. When I insert a new item into the database, the Flow's collect function only triggers if the insert was performed from the same Fragment/ViewModel.

I have recorded a quick video showcasing the issue: https://www.youtube.com/watch?v=7HJkJ7M1WLg

Here is my code setup for the relevant files:

AchievementDao.kt:

@Dao
interface AchievementDao {

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    fun insert(achievement: Achievement)

    @Query("SELECT * FROM achievement")
    fun getAllAchievements(): Flow<List<Achievement>>
}

AppDB.kt:

@Database(entities = [Achievement::class], version = 1, exportSchema = false)
abstract class AppDB : RoomDatabase() {

    abstract fun achievementDao(): AchievementDao
}

AchievementRepository.kt:

class AchievementRepository @Inject constructor(appDB: AppDB) {

    private val achievementDao = appDB.achievementDao()

    suspend fun insert(achievement: Achievement) {
        withContext(Dispatchers.IO) {
            achievementDao.insert(achievement)
        }
    }

    fun getAllAchievements() = achievementDao.getAllAchievements()
}

HomeFragment.kt:

@AndroidEntryPoint
class HomeFragment : Fragment() {

    private val viewModel: HomeViewModel by viewModels()

    private lateinit var homeText: TextView

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        return inflater.inflate(R.layout.fragment_home, container, false)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        bindViews()
        subscribeObservers()
    }

    private fun bindViews() {
        homeText = requireView().findViewById(R.id.txt_home)
        requireView().findViewById<ExtendedFloatingActionButton>(R.id.fab_add_achievement).setOnClickListener {
            AddAchievementBottomSheet().show(parentFragmentManager, "AddAchievementDialog")
        }
        requireView().findViewById<ExtendedFloatingActionButton>(R.id.fab_add_achievement_same_fragment).setOnClickListener {
            viewModel.add()
        }
    }

    private fun subscribeObservers() {
        viewModel.count.observe(viewLifecycleOwner, { count ->
            if(count != null) {
                homeText.text = count.toString()
            } else {
                homeText.text = resources.getString(R.string.app_name)
            }
        })
    }
}

HomeViewModel.kt:

class HomeViewModel @ViewModelInject constructor(private val achievementRepository: AchievementRepository) :
        ViewModel() {

    private val _count = MutableLiveData<Int>(null)
    val count = _count as LiveData<Int>

    init {
        viewModelScope.launch {
            achievementRepository.getAllAchievements()
                .collect { values ->
                    // FIXME this is only called when inserting from the same Fragment
                    _count.postValue(values.count())
                }
        }
    }

    fun add() {
        viewModelScope.launch {
            achievementRepository.insert(Achievement(0, 0, "Test"))
        }
    }
}

AddAchievementBottomSheet.kt:

@AndroidEntryPoint
class AddAchievementBottomSheet : BottomSheetDialogFragment() {

    private val viewModel: AddAchievementViewModel by viewModels()
    private lateinit var addButton: MaterialButton

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        return inflater.inflate(R.layout.dialog_add_achievement, container, false)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        addButton = requireView().findViewById(R.id.btn_add_achievement)
        addButton.setOnClickListener {
            viewModel.add(::close)
        }
    }

    private fun close() {
        dismiss()
    }
}

AddAchievementBottomSheetViewModel.kt:

class AddAchievementViewModel @ViewModelInject constructor(private val achievementRepository: AchievementRepository) :
        ViewModel() {

    fun add(closeCallback: () -> Any) {
        viewModelScope.launch {
            achievementRepository.insert(Achievement(0, 0, "Test"))
            closeCallback()
        }
    }
}

build.gradle (app):

plugins {
    id 'com.android.application'
    id 'kotlin-android'
    id 'kotlin-kapt'
    id 'dagger.hilt.android.plugin'
}

android {
    compileSdkVersion 30

    defaultConfig {
        applicationId "com.marcdonald.achievementtracker"
        minSdkVersion 23
        targetSdkVersion 30
        versionCode 1
        versionName "1.0.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
    kotlinOptions {
        jvmTarget = '1.8'
    }
}

dependencies {
    // Kotlin
    implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
    implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.0'
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.0'
    implementation 'androidx.core:core-ktx:1.3.2'

    // Android
    implementation 'androidx.appcompat:appcompat:1.2.0'
    implementation 'com.google.android.material:material:1.2.1'
    implementation "androidx.activity:activity-ktx:1.1.0"
    implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
    implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
    implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0'

    // Navigation
    implementation 'androidx.navigation:navigation-fragment-ktx:2.3.1'
    implementation 'androidx.navigation:navigation-ui-ktx:2.3.1'

    // Testing
    testImplementation 'junit:junit:4.13.1'
    androidTestImplementation 'androidx.test.ext:junit:1.1.2'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'

    // Dagger Hilt
    implementation 'com.google.dagger:hilt-android:2.29.1-alpha'
    implementation 'androidx.hilt:hilt-lifecycle-viewmodel:1.0.0-alpha02'
    kapt 'androidx.hilt:hilt-compiler:1.0.0-alpha02'
    kapt 'com.google.dagger:hilt-android-compiler:2.29.1-alpha'

    // Timber for logging
    implementation 'com.jakewharton.timber:timber:4.7.1'

    // Room
    implementation 'androidx.room:room-runtime:2.2.5'
    implementation 'androidx.room:room-ktx:2.2.5'
    kapt 'androidx.room:room-compiler:2.2.5'
    androidTestImplementation 'androidx.room:room-testing:2.2.5'
}

build.gradle (project):

buildscript {
    ext.kotlin_version = "1.4.10"
    repositories {
        google()
        jcenter()
        mavenCentral()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:4.2.0-alpha16'
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
        classpath 'android.arch.navigation:navigation-safe-args-gradle-plugin:1.0.0'
        classpath 'com.google.dagger:hilt-android-gradle-plugin:2.29.1-alpha'
    }
}

allprojects {
    repositories {
        google()
        jcenter()
        maven { url 'https://jitpack.io' }
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}

I'm not sure if my understanding of Kotlin Flow is to blame or whether my setup is incorrect in some way, but I'd appreciate some help with the issue.

like image 273
Marc Avatar asked Nov 16 '20 21:11

Marc


Video Answer


1 Answers

Make sure you use the same instance of your RoomDatabase. Add a @Singleton where you provide AppDB might do the trick.

like image 97
Wärting Avatar answered Oct 06 '22 23:10

Wärting