androidkotlinandroid-jetpack-composeandroid-room

A white screen instead of displaying information in Compose and Room Library


This is the complete code I wrote using artificial intelligence. Where do you think the problem comes from?

there is no error!

entity:


@Entity(tableName = "fixed_info")
data class FixedInfo(
    @PrimaryKey val id: Int,
    val description: String
)

and my Dao:


@Dao
interface FixedInfoDao {
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insert(info: FixedInfo)

    @Query("SELECT * FROM fixed_info")
    suspend fun getAllFixedInfo(): List<FixedInfo>
}

this is my database:



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

    abstract fun fixedInfoDao(): FixedInfoDao

    companion object {
        @Volatile
        private var INSTANCE: AppDatabase? = null

        fun getDatabase(context: Context): AppDatabase {
            return INSTANCE ?: synchronized(this) {
                val instance = Room.databaseBuilder(
                    context.applicationContext,
                    AppDatabase::class.java,
                    "app_database"
                )
                    .addCallback(object : Callback() {
                        override fun onCreate(db: SupportSQLiteDatabase) {
                            super.onCreate(db)
                            INSTANCE?.let { database ->
                                CoroutineScope(Dispatchers.IO).launch {
                                    // Add default fixed information
                                    database.fixedInfoDao().insert(FixedInfo(id = 0, description = "Default info"))
                                    database.fixedInfoDao().insert(FixedInfo(id = 2, description = "Default info"))
                                    database.fixedInfoDao().insert(FixedInfo(id = 3, description = "Default info"))
                                    database.fixedInfoDao().insert(FixedInfo(id = 4, description = "Default info"))
                                }
                            }
                        }
                    })
                    .build()
                INSTANCE = instance
                instance
            }
        }
    }
}

and this is my viewModel:


class FixedInfoViewModel(private val fixedInfoDao: FixedInfoDao) : ViewModel() {
    var fixedInfoList = mutableListOf<FixedInfo>()
        private set

    init {
        loadFixedInfo()
    }

    private fun loadFixedInfo() {
        viewModelScope.launch {
            fixedInfoList = fixedInfoDao.getAllFixedInfo().toMutableList()
        }
    }
}

mainActivity:


class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
                MyApp()
        }
    }
}


@Composable
fun MyApp() {
    val context = LocalContext.current
    val fixedInfoDao = AppDatabase.getDatabase(context).fixedInfoDao()
    val viewModel: FixedInfoViewModel = viewModel(factory = FixedInfoViewModelFactory(fixedInfoDao))
    FixedInfoList(viewModel = viewModel)
}

class FixedInfoViewModelFactory(private val fixedInfoDao: FixedInfoDao) : ViewModelProvider.Factory {
    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        if (modelClass.isAssignableFrom(FixedInfoViewModel::class.java)) {
            @Suppress("UNCHECKED_CAST")
            return FixedInfoViewModel(fixedInfoDao) as T
        }
        throw IllegalArgumentException("Unknown ViewModel class")
    }
}

@Composable
fun FixedInfoList(viewModel: FixedInfoViewModel) {
    Column(modifier = Modifier.padding(16.dp)) {
        viewModel.fixedInfoList.forEach { info ->
            Text(text = "Description: ${info.description}")
            Spacer(modifier = Modifier.height(8.dp))
        }
    }
}

I have a simple program that aims to display simple data stored in a room library in compose but when i run the application, it just displays a white screen.


Solution

  • You expose the database entries as a MutableList in your view model. On startup the UI displays the empty mutableListOf<FixedInfo>() first, and only then, after some time, loadFixedInfo replaces that list with a new one containing your database entries. The UI won't update, though, because Compose only detects changes in State objects.

    The solution is to expose data in the view model only as flows which can easily be converted to Compose State objects.

    The first thing to do would be to change your dao query to this:

    fun getAllFixedInfo(): Flow<List<FixedInfo>>
    

    The return type is now a flow and Room will automatically update the flow's content when anything changes in the database. When a function returns a flow it usually doesn't need to suspend anymore, so that keyword is removed here as well.

    Next, the view model must be refactored. The only thing you need is this:

    val fixedInfoList: StateFlow<List<FixedInfo>> = fixedInfoDao.getAllFixedInfo()
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5_000),
            initialValue = emptyList(),
        )
    

    Everything else can be removed. What this does is to convert the database flow into a StateFlow. That is a special kind of flow which always has a single, current value. The semantics are similar to an observable variable, like a LiveData or a MutableState.

    That's also the reason why this flow can now easily be converted into a State object in your MyApp composable:

    val fixedInfoList: List<FixedInfo> by viewModel.fixedInfoList.collectAsStateWithLifecycle()
    

    You need the gradle dependency androidx.lifecycle:lifecycle-runtime-compose for this to work. Please note the by keyword, that is a delegate that unwraps the State so fixedInfoList is a List<FixedInfo> and not a State<List<FixedInfo>>.

    This list can now be passed to the FixedInfoList composable. Please keep in mind that you shouldn't pass view model instances around, only pass simple (data) objects and callbacks to composables, in this case the unwrapped list from the flow. FixedInfoList should be declared like this:

    @Composable
    fun FixedInfoList(fixedInfoList: List<FixedInfo>)
    

    And that's it: Because collectAsStateWithLifecycle converts the view model data into a State object, any changes to the data will now trigger a recomposition, updating the UI. This happens, for example, when the database provides the the data on app startup, but it also updates automatically when you would change the data in the database, like calling insert to add a new entry.