■アプリの作成(Jetpack Compose / リストの登録編集削除)
#63 Jetpack ComposeでToDoアプリを作る - 概要 | Mokelab Blog
https://blog.mokelab.com/63/compose_todo1.html
リストの並び替えは、現状対応していないみたい
【Android】 Jetpack Composeでドラッグ&ドロップの並び替えを実現する 〜ライブラリに頼って〜 - Qiita
https://qiita.com/tsumuchan/items/d0fc2a4bd4af6802f9fc
Sorting List Items in LazyColumn - Android Jetpack Compose - Stack Overflow
https://stackoverflow.com/questions/73915584/sorting-list-items-in-lazycolumn-android-jetpack-compos...
以下はデータ追加用に「+」ボタンを表示する例
Scaffold(
topBar = {
TopAppBar(
title = { Text("リストのサンプル") }
)
},
floatingActionButton = {
FloatingActionButton(onClick = {
Log.d("FloatingActionButton", "Clicked!")
}) {
Icon(
imageVector = Icons.Filled.Add,
contentDescription = "追加"
)
}
}
) { innerPadding ->
上記を踏まえて、リストの登録編集削除を作成する
■リストの登録編集削除(調整中)
build.gradle
plugins {
id 'com.android.application' version '8.0.2' apply false
id 'com.android.library' version '8.0.2' apply false
id 'org.jetbrains.kotlin.android' version '1.7.20' apply false
id 'org.jetbrains.kotlin.plugin.serialization' version '1.6.20' apply false
}
app/build.gradle
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
id 'org.jetbrains.kotlin.plugin.serialization'
}
〜中略〜
dependencies {
implementation 'androidx.core:core-ktx:1.8.0'
implementation platform('org.jetbrains.kotlin:kotlin-bom:1.8.0')
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1'
implementation 'androidx.activity:activity-compose:1.5.1'
implementation platform('androidx.compose:compose-bom:2022.10.00')
implementation 'androidx.compose.ui:ui'
implementation 'androidx.compose.ui:ui-graphics'
implementation 'androidx.compose.ui:ui-tooling-preview'
implementation 'androidx.compose.material3:material3'
implementation "androidx.navigation:navigation-compose:2.5.3"
implementation "androidx.constraintlayout:constraintlayout-compose:1.0.1"
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.2"
implementation "androidx.datastore:datastore-preferences:1.0.0"
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
androidTestImplementation platform('androidx.compose:compose-bom:2022.10.00')
androidTestImplementation 'androidx.compose.ui:ui-test-junit4'
debugImplementation 'androidx.compose.ui:ui-tooling'
debugImplementation 'androidx.compose.ui:ui-test-manifest'
}
java/jp/terraport/list/MainActivity.kt
package jp.terraport.list
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import jp.terraport.list.ui.MainApp
import jp.terraport.list.ui.theme.ListTheme
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
ListTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
MainApp()
}
}
}
}
}
@Preview(showSystemUi = true)
@Composable
fun GreetingPreview() {
ListTheme {
MainApp()
}
}
java/jp/terraport/list/ui/MainApp.kt
package jp.terraport.list.ui
import android.net.Uri
import androidx.compose.runtime.Composable
import androidx.navigation.NavType
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import androidx.navigation.navArgument
import jp.terraport.list.ui.add.AddScreen
import jp.terraport.list.ui.edit.EditScreen
import jp.terraport.list.ui.list.ListScreen
@Composable
fun MainApp() {
val navController = rememberNavController()
NavHost(navController = navController, startDestination = "ListScreen") {
composable("ListScreen") {
ListScreen(
onNavigateToAddScreen = { navController.navigate("AddScreen") },
onNavigateToEditScreen = { id, title, body -> navController.navigate("EditScreen?id=" + Uri.encode(id) + "&title=" + Uri.encode(title) + "&body=" + Uri.encode(body)) }
)
}
composable("AddScreen") {
AddScreen(
onNavigateToFirstScreen = { navController.navigateUp() }
)
}
composable(
"EditScreen?id={id}&title={title}&body={body}",
arguments = listOf(
navArgument("id") { type = NavType.StringType },
navArgument("title") { type = NavType.StringType },
navArgument("body") { type = NavType.StringType }
)
) { backStackEntry ->
EditScreen(
backStackEntry.arguments?.getString("id") ?: "",
backStackEntry.arguments?.getString("title") ?: "",
backStackEntry.arguments?.getString("body") ?: "",
onNavigateToFirstScreen = { navController.navigateUp() }
)
}
}
}
java/jp/terraport/list/ui/list/ListScreen.kt
package jp.terraport.list.ui.list
import android.util.Log
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.constraintlayout.compose.ConstraintLayout
import jp.terraport.list.utils.Memo
import jp.terraport.list.utils.getMemo
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ListScreen(
onNavigateToAddScreen: () -> Unit,
onNavigateToEditScreen: (String, String, String) -> Unit
) {
val context = LocalContext.current
val memos = remember { mutableStateListOf<Memo>() }
LaunchedEffect(Unit) {
memos.addAll(getMemo(context))
}
Scaffold(
topBar = {
TopAppBar(
title = { Text("リストのサンプル") }
)
},
floatingActionButton = {
FloatingActionButton(onClick = {
onNavigateToAddScreen()
}) {
Icon(
imageVector = Icons.Filled.Add,
contentDescription = "追加"
)
}
}
) { innerPadding ->
ConstraintLayout(
modifier = Modifier.padding(innerPadding)
) {
LazyColumn {
items(memos.size) { index ->
val content = memos[index]
ListTitle(title = content.title, body = content.detail)
{
onNavigateToEditScreen(content.id, content.title, content.detail)
}
}
}
}
}
}
java/jp/terraport/list/ui/list/ListTitle.kt
package jp.terraport.list.ui.list
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
fun ListTitle(
title: String,
body: String,
onClick: () -> Unit
) {
Surface(
modifier = Modifier.clickable { onClick() },
shape = MaterialTheme.shapes.medium
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(all = 8.dp)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 8.dp, vertical = 16.dp),
) {
Text(title)
Spacer(modifier = Modifier.height(4.dp))
Text(body)
}
}
}
}
java/jp/terraport/list/ui/add/AddScreen.kt
package jp.terraport.list.ui.add
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
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.ArrowBack
import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.unit.dp
import androidx.constraintlayout.compose.ConstraintLayout
import jp.terraport.list.utils.Memo
import jp.terraport.list.utils.generateRandomString
import jp.terraport.list.utils.getMemo
import jp.terraport.list.utils.putMemo
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AddScreen(
onNavigateToFirstScreen: () -> Unit
) {
val context = LocalContext.current
var titleText by remember { mutableStateOf(TextFieldValue("")) }
var detailText by remember { mutableStateOf(TextFieldValue("")) }
Scaffold(
topBar = {
TopAppBar(
title = { Text("登録") },
navigationIcon = {
IconButton(onClick = onNavigateToFirstScreen) {
Icon(
imageVector = Icons.Default.ArrowBack,
contentDescription = "戻る"
)
}
}
)
}
) { innerPadding ->
ConstraintLayout(
modifier = Modifier.padding(innerPadding),
) {
Column {
OutlinedTextField(
value = titleText,
onValueChange = {
titleText = it
},
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(16.dp))
OutlinedTextField(
value = detailText,
onValueChange = {
detailText = it
},
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(16.dp))
Button(onClick = {
runBlocking(Dispatchers.IO) {
val memos: MutableList<Memo> = mutableListOf()
memos.add(Memo(generateRandomString(16), titleText.text, detailText.text))
memos.addAll(getMemo(context))
putMemo(context, memos)
}
onNavigateToFirstScreen()
}) {
Text("保存")
}
}
}
}
}
java/jp/terraport/list/ui/edit/EditScreen.kt
package jp.terraport.list.ui.edit
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
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.ArrowBack
import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.unit.dp
import androidx.constraintlayout.compose.ConstraintLayout
import jp.terraport.list.utils.Memo
import jp.terraport.list.utils.getMemo
import jp.terraport.list.utils.putMemo
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun EditScreen(
id: String,
title: String,
body: String,
onNavigateToFirstScreen: () -> Unit
) {
val context = LocalContext.current
var titleText by remember { mutableStateOf(TextFieldValue(title)) }
var detailText by remember { mutableStateOf(TextFieldValue(body)) }
Scaffold(
topBar = {
TopAppBar(
title = { Text("詳細") },
navigationIcon = {
IconButton(onClick = onNavigateToFirstScreen) {
Icon(
imageVector = Icons.Default.ArrowBack,
contentDescription = "戻る"
)
}
}
)
}
) { innerPadding ->
ConstraintLayout(
modifier = Modifier.padding(innerPadding),
) {
Column {
OutlinedTextField(
value = titleText,
onValueChange = {
titleText = it
},
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(16.dp))
OutlinedTextField(
value = detailText,
onValueChange = {
detailText = it
},
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(16.dp))
Button(onClick = {
runBlocking(Dispatchers.IO) {
val memos: MutableList<Memo> = mutableListOf()
getMemo(context).forEach { memo ->
if (id == memo.id) {
memos.add(Memo(id, titleText.text, detailText.text))
} else {
memos.add(Memo(memo.id, memo.title, memo.detail))
}
}
putMemo(context, memos)
}
onNavigateToFirstScreen()
}) {
Text("保存")
}
Spacer(modifier = Modifier.height(16.dp))
Button(onClick = {
runBlocking(Dispatchers.IO) {
val memos: MutableList<Memo> = mutableListOf()
getMemo(context).forEach { memo ->
if (id != memo.id) {
memos.add(Memo(memo.id, memo.title, memo.detail))
}
}
putMemo(context, memos)
}
onNavigateToFirstScreen()
}) {
Text("削除")
}
}
}
}
}
java/jp/terraport/list/utils/Memo.kt
package jp.terraport.list.utils
import android.content.Context
import android.util.Log
import kotlinx.serialization.Serializable
import kotlinx.serialization.builtins.ListSerializer
import kotlinx.serialization.json.Json
import java.io.IOException
@Serializable
data class Memo(
val id: String,
val title: String,
val detail: String
)
suspend fun getMemo(context: Context): List<Memo> {
return try {
Json.decodeFromString(ListSerializer(Memo.serializer()), getData(context, "memo"))
} catch (e: Exception) {
Log.e("getMemo", "Decode failed.", e)
emptyList()
}
}
suspend fun putMemo(context: Context, memos: List<Memo>) {
try {
putData(context, "memo", Json.encodeToString(ListSerializer(Memo.serializer()), memos))
} catch (e: IOException) {
Log.e("putMemo", "Encode Failed.", e)
}
}
java/jp/terraport/list/utils/Common.kt
package jp.terraport.list.utils
import android.content.Context
import android.util.Log
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import kotlinx.coroutines.flow.first
import java.io.IOException
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "list")
suspend fun getData(context: Context, name: String): String {
var data: String? = null
try {
data = context.dataStore.data.first()[stringPreferencesKey(name)]
} catch (e: IOException) {
Log.e("getData", "Get failed.", e)
}
return data ?: "[]"
}
suspend fun putData(context: Context, name: String, data: String) {
try {
context.dataStore.edit { entries ->
entries[stringPreferencesKey(name)] = data
}
} catch (e: IOException) {
Log.e("putData", "Put failed.", e)
}
}
fun generateRandomString(length: Int): String {
val allowedChars = ('A'..'Z') + ('a'..'z') + ('0'..'9')
return (1..length)
.map { allowedChars.random() }
.joinToString("")
}