Search code examples
androidkotlinandroid-jetpack-composeandroid-livedata

Why am I getting a NULL pointer exception when using .observeAsState()?


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


Solution

  • 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:

    1. Your repository should only depend on GoalDatabase, not the Dao:

      class GoalRepository(goalDb: GoalDatabase) {
          private val goalDao: GoalDao = goalDb.goalDao()
          // ...
      }
      
    2. 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.

    3. Use the Kotlin Clock.System.now() to get the current time instead of the Java LocalDate.now().