Xaver
1 week ago
17 changed files with 561 additions and 49 deletions
@ -0,0 +1,24 @@
@@ -0,0 +1,24 @@
|
||||
package at.xaxa.demonstrator2 |
||||
|
||||
import android.app.Application |
||||
import at.xaxa.demonstrator2.data.TaskRepository |
||||
import at.xaxa.demonstrator2.data.db.TaskDatabase |
||||
import kotlinx.serialization.json.Json |
||||
import okhttp3.MediaType |
||||
import retrofit2.Retrofit |
||||
import retrofit2.converter.kotlinx.serialization.asConverterFactory |
||||
|
||||
|
||||
class TaskApplication : Application() { |
||||
|
||||
val taskRepository by lazy { |
||||
val retrofit = Retrofit.Builder() |
||||
.baseUrl("https://my-json-server.typicode.com/GithubGenericUsername/find-your-pet/") |
||||
.addConverterFactory(Json { ignoreUnknownKeys = true }.asConverterFactory(MediaType.get("application/json"))) |
||||
.build() |
||||
|
||||
TaskRepository( |
||||
TaskDatabase.getDatabase(this).TaskDao() |
||||
) |
||||
} |
||||
} |
@ -0,0 +1,7 @@
@@ -0,0 +1,7 @@
|
||||
package at.xaxa.demonstrator2.data |
||||
|
||||
data class Task( |
||||
val id: Int, |
||||
val name :String, |
||||
val details: String |
||||
); |
@ -0,0 +1,42 @@
@@ -0,0 +1,42 @@
|
||||
package at.xaxa.demonstrator2.data |
||||
|
||||
import android.util.Log |
||||
import at.xaxa.demonstrator2.data.db.TaskDao |
||||
import at.xaxa.demonstrator2.data.db.TaskEntity |
||||
import kotlinx.coroutines.flow.Flow |
||||
import kotlinx.coroutines.flow.map |
||||
|
||||
class TaskRepository(private val taskDao: TaskDao) { |
||||
|
||||
fun getAllTasks(): Flow<List<Task>> { |
||||
return taskDao.getAllTasks().map { |
||||
it.map {item -> Task(item._id, item.name, item.details) } |
||||
} |
||||
} |
||||
|
||||
suspend fun getTaskById(id: Int): Task { |
||||
val item = taskDao.getTaskById(id) |
||||
return Task(item._id, item.name, item.details) |
||||
} |
||||
|
||||
suspend fun addTask(task: Task) { |
||||
taskDao.addTask(TaskEntity(_id=0, task.name, task.details)) |
||||
} |
||||
|
||||
suspend fun updateTask(task: Task) { |
||||
taskDao.updateTask(TaskEntity(task.id, task.name, task.details)) |
||||
} |
||||
|
||||
suspend fun addRandomTask() { |
||||
val randomTask:String = tasks.random() |
||||
addTask(Task(0, randomTask, "Wow I have to ${randomTask.lowercase()}")) |
||||
} |
||||
|
||||
val tasks = listOf( |
||||
"Buy Milk", |
||||
"Mow lawn", |
||||
"Chill", |
||||
"Drink Water" |
||||
) |
||||
|
||||
} |
@ -0,0 +1,26 @@
@@ -0,0 +1,26 @@
|
||||
package at.xaxa.demonstrator2.data.db |
||||
|
||||
import androidx.room.Dao |
||||
import androidx.room.Delete |
||||
import androidx.room.Insert |
||||
import androidx.room.Query |
||||
import androidx.room.Update |
||||
import kotlinx.coroutines.flow.Flow |
||||
|
||||
@Dao |
||||
interface TaskDao { |
||||
@Insert |
||||
suspend fun addTask(taskEntity: TaskEntity) |
||||
|
||||
@Update |
||||
suspend fun updateTask(taskEntity: TaskEntity) |
||||
|
||||
@Delete |
||||
suspend fun deleteTask(taskEntity: TaskEntity) |
||||
|
||||
@Query("SELECT * from tasks") |
||||
fun getAllTasks(): Flow<List<TaskEntity>> |
||||
|
||||
@Query("SELECT * FROM tasks WHERE _id = :id") |
||||
suspend fun getTaskById(id: Int): TaskEntity |
||||
} |
@ -0,0 +1,33 @@
@@ -0,0 +1,33 @@
|
||||
package at.xaxa.demonstrator2.data.db |
||||
|
||||
import android.content.Context |
||||
import androidx.room.Database |
||||
import androidx.room.Room |
||||
import androidx.room.RoomDatabase |
||||
|
||||
@Database(entities = [TaskEntity::class], version = 1) |
||||
abstract class TaskDatabase : RoomDatabase() { |
||||
abstract fun TaskDao(): TaskDao |
||||
|
||||
companion object { |
||||
@Volatile |
||||
private var Instance: TaskDatabase? = null |
||||
|
||||
fun getDatabase(context: Context): TaskDatabase { |
||||
// if the Instance is not null, return it, otherwise create a new database instance. |
||||
return Instance ?: synchronized(this) { |
||||
val instance = Room.databaseBuilder(context, TaskDatabase::class.java, "task_database") |
||||
/** |
||||
* Setting this option in your app's database builder means that Room |
||||
* permanently deletes all data from the tables in your database when it |
||||
* attempts to perform a migration with no defined migration path. |
||||
*/ |
||||
.fallbackToDestructiveMigration() |
||||
.build() |
||||
Instance = instance |
||||
return instance |
||||
} |
||||
} |
||||
} |
||||
|
||||
} |
@ -0,0 +1,10 @@
@@ -0,0 +1,10 @@
|
||||
package at.xaxa.demonstrator2.data.db |
||||
|
||||
import androidx.room.Entity |
||||
|
||||
@Entity(tableName = "tasks") |
||||
data class TaskEntity( |
||||
val _id: Int = 0, |
||||
val name :String, |
||||
val details: String = "empty" |
||||
) |
@ -0,0 +1,23 @@
@@ -0,0 +1,23 @@
|
||||
package at.xaxa.demonstrator2.ui |
||||
|
||||
import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY |
||||
import androidx.lifecycle.createSavedStateHandle |
||||
import androidx.lifecycle.viewmodel.initializer |
||||
import androidx.lifecycle.viewmodel.viewModelFactory |
||||
import at.xaxa.demonstrator2.TaskApplication |
||||
|
||||
object AppViewModelProvider { |
||||
val Factory = viewModelFactory { |
||||
initializer { |
||||
DemoViewModel((this[APPLICATION_KEY] as TaskApplication).taskRepository) |
||||
} |
||||
|
||||
initializer { |
||||
DemoDetailsViewModel(this.createSavedStateHandle(), (this[APPLICATION_KEY] as TaskApplication).taskRepository) |
||||
} |
||||
|
||||
initializer { |
||||
DemoDetailsViewModel(this.createSavedStateHandle(), (this[APPLICATION_KEY] as TaskApplication).taskRepository) |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,37 @@
@@ -0,0 +1,37 @@
|
||||
package at.xaxa.demonstrator2.ui |
||||
|
||||
import androidx.lifecycle.SavedStateHandle |
||||
import androidx.lifecycle.ViewModel |
||||
import androidx.lifecycle.viewModelScope |
||||
import at.xaxa.demonstrator2.data.Task |
||||
import at.xaxa.demonstrator2.data.TaskRepository |
||||
import kotlinx.coroutines.Dispatchers |
||||
import kotlinx.coroutines.flow.MutableStateFlow |
||||
import kotlinx.coroutines.flow.asStateFlow |
||||
import kotlinx.coroutines.flow.update |
||||
import kotlinx.coroutines.launch |
||||
import kotlinx.coroutines.withContext |
||||
|
||||
data class TaskDetailUi( |
||||
val task: Task = Task(0, "", "") |
||||
) |
||||
|
||||
class DemoDetailsViewModel(savedStateHandle: SavedStateHandle, private val taskRepository: TaskRepository): ViewModel() { |
||||
|
||||
private val taskId: Int = checkNotNull(savedStateHandle["taskId"]) |
||||
|
||||
private val _detailUiState = MutableStateFlow(TaskDetailUi()) |
||||
val detailUiState = _detailUiState.asStateFlow() |
||||
|
||||
init { |
||||
viewModelScope.launch { |
||||
val task = withContext(Dispatchers.IO) { |
||||
taskRepository.getTaskById(taskId) |
||||
} |
||||
_detailUiState.update { |
||||
TaskDetailUi(task) |
||||
} |
||||
} |
||||
} |
||||
|
||||
} |
@ -0,0 +1,177 @@
@@ -0,0 +1,177 @@
|
||||
package at.xaxa.demonstrator2.ui |
||||
|
||||
import androidx.compose.foundation.layout.Box |
||||
import androidx.compose.foundation.layout.Column |
||||
import androidx.compose.foundation.layout.Row |
||||
import androidx.compose.foundation.layout.Spacer |
||||
import androidx.compose.foundation.layout.fillMaxSize |
||||
import androidx.compose.foundation.layout.fillMaxWidth |
||||
import androidx.compose.foundation.layout.padding |
||||
import androidx.compose.foundation.layout.width |
||||
import androidx.compose.foundation.lazy.LazyColumn |
||||
import androidx.compose.foundation.lazy.itemsIndexed |
||||
import androidx.compose.material.icons.Icons |
||||
import androidx.compose.material.icons.automirrored.filled.List |
||||
import androidx.compose.material.icons.filled.Add |
||||
import androidx.compose.material.icons.outlined.Edit |
||||
import androidx.compose.material3.Icon |
||||
import androidx.compose.material3.IconButton |
||||
import androidx.compose.material3.NavigationBarItem |
||||
import androidx.compose.material3.OutlinedCard |
||||
import androidx.compose.material3.Scaffold |
||||
import androidx.compose.material3.Text |
||||
import androidx.compose.runtime.Composable |
||||
import androidx.compose.runtime.getValue |
||||
import androidx.compose.ui.Alignment |
||||
import androidx.compose.ui.Modifier |
||||
import androidx.compose.ui.tooling.preview.Preview |
||||
import androidx.compose.ui.unit.dp |
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle |
||||
import androidx.lifecycle.viewmodel.compose.viewModel |
||||
import androidx.navigation.NavType |
||||
import androidx.navigation.compose.NavHost |
||||
import androidx.navigation.compose.composable |
||||
import androidx.navigation.compose.currentBackStackEntryAsState |
||||
import androidx.navigation.compose.rememberNavController |
||||
import androidx.navigation.navArgument |
||||
import at.xaxa.demonstrator2.data.Task |
||||
import at.xaxa.demonstrator2.ui.edit.TaskEditScreen |
||||
import at.xaxa.demonstrator2.ui.theme.Typography |
||||
|
||||
enum class DemoRoutes(val route: String) { |
||||
List("list"), |
||||
Add("add"), |
||||
Detail("task/{taskId}"), |
||||
Edit("task/{taskId}/edit") |
||||
} |
||||
|
||||
@Composable |
||||
fun DemoApp(modifier: Modifier = Modifier) { |
||||
val navController = rememberNavController() |
||||
|
||||
Scaffold( |
||||
bottomBar = { |
||||
androidx.compose.material3.NavigationBar { |
||||
NavigationBarItem( |
||||
icon = { Icon(Icons.AutoMirrored.Filled.List, contentDescription = "List") }, |
||||
label = { Text("List") }, |
||||
selected = navController.currentBackStackEntryAsState().value?.destination?.route == "list", |
||||
onClick = { navController.navigate(DemoRoutes.List.route) } |
||||
) |
||||
NavigationBarItem( |
||||
icon = { Icon(Icons.Default.Add, contentDescription = "Add") }, |
||||
label = { Text("Add") }, |
||||
selected = navController.currentBackStackEntryAsState().value?.destination?.route == "add", |
||||
onClick = { navController.navigate(DemoRoutes.Add.route) } |
||||
) |
||||
} |
||||
} |
||||
) { innerPadding -> |
||||
NavHost( |
||||
navController = navController, |
||||
startDestination = DemoRoutes.List.route, |
||||
modifier = Modifier.padding(innerPadding) |
||||
) { |
||||
composable(DemoRoutes.List.route){ |
||||
ListScreen( onEditClick = { |
||||
navController.navigate(DemoRoutes.Edit.route.replace("{taskId}", "$it")) |
||||
}){ |
||||
navController.navigate(DemoRoutes.Detail.route.replace("{taskId}", "$it")) |
||||
} |
||||
} |
||||
composable(DemoRoutes.Add.route) { AddScreen() } |
||||
composable( |
||||
route = DemoRoutes.Detail.route, |
||||
arguments = listOf(navArgument("taskId") { |
||||
type = NavType.IntType |
||||
}) |
||||
) { |
||||
TaskDetailsScreen() |
||||
} |
||||
composable( |
||||
route = DemoRoutes.Edit.route, |
||||
arguments = listOf(navArgument("contactId") { |
||||
type = NavType.IntType |
||||
}) |
||||
) { |
||||
TaskEditScreen() { |
||||
navController.navigateUp() |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
@Composable |
||||
fun ListScreen( |
||||
modifier: Modifier = Modifier, |
||||
demoViewModel: DemoViewModel = viewModel(factory = AppViewModelProvider.Factory), |
||||
onEditClick: (Int) -> Unit, |
||||
onCardClick: (Int) -> Unit |
||||
) { |
||||
Box(Modifier.fillMaxSize()) { |
||||
val state by demoViewModel.taskUiState.collectAsStateWithLifecycle(); |
||||
|
||||
LazyColumn { |
||||
itemsIndexed(state.tasks) { index, task -> |
||||
TaskListItem(task, onCardClick = { |
||||
onCardClick(task.id) |
||||
}, onEditClick = { |
||||
onEditClick(task.id) |
||||
}) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
@Composable |
||||
fun TaskDetailsScreen(modifier: Modifier = Modifier, demoDetailsViewModel: DemoDetailsViewModel = viewModel(factory = AppViewModelProvider.Factory)) { |
||||
val detailUiState by demoDetailsViewModel.detailUiState.collectAsStateWithLifecycle() |
||||
|
||||
TaskDetails(detailUiState.task, modifier) |
||||
} |
||||
|
||||
@Composable |
||||
fun TaskDetails(task: Task, modifier: Modifier = Modifier) { |
||||
OutlinedCard( |
||||
modifier |
||||
.fillMaxWidth() |
||||
.padding(8.dp) |
||||
) { |
||||
Column(Modifier.padding(16.dp)) { |
||||
Text(task.name, style = Typography.headlineMedium) |
||||
Row { |
||||
Text("Details: ${task.details}", style = Typography.headlineMedium) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
@Composable |
||||
fun AddScreen() { |
||||
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { |
||||
Text("Add Screen") |
||||
} |
||||
} |
||||
|
||||
@Composable |
||||
fun TaskListItem(task: Task, onCardClick: () -> Unit, onEditClick: ()->Unit, modifier: Modifier = Modifier) { |
||||
OutlinedCard( |
||||
onClick = { onCardClick() }, modifier = modifier |
||||
.fillMaxWidth() |
||||
.padding(8.dp) |
||||
) { |
||||
Row(Modifier.padding(16.dp), verticalAlignment = Alignment.CenterVertically) { |
||||
Text(task.name, style = Typography.headlineMedium) |
||||
IconButton(onEditClick) { |
||||
Icon(Icons.Outlined.Edit, "Edit contact") |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
@Preview |
||||
@Composable |
||||
fun PreviewTaskListItem(){ |
||||
TaskListItem(Task(0, "Buy Milk", "buy milk"), {}, {}) |
||||
} |
@ -0,0 +1,7 @@
@@ -0,0 +1,7 @@
|
||||
package at.xaxa.demonstrator2.ui |
||||
|
||||
import at.xaxa.demonstrator2.data.Task |
||||
|
||||
data class DemoUiState( |
||||
val tasks : List<Task> |
||||
) |
@ -0,0 +1,33 @@
@@ -0,0 +1,33 @@
|
||||
package at.xaxa.demonstrator2.ui |
||||
|
||||
import androidx.lifecycle.ViewModel |
||||
import androidx.lifecycle.viewModelScope |
||||
import at.xaxa.demonstrator2.data.TaskRepository |
||||
import kotlinx.coroutines.flow.SharingStarted |
||||
import kotlinx.coroutines.flow.map |
||||
import kotlinx.coroutines.flow.stateIn |
||||
import kotlinx.coroutines.launch |
||||
|
||||
class DemoViewModel(val repository: TaskRepository) : ViewModel() { |
||||
|
||||
init { |
||||
viewModelScope.launch { |
||||
repository.getAllTasks() |
||||
} |
||||
} |
||||
|
||||
|
||||
val taskUiState = repository.getAllTasks() |
||||
.map { DemoUiState(it) } |
||||
.stateIn( |
||||
scope = viewModelScope, |
||||
started = SharingStarted.WhileSubscribed(5000), |
||||
initialValue = DemoUiState(emptyList()) |
||||
) |
||||
|
||||
fun onAddButtonClicked() { |
||||
viewModelScope.launch { |
||||
repository.addRandomTask() |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,78 @@
@@ -0,0 +1,78 @@
|
||||
package at.xaxa.demonstrator2.ui.edit |
||||
|
||||
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.material3.Button |
||||
import androidx.compose.material3.OutlinedCard |
||||
import androidx.compose.material3.OutlinedTextField |
||||
import androidx.compose.material3.Text |
||||
import androidx.compose.runtime.Composable |
||||
import androidx.compose.ui.Alignment |
||||
import androidx.compose.ui.Modifier |
||||
import androidx.compose.ui.tooling.preview.Preview |
||||
import androidx.compose.ui.unit.dp |
||||
import androidx.lifecycle.viewmodel.compose.viewModel |
||||
import at.xaxa.demonstrator2.data.Task |
||||
import at.xaxa.demonstrator2.ui.AppViewModelProvider |
||||
|
||||
|
||||
@Composable |
||||
fun TaskEditScreen( |
||||
modifier: Modifier = Modifier, |
||||
viewModel: TaskEditViewModel = viewModel(factory = AppViewModelProvider.Factory), |
||||
onSave: () -> Unit |
||||
) { |
||||
val task = viewModel.editUiState.task |
||||
|
||||
|
||||
TaskEditForm(task, modifier, onValueChange = { taskChanged -> |
||||
viewModel.updateTask(taskChanged) |
||||
}) { |
||||
viewModel.saveTask() |
||||
onSave() |
||||
} |
||||
} |
||||
|
||||
@Composable |
||||
fun TaskEditForm( |
||||
task: Task, |
||||
modifier: Modifier = Modifier, |
||||
onValueChange: (Task) -> Unit = {}, |
||||
onSaveButtonClicked: () -> Unit = {} |
||||
) { |
||||
OutlinedCard( |
||||
modifier = modifier |
||||
.fillMaxWidth() |
||||
) |
||||
{ |
||||
Column(Modifier.padding(16.dp).fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) { |
||||
Row { |
||||
OutlinedTextField( |
||||
value = task.name, |
||||
label = { Text("Name") }, |
||||
onValueChange = { newText -> |
||||
onValueChange(task.copy(name = newText)) |
||||
}) |
||||
} |
||||
Row { |
||||
OutlinedTextField( |
||||
value = task.details.toString(), |
||||
label = { Text("Details") }, |
||||
onValueChange = { newText -> |
||||
onValueChange(task.copy(details = newText)) |
||||
}) |
||||
} |
||||
Button(onClick = { onSaveButtonClicked() }) { |
||||
Text("Save changes") |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
@Preview |
||||
@Composable |
||||
private fun TaskEditPreview() { |
||||
TaskEditForm(Task(234, "Buy milk", "buy milk")) { } |
||||
} |
@ -0,0 +1,47 @@
@@ -0,0 +1,47 @@
|
||||
package at.xaxa.demonstrator2.ui.edit |
||||
|
||||
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 at.xaxa.demonstrator2.data.Task |
||||
import at.xaxa.demonstrator2.data.TaskRepository |
||||
import kotlinx.coroutines.Dispatchers |
||||
import kotlinx.coroutines.launch |
||||
import kotlinx.coroutines.withContext |
||||
|
||||
|
||||
data class TaskEditUi( |
||||
val task: Task = Task(0, "", "") |
||||
) |
||||
|
||||
class TaskEditViewModel(private val savedStateHandle: SavedStateHandle, |
||||
private val taskRepository: TaskRepository) : ViewModel() { |
||||
|
||||
private val taskId: Int = checkNotNull(savedStateHandle["taskId"]) |
||||
|
||||
var editUiState by mutableStateOf(TaskEditUi()) |
||||
private set |
||||
|
||||
init { |
||||
viewModelScope.launch { |
||||
val task = withContext(Dispatchers.IO) { |
||||
taskRepository.getTaskById(taskId) |
||||
} |
||||
editUiState = TaskEditUi(task) |
||||
} |
||||
} |
||||
|
||||
fun updateTask(task: Task) { |
||||
editUiState = editUiState.copy(task=task) |
||||
} |
||||
|
||||
fun saveTask() { |
||||
viewModelScope.launch { |
||||
taskRepository.updateTask(editUiState.task) |
||||
} |
||||
} |
||||
|
||||
} |
Loading…
Reference in new issue