Androidアプリ開発入門(お実装編)
はじめに
前回お勉強編をやりましたので、お実装編です。
Android Studioの使い方
Android Studioを使います。普段私はAI開発でVSCodeを使っており、あまりの操作の違いに困惑しております。
まずはAndroid Studioの使い方を...(インストールは終わっている前提です)
アプリ作成
- Android Studioを起動して
New Project
をクリック
-
Empty Activity
を選択(Jetpack Composeを使ったテンプレート)- ActivityはAndroidアプリの画面のことを指します
- 設定をする
-
Name
は適当につける -
Package name
はアプリのIDとして使われるのでユニークである必要がある(ドメインの逆順.アプリ名が一般的) -
Save location
はそのままで良い(隠すために空欄にしてるだけです) -
Minimum SDK
で設定したVersion未満はインストールできなくなる
-
アプリ立ち上げ
エミュレーターを使って立ち上げます。
再生マークを押すとアプリが起動し、Hello Android!が出てくればOKです!
⚠️ 仮想デバイスが未作成の場合は作成します。(以下のCreate Virtual Device
からできます)
操作の説明
便利なショートカットは...
-
Shift
を2回- ファイル名の検索ができます
- VSCodeでいうところの
cmd
+P
です
-
cmd
+Shift
+F
- ファイルの中身を検索できます
-
option
+up
- カーソルの選択範囲を段階的に大きくできます
最初に起動すると、実際のフォルダ構造になっていないので、これを変えてみます。
左上のAndroid
をProject
に変更します
デバック
Androidでは2種類ある
- 実行中のアプリにデバッカーをアタッチする
- アプリの起動と同時にデバッカーをアタッチする
デバッグ中画面詳細
適切なところでブレークポイントを設定しデバッグをすると、ブレークポイントで止まり、デバッグウィンドウが出てくる。
実際の値の確認や、計算も可能
簡素なTodoアプリ
機能
- 投稿するとタスクが溜まる(データベース機能はなし)
- 投稿した時の時刻も記録
- 削除ボタンを押すと消える
アーキテクチャーの整理
完成イメージ
- ToDoScreen
- UI部分
- TaskItem(1個のToDoを表すUI)を含む
- ToDoUiState
- UIの状態を表す
- ToDoViewModel
- UIから受け取った情報をもとにUIに状態を公開
- ToDoRepository
- ビジネスロジック部分
- 今回だと時刻を生成する役割
- Task
- 1個のTaskのデータクラス
フォルダ構成
.
└── app/
└── src/
└── main/
├── java/
│ └── {package_name}/
│ ├── data/
│ │ ├── repository/
│ │ │ └── ToDoRepository.kt
│ │ └── entity/
│ │ └── Task.kt
│ └── ui/
│ ├── TaskItem.kt
│ ├── ToDoScreen.kt
│ ├── ToDoUiState.kt
│ └── ToDoViewModel.kt
└── res/
└── ...
準備体操
アーキテクチャもフォルダ構成も決まったので、さぁやるぞ!となる前に、これを開発するのに使う知識を先に詰め込んでおきます。
TextFiledの作成
そのまま実装
一旦レイアウトとかは忘れてToDoScreen.kt
にテキストボックスを作ろうとすると以下のようになります。(一応Previewもつけておきます)
@Composable
fun ToDoScreen(
modifier: Modifier = Modifier
) {
TextField(
value = "",
onValueChange = {},
modifier = modifier
)
}
@Preview
@Composable
fun ToDoScreenPreview() {
ToDoScreen()
}
エミュレーターを起動して出てきたテキストボックスに値を入れると...入力できません!
これはテキストボックスに値を入れると、確かに値は更新されているのですが、UIが更新されているわけではない(Recomposeされていない)ことが原因です。
そこで、MutableState
を使ってTextField
の中にある値をComposeに監視してもらい、変わったらRecomposeされるようにします。
先ほどとの変更点
-
text
という変数を導入- 再代入できる
var
を使いましょう -
by
はDelegated propetryといってMutableState型をStringやintとして扱えるようになります -
remember
をつけると、Recomposeされても値がリセットされないようになるAPI
- 再代入できる
@Composable
fun ToDoScreen(
modifier: Modifier = Modifier
) {
var text: String by remember { mutableStateOf("") }
TextField(
value = text,
onValueChange = {value -> text = value},
modifier = modifier
)
}
これで値が入れられるようになりました!
少しリファクタリング
状態を監視するものとUIは分けて実装します。
変更する場所は以下です。
-
gradle/libs.version.toml
とapp/build.gradle.kts
に追記してviewModelを取得できるようにする - UIの状態を定義する
ToDoUiState.kt
でtext
を定義 - UIの状態を管理しUIに公開するための
ToDoViewModel.kt
でTextField
のonValueChange
の中身を記載
- 1について
以下追記してsyncする。
[versions]
lifecycleViewModelCompose = "2.8.7"
[libraries]
androidx-lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "lifecycleViewModelCompose" }
dependencies {
implementation(libs.androidx.lifecycle.viewmodel.compose) // ✅ TOML のエイリアスを正しく参照
}
- 2について
- データクラスとして定義
- 画面が持っているデータはテキストボックスに入るテキストだけなので1個だけ定義
package com.example.todoapp2.ui
data class ToDoUiState(
val text: String = ""
) {
}
- 3について
- UIが持つべき変数を
uiState
として、先に定義したToDoUiState
を入れる - 必要なメソッドは「テキストボックスに値が入ったら更新する」のみ
- UIが持つべき変数を
package com.example.todoapp2.ui
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
class ToDoViewModel: ViewModel() {
var uiState by mutableStateOf(ToDoUiState())
private set
fun inputText(text: String) {
uiState = uiState.copy(text = text)
}
}
そして、ToDoScreen.kt
も変更します。
@Composable
fun ToDoScreen(
modifier: Modifier = Modifier,
viewModel: ToDoViewModel = viewModel(),
) {
val uiState = viewModel.uiState
TextField(
value = uiState.text,
onValueChange = viewModel::inputText,
modifier = modifier
)
}
TextFiledの強化
テキストボックスにボタンをつけて、ボタンを押した際にテキストボックスに入ってる文字がTextで表示されるようにしてみます。
変更箇所は以下になります。
-
ToDoScreen.kt
- TextFieldに
TrailingIcon
をつける(TrailingIconをつけるとテキストボックスの最後にアイコンをつけることができます) - この中に
IconButton
を定義し、Clickされた時の処理、表示するアイコンなどの設定をしていく - またボタンが押された際に表示されるTextも用意しておく
- そのまま書くと重なって見れないので、Columnで縦に並べる
- TextFieldに
@Composable
fun ToDoScreen(
modifier: Modifier = Modifier,
viewModel: ToDoViewModel = viewModel(),
) {
Column (modifier = modifier) {
val uiState = viewModel.uiState
TextField(
value = uiState.text,
onValueChange = viewModel::inputText,
modifier = modifier,
trailingIcon = {
IconButton(
onClick = { viewModel.addToDo(uiState.text) },
enabled = uiState.text.isNotBlank()
) {
Icon(
imageVector = Icons.Default.Add,
contentDescription = null
)
}
}
)
Text(text = uiState.displayText)
}
}
Add
ボタン以外にも色々あります。こちらのページがまとまっています。
-
ToDoUiState.kt
- UIの状態として表示するtextが増えたので
val displayText: String = ""
を追加する
- UIの状態として表示するtextが増えたので
-
ToDoViewModel.kt
- ボタンが押された時の処理を書く
class ToDoViewModel: ViewModel() {
...
fun addToDo(text: String) {
uiState = uiState.copy(displayText = text)
}
}
できた!
実装
ファイル作成
最初に定義したフォルダ構成に合わせてファイルを作成していきたいと思います。
Task.kt
とToDoUiState.kt
はデータクラスとして作成してください。
データクラスはデータの保持に適したクラスで、equals()
やcopy()
などが使えるようになります(後で出てきます)
このようになればOKです。
Screenの作成
-
ToDoScreen.kt
- UIを
TODO一覧が載る場所
とTODOを入力する場所
の2つをColumnで分ける - TODO一覧はTaskListというComposable関数で定義
- UIを
@Composable
fun ToDoScreen(
modifier: Modifier = Modifier,
viewModel: ToDoViewModel = viewModel(),
) {
Column (modifier = modifier) {
val uiState = viewModel.uiState
val scrollState = rememberScrollState()
TaskList(
tasks = uiState.tasks,
modifier = modifier
.weight(1f)
.verticalScroll(scrollState)
)
TextField(
value = uiState.inputText,
onValueChange = viewModel::inputText,
modifier = modifier.fillMaxWidth(),
trailingIcon = {
IconButton(
onClick = viewModel::addToDo,
enabled = uiState.inputText.isNotBlank()
) {
Icon(
imageVector = Icons.Default.Add,
contentDescription = null
)
}
}
)
}
}
@Composable
private fun TaskList(
tasks: List<Task>,
modifier: Modifier = Modifier,
viewModel: ToDoViewModel = viewModel(),
) {
val scrollState = rememberScrollState()
Column(modifier = modifier) {
tasks.forEach { task ->
TaskItem(
task = task,
onDeleteButtonClick = { viewModel.deleteToDo(task) }
)
Divider()
}
}
}
- TaskItem.kt
- TaskListの中の1個のToDoを定義する
@Composable
fun TaskItem(
task: Task,
onDeleteButtonClick: () -> Unit = {},
modifier: Modifier = Modifier
) {
Row(
modifier = modifier.padding(vertical = 8.dp),
) {
Column (
modifier = Modifier.weight(1f).padding(8.dp)
) {
Text(text = task.title)
Text(text = task.createAt, fontSize=10.sp)
}
IconButton(onClick = onDeleteButtonClick) {
Icon(
imageVector = Icons.Default.Delete,
contentDescription = null
)
}
}
}
UIが持つべき状態の定義
-
Task.kt
- ToDo1個が持つべき状態
data class Task (
val title: String = "",
val createAt: String = ""
)
-
ToDoUiState.kt
- UI全体が持つべき状態
- 1個目はToDoリスト全て
- つまりList型の
Task
データクラスの集合
- つまりList型の
- 2個目はTextField
data class ToDoUiState(
// 初期値としてtask1, 2024-11-10とtask2, 2024-11-12を定義
val tasks: List<Task> = listOf(
Task("task1", "2024-11-10"),
Task("task2", "2024-11-12")
),
val inputText: String = ""
)
UIの状態を管理するviewModel
-
ToDoViewModel.kt
- UIの状態を更新したり制御するクラス
- ex: ToDoを追加、削除など
class ToDoViewModel(
private val repository: ToDoRepository = ToDoRepository()
): ViewModel() {
var uiState by mutableStateOf(ToDoUiState())
private set
fun inputText(text: String) {
uiState = uiState.copy(inputText = text)
}
fun addToDo() {
val currentDateTime = repository.getDateTime()
uiState = uiState.copy(
tasks = uiState.tasks + Task(
title = uiState.inputText,
createAt = currentDateTime
),
inputText = ""
)
}
fun deleteToDo(task: Task) {
uiState = uiState.copy(
tasks = uiState.tasks - task
)
}
}
ビジネスロジック部分の定義
といっても今回はタスク投稿時間を裏で取得するだけなのですが...
ToDoRepository.kt
class ToDoRepository {
fun getDateTime(): String {
val currentDateTime = LocalDateTime.now()
val year = currentDateTime.year
val month = currentDateTime.monthValue
val dayOfMonth = currentDateTime.dayOfMonth
val hour = currentDateTime.hour
val minute = currentDateTime.minute
val second = currentDateTime.second
return "$year-$month-$dayOfMonth $hour:$minute:$second"
}
}
成果物
できた!
Discussion