概要
前回の記事で、DB接続のためのコードについて悪戦苦闘しながらエラーを解消しました。
何とか動くようになったので、次にUI周りを開発していこうと思います。
前回の記事はこちら!
UI周りの開発は見た目が大分わかりやすく変わるので、好きな人は好きかもしれません。これ実装したいな、違和感だなと思ったら生成AIに投げて修正していこうと思います。
使用環境
- Windows 11
- Claude (無料プラン)
前回と引き続きですが、今回は情報の見比べをする訳でもないのでClaude君だけで頑張っていきます。
UI開発
現状整理→簡単なたたき台作成
今回もClaude君に頼みますが、エラー解消で本来の目的を忘れていそうなので、最初に現状報告をしながら何をしたいか伝えていきましょうか。
諸々エラーの解消ができ、DBのテストが確認できたので、カラオケアプリのUIを作成していきたいです。次の部分を作成していきたいのですが、教えてください。
3.`ViewModel` を作成
4.UI(Compose)を組み立て
内容を再度整理すると以下でした。
アプリの機能としては以下の機能を入れたいです。
・持ち歌、練習曲、歌いたい曲をメモしておける
・追加のとき、曲名と歌手名をフリーで入力できる
・メモした内容を検索する機能も入れておきたい
持ち歌、練習曲、歌いたい曲はタブのように切り替えられるように画面下部に配置し、追加は画面の右下にプラスマークみたいなもので追加できたらいいなと思っています。また、画面上部には検索できる入力フォームがあればいいです。
MainActivity.ktは以下の内容にしています。
```
package com.example.karaokememo
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.example.karaokememo.data.Song
import com.example.karaokememo.data.SongDatabase
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val db = SongDatabase.getDatabase(this)
val dao = db.songDao()
// テスト用:DB書き込み&読み込み
CoroutineScope(Dispatchers.IO).launch {
// 1件追加
dao.insert(Song(title = "テスト曲", artist = "テスト歌手", category = "my_songs"))
// 取得して確認
val songs = dao.getSongsByCategoryAndQuery("my_songs", "").first()
println("DB取得件数: ${songs.size}")
songs.forEach { println("曲名: ${it.title}, 歌手: ${it.artist}") }
}
setContent {
MaterialTheme {
Surface(modifier = Modifier.fillMaxSize()) {
Text("DBテスト中…Logcatを確認してください", modifier = Modifier.padding(16.dp))
}
}
}
}
}
```
以下のような回答で、2つファイルを新規作成と1つファイルの変更をしていきます。「ui」パッケージは既存のパッケージと競合して作成できなかったので、「appui」パッケージを作成してその下に作成していきます。

パッケージの位置を直したコードが以下です。コピペに使って下さい。
appui/SongViewModel.kt
package com.example.karaokememo.appui
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import com.example.karaokememo.data.Song
import com.example.karaokememo.data.SongDatabase
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
class SongViewModel(application: Application) : AndroidViewModel(application) {
private val dao = SongDatabase.getDatabase(application).songDao()
private val _category = MutableStateFlow("my_songs")
val category: StateFlow<String> = _category
private val _query = MutableStateFlow("")
val query: StateFlow<String> = _query
val songs: StateFlow<List<Song>> = _category
.combine(_query) { category, query -> Pair(category, query) }
.flatMapLatest { (category, query) ->
dao.getSongsByCategoryAndQuery(category, query)
}
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
fun setCategory(category: String) {
_category.value = category
}
fun setQuery(query: String) {
_query.value = query
}
fun addSong(title: String, artist: String) {
viewModelScope.launch {
dao.insert(Song(title = title, artist = artist, category = _category.value))
}
}
fun deleteSong(song: Song) {
viewModelScope.launch {
dao.delete(song)
}
}
}appui/SongApp.kt
package com.example.karaokememo.appui
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.example.karaokememo.data.Song
// タブの定義
val tabs = listOf(
Triple("my_songs", "持ち歌", Icons.Default.Star),
Triple("practice", "練習曲", Icons.Default.Build),
Triple("want_to_sing", "歌いたい曲", Icons.Default.Favorite)
)
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SongApp(viewModel: SongViewModel = viewModel()) {
val songs by viewModel.songs.collectAsState()
val query by viewModel.query.collectAsState()
val category by viewModel.category.collectAsState()
var showDialog by remember { mutableStateOf(false) }
Scaffold(
topBar = {
TopAppBar(
title = {
OutlinedTextField(
value = query,
onValueChange = { viewModel.setQuery(it) },
placeholder = { Text("曲名・歌手名で検索") },
singleLine = true,
modifier = Modifier
.fillMaxWidth()
.padding(end = 8.dp),
leadingIcon = { Icon(Icons.Default.Search, contentDescription = null) }
)
}
)
},
bottomBar = {
NavigationBar {
tabs.forEach { (key, label, icon) ->
NavigationBarItem(
selected = category == key,
onClick = { viewModel.setCategory(key) },
icon = { Icon(icon, contentDescription = label) },
label = { Text(label) }
)
}
}
},
floatingActionButton = {
FloatingActionButton(onClick = { showDialog = true }) {
Icon(Icons.Default.Add, contentDescription = "追加")
}
}
) { padding ->
if (songs.isEmpty()) {
Box(
modifier = Modifier
.fillMaxSize()
.padding(padding),
contentAlignment = Alignment.Center
) {
Text("曲が登録されていません", color = MaterialTheme.colorScheme.onSurfaceVariant)
}
} else {
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(padding)
.padding(horizontal = 16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
contentPadding = PaddingValues(vertical = 8.dp)
) {
items(songs, key = { it.id }) { song ->
SongCard(song = song, onDelete = { viewModel.deleteSong(song) })
}
}
}
}
if (showDialog) {
AddSongDialog(
onDismiss = { showDialog = false },
onAdd = { title, artist ->
viewModel.addSong(title, artist)
showDialog = false
}
)
}
}
@Composable
fun SongCard(song: Song, onDelete: () -> Unit) {
Card(
modifier = Modifier.fillMaxWidth(),
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Column(modifier = Modifier.weight(1f)) {
Text(text = song.title, style = MaterialTheme.typography.titleMedium)
Text(text = song.artist, style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant)
}
IconButton(onClick = onDelete) {
Icon(Icons.Default.Delete, contentDescription = "削除",
tint = MaterialTheme.colorScheme.error)
}
}
}
}
@Composable
fun AddSongDialog(onDismiss: () -> Unit, onAdd: (String, String) -> Unit) {
var title by remember { mutableStateOf("") }
var artist by remember { mutableStateOf("") }
AlertDialog(
onDismissRequest = onDismiss,
title = { Text("曲を追加") },
text = {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
OutlinedTextField(
value = title,
onValueChange = { title = it },
label = { Text("曲名") },
singleLine = true,
modifier = Modifier.fillMaxWidth()
)
OutlinedTextField(
value = artist,
onValueChange = { artist = it },
label = { Text("歌手名") },
singleLine = true,
modifier = Modifier.fillMaxWidth()
)
}
},
confirmButton = {
TextButton(
onClick = { if (title.isNotBlank()) onAdd(title, artist) },
enabled = title.isNotBlank()
) { Text("追加") }
},
dismissButton = {
TextButton(onClick = onDismiss) { Text("キャンセル") }
}
)
}MainActivity.kt
package com.example.karaokememo
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.material3.MaterialTheme
import com.example.karaokememo.appui.SongApp
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MaterialTheme {
SongApp()
}
}
}
}これで「SyncNow」をぽちっとしてもSongAppが真っ赤っかです。それを聞きます。
1. エラー解消
エラーをコピーしつつ、appuiというパッケージに変更したことも報告しています。報連相大事!
uiというパッケージ名が被っていて作成できなかったためappuiにしています。また、SongAppが以下のエラーが出ていました。
Unresolved reference 'icons'.
Unresolved reference 'icons'.
Cannot infer type for type parameter 'T'. Specify it explicitly.
Cannot infer type for type parameter 'C'. Specify it explicitly.
Unresolved reference 'Icons'.
Cannot infer type for type parameter 'C'. Specify it explicitly.
Unresolved reference 'Icons'.
Cannot infer type for type parameter 'C'. Specify it explicitly.
Unresolved reference 'Icons'.
Unresolved reference 'Icons'.
Cannot infer type for type parameter 'T'. Specify it explicitly.
Cannot infer type for type parameter 'T'. Specify it explicitly.
<html>Operator call 'component1()' is ambiguous for destructuring of type 'uninferred ??? (Unknown type for type parameter T)'. Applicable candidates:<br/>fun <T> Array<out T>.component1(): T<br/>fun ByteArray.component1(): Byte<br/>fun ShortArray.component1(): Short<br/>fun IntArray.component1(): Int<br/>fun LongArray.component1(): Long<br/>fun FloatArray.component1(): Float<br/>fun DoubleArray.component1(): Double<br/>fun BooleanArray.component1(): Boolean<br/>fun CharArray.component1(): Char<br/>fun <T> List<T>.component1(): T<br/>fun <K, V> Map.Entry<K, V>.component1(): K<br/>fun UIntArray.component1(): UInt<br/>fun ULongArray.component1(): ULong<br/>fun UByteArray.component1(): UByte<br/>fun UShortArray.component1(): UShort
<html>Operator call 'component2()' is ambiguous for destructuring of type 'uninferred ??? (Unknown type for type parameter T)'. Applicable candidates:<br/>fun <T> Array<out T>.component2(): T<br/>fun ByteArray.component2(): Byte<br/>fun ShortArray.component2(): Short<br/>fun IntArray.component2(): Int<br/>fun LongArray.component2(): Long<br/>fun FloatArray.component2(): Float<br/>fun DoubleArray.component2(): Double<br/>fun BooleanArray.component2(): Boolean<br/>fun CharArray.component2(): Char<br/>fun <T> List<T>.component2(): T<br/>fun <K, V> Map.Entry<K, V>.component2(): V<br/>fun UIntArray.component2(): UInt<br/>fun ULongArray.component2(): ULong<br/>fun UByteArray.component2(): UByte<br/>fun UShortArray.component2(): UShort
<html>Operator call 'component3()' is ambiguous for destructuring of type 'uninferred ??? (Unknown type for type parameter T)'. Applicable candidates:<br/>fun <T> Array<out T>.component3(): T<br/>fun ByteArray.component3(): Byte<br/>fun ShortArray.component3(): Short<br/>fun IntArray.component3(): Int<br/>fun LongArray.component3(): Long<br/>fun FloatArray.component3(): Float<br/>fun DoubleArray.component3(): Double<br/>fun BooleanArray.component3(): Boolean<br/>fun CharArray.component3(): Char<br/>fun <T> List<T>.component3(): T<br/>fun UIntArray.component3(): UInt<br/>fun ULongArray.component3(): ULong<br/>fun UByteArray.component3(): UByte<br/>fun UShortArray.component3(): UShort
Unresolved reference 'Icons'.
Unresolved reference 'Icons'.
こう俯瞰してみると、「icons」が解決できていないように見えます。
Claude君回答は以下の通りでした。依存関係の解決と、型推論エラーの解消らしいですね。ふむふむ。

修正版コードは以下!都度「SyncNow」をして、反映していく感じですね。
bundle.gradle.kts(app)
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.compose)
id("com.google.devtools.ksp")
}
android {
namespace = "com.example.karaokememo"
compileSdk {
version = release(37) {
minorApiLevel = 0
}
}
defaultConfig {
applicationId = "com.example.karaokememo"
minSdk = 26
targetSdk = 36
versionCode = 1
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
buildFeatures {
compose = true
}
}
dependencies {
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.activity.compose)
implementation(libs.androidx.compose.material3)
implementation(libs.androidx.compose.ui)
implementation(libs.androidx.compose.ui.graphics)
implementation(libs.androidx.compose.ui.tooling.preview)
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
testImplementation(libs.junit)
androidTestImplementation(platform(libs.androidx.compose.bom))
androidTestImplementation(libs.androidx.compose.ui.test.junit4)
androidTestImplementation(libs.androidx.espresso.core)
androidTestImplementation(libs.androidx.junit)
debugImplementation(libs.androidx.compose.ui.test.manifest)
debugImplementation(libs.androidx.compose.ui.tooling)
// Room Database
implementation("androidx.room:room-runtime:2.8.4")
implementation("androidx.room:room-ktx:2.8.4")
ksp("androidx.room:room-compiler:2.8.4")
// ViewModel & LiveData
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0")
// Compose (すでに入っているはずですが念のため)
implementation("androidx.compose.material3:material3:1.2.0")
implementation("androidx.navigation:navigation-compose:2.7.6")
implementation("androidx.compose.material:material-icons-extended")
}SongApp.kt
package com.example.karaokememo.appui
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.example.karaokememo.data.Song
import androidx.compose.ui.graphics.vector.ImageVector
// タブの定義
data class TabItem(val key: String, val label: String, val icon: ImageVector)
val tabs = listOf(
TabItem("my_songs", "持ち歌", Icons.Default.Star),
TabItem("practice", "練習曲", Icons.Default.Build),
TabItem("want_to_sing", "歌いたい曲", Icons.Default.Favorite)
)
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SongApp(viewModel: SongViewModel = viewModel()) {
val songs by viewModel.songs.collectAsState()
val query by viewModel.query.collectAsState()
val category by viewModel.category.collectAsState()
var showDialog by remember { mutableStateOf(false) }
Scaffold(
topBar = {
TopAppBar(
title = {
OutlinedTextField(
value = query,
onValueChange = { viewModel.setQuery(it) },
placeholder = { Text("曲名・歌手名で検索") },
singleLine = true,
modifier = Modifier
.fillMaxWidth()
.padding(end = 8.dp),
leadingIcon = { Icon(Icons.Default.Search, contentDescription = null) }
)
}
)
},
bottomBar = {
NavigationBar {
tabs.forEach { tab ->
NavigationBarItem(
selected = category == tab.key,
onClick = { viewModel.setCategory(tab.key) },
icon = { Icon(tab.icon, contentDescription = tab.label) },
label = { Text(tab.label) }
)
}
}
},
floatingActionButton = {
FloatingActionButton(onClick = { showDialog = true }) {
Icon(Icons.Default.Add, contentDescription = "追加")
}
}
) { padding ->
if (songs.isEmpty()) {
Box(
modifier = Modifier
.fillMaxSize()
.padding(padding),
contentAlignment = Alignment.Center
) {
Text("曲が登録されていません", color = MaterialTheme.colorScheme.onSurfaceVariant)
}
} else {
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(padding)
.padding(horizontal = 16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
contentPadding = PaddingValues(vertical = 8.dp)
) {
items(songs, key = { it.id }) { song ->
SongCard(song = song, onDelete = { viewModel.deleteSong(song) })
}
}
}
}
if (showDialog) {
AddSongDialog(
onDismiss = { showDialog = false },
onAdd = { title, artist ->
viewModel.addSong(title, artist)
showDialog = false
}
)
}
}
@Composable
fun SongCard(song: Song, onDelete: () -> Unit) {
Card(
modifier = Modifier.fillMaxWidth(),
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Column(modifier = Modifier.weight(1f)) {
Text(text = song.title, style = MaterialTheme.typography.titleMedium)
Text(text = song.artist, style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant)
}
IconButton(onClick = onDelete) {
Icon(Icons.Default.Delete, contentDescription = "削除",
tint = MaterialTheme.colorScheme.error)
}
}
}
}
@Composable
fun AddSongDialog(onDismiss: () -> Unit, onAdd: (String, String) -> Unit) {
var title by remember { mutableStateOf("") }
var artist by remember { mutableStateOf("") }
AlertDialog(
onDismissRequest = onDismiss,
title = { Text("曲を追加") },
text = {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
OutlinedTextField(
value = title,
onValueChange = { title = it },
label = { Text("曲名") },
singleLine = true,
modifier = Modifier.fillMaxWidth()
)
OutlinedTextField(
value = artist,
onValueChange = { artist = it },
label = { Text("歌手名") },
singleLine = true,
modifier = Modifier.fillMaxWidth()
)
}
},
confirmButton = {
TextButton(
onClick = { if (title.isNotBlank()) onAdd(title, artist) },
enabled = title.isNotBlank()
) { Text("追加") }
},
dismissButton = {
TextButton(onClick = onDismiss) { Text("キャンセル") }
}
)
}2. 実行結果
上記の方法で一通りエラーが解決できましたので、実行してみました!
エミュレータを起動後、「Run」をぽちっと。
少し実行に時間はかかるものの、以下の画面が起動できました!

おおー!!かなりいい感じですね。
色々と触ってみましょうか。下のタブボタンを選択したり、「+」ボタンをクリック、削除もしてみます。



ほお、なるほど!実際に触ると、こここうしたいなというのが出てきますね。
ここからは好きに機能を追加してほしいので、一旦記事としてはここまでにします!
これを基にぜひ皆様個人個人で開発を進めてください。
番外編
Claude君との対話を載せて開発するのは上記までなのですが、触ってみて修正したいなという部分が何個か出てきたので、それを実装します。
一旦、改善点は以下の3項目です。
- 曲の追加時、「持ち歌」「練習曲」「歌いたい曲」を選択できるようにドロップダウンリストを入れる
- 曲の削除時、本当に削除していいかの確認ダイアログを出す
- 保存済みの曲を「持ち歌」「練習曲」「歌いたい曲」で移動できるようにする
これをClaude君に投げて追加していきました。
実行結果は以下です!





いい感じですね!!!
エラーが出たら一度投げますが、かなりサクサク変更ができました。さらに追加したい機能があれば、対話しながらできそうです。
まとめ
UI開発をClaude君に投げて作成していきました。
知識がなくてもUIが実装されていくのは面白いですね!
今回のようなAndroidアプリやフロントエンドなどは情報が豊富なので、生成AIにとっては得意な領分だと思います。ただ、エラーにぶつかったときに知識がないと修正箇所がわからずその場しのぎになってしまいがちなので、生成AIを使用しながら知識を一緒に得ていく、並行して勉強していく姿勢は大事だなと改めて感じました。コード先行で、知識が追い付いていない状況ですね。
連載していました全5回にわたるAndroideアプリ開発入門はこれにて終了です。引き続きアプリ開発など楽しみながら勉強していきましょう。
ではではまた別の記事で!
コメントを残す