Composableを使ったListView表現【Android アプリ開発】

Composableを使ったListView表現【Android アプリ開発】
Composableを使ったListView表現【Android アプリ開発】

ゴール

下図のようなリストビューを、Composableを使って作ります。

Composableを使ったListView表現
Composableを使ったListView表現

パッケージディレクトリ下の構成

今回試作した、アプリケーションのパッケージディレクトリ下の主な構成は次のとおりです。

zsh
.
├── MainActivity.kt
├── model
│   └── ListItem.kt
├── view
│   ├── ListItemView.kt
│   ├── ListView.kt
│   └── ListViewWithButtons.kt
└── viewmodel
    └── ListViewModel.kt

MVVMアーキテクチャ

MVVMアーキテクチャ
MVVMアーキテクチャ

モデル-ビュー-ビューモデル(MVVM)アーキテクチャに基づいて設計しました。各ファイルの役割は以下の通りです。

MainActivity.kt

アプリケーションのメインアクティビティであり、アプリケーションのエントリポイントです。setContent メソッドを使って UI コンテンツを設定し、Jetpack Compose を使用して UI をレンダリングします。このファイルは、アプリケーションのライフサイクルイベント(例えば onCreate)を管理し、適切なビューモデルやコンポーザブル関数をロードします。

model/ListItem.kt

ListViewで表示するデータを管理するデータモデルクラスです。ID、アイコン、テキスト、サブテキスト、チェック状態などのプロパティを持っています。

view/ListItemView.kt

個々のリストアイテムを表示するための Composable 関数を含んでいます。ListItem オブジェクトを受け取り、それをUIコンポーネント(アイコン、テキスト、チェックボックスなど)で表示します。このコンポーネントはリストの各行をどのように描画するかを定義します。

view/ListView.kt

リストビューを構築するための Composable 関数を含んでいます。ビューモデル経由でリストのデータを受け取ります。

view/ListViewWithButtons.kt

リストビューの下部にボタンを設置するための Composable 関数を含んでいます。ビューモデルを介してユーザーアクションをリストアイテムを更新し、リストビューへ反映されます。

viewmodel/ListViewModel.kt

ビューモデルクラスで、アプリケーションのビジネスロジックとデータ管理を担当します。リストのデータを保持し、データの変更をハンドリングします。

必要なパッケージ

今回のプロジェクトで依存するパッケージ一覧です:

Module(app)
dependencies {

    implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0") // viewModel()で必要
    implementation("androidx.compose.runtime:runtime-livedata:1.6.0") // observeAsStateで必要
    implementation("androidx.core:core-ktx:1.9.0")
    implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0")
    implementation("androidx.activity:activity-compose:1.8.2")
    implementation(platform("androidx.compose:compose-bom:2023.03.00"))
    implementation("androidx.compose.ui:ui")
    implementation("androidx.compose.ui:ui-graphics")
    implementation("androidx.compose.ui:ui-tooling-preview")
    implementation("androidx.compose.material3:material3")
    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:2023.03.00"))
    androidTestImplementation("androidx.compose.ui:ui-test-junit4")
    debugImplementation("androidx.compose.ui:ui-tooling")
    debugImplementation("androidx.compose.ui:ui-test-manifest")
}

Modelのソースコード

model/ListItem.kt
package com.apppppp.listviewtest.model

data class ListItem(
    val id: Int,
    val icon: Int, // リソースIDとして扱う
//    val icon: String, // アイコンを画像のURLまたはパスとして扱う
    val text: String,
    val subText: String,
    var isChecked: Boolean
)

Viewのソースコード

Viewではビジネスロジックを持たせず、再利用可能なコンポーネントとして提供させます。

ListItemView.kt
package com.apppppp.listviewtest.view

import androidx.compose.foundation.Image
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.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.material3.Checkbox
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import com.apppppp.listviewtest.model.ListItem

@Composable
fun ListItemView(item: ListItem, onCheckChanged: (Boolean) -> Unit) {
    Row(
        modifier = Modifier
            .fillMaxWidth()
            .padding(16.dp),
        verticalAlignment = Alignment.CenterVertically
    ) {
        Image(
            painter = painterResource(id = item.icon),
            contentDescription = null, // アクセシビリティのための説明文
            modifier = Modifier.size(50.dp) // 画像のサイズを制限
        )
        Spacer(Modifier.width(16.dp))
        Column {
            Text(text = item.text)
            Text(text = item.subText, style = MaterialTheme.typography.bodySmall)
        }
        Spacer(Modifier.weight(1f))
        Checkbox(
            checked = item.isChecked,
            onCheckedChange = onCheckChanged
        )
    }
}
ListView.kt
package com.apppppp.listviewtest.view

import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.tooling.preview.Preview
import com.apppppp.listviewtest.R
import com.apppppp.listviewtest.model.ListItem
import com.apppppp.listviewtest.viewmodel.ListViewModel

@Composable
fun ListView(viewModel: ListViewModel) {
    val items by viewModel.items.observeAsState(listOf())

    LazyColumn {
        items(items) { item ->
            ListItemView(item = item) { isChecked ->
                viewModel.onItemCheckChanged(item, isChecked)
            }
        }
    }
}
ListViewWithButtons.kt
package com.apppppp.listviewtest.view

import androidx.compose.foundation.layout.*
import androidx.compose.material3.Button
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
import com.apppppp.listviewtest.viewmodel.ListViewModel

@Composable
fun ListViewWithButtons(viewModel: ListViewModel) {
    Column(
        modifier = Modifier.fillMaxSize() // コンテナを画面サイズに合わせる
    ) {
        Box(modifier = Modifier.weight(1f)) { // 重みを与えて残りのスペースを使わせる
            // リストビュー
            ListView(viewModel)
        }

        // ボタンを配置する行
        Row(
            modifier = Modifier
                .fillMaxWidth()
                .padding(16.dp),
            horizontalArrangement = Arrangement.SpaceBetween,
            verticalAlignment = Alignment.CenterVertically
        ) {
            Button(onClick = { viewModel.addItem() }) {
                Text("アイテム追加")
            }
            Button(onClick = { viewModel.removeCheckedItems() }) {
                Text("チェック済み削除")
            }
        }
    }
}

ViewModelのソースコード

ここでビジネスロジックとリストデータの管理を行います。MutableLiveData使って、リストデータが更新されると、自動でビューに反映される仕組みです。

ListViewModel.kt
package com.apppppp.listviewtest.viewmodel

import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.apppppp.listviewtest.R
import com.apppppp.listviewtest.model.ListItem

class ListViewModel : ViewModel() {
    private val _items = MutableLiveData<List<ListItem>>()

    init {
        val tempList = mutableListOf<ListItem>()
        for (i in 1..5) {
            tempList.add(
                ListItem(
                    id = i,
                    icon = R.drawable.crystal,
                    text = "アイテム $i",
                    subText = "ここに詳細な説明を入れることができます。",
                    isChecked = false
                )
            )
        }
        _items.value = tempList
    }

    val items: LiveData<List<ListItem>> = _items

    // チェック状態の更新
    fun onItemCheckChanged(item: ListItem, isChecked: Boolean) {
        _items.value = _items.value?.map {
            if (it.id == item.id) it.copy(isChecked = isChecked) else it
        }
    }

    // 新しいアイテムを追加
    fun addItem() {
        // 現在のアイテムリストから最大のIDを取得し、1を加える
        val newId = (_items.value?.maxByOrNull { it.id }?.id ?: 0) + 1

        val newItem = ListItem(
            id = newId,
            icon = R.drawable.ruby, // アイコンを適宜設定
            text = "アイテム $newId",
            subText = "説明",
            isChecked = false
        )

        // 新しいアイテムをリストに追加
        _items.value = _items.value.orEmpty() + newItem
    }

    // チェックされたアイテムを削除
    fun removeCheckedItems() {
        _items.value = _items.value?.filterNot { it.isChecked }
    }
}

MainActivityのソースコード

MainActivity.kt
package com.apppppp.listviewtest

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.lifecycle.viewmodel.compose.viewModel
import com.apppppp.listviewtest.viewmodel.ListViewModel
import com.apppppp.listviewtest.view.ListViewWithButtons

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MyApp {
                val viewModel: ListViewModel = viewModel()
                ListViewWithButtons(viewModel)
            }
        }
    }
}

@Composable
fun MyApp(content: @Composable () -> Unit) {
    MaterialTheme() {
        Surface() {
            content()
        }
    }
}

関連記事

最後までご覧いただきありがとうございます!

▼ 記事に関するご質問やお仕事のご相談は以下よりお願いいたします。
お問い合わせフォーム