[Jetpack Compose] 簡単なMemoアプリでMVVMアーキテクチャを掴む

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

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
	}
}