mirror of
https://github.com/wismna/DoNext.git
synced 2025-10-03 07:30:13 -04:00
Add a FAB to create a task
Add bottom sheet to display the new task form Set up the tasks list screen to display tasks Implement a working migration from the old Donext database
This commit is contained in:
13
.idea/deviceManager.xml
generated
Normal file
13
.idea/deviceManager.xml
generated
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="DeviceTable">
|
||||||
|
<option name="columnSorters">
|
||||||
|
<list>
|
||||||
|
<ColumnSorterState>
|
||||||
|
<option name="column" value="Name" />
|
||||||
|
<option name="order" value="ASCENDING" />
|
||||||
|
</ColumnSorterState>
|
||||||
|
</list>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
45
.idea/inspectionProfiles/Project_Default.xml
generated
45
.idea/inspectionProfiles/Project_Default.xml
generated
@@ -4,5 +4,50 @@
|
|||||||
<inspection_tool class="AutoCloseableResource" enabled="true" level="WARNING" enabled_by_default="true">
|
<inspection_tool class="AutoCloseableResource" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
<option name="METHOD_MATCHER_CONFIG" value="java.util.Formatter,format,java.io.Writer,append,com.google.common.base.Preconditions,checkNotNull,org.hibernate.Session,close,java.io.PrintWriter,printf,java.io.PrintStream,printf,android.content.Context,obtainStyledAttributes" />
|
<option name="METHOD_MATCHER_CONFIG" value="java.util.Formatter,format,java.io.Writer,append,com.google.common.base.Preconditions,checkNotNull,org.hibernate.Session,close,java.io.PrintWriter,printf,java.io.PrintStream,printf,android.content.Context,obtainStyledAttributes" />
|
||||||
</inspection_tool>
|
</inspection_tool>
|
||||||
|
<inspection_tool class="ComposePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="ComposePreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="ComposePreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="ComposePreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="GlancePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="GlancePreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="GlancePreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="GlancePreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PreviewAnnotationInFunctionWithParameters" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PreviewApiLevelMustBeValid" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PreviewDeviceShouldUseNewSpec" enabled="true" level="WEAK WARNING" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PreviewFontScaleMustBeGreaterThanZero" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PreviewMultipleParameterProviders" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PreviewParameterProviderOnFirstParameter" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PreviewPickerAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="composableFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
</profile>
|
</profile>
|
||||||
</component>
|
</component>
|
@@ -1,12 +1,12 @@
|
|||||||
apply plugin: 'com.android.application'
|
apply plugin: 'com.android.application'
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdk 35
|
compileSdk 36
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "com.wismna.geoffroy.donext"
|
applicationId "com.wismna.geoffroy.donext"
|
||||||
minSdkVersion 21
|
minSdkVersion 21
|
||||||
targetSdkVersion 35
|
targetSdkVersion 36
|
||||||
versionCode 33
|
versionCode 33
|
||||||
versionName "1.12"
|
versionName "1.12"
|
||||||
}
|
}
|
||||||
|
@@ -14,8 +14,8 @@ android {
|
|||||||
applicationId = "com.wismna.geoffroy.donext"
|
applicationId = "com.wismna.geoffroy.donext"
|
||||||
minSdk = 26
|
minSdk = 26
|
||||||
targetSdk = 36
|
targetSdk = 36
|
||||||
versionCode = 1
|
versionCode = 34
|
||||||
versionName = "1.0"
|
versionName = "2.0"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
|
@@ -9,33 +9,39 @@ import java.time.Instant
|
|||||||
fun TaskEntity.toDomain() = Task(
|
fun TaskEntity.toDomain() = Task(
|
||||||
id = id,
|
id = id,
|
||||||
name = name,
|
name = name,
|
||||||
isDone = isDone,
|
|
||||||
taskListId = taskListId,
|
taskListId = taskListId,
|
||||||
description = description,
|
description = description,
|
||||||
cycles = cycles,
|
cycle = cycle,
|
||||||
|
isDone = isDone,
|
||||||
isDeleted = isDeleted,
|
isDeleted = isDeleted,
|
||||||
updateDate = Instant.ofEpochMilli(updateDate)
|
dueDate = if (dueDate == null) null else Instant.ofEpochMilli(dueDate),
|
||||||
|
priority = priority,
|
||||||
|
order = order
|
||||||
)
|
)
|
||||||
|
|
||||||
fun Task.toEntity() = TaskEntity(
|
fun Task.toEntity() = TaskEntity(
|
||||||
id = id,
|
id = id ?: 0,
|
||||||
name = name,
|
name = name,
|
||||||
isDone = isDone,
|
|
||||||
taskListId = taskListId,
|
taskListId = taskListId,
|
||||||
description = description,
|
description = description,
|
||||||
cycles = cycles,
|
cycle = cycle,
|
||||||
|
priority = priority,
|
||||||
|
order = order,
|
||||||
|
isDone = isDone,
|
||||||
isDeleted = isDeleted,
|
isDeleted = isDeleted,
|
||||||
updateDate = updateDate.toEpochMilli()
|
dueDate = dueDate?.toEpochMilli()
|
||||||
)
|
)
|
||||||
|
|
||||||
fun TaskListEntity.toDomain() = TaskList(
|
fun TaskListEntity.toDomain() = TaskList(
|
||||||
id = id,
|
id = id,
|
||||||
name = name,
|
name = name,
|
||||||
isDeleted = isDeleted
|
isDeleted = isDeleted,
|
||||||
|
order = order
|
||||||
)
|
)
|
||||||
|
|
||||||
fun TaskList.toEntity() = TaskListEntity(
|
fun TaskList.toEntity() = TaskListEntity(
|
||||||
id = id,
|
id = id,
|
||||||
name = name,
|
name = name,
|
||||||
isDeleted = isDeleted
|
isDeleted = isDeleted,
|
||||||
|
order = order
|
||||||
)
|
)
|
||||||
|
@@ -9,14 +9,17 @@ data class TaskEntity(
|
|||||||
@PrimaryKey(autoGenerate = true)
|
@PrimaryKey(autoGenerate = true)
|
||||||
val id: Long = 0,
|
val id: Long = 0,
|
||||||
val name: String,
|
val name: String,
|
||||||
val description: String,
|
val description: String?,
|
||||||
val cycles: Int = 0,
|
val cycle: Int = 0,
|
||||||
|
val priority: Int,
|
||||||
|
@ColumnInfo(name = "display_order")
|
||||||
|
val order: Int,
|
||||||
@ColumnInfo(name = "done")
|
@ColumnInfo(name = "done")
|
||||||
val isDone: Boolean = false,
|
val isDone: Boolean = false,
|
||||||
@ColumnInfo(name = "deleted")
|
@ColumnInfo(name = "deleted")
|
||||||
val isDeleted: Boolean = false,
|
val isDeleted: Boolean = false,
|
||||||
@ColumnInfo(name = "task_list_id")
|
@ColumnInfo(name = "task_list_id")
|
||||||
val taskListId: Long,
|
val taskListId: Long,
|
||||||
@ColumnInfo(name = "update_date")
|
@ColumnInfo(name = "due_date")
|
||||||
val updateDate: Long = System.currentTimeMillis()
|
val dueDate: Long? = null
|
||||||
)
|
)
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
package com.wismna.geoffroy.donext.data.entities
|
package com.wismna.geoffroy.donext.data.entities
|
||||||
|
|
||||||
|
import androidx.room.ColumnInfo
|
||||||
import androidx.room.Entity
|
import androidx.room.Entity
|
||||||
import androidx.room.PrimaryKey
|
import androidx.room.PrimaryKey
|
||||||
|
|
||||||
@@ -8,5 +9,8 @@ data class TaskListEntity(
|
|||||||
@PrimaryKey(autoGenerate = true)
|
@PrimaryKey(autoGenerate = true)
|
||||||
val id: Long = 0,
|
val id: Long = 0,
|
||||||
val name: String,
|
val name: String,
|
||||||
|
@ColumnInfo(name = "display_order")
|
||||||
|
val order: Int,
|
||||||
|
@ColumnInfo(name = "deleted")
|
||||||
val isDeleted: Boolean = false
|
val isDeleted: Boolean = false
|
||||||
)
|
)
|
||||||
|
@@ -31,7 +31,89 @@ abstract class AppDatabase : RoomDatabase() {
|
|||||||
|
|
||||||
val MIGRATION_6_7 = object : Migration(6, 7) {
|
val MIGRATION_6_7 = object : Migration(6, 7) {
|
||||||
override fun migrate(db: SupportSQLiteDatabase) {
|
override fun migrate(db: SupportSQLiteDatabase) {
|
||||||
// TODO: migrate from old Donext database (v6)
|
db.beginTransaction()
|
||||||
|
try {
|
||||||
|
// --- TASKS TABLE ---
|
||||||
|
// 1. Create the new tasks table with the updated schema
|
||||||
|
db.execSQL(
|
||||||
|
"""
|
||||||
|
CREATE TABLE tasks_new (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
cycle INTEGER NOT NULL DEFAULT 0,
|
||||||
|
priority INTEGER NOT NULL,
|
||||||
|
display_order INTEGER NOT NULL,
|
||||||
|
done INTEGER NOT NULL DEFAULT 0,
|
||||||
|
deleted INTEGER NOT NULL DEFAULT 0,
|
||||||
|
task_list_id INTEGER NOT NULL,
|
||||||
|
due_date INTEGER
|
||||||
|
)
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
|
||||||
|
// 2. Copy old data into the new table
|
||||||
|
// Map old column names to new ones
|
||||||
|
db.execSQL(
|
||||||
|
"""
|
||||||
|
INSERT INTO tasks_new (
|
||||||
|
id, name, description, cycle, priority, display_order,
|
||||||
|
done, deleted, task_list_id, due_date
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
_id, -- old '_id' mapped to id
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
cycle,
|
||||||
|
priority,
|
||||||
|
displayorder, -- old 'displayorder' mapped to display_order
|
||||||
|
done,
|
||||||
|
deleted,
|
||||||
|
list, -- old 'list' mapped to task_list_id
|
||||||
|
duedate -- old column renamed to due_date
|
||||||
|
FROM tasks
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
|
||||||
|
// 3. Drop the old table
|
||||||
|
db.execSQL("DROP TABLE tasks")
|
||||||
|
|
||||||
|
// 4. Rename the new table
|
||||||
|
db.execSQL("ALTER TABLE tasks_new RENAME TO tasks")
|
||||||
|
|
||||||
|
// --- TASK_LISTS TABLE ---
|
||||||
|
db.execSQL(
|
||||||
|
"""
|
||||||
|
CREATE TABLE task_lists_new (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
deleted INTEGER NOT NULL DEFAULT 0,
|
||||||
|
display_order INTEGER NOT NULL
|
||||||
|
)
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
|
||||||
|
db.execSQL(
|
||||||
|
"""
|
||||||
|
INSERT INTO task_lists_new (
|
||||||
|
id, name, display_order, deleted
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
_id, -- old '_id' mapped to id
|
||||||
|
name,
|
||||||
|
displayorder, -- old 'displayorder' mapped to display_order
|
||||||
|
1 - visible -- old 'visible' mapped to deleted
|
||||||
|
FROM tasklist
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
|
||||||
|
db.execSQL("DROP TABLE tasklist")
|
||||||
|
db.execSQL("ALTER TABLE task_lists_new RENAME TO task_lists")
|
||||||
|
|
||||||
|
db.setTransactionSuccessful()
|
||||||
|
} finally {
|
||||||
|
db.endTransaction()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,9 +132,9 @@ abstract class AppDatabase : RoomDatabase() {
|
|||||||
// insert default lists
|
// insert default lists
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
val dao = DB_INSTANCE?.taskListDao()
|
val dao = DB_INSTANCE?.taskListDao()
|
||||||
dao?.insertTaskList(TaskListEntity(name = "Work"))
|
dao?.insertTaskList(TaskListEntity(name = "Work", order = 2))
|
||||||
dao?.insertTaskList(TaskListEntity(name = "Personal"))
|
dao?.insertTaskList(TaskListEntity(name = "Personal", order = 1))
|
||||||
dao?.insertTaskList(TaskListEntity(name = "Shopping"))
|
dao?.insertTaskList(TaskListEntity(name = "Shopping", order = 3))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@@ -6,11 +6,12 @@ import androidx.room.OnConflictStrategy
|
|||||||
import androidx.room.Query
|
import androidx.room.Query
|
||||||
import androidx.room.Update
|
import androidx.room.Update
|
||||||
import com.wismna.geoffroy.donext.data.entities.TaskEntity
|
import com.wismna.geoffroy.donext.data.entities.TaskEntity
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
interface TaskDao {
|
interface TaskDao {
|
||||||
@Query("SELECT * FROM tasks WHERE task_list_id = :listId")
|
@Query("SELECT * FROM tasks WHERE task_list_id = :listId ORDER BY display_order ASC")
|
||||||
suspend fun getTasksForList(listId: Long): List<TaskEntity>
|
fun getTasksForList(listId: Long): Flow<List<TaskEntity>>
|
||||||
|
|
||||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
suspend fun insertTask(task: TaskEntity)
|
suspend fun insertTask(task: TaskEntity)
|
||||||
@@ -18,14 +19,14 @@ interface TaskDao {
|
|||||||
@Update
|
@Update
|
||||||
suspend fun updateTask(task: TaskEntity)
|
suspend fun updateTask(task: TaskEntity)
|
||||||
|
|
||||||
@Query("UPDATE tasks SET done = :done, update_date = :updateDate WHERE id = :taskId")
|
@Query("UPDATE tasks SET done = :done WHERE id = :taskId")
|
||||||
suspend fun markTaskDone(taskId: Long, done: Boolean, updateDate: Long = System.currentTimeMillis())
|
suspend fun markTaskDone(taskId: Long, done: Boolean)
|
||||||
|
|
||||||
@Query("UPDATE tasks SET deleted = :deleted, update_date = :updateDate WHERE id = :taskId")
|
@Query("UPDATE tasks SET deleted = :deleted WHERE id = :taskId")
|
||||||
suspend fun markTaskDeleted(taskId: Long, deleted: Boolean, updateDate: Long = System.currentTimeMillis())
|
suspend fun markTaskDeleted(taskId: Long, deleted: Boolean)
|
||||||
|
|
||||||
@Query("UPDATE tasks SET cycles = cycles + 1, update_date = :updateDate WHERE id = :taskId")
|
@Query("UPDATE tasks SET cycle = cycle + 1 WHERE id = :taskId")
|
||||||
suspend fun increaseCycle(taskId: Long, updateDate: Long = System.currentTimeMillis())
|
suspend fun increaseCycle(taskId: Long)
|
||||||
|
|
||||||
@Query("UPDATE tasks SET deleted = :deleted WHERE id = :taskListId")
|
@Query("UPDATE tasks SET deleted = :deleted WHERE id = :taskListId")
|
||||||
suspend fun deleteAllTasksFromList(taskListId: Long, deleted: Boolean)
|
suspend fun deleteAllTasksFromList(taskListId: Long, deleted: Boolean)
|
||||||
|
@@ -10,7 +10,7 @@ import kotlinx.coroutines.flow.Flow
|
|||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
interface TaskListDao {
|
interface TaskListDao {
|
||||||
@Query("SELECT * FROM task_lists WHERE isDeleted = 0")
|
@Query("SELECT * FROM task_lists WHERE deleted = 0 ORDER BY display_order ASC")
|
||||||
fun getTaskLists(): Flow<List<TaskListEntity>>
|
fun getTaskLists(): Flow<List<TaskListEntity>>
|
||||||
|
|
||||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
@@ -19,6 +19,6 @@ interface TaskListDao {
|
|||||||
@Update
|
@Update
|
||||||
suspend fun updateTaskList(taskList: TaskListEntity)
|
suspend fun updateTaskList(taskList: TaskListEntity)
|
||||||
|
|
||||||
@Query("UPDATE task_lists SET isDeleted = :isDeleted WHERE id = :listId")
|
@Query("UPDATE task_lists SET deleted = :isDeleted WHERE id = :listId")
|
||||||
suspend fun deleteTaskList(listId: Long, isDeleted: Boolean)
|
suspend fun deleteTaskList(listId: Long, isDeleted: Boolean)
|
||||||
}
|
}
|
@@ -9,15 +9,14 @@ import com.wismna.geoffroy.donext.domain.model.TaskList
|
|||||||
import com.wismna.geoffroy.donext.domain.repository.TaskRepository
|
import com.wismna.geoffroy.donext.domain.repository.TaskRepository
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import java.time.Instant
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class TaskRepositoryImpl @Inject constructor(
|
class TaskRepositoryImpl @Inject constructor(
|
||||||
private val taskDao: TaskDao,
|
private val taskDao: TaskDao,
|
||||||
private val taskListDao: TaskListDao
|
private val taskListDao: TaskListDao
|
||||||
): TaskRepository {
|
): TaskRepository {
|
||||||
override suspend fun getTasksForList(listId: Long): List<Task> {
|
override fun getTasksForList(listId: Long): Flow<List<Task>> {
|
||||||
return taskDao.getTasksForList(listId).map { it.toDomain() }
|
return taskDao.getTasksForList(listId).map {entity -> entity.map { it.toDomain() }}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun insertTask(task: Task) {
|
override suspend fun insertTask(task: Task) {
|
||||||
@@ -25,8 +24,7 @@ class TaskRepositoryImpl @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun updateTask(task: Task) {
|
override suspend fun updateTask(task: Task) {
|
||||||
val updated = task.copy(updateDate = Instant.now())
|
taskDao.updateTask(task.toEntity())
|
||||||
taskDao.updateTask(updated.toEntity())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun deleteTask(taskId: Long, isDeleted: Boolean) {
|
override suspend fun deleteTask(taskId: Long, isDeleted: Boolean) {
|
||||||
|
@@ -3,12 +3,14 @@ package com.wismna.geoffroy.donext.domain.model
|
|||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
|
|
||||||
data class Task(
|
data class Task(
|
||||||
val id: Long,
|
val id: Long? = null,
|
||||||
val name: String,
|
val name: String,
|
||||||
val description: String,
|
val description: String?,
|
||||||
val cycles: Int,
|
val cycle: Int,
|
||||||
|
val priority: Int,
|
||||||
|
val order: Int,
|
||||||
val isDone: Boolean,
|
val isDone: Boolean,
|
||||||
val isDeleted: Boolean,
|
val isDeleted: Boolean,
|
||||||
val taskListId: Long,
|
val taskListId: Long,
|
||||||
val updateDate: Instant = Instant.now()
|
val dueDate: Instant? = null
|
||||||
)
|
)
|
||||||
|
@@ -3,5 +3,6 @@ package com.wismna.geoffroy.donext.domain.model
|
|||||||
data class TaskList(
|
data class TaskList(
|
||||||
val id: Long,
|
val id: Long,
|
||||||
val name: String,
|
val name: String,
|
||||||
val isDeleted: Boolean
|
val isDeleted: Boolean,
|
||||||
|
val order: Int
|
||||||
)
|
)
|
||||||
|
@@ -5,7 +5,7 @@ import com.wismna.geoffroy.donext.domain.model.TaskList
|
|||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
interface TaskRepository {
|
interface TaskRepository {
|
||||||
suspend fun getTasksForList(listId: Long): List<Task>
|
fun getTasksForList(listId: Long): Flow<List<Task>>
|
||||||
suspend fun insertTask(task: Task)
|
suspend fun insertTask(task: Task)
|
||||||
suspend fun updateTask(task: Task)
|
suspend fun updateTask(task: Task)
|
||||||
suspend fun deleteTask(taskId: Long, isDeleted: Boolean)
|
suspend fun deleteTask(taskId: Long, isDeleted: Boolean)
|
||||||
|
@@ -0,0 +1,24 @@
|
|||||||
|
package com.wismna.geoffroy.donext.domain.usecase
|
||||||
|
|
||||||
|
import com.wismna.geoffroy.donext.domain.model.Task
|
||||||
|
import com.wismna.geoffroy.donext.domain.repository.TaskRepository
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class AddTaskUseCase @Inject constructor(
|
||||||
|
private val repository: TaskRepository
|
||||||
|
) {
|
||||||
|
suspend operator fun invoke(taskListId: Long, title: String, description: String?) {
|
||||||
|
repository.insertTask(
|
||||||
|
Task(
|
||||||
|
taskListId = taskListId,
|
||||||
|
name = title,
|
||||||
|
description = description ?: "",
|
||||||
|
isDeleted = false,
|
||||||
|
cycle = 0,
|
||||||
|
isDone = false,
|
||||||
|
priority = 0,
|
||||||
|
order = 0
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,10 @@
|
|||||||
|
package com.wismna.geoffroy.donext.domain.usecase
|
||||||
|
|
||||||
|
import com.wismna.geoffroy.donext.domain.model.Task
|
||||||
|
import com.wismna.geoffroy.donext.domain.repository.TaskRepository
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class GetTasksForListUseCase @Inject constructor(private val repository: TaskRepository) {
|
||||||
|
operator fun invoke(taskListId: Long): Flow<List<Task>> = repository.getTasksForList(taskListId)
|
||||||
|
}
|
@@ -4,30 +4,12 @@ import android.os.Bundle
|
|||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.activity.enableEdgeToEdge
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
|
||||||
import androidx.compose.material3.PrimaryTabRow
|
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Tab
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableIntStateOf
|
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import androidx.navigation.NavHostController
|
import com.wismna.geoffroy.donext.presentation.screen.MainScreen
|
||||||
import androidx.navigation.compose.NavHost
|
|
||||||
import androidx.navigation.compose.composable
|
|
||||||
import androidx.navigation.compose.rememberNavController
|
|
||||||
import com.wismna.geoffroy.donext.domain.model.TaskList
|
|
||||||
import com.wismna.geoffroy.donext.presentation.screen.TaskListScreen
|
|
||||||
import com.wismna.geoffroy.donext.presentation.ui.theme.DoNextTheme
|
import com.wismna.geoffroy.donext.presentation.ui.theme.DoNextTheme
|
||||||
import com.wismna.geoffroy.donext.presentation.viewmodel.MainViewModel
|
import com.wismna.geoffroy.donext.presentation.viewmodel.MainViewModel
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
@@ -49,66 +31,4 @@ class MainActivity : ComponentActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
|
||||||
fun MainScreen(viewModel: MainViewModel, modifier: Modifier = Modifier) {
|
|
||||||
val navController = rememberNavController()
|
|
||||||
|
|
||||||
if (viewModel.isLoading) {
|
|
||||||
// Show loading or empty state
|
|
||||||
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
|
||||||
CircularProgressIndicator()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
val startDestination = viewModel.taskLists[0]
|
|
||||||
// TODO: get last opened tab from saved settings
|
|
||||||
var selectedDestination by rememberSaveable { mutableIntStateOf(0) }
|
|
||||||
|
|
||||||
Scaffold(modifier = modifier) { contentPadding ->
|
|
||||||
PrimaryTabRow(
|
|
||||||
selectedTabIndex = selectedDestination,
|
|
||||||
modifier = Modifier.padding(contentPadding)
|
|
||||||
) {
|
|
||||||
viewModel.taskLists.forEachIndexed { index, destination ->
|
|
||||||
Tab(
|
|
||||||
selected = selectedDestination == index,
|
|
||||||
onClick = {
|
|
||||||
navController.navigate(route = destination.name)
|
|
||||||
selectedDestination = index
|
|
||||||
},
|
|
||||||
text = {
|
|
||||||
Text(
|
|
||||||
text = destination.name,
|
|
||||||
maxLines = 2,
|
|
||||||
overflow = TextOverflow.Ellipsis
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
AppNavHost(navController, startDestination, viewModel)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun AppNavHost(
|
|
||||||
navController: NavHostController,
|
|
||||||
startDestination: TaskList,
|
|
||||||
viewModel: MainViewModel,
|
|
||||||
modifier: Modifier = Modifier
|
|
||||||
) {
|
|
||||||
NavHost(
|
|
||||||
navController,
|
|
||||||
startDestination = startDestination.name
|
|
||||||
) {
|
|
||||||
viewModel.taskLists.forEach { destination ->
|
|
||||||
composable(destination.name) {
|
|
||||||
TaskListScreen(destination, modifier)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@@ -0,0 +1,165 @@
|
|||||||
|
package com.wismna.geoffroy.donext.presentation.screen
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Add
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.ExtendedFloatingActionButton
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.ModalBottomSheet
|
||||||
|
import androidx.compose.material3.OutlinedTextField
|
||||||
|
import androidx.compose.material3.PrimaryTabRow
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Tab
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
|
import androidx.navigation.NavHostController
|
||||||
|
import androidx.navigation.NavType
|
||||||
|
import androidx.navigation.compose.NavHost
|
||||||
|
import androidx.navigation.compose.composable
|
||||||
|
import androidx.navigation.compose.rememberNavController
|
||||||
|
import androidx.navigation.navArgument
|
||||||
|
import com.wismna.geoffroy.donext.domain.model.TaskList
|
||||||
|
import com.wismna.geoffroy.donext.presentation.viewmodel.MainViewModel
|
||||||
|
import com.wismna.geoffroy.donext.presentation.viewmodel.TaskListViewModel
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun MainScreen(
|
||||||
|
viewModel: MainViewModel,
|
||||||
|
modifier: Modifier = Modifier) {
|
||||||
|
val navController = rememberNavController()
|
||||||
|
var showBottomSheet by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
if (viewModel.isLoading) {
|
||||||
|
// Show loading or empty state
|
||||||
|
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||||
|
CircularProgressIndicator()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val startDestination = viewModel.taskLists[0]
|
||||||
|
// TODO: get last opened tab from saved settings
|
||||||
|
var selectedDestination by rememberSaveable { mutableIntStateOf(0) }
|
||||||
|
|
||||||
|
if (showBottomSheet) {
|
||||||
|
ModalBottomSheet(onDismissRequest = { showBottomSheet = false }) {
|
||||||
|
Column(Modifier.padding(16.dp)) {
|
||||||
|
Text("New Task", style = MaterialTheme.typography.titleLarge)
|
||||||
|
Spacer(Modifier.height(8.dp))
|
||||||
|
OutlinedTextField(
|
||||||
|
value = viewModel.title,
|
||||||
|
singleLine = true,
|
||||||
|
onValueChange = { viewModel.onTitleChanged(it) },
|
||||||
|
label = { Text("Title") },
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
isError = !viewModel.isTitleValid && viewModel.title.isNotEmpty(),
|
||||||
|
)
|
||||||
|
Spacer(Modifier.height(8.dp))
|
||||||
|
OutlinedTextField(
|
||||||
|
value = viewModel.description,
|
||||||
|
onValueChange = { viewModel.onDescriptionChanged(it) },
|
||||||
|
label = { Text("Description") },
|
||||||
|
maxLines = 3,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
Spacer(Modifier.height(16.dp))
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
val currentListId = viewModel.taskLists[selectedDestination].id
|
||||||
|
viewModel.createTask(currentListId)
|
||||||
|
showBottomSheet = false
|
||||||
|
},
|
||||||
|
modifier = Modifier.align(Alignment.End)
|
||||||
|
) {
|
||||||
|
Text("Add")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
modifier = modifier,
|
||||||
|
floatingActionButton = {
|
||||||
|
AddNewTaskButton {
|
||||||
|
showBottomSheet = true
|
||||||
|
}
|
||||||
|
}, topBar = {
|
||||||
|
PrimaryTabRow(selectedTabIndex = selectedDestination) {
|
||||||
|
viewModel.taskLists.forEachIndexed { index, destination ->
|
||||||
|
Tab(
|
||||||
|
selected = selectedDestination == index,
|
||||||
|
onClick = {
|
||||||
|
navController.navigate(route = "taskList/${destination.id}")
|
||||||
|
selectedDestination = index
|
||||||
|
},
|
||||||
|
text = {
|
||||||
|
Text(
|
||||||
|
text = destination.name,
|
||||||
|
maxLines = 2,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}) { contentPadding ->
|
||||||
|
// NavHost will now automatically be below the tabs
|
||||||
|
Box(modifier = Modifier
|
||||||
|
.padding(contentPadding)
|
||||||
|
.fillMaxSize()
|
||||||
|
) {
|
||||||
|
AppNavHost(navController, startDestination, viewModel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AppNavHost(
|
||||||
|
navController: NavHostController,
|
||||||
|
startDestination: TaskList,
|
||||||
|
viewModel: MainViewModel
|
||||||
|
) {
|
||||||
|
NavHost(
|
||||||
|
navController,
|
||||||
|
startDestination = "taskList/${startDestination.id}"
|
||||||
|
) {
|
||||||
|
viewModel.taskLists.forEach { destination ->
|
||||||
|
composable(
|
||||||
|
route = "taskList/{taskListId}",
|
||||||
|
arguments = listOf(navArgument("taskListId") { type = NavType.LongType })) {
|
||||||
|
val viewModel: TaskListViewModel = hiltViewModel<TaskListViewModel>()
|
||||||
|
TaskListScreen(viewModel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AddNewTaskButton(onClick: () -> Unit) {
|
||||||
|
ExtendedFloatingActionButton(
|
||||||
|
onClick = { onClick() },
|
||||||
|
icon = { Icon(Icons.Filled.Add, "Create a task.") },
|
||||||
|
text = { Text(text = "Create a task") },
|
||||||
|
)
|
||||||
|
}
|
@@ -1,19 +1,43 @@
|
|||||||
package com.wismna.geoffroy.donext.presentation.screen
|
package com.wismna.geoffroy.donext.presentation.screen
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import com.wismna.geoffroy.donext.domain.model.TaskList
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.text.style.TextDecoration
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
|
import com.wismna.geoffroy.donext.presentation.viewmodel.TaskListViewModel
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TaskListScreen(taskList: TaskList, modifier: Modifier = Modifier) {
|
fun TaskListScreen(viewModel: TaskListViewModel = hiltViewModel()) {
|
||||||
Box(
|
val tasks = viewModel.tasks
|
||||||
modifier = Modifier.fillMaxSize(),
|
|
||||||
contentAlignment = Alignment.Center
|
LazyColumn(
|
||||||
) {
|
verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||||
Text("${taskList.name} Screen")
|
items(tasks) { task ->
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(8.dp)
|
||||||
|
) {
|
||||||
|
Column {
|
||||||
|
}
|
||||||
|
Column {
|
||||||
|
val textStyle = TextStyle(textDecoration = if (task.isDeleted) TextDecoration.LineThrough else TextDecoration.None)
|
||||||
|
Text(task.name, fontSize = 18.sp, color = Color.DarkGray, style = textStyle)
|
||||||
|
Text(task.description.orEmpty(), fontSize = 14.sp, color = Color.LightGray, style = textStyle, maxLines = 3)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -6,23 +6,34 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.wismna.geoffroy.donext.domain.model.TaskList
|
import com.wismna.geoffroy.donext.domain.model.TaskList
|
||||||
|
import com.wismna.geoffroy.donext.domain.usecase.AddTaskUseCase
|
||||||
import com.wismna.geoffroy.donext.domain.usecase.GetTaskListsUseCase
|
import com.wismna.geoffroy.donext.domain.usecase.GetTaskListsUseCase
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class MainViewModel @Inject constructor(
|
class MainViewModel @Inject constructor(
|
||||||
getTaskLists: GetTaskListsUseCase
|
getTaskLists: GetTaskListsUseCase,
|
||||||
|
private val addTask: AddTaskUseCase
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
var taskLists by mutableStateOf<List<TaskList>>(emptyList())
|
var taskLists by mutableStateOf<List<TaskList>>(emptyList())
|
||||||
private set
|
private set
|
||||||
|
|
||||||
var isLoading by mutableStateOf(true)
|
var isLoading by mutableStateOf(true)
|
||||||
private set
|
private set
|
||||||
|
|
||||||
|
// --- Form state ---
|
||||||
|
var title by mutableStateOf("")
|
||||||
|
private set
|
||||||
|
var description by mutableStateOf("")
|
||||||
|
private set
|
||||||
|
|
||||||
|
val isTitleValid: Boolean
|
||||||
|
get() = title.isNotBlank()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
getTaskLists()
|
getTaskLists()
|
||||||
.onEach { lists ->
|
.onEach { lists ->
|
||||||
@@ -31,4 +42,19 @@ class MainViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
.launchIn(viewModelScope)
|
.launchIn(viewModelScope)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun createTask(taskListId: Long) {
|
||||||
|
if (!isTitleValid) return
|
||||||
|
viewModelScope.launch {
|
||||||
|
addTask(taskListId, title, description)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onTitleChanged(newTitle: String) {
|
||||||
|
title = newTitle
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onDescriptionChanged(newDesc: String) {
|
||||||
|
description = newDesc
|
||||||
|
}
|
||||||
}
|
}
|
@@ -0,0 +1,38 @@
|
|||||||
|
package com.wismna.geoffroy.donext.presentation.viewmodel
|
||||||
|
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.lifecycle.SavedStateHandle
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.wismna.geoffroy.donext.domain.model.Task
|
||||||
|
import com.wismna.geoffroy.donext.domain.usecase.GetTasksForListUseCase
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@HiltViewModel
|
||||||
|
class TaskListViewModel @Inject constructor(
|
||||||
|
getTasks: GetTasksForListUseCase,
|
||||||
|
savedStateHandle: SavedStateHandle
|
||||||
|
) : ViewModel() {
|
||||||
|
|
||||||
|
var tasks by mutableStateOf<List<Task>>(emptyList())
|
||||||
|
private set
|
||||||
|
|
||||||
|
var isLoading by mutableStateOf(true)
|
||||||
|
private set
|
||||||
|
|
||||||
|
private val taskListId: Long = checkNotNull(savedStateHandle["taskListId"])
|
||||||
|
|
||||||
|
init {
|
||||||
|
getTasks(taskListId)
|
||||||
|
.onEach { list ->
|
||||||
|
tasks = list
|
||||||
|
isLoading = false
|
||||||
|
}
|
||||||
|
.launchIn(viewModelScope)
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user