androidandroid-jetpack-composeroomdb

Jetpack Compose, persist and retrieve data (Flow<List<Item>>) Error : lateinit property appData has not been initialized


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) 


Solution

  • 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()
        }
    }