I'm new to android development and kotlin. I'm trying to make an app that stores some user provided values in Room, and shows them in a list.
My biggest difficulty so far is that there's a mess of tutorials and references from various versions of kotlin/compose/android functions that is very hard for someone who is new to parse through.
My Entity
@Entity(tableName = "goals")
data class Goal(
@PrimaryKey(autoGenerate = true)
val id : Int = 0,
val text : String,
val date : String,
val complete : Boolean = false
)
My DAO
@Dao
interface GoalDao {
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insert(goal : Goal)
@Update
suspend fun update(goal : Goal)
@Delete
suspend fun delete(goal : Goal)
@Query("SELECT * FROM goals WHERE id = :id")
fun getGoal(id : Int) : Flow<Goal>
@Query("SELECT * FROM goals ORDER BY date DESC")
fun getAllGoals() : Flow<List<Goal>>
}
My Database
@Database(
entities = [Goal::class],
version = 1,
exportSchema = false
)
abstract class GoalDatabase : RoomDatabase() {
abstract fun goalDao() : GoalDao
companion object {
@Volatile
private var Instance : GoalDatabase? = null
fun getDatabase(context : Context) : GoalDatabase {
return Instance ?: synchronized(this) {
Room.databaseBuilder(context, GoalDatabase::class.java, "goal_database")
.fallbackToDestructiveMigration()
.build()
.also { Instance = it }
}
}
}
}
My Repository
class GoalRepository(private val goalDao : GoalDao) {
fun getAllGoals(): Flow<List<Goal>> = goalDao.getAllGoals()
@Suppress("RedundantSuspendModifier")
@WorkerThread
suspend fun insertGoal(goal : Goal) {
goalDao.insert(goal)
}
}
My View Model
class GoalViewModel(private val repository: GoalRepository) : ViewModel() {
val allGoals : LiveData<List<Goal>> = repository.getAllGoals().asLiveData()
fun insert(goal : Goal) = viewModelScope.launch {
repository.insertGoal(goal)
}
}
class GoalViewModelFactory(private val repository: GoalRepository) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass : Class<T>) : T {
if(modelClass.isAssignableFrom(GoalViewModel::class.java)) {
@Suppress("UNCHECKED_CAST")
return GoalViewModel(repository) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
And then here is my main activity:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val goalRepository = GoalRepository(GoalDatabase.getDatabase(application as Context).goalDao())
setContent {
PositiveTrackerTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
MainScreen(goalRepository)
}
}
}
}
}
@Composable
fun MainScreen(goalRepository: GoalRepository) {
val navController = rememberNavController()
NavHost(navController = navController, startDestination = "main") {
composable("main") {
Home(goalRepository)
}
}
}
@Composable
fun NewGoal(
onCompleteGoal: (String) -> Unit,
onCancelGoal: () -> Unit
) {
val focusRequester = remember { FocusRequester() }
LaunchedEffect(Unit) {
focusRequester.requestFocus()
}
Dialog(
onDismissRequest = {
onCancelGoal()
}
)
{
Card {
var goalText by remember { mutableStateOf("") }
Column(
modifier = Modifier.padding(16.dp)
) {
Text(
text = "New Goal",
style = MaterialTheme.typography.titleMedium,
modifier = Modifier.padding(bottom = 16.dp)
)
TextField(
value = goalText,
onValueChange = {
goalText = it
},
singleLine = true,
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 16.dp)
.focusRequester(focusRequester)
)
Row {
TextButton(
onClick = { onCancelGoal() },
modifier = Modifier.padding(8.dp)
) {
Text(text = "Cancel")
}
TextButton(
onClick = { onCompleteGoal(goalText) },
modifier = Modifier.padding(8.dp)
) {
Text(text = "Create Goal")
}
}
}
}
}
}
@Composable
fun GoalCard(goal : Goal) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
.clip(shape = RoundedCornerShape(10.dp)),
colors = CardDefaults.cardColors()
) {
Column(
modifier = Modifier.fillMaxWidth()
) {
Text(
text = goal.text,
style = MaterialTheme.typography.affirmationQuote,
modifier = Modifier.padding(16.dp)
)
Text(
text = goal.date,
style = MaterialTheme.typography.labelSmall,
modifier = Modifier
.padding(16.dp)
.align(Alignment.End)
)
}
}
}
@Composable
fun Home(goalRepository : GoalRepository) {
var showNewGoal by remember { mutableStateOf(false) }
val coroutineScope = rememberCoroutineScope()
val viewModel : GoalViewModel = viewModel(factory = GoalViewModelFactory(goalRepository))
val goalList by viewModel.allGoals.observeAsState()
if(showNewGoal) {
NewGoal(
onCompleteGoal = {
showNewGoal = false
coroutineScope.launch {
goalRepository.insertGoal(
Goal(
text = it,
date = LocalDate.now().toString()
)
)
}
},
onCancelGoal = {
showNewGoal = false
}
)
}
Box(
modifier = Modifier.fillMaxSize()
)
{
Column(
modifier = Modifier.fillMaxSize()
) {
goalList!!.forEach {
GoalCard(it)
}
}
}
}
When I run the app, log cat gives me a big dump of problems:
java.lang.NullPointerException
at com.positivetracker.MainActivityKt.Home(MainActivity.kt:228)
at com.positivetracker.MainActivityKt$MainScreen$1$1.invoke(MainActivity.kt:92)
at com.positivetracker.MainActivityKt$MainScreen$1$1.invoke(MainActivity.kt:91)
at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:139)
at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:35)
at androidx.navigation.compose.NavHostKt$NavHost$14$1.invoke(NavHost.kt:308)
at androidx.navigation.compose.NavHostKt$NavHost$14$1.invoke(NavHost.kt:306)
at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:109)
at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:35)
at androidx.compose.runtime.CompositionLocalKt.CompositionLocalProvider(CompositionLocal.kt:248)
at androidx.compose.runtime.saveable.SaveableStateHolderImpl.SaveableStateProvider(SaveableStateHolder.kt:84)
at androidx.navigation.compose.NavBackStackEntryProviderKt.SaveableStateProvider(NavBackStackEntryProvider.kt:65)
at androidx.navigation.compose.NavBackStackEntryProviderKt.access$SaveableStateProvider(NavBackStackEntryProvider.kt:1)
at androidx.navigation.compose.NavBackStackEntryProviderKt$LocalOwnersProvider$1.invoke(NavBackStackEntryProvider.kt:52)
at androidx.navigation.compose.NavBackStackEntryProviderKt$LocalOwnersProvider$1.invoke(NavBackStackEntryProvider.kt:51)
at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:109)
at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:35)
at androidx.compose.runtime.CompositionLocalKt.CompositionLocalProvider(CompositionLocal.kt:228)
at androidx.navigation.compose.NavBackStackEntryProviderKt.LocalOwnersProvider(NavBackStackEntryProvider.kt:47)
at androidx.navigation.compose.NavHostKt$NavHost$14.invoke(NavHost.kt:306)
at androidx.navigation.compose.NavHostKt$NavHost$14.invoke(NavHost.kt:295)
at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:139)
at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:35)
at androidx.compose.animation.AnimatedContentKt$AnimatedContent$6$1$5.invoke(AnimatedContent.kt:755)
at androidx.compose.animation.AnimatedContentKt$AnimatedContent$6$1$5.invoke(AnimatedContent.kt:744)
at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:118)
at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:35)
at androidx.compose.animation.AnimatedVisibilityKt.AnimatedEnterExitImpl(AnimatedVisibility.kt:818)
at androidx.compose.animation.AnimatedContentKt$AnimatedContent$6$1.invoke(AnimatedContent.kt:726)
at androidx.compose.animation.AnimatedContentKt$AnimatedContent$6$1.invoke(AnimatedContent.kt:709)
at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:109)
at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:35)
at androidx.compose.animation.AnimatedContentKt.AnimatedContent(AnimatedContent.kt:768)
at androidx.navigation.compose.NavHostKt.NavHost(NavHost.kt:273)
at androidx.navigation.compose.NavHostKt.NavHost(NavHost.kt:128)
at com.positivetracker.MainActivityKt.MainScreen(MainActivity.kt:90)
at com.positivetracker.MainActivity$onCreate$1$1$1.invoke(MainActivity.kt:78)
at com.positivetracker.MainActivity$onCreate$1$1$1.invoke(MainActivity.kt:77)
at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:109)
at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:35)
at androidx.compose.material3.SurfaceKt$Surface$1.invoke(Surface.kt:134)
at androidx.compose.material3.SurfaceKt$Surface$1.invoke(Surface.kt:115)
E at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:109)
at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:35)
at androidx.compose.runtime.CompositionLocalKt.CompositionLocalProvider(CompositionLocal.kt:228)
at androidx.compose.material3.SurfaceKt.Surface-T9BRK9s(Surface.kt:112)
at com.positivetracker.MainActivity$onCreate$1$1.invoke(MainActivity.kt:74)
at com.positivetracker.MainActivity$onCreate$1$1.invoke(MainActivity.kt:72)
at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:109)
at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:35)
at androidx.compose.runtime.CompositionLocalKt.CompositionLocalProvider(CompositionLocal.kt:248)
at androidx.compose.material3.TextKt.ProvideTextStyle(Text.kt:352)
at androidx.compose.material3.MaterialThemeKt$MaterialTheme$1.invoke(MaterialTheme.kt:72)
at androidx.compose.material3.MaterialThemeKt$MaterialTheme$1.invoke(MaterialTheme.kt:71)
at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:109)
at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:35)
at androidx.compose.runtime.CompositionLocalKt.CompositionLocalProvider(CompositionLocal.kt:228)
at androidx.compose.material3.MaterialThemeKt.MaterialTheme(MaterialTheme.kt:64)
at com.positivetracker.ui.theme.ThemeKt.PositiveTrackerTheme(Theme.kt:87)
at com.positivetracker.MainActivity$onCreate$1.invoke(MainActivity.kt:72)
at com.positivetracker.MainActivity$onCreate$1.invoke(MainActivity.kt:71)
at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:109)
at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:35)
at androidx.compose.ui.platform.ComposeView.Content(ComposeView.android.kt:428)
at androidx.compose.ui.platform.AbstractComposeView$ensureCompositionCreated$1.invoke(ComposeView.android.kt:252)
at androidx.compose.ui.platform.AbstractComposeView$ensureCompositionCreated$1.invoke(ComposeView.android.kt:251)
at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:109)
at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:35)
at androidx.compose.runtime.CompositionLocalKt.CompositionLocalProvider(CompositionLocal.kt:228)
at androidx.compose.ui.platform.CompositionLocalsKt.ProvideCommonCompositionLocals(CompositionLocals.kt:186)
at androidx.compose.ui.platform.AndroidCompositionLocals_androidKt$ProvideAndroidCompositionLocals$3.invoke(AndroidCompositionLocals.android.kt:119)
at androidx.compose.ui.platform.AndroidCompositionLocals_androidKt$ProvideAndroidCompositionLocals$3.invoke(AndroidCompositionLocals.android.kt:118)
at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:109)
at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:35)
at androidx.compose.runtime.CompositionLocalKt.CompositionLocalProvider(CompositionLocal.kt:228)
at androidx.compose.ui.platform.AndroidCompositionLocals_androidKt.ProvideAndroidCompositionLocals(AndroidCompositionLocals.android.kt:110)
at androidx.compose.ui.platform.WrappedComposition$setContent$1$1$2.invoke(Wrapper.android.kt:139)
at androidx.compose.ui.platform.WrappedComposition$setContent$1$1$2.invoke(Wrapper.android.kt:138)
at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:109)
at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:35)
at androidx.compose.runtime.CompositionLocalKt.CompositionLocalProvider(CompositionLocal.kt:248)
at androidx.compose.ui.platform.WrappedComposition$setContent$1$1.invoke(Wrapper.android.kt:138)
at androidx.compose.ui.platform.WrappedComposition$setContent$1$1.invoke(Wrapper.android.kt:123)
E at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:109)
at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:35)
at androidx.compose.runtime.ActualJvm_jvmKt.invokeComposable(ActualJvm.jvm.kt:90)
at androidx.compose.runtime.ComposerImpl.doCompose(Composer.kt:3302)
at androidx.compose.runtime.ComposerImpl.composeContent$runtime_release(Composer.kt:3235)
at androidx.compose.runtime.CompositionImpl.composeContent(Composition.kt:725)
at androidx.compose.runtime.Recomposer.composeInitial$runtime_release(Recomposer.kt:1071)
at androidx.compose.runtime.CompositionImpl.composeInitial(Composition.kt:633)
at androidx.compose.runtime.CompositionImpl.setContent(Composition.kt:619)
at androidx.compose.ui.platform.WrappedComposition$setContent$1.invoke(Wrapper.android.kt:123)
at androidx.compose.ui.platform.WrappedComposition$setContent$1.invoke(Wrapper.android.kt:114)
at androidx.compose.ui.platform.AndroidComposeView.setOnViewTreeOwnersAvailable(AndroidComposeView.android.kt:1289)
at androidx.compose.ui.platform.WrappedComposition.setContent(Wrapper.android.kt:114)
at androidx.compose.ui.platform.WrappedComposition.onStateChanged(Wrapper.android.kt:164)
at androidx.lifecycle.LifecycleRegistry$ObserverWithState.dispatchEvent(LifecycleRegistry.kt:322)
at androidx.lifecycle.LifecycleRegistry.addObserver(LifecycleRegistry.kt:199)
at androidx.compose.ui.platform.WrappedComposition$setContent$1.invoke(Wrapper.android.kt:121)
at androidx.compose.ui.platform.WrappedComposition$setContent$1.invoke(Wrapper.android.kt:114)
at androidx.compose.ui.platform.AndroidComposeView.onAttachedToWindow(AndroidComposeView.android.kt:1364)
at android.view.View.dispatchAttachedToWindow(View.java:20479)
at android.view.ViewGroup.dispatchAttachedToWindow(ViewGroup.java:3489)
at android.view.ViewGroup.dispatchAttachedToWindow(ViewGroup.java:3496)
at android.view.ViewGroup.dispatchAttachedToWindow(ViewGroup.java:3496)
at android.view.ViewGroup.dispatchAttachedToWindow(ViewGroup.java:3496)
at android.view.ViewGroup.dispatchAttachedToWindow(ViewGroup.java:3496)
at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:2417)
at android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:1952)
at android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:8171)
at android.view.Choreographer$CallbackRecord.run(Choreographer.java:972)
at android.view.Choreographer.doCallbacks(Choreographer.java:796)
at android.view.Choreographer.doFrame(Choreographer.java:731)
at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:957)
at android.os.Handler.handleCallback(Handler.java:938)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:223)
at android.app.ActivityThread.main(ActivityThread.java:7656)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:947)
But I've got no idea where to begin looking. I suspect I'm not doing something right with the observeAsState
, though I read that's the easier way to do things as opposed to the far more verbose ways I've seen older code do things.
EDIT The exception begins on the line
goalList!!.forEach {
GoalCard(it)
}
in the MainActivity
The reason why you get a NullPointerException is, that you explicitly asked for it by using !!
.
If you don't want that exception, just remove !!
. The compile error you now get can be easily fixed by providing an initial value for observeAsState
:
val goalList by viewModel.allGoals.observeAsState(emptyList())
That said, when using Compose there is no need to use the old LiveData
at all. Instead, you should use a StateFlow in your view model:
val allGoals: StateFlow<List<Goal>> = repository.getAllGoals().stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = emptyList(),
)
Then, in your composable, you use this:
val goalList by viewModel.allGoals.collectAsStateWithLifecycle()
You need the gradle dependency androidx.lifecycle:lifecycle-runtime-compose
for this. androidx.compose.runtime:runtime-livedata
can now be removed.
Although not related to your problem, you should also fix the following:
Your repository should only depend on GoalDatabase
, not the Dao:
class GoalRepository(goalDb: GoalDatabase) {
private val goalDao: GoalDao = goalDb.goalDao()
// ...
}
You should not pass down repositories to your composables. Better, create the view model in the activity and pass that down. The proper way, however, would be to only pass states and callbacks down:
val goalRepository = GoalRepository(GoalDatabase.getDatabase(applicationContext))
setContent {
val viewModel: GoalViewModel = viewModel(factory = GoalViewModelFactory(goalRepository))
val goalList by viewModel.allGoals.collectAsStateWithLifecycle()
// PositiveTrackerTheme and Surface here, just omitted for brevity
MainScreen(
goalList = goalList,
insertGoal = viewModel::insert,
)
}
MainScreen
and Home
then need to have these parameters:
goalList: List<Goal>,
insertGoal: (Goal) -> Unit,
That also removes the need for coroutineScope.launch
in your Home
composable. The coroutine stuff with the suspend function is now done in the view model, where it belongs.
Use the Kotlin Clock.System.now()
to get the current time instead of the Java LocalDate.now()
.