
そもそもMVVMとはなんぞやという方は以下のAIがポケモンを例に説明してくれた内容を一読ください。

Contents 非表示
Model層
Note
Note内容の元になるデータを定義
data class Note(
val id: UUID = UUID.randomUUID(), // 各Note識別用のユニークID
val title: String, // タイトル
val description: String, // 内容
val entryDate: LocalDateTime = LocalDateTime.now() // 日付
)
NoteData.kt
初期表示用のダミーデータ、なくてもOK。
class NoteDataSource {
companion object {
fun loadNotes(): List<Note> {
return listOf(
Note(title = "近所のカフェ", description = "新しいカフェのコーヒーが美味しかった。次はケーキも試したい。"),
Note(title = "週末の予定", description = "土曜日は友達と映画、日曜日は公園でピクニック。"),
Note(title = "読書リスト", description = "村上春樹の新刊、東野圭吾のミステリー、湊かなえの小説。"),
Note(title = "今日の出来事", description = "電車で面白い人に遭遇。帰り道に綺麗な夕焼けを見た。"),
Note(title = "欲しいもの", description = "新しいリュックサック、ワイヤレスイヤホン、デザインが良いマグカップ。"),
Note(title = "旅行の計画", description = "来月は京都へ旅行。おすすめの観光スポットを調べよう。"),
Note(title = "仕事のメモ", description = "会議の資料作成、プレゼンの練習、顧客へのメール送信。"),
Note(title = "健康管理", description = "毎日30分のウォーキング、バランスの取れた食事、十分な睡眠。"),
Note(title = "趣味の時間", description = "ギターの練習、新しい料理に挑戦、絵を描く。"),
Note(title = "アイデアメモ", description = "アプリの新機能、ブログの記事テーマ、週末に作りたい料理のレシピ。")
)
}
}
}
View層
MainActivity.kt
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyNoteAppTheme {
Surface(modifier = Modifier.fillMaxSize()) {
val noteViewModel: NoteViewModel by viewModels()
NotesApp(noteViewModel = noteViewModel)
}
}
}
}
}
@Composable
private fun NotesApp(
noteViewModel: NoteViewModel = viewModel()
) {
val notesList = noteViewModel.getAllNotes()
NoteScreen(
notes = notesList,
// 追加処理
onAddNote = {
noteViewModel.addNote(it)
},
// 削除処理
onRemoveNote = {
noteViewModel.removeNote(it)
}
)
}
NoteScreen.kt
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun NoteScreen(
notes: List<Note>,
onAddNote: (Note) -> Unit,
onRemoveNote: (Note) -> Unit
) {
var title by remember {
mutableStateOf("")
}
var description by remember {
mutableStateOf("")
}
val context = LocalContext.current
Column(
modifier = Modifier.padding(6.dp)
) {
TopAppBar(
title = {
Text(text = "Note App")
},
navigationIcon = {
Icon(
imageVector = Icons.Rounded.Menu,
contentDescription = "Menu Icon",
tint = Color.White
)
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = Color(0xFF3DC20D),
titleContentColor = Color.White
)
)
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
// タイトルフィールド
NoteInputTextField(
modifier = Modifier.padding(
top = 8.dp,
bottom = 8.dp
),
text = title,
label = "タイトル",
onTextChange = {
if (it.all { char ->
char.isLetter() || char.isWhitespace()
}) title = it
}
)
// 内容フィールド
NoteInputTextField(
modifier = Modifier.padding(
top = 8.dp,
bottom = 8.dp
),
text = description,
label = "内容",
onTextChange = {
if (it.all { char ->
char.isLetter() || char.isWhitespace()
}) description = it
}
)
// 保存ボタン
SaveNoteButton(
modifier = Modifier
.padding(top = 8.dp)
.width(240.dp)
.height(44.dp),
text = "保存する",
enabled = (title.isNotEmpty() && description.isNotEmpty()),
onClick = {
onAddNote(Note(title = title, description = description))
Toast.makeText(context, "Note Added", Toast.LENGTH_SHORT).show()
title = ""
description = ""
}
)
}
Divider(modifier = Modifier.padding(12.dp))
LazyColumn {
items(notes) { note ->
NoteItemCell(
note = note,
onNoteClicked = {
onRemoveNote(note)
}
)
}
}
}
}
@Composable
fun NoteItemCell(
modifier: Modifier = Modifier,
note: Note,
onNoteClicked: (Note) -> Unit
) {
Surface(
modifier = Modifier
.padding(4.dp)
.clip(RoundedCornerShape(12.dp))
.fillMaxWidth(),
color = Color(0xFFE7EAEC),
shadowElevation = 20.dp
) {
Column(
modifier
.clickable { onNoteClicked(note) }
.padding(horizontal = 16.dp, vertical = 8.dp),
horizontalAlignment = Alignment.Start
) {
Text(
text = note.title,
style = MaterialTheme.typography.bodyMedium
)
Text(
text = note.description,
style = MaterialTheme.typography.bodySmall
)
Text(
text = note.entryDate.format(DateTimeFormatter.ofPattern("EEE, d MMM")),
style = MaterialTheme.typography.bodySmall
)
}
}
}
NoteComponents
TextField, Buttonの基底レイアウトを管理するファイル、細かいデザインはお好きに。
@Composable
fun NoteInputTextField(
modifier: Modifier = Modifier,
text: String,
label: String,
maxLine: Int = 1,
onTextChange: (String) -> Unit,
onImeAction: () -> Unit = {}
) {
val keyboardController = LocalSoftwareKeyboardController.current
TextField(
value = text,
onValueChange = onTextChange,
colors = TextFieldDefaults.colors(
focusedContainerColor = Color.Transparent,
unfocusedContainerColor = Color.Transparent,
disabledContainerColor = Color.Transparent,
focusedIndicatorColor = Color.Black,
unfocusedIndicatorColor = Color.Gray,
focusedLabelColor = Color.Black,
unfocusedLabelColor = Color.Gray
),
maxLines = maxLine,
label = {
Text(text = label)
},
keyboardOptions = KeyboardOptions.Default.copy(
imeAction = ImeAction.Done
),
keyboardActions = KeyboardActions(onDone = {
onImeAction()
keyboardController?.hide()
}),
modifier = modifier
)
}
@Composable
fun SaveNoteButton(
modifier: Modifier = Modifier,
text: String,
onClick: () -> Unit,
enabled: Boolean = true
) {
Button(
onClick = { onClick.invoke() },
shape = CircleShape,
enabled = enabled,
modifier = modifier
) {
Text(text = text)
}
}
ViewModel層
NoteViewModel.kt
class NoteViewModel: ViewModel() {
private var noteList = mutableStateListOf<Note>()
init {
// 初期表示用のダミーデータ取得
noteList.addAll(NoteDataSource.loadNotes())
}
fun addNote(note: Note) {
noteList.add(note)
}
fun removeNote(note: Note) {
noteList.remove(note)
}
fun getAllNotes(): List<Note> {
return noteList
}
}