Я новичок в разработке Android и Котлине. Я пытаюсь создать приложение, которое хранит некоторые предоставленные пользователем значения в комнате и показывает их в списке.
Моя самая большая трудность на данный момент заключается в том, что существует беспорядок учебных пособий и ссылок из различных версий функций kotlin/compose/android, которые очень сложно разобрать новичку.
Моя сущность
@Entity(tableName = "goals")
data class Goal(
@PrimaryKey(autoGenerate = true)
val id : Int = 0,
val text : String,
val date : String,
val complete : Boolean = false
)
Мой ДАО
@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>>
}
Моя база данных
@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 }
}
}
}
}
Мой репозиторий
class GoalRepository(private val goalDao : GoalDao) {
fun getAllGoals(): Flow<List<Goal>> = goalDao.getAllGoals()
@Suppress("RedundantSuspendModifier")
@WorkerThread
suspend fun insertGoal(goal : Goal) {
goalDao.insert(goal)
}
}
Моя модель просмотра
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")
}
}
И вот моя основная деятельность:
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)
}
}
}
}
Когда я запускаю приложение, log cat выдает мне кучу проблем:
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)
Но я понятия не имею, с чего начать поиски. Я подозреваю, что делаю что-то не так с observeAsState
, хотя я читал, что это более простой способ сделать что-то в отличие от гораздо более многословных способов, которые я видел в старом коде.
РЕДАКТИРОВАТЬ Исключение начинается в строке
goalList!!.forEach {
GoalCard(it)
}
в основной активности
Это не «большая куча проблем». Это трассировка стека для одной проблемы. Вам нужно сообщить нам, что это за строка 228. Но все NPE одинаковы. Вы попытались использовать ссылочную переменную, значение которой было нулевым.
Причина, по которой вы получаете исключение NullPointerException, заключается в том, что вы явно запросили его, используя !!.
Если вам не нужно это исключение, просто удалите !!
. Ошибку компиляции, которую вы сейчас получаете, можно легко исправить, указав начальное значение для observeAsState
:
val goalList by viewModel.allGoals.observeAsState(emptyList())
Тем не менее, при использовании Compose вообще нет необходимости использовать старый LiveData
. Вместо этого вам следует использовать StateFlow в вашей модели представления:
val allGoals: StateFlow<List<Goal>> = repository.getAllGoals().stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = emptyList(),
)
Затем в вашей компоновке вы используете это:
val goalList by viewModel.allGoals.collectAsStateWithLifecycle()
Для этого вам понадобится зависимость Gradle androidx.lifecycle:lifecycle-runtime-compose
. androidx.compose.runtime:runtime-livedata
теперь можно удалить.
Хотя это не имеет отношения к вашей проблеме, вам также следует исправить следующее:
Ваш репозиторий должен зависеть только от GoalDatabase
, а не от Dao:
class GoalRepository(goalDb: GoalDatabase) {
private val goalDao: GoalDao = goalDb.goalDao()
// ...
}
Вы не должны передавать репозитории вашим составным объектам. Лучше создайте модель представления в действии и передайте ее. Однако правильным способом было бы передавать только состояния и обратные вызовы:
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
и Home
тогда должны иметь следующие параметры:
goalList: List<Goal>,
insertGoal: (Goal) -> Unit,
Это также устраняет необходимость в coroutineScope.launch
в вашей композиции Home
. Работа сопрограммы с функцией приостановки теперь выполняется в модели представления, которой она и принадлежит.
Используйте Kotlin Clock.System.now()
, чтобы получить текущее время вместо Java LocalDate.now()
.
Спасибо за все советы здесь. Как я уже сказал, я новичок в разработке Android, и существует так много устаревших руководств и ссылок, что мне трудно понять, как правильно что-то делать. еще раз спасибо
Можете ли вы выделить строку в MainActivity, из которой генерируется исключение?