Hello helpful community,
I have been working on an application where I am having difficulties understanding how to persist and retrieve a Flow<List>> in a room database. I believe what I wrote in the ViewModel and MainActivity classes is wrong and I cannot figure out the proper way to do it. I tried to persist from the main activity but I receive the following error :
kotlin.UninitializedPropertyAccessException: lateinit property appData has not been initialized at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3822) at
Model
@Entity(tableName="items")
data class (Item
@PrimaryKey @ColumnInfo(name = "id") val itemId: String,
@StringRes val title: Int,
@StringRes val description: Int,
@DrawableRes val drawable: Int
)
Dao
@Dao
interface ItemDao {
@Query("SELECT * FROM items")
fun getItems(): Flow<List<Item>>
@Query("SELECT * FROM items WHERE id=:itemId")
fun getItem(itemId: String): Flow<Item>
@Insert
fun insertItem(item: Item)
@Insert
suspend fun insertAll(items: List<Item>)
}
Repository
@Singleton
class ItemRepository @Inject constructor (
private val itemDao : ItemDao,
) {
fun getItems(): Flow<List<Item>> {
return itemDao.getItems()
}
fun getItem(itemId: String) = itemDao.getItem(itemId)
suspend fun insertItem(item : Item) {
return itemDao.insertItem(item)
}
suspend fun insertItems(item : List<Item>) {
return workoutDao.insertAll(item)
}
ViewModel
@HiltViewModel
class ItemListViewModel @Inject constructor(
private val itemRepository: ItemRepository
) : ViewModel() {
private val _items = MutableStateFlow<List<Item>>(emptyList())
val items = _items.asStateFlow()
init {
loadTask()
}
private fun loadTask() {
viewModelScope.launch() {
itemRepository.getItems()
}
}
DatabaseModule
@InstallIn(SingletonComponent::class)
@Module
object DatabaseModule {
@Singleton
@Provides
fun provideAppDatabase(@ApplicationContext context: Context): AppDatabase {
return Room.databaseBuilder(
context.applicationContext,
AppDatabase::class.java,
DATABASE_NAME
)
.build()
}
@Provides
fun provideItemDao(appDatabase: AppDatabase): ItemDao
= appDatabase.itemDao()
AppDatabase
@Database(entities = [Item::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun itemDao(): ItemDao
ItemListScreen
@Composable
fun ItemListScreen(
onItemClick: (Item) -> Unit,
modifier: Modifier = Modifier,
viewModel: ItemListViewModel = hiltViewModel(),
) {
val items by viewModel.items.collectAsState(initial = emptyList())
ItemListScreen(items = items, modifier, onItemClick = onItemClick)
}
MainActivity
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
private lateinit var appData: AppDatabase
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//WindowCompat.setDecorFitsSystemWindows(window, false)
setContent {
FitTestAppTheme {
ItemApp()
}
}
val item = Item("1",R.string.title,R.string.description, R.drawable.image)
if (::appData.isInitialized) {}
appData.itemDao().insertItem(item)
}
}
Error
java.lang.RuntimeException: Unable to start activity ComponentInfo{com.example.fittestapp/com.example.fittestapp.MainActivity}: kotlin.UninitializedPropertyAccessException: lateinit property appData has not been initialized
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3822)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3963)
at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:103)
at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:139)
at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:96)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2468)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loopOnce(Looper.java:205)
at android.os.Looper.loop(Looper.java:294)
at android.app.ActivityThread.main(ActivityThread.java:8248)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:552)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:971)
Caused by: kotlin.UninitializedPropertyAccessException: lateinit property appData has not been initialized
at com.example.fittestapp.MainActivity.onCreate(MainActivity.kt:41)
at android.app.Activity.performCreate(Activity.java:8621)
at android.app.Activity.performCreate(Activity.java:8599)
at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1456)
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3804)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3963)
at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:103)
at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:139)
at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:96)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2468)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loopOnce(Looper.java:205)
at android.os.Looper.loop(Looper.java:294)
at android.app.ActivityThread.main(ActivityThread.java:8248)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:552)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:971)
You are missing @Inject
annotation on your appData, this will tell the hilt
to inject AppDatabase from your DatabaseModule
.
Edit:
Also do not call database operations on main thread, you can allow it by Room.databaseBuilder().allowMainThreadQueries()
or wrap the insert with thread or coroutine.
I am not sure if you want to use Flow
or StateFlow
, both ways are valid and it depends on context of your project, so i implemented both of them:
Updated ItemsDao
:
@Dao
interface ItemDao {
@Query("SELECT * FROM items")
fun getItemsFlow(): Flow<List<Item>>
@Query("SELECT * FROM items")
fun getItems(): List<Item>
@Query("SELECT * FROM items WHERE id=:itemId")
fun getItem(itemId: String): Flow<Item>
@Insert
fun insertItem(item: Item)
@Insert
suspend fun insertAll(items: List<Item>)
}
Updated ItemsRepository
:
@Singleton
class ItemRepository @Inject constructor(
private val itemDao: ItemDao,
) {
fun getItems(): List<Item> {
return itemDao.getItems()
}
fun getItemsFlow(): Flow<List<Item>> {
return itemDao.getItemsFlow()
}
fun getItem(itemId: String) = itemDao.getItem(itemId)
suspend fun insertItem(item: Item) {
return itemDao.insertItem(item)
}
suspend fun insertItems(item: List<Item>) {
return workoutDao.insertAll(item)
}
}
Updated ItemViewModel
:
@HiltViewModel
class ItemListViewModel @Inject constructor(
private val itemRepository: ItemRepository,
) : ViewModel() {
private val _items = MutableStateFlow<List<Item>>(emptyList())
val items = _items.asStateFlow()
//Using the flow returned by itemsRepository directly
val itemsFlow = itemRepository.getItemsFlow()
init {
loadTask()
}
fun addItem(item: Item) {
viewModelScope.launch {
withContext(Dispatchers.IO) {
itemRepository.insertItem(item = item)
loadTask()
}
}
}
fun loadTask() {
viewModelScope.launch() {
//Loading items as simple list and setting it as _items value
_items.value = itemRepository.getItems()
}
}
}
Now for the MainActivity
i removed appData
and using viewModel only. It's better to have clean structure of project. I made up collection for bot flow
and stateFlow
items. After the item is inserted, items are reloaded.
Also keep in mind that you can't insert two items with same id, that's why it looks like item is not being inserted. You probably inserted it before.
Updated MainActivity
:
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
private val viewModel: ItemListViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//WindowCompat.setDecorFitsSystemWindows(window, false)
setContent {
val itemsFromStateFlow by viewModel.items.collectAsState()
val itemsFromFlow by viewModel.itemsFlow.collectAsState(initial = emptyList())
StackOverflowTheme {
Scaffold(containerColor = Color.White) { paddingValues ->
Column {
Text(text = "Items from state flow")
LazyColumn {
items(items = itemsFromStateFlow) {
Text(text = it.itemId)
}
}
Text(text = "Items from flow")
LazyColumn {
items(items = itemsFromFlow) {
Text(text = it.itemId)
}
}
}
}
}
}
val item = Item("1",R.string.title,R.string.description, R.drawable.image)
//insert
viewModel.addItem(item = item)
//refresh data
viewModel.loadTask()
}
}