Jetpack Compose入門 アプリを作る知識-1(一覧作成~Modifier/Scaffold/Surface/Columnなど)
概要
前回は、本当の最初の一歩の記事を書きました。
今回はその続きです。
アーキテクチャとか考えずにシンプルなアプリを作る知識を書いていきます。
ひとつづつステップを踏んで作ります。
簡単なアプリですが、ひとつづつ少しだけですが、深堀りしていくのでそれなりの学びがあるかもです。
作成アプリ
サンプルアプリは以下です。単純な一覧表示です。
100件のリストですが、表示されているところだけ作成されるようにはします。
アイテムをタップしたらリップルエフェクトがかかります。
主に以下のコンポーザブル関数とModifierを使います。
- Column
- Row
- Surface
- Scaffold
- LazyColumn
Column
まずは文字列の縦積み分です。
この手のものはColumnコンポーザブル関数を使います。
こんな関数です。
@Composable
inline fun Column(
modifier: Modifier! = Modifier,
verticalArrangement: Arrangement.Vertical! = Arrangement.Top,
horizontalAlignment: Alignment.Horizontal! = Alignment.Start,
content: (@Composable @ExtensionFunctionType ColumnScope.() -> Unit)?
): Unit
引数のcontentに縦に置きたい関数を順番に記述していきます。
contentは関数を指定する最後のパラメータなので Column(){}
のように {}
の中に記述していきます。
(Kotlinでは、関数の最後のパラメータが関数型である場合、そのパラメータは括弧の外に指定することができるという仕様があります)
@Composable
private fun ListTitle(
title: String,
body: String,
) {
Column(
) {
Text(
modifier = Modifier.align(Alignment.Start),
text = title,
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = body,
style = MaterialTheme.typography.body2,
)
}
}
@Preview(
uiMode = Configuration.UI_MODE_NIGHT_YES,
showBackground = true
)
@Composable
fun DarkPreview() {
SampleComposeTheme {
ListTitle("This is a title", "Detail Content")
}
}
こんな結果です。
この結果を見るとデザインが微妙ですね。デザインを調整するのにModifierを使っていきます。
Modifier
日本語にすると修飾子です。コンポーザブルを装飾または拡張できます。次のことができます。
- コンポーザブルのサイズ、レイアウト、動作、外観を変更できる
- ユーザー補助ラベルなどの情報を追加できる
- ユーザー入力を処理できる
- コンポーザブルの要素にクリック、スクロール、ドラッグ、ズームなどの機能を追加できる
上のコードでいうとSpacer(modifier = Modifier.height(4.dp))
やText(modifier = Modifier.align(Alignment.Start),
のようにコンポーザブル関数の引数に渡します。ほとんどのコンポーザブル関数では、引数にModifierを指定できます。
たとえば、Column
を横幅一杯に左右に8dp、上下に16dpのパディングを入れると
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 8.dp, vertical = 16.dp),
こんなデザインになります。こんな感じで調整してデザインを洗練させていくわけです。
Modifierのスコープ
話が全くずれますが、ここでModifierのスコープについて書いてみます。
Modifierのスコープは、全てのコンポーザブル関数で修飾子を利用する場合に重要な概念になります。
今回の場合のスコープに関連する箇所は Modifier.align()
です。
このサンプルでは左寄せなので記述しなくても良いのですが、中央揃えにしたい場合は、Modifier.align(Alignment.CenterHorizontally)
を指定できたりします。
これは、Column
関数の中でしか利用できない修飾子になります。(とはいえ同じインターフェイスを持ってる関数であれば利用できます)
Column(...) {
Text(
modifier = Modifier.align(Alignment.Start), // コンパイルエラーにならない
text = title,
)
}
Column
関数内ではない場合はコンパイルエラーになります。
Text(
modifier = Modifier.align(Alignment.Start), // コンパイルエラーになる
text = title,
)
なぜ、Column
関数内で利用できるのでしょうか。それは、Kotlinの仕様のFunction literals with receiverが関係しています。
Function literals with receiver
これはKotlinの仕様に記述されています。
ちなみにレシーバーとは、インスタンスメソッドを呼び出すときのインスタンスオブジェクトのことです。
以下の場合、Personクラスのgreet()メソッドを呼んでいますが、この時の変数person
がレシーバーになります。
val person = Person()
person.greet()
Column
関数のcontent引数の型は、content: @Composable ColumnScope.() -> Unit
になっています。
この書き方は、どういうことかというと関数オブジェクトを生成する拡張関数を作っていることだと理解するとわかりやすいです。
関数オブジェクトとは、無名関数やラムダ式のような関数型のオブジェクトのことです。拡張関数とは、決まった型に後から関数を追加できるKotlinの仕様です。
ColumnScope
に、() -> Unit
という関数を後から付けているということです。
() -> Unit
の部分はラムダ式で記述できるので、ColumnScope
にラムダ式オブジェクトを生成する関数を追加していて、そのラムダ式内でのthis
は、暗黙的にColumnScope
になります。
なぜ、暗黙的にthis
になるかというと、拡張関数を作るときとわかります。
fun String.hello() = "$this hello"
のようにStringにhello()関数を追加し、呼び出し時に"Please say".hello()
とした場合、Please say
が関数内のthis
になって、結果、"Please say Hello"と返されます。
これと同じことです。
ちなみにColumnScopeの実装は、ColumnScopeInstance
クラスです。
Layout(
content = { ColumnScopeInstance.content() },
このため、Column
関数の中では、ColumnScopeInstanceのメソッドのModifier.align()
が利用できるわけです。
Row
次は横のレイアウトを組んでいきます。Columnコンポーザブルが縦で、その横バージョンがRow
コンポーザブルになります。
こんな関数になります。
@Composable
inline fun Row(
modifier: Modifier! = Modifier,
horizontalArrangement: Arrangement.Horizontal! = Arrangement.Start,
verticalAlignment: Alignment.Vertical! = Alignment.Top,
content: (@Composable @ExtensionFunctionType RowScope.() -> Unit)?
): Unit
左側に画像で、その右に上記で記述したレイアウトが表示されるようにします。
Row(
modifier = Modifier.padding(all = 8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Image(
painter = painterResource(R.drawable.ic_launcher_background),
contentDescription = "Contact profile picture",
modifier = Modifier
.size(40.dp)
.clip(CircleShape)
.border(1.5.dp, MaterialTheme.colors.secondary, CircleShape)
)
Column() {
・・・
}
}
Image
コンポーザブルはModifierを使って丸くしたりborderをつけたりサイズを指定したりしています。
今回の画像は、リソースファイルから表示しましたがネットワーク経由で表示する場合は、Glideなどのライブラリを使うと良いでしょう。
その場合は、painter引数にGlideの関数を指定するだけです。
painter = rememberGlidePainter("画像のURL"),
Surface
次に上記で作ったレイアウトの親にSurface
コンポーザブル関数を指定します。
こんな関数です。
@Composable
@NonRestartableComposable
fun Surface(
modifier: Modifier! = Modifier,
shape: Shape! = RectangleShape,
color: Color! = MaterialTheme.colors.surface,
contentColor: Color! = contentColorFor(color),
border: BorderStroke? = null,
elevation: Dp! = 0.dp,
content: (@Composable () -> Unit)?
): Unit
Surface
の説明は前回記述しています。
このサンプルではSurface
を使うことで以下を実現しています
- shapeを指定することで、形状を角丸にクリッピングしています
- elevationによって奥行きを表現しています
- MaterialThemeのcolorとcontentColorのデフォルトが利用されるようになります
- contentColorとはたとえば、backgroudColorが背景色であり、その中にある文字などの色のことです
- これによりダークモード対応がされます
Modifier.clickable
を記述すことでリップエフェクトがついた状態でクリック可能になります。
Surface(
modifier = Modifier.clickable { onClick() },
shape = MaterialTheme.shapes.medium, elevation = 1.dp,
) {
Row(
・・・
結果は次の画像です。いままでダークモードになってませんでしたが、Surfaceを入れたことで変わりました。
角丸になり、わかりづらいですが奥行きのあるレイアウトになりました。
ちなみにScaffold
コンポーザブルを使うとダークモード対応されます。これは、内部でSurface
を使っているからです。
LazyColumn
次に上記まで作ったアイテムを複数件表示してスクロールで見れるようにします。いわゆる一覧画面です。
このときのポイントはColumn
ではなく、LazyColumn
コンポーザブルを使うことです。
たとえば、100件のアイテムがあった場合にColumn
を使うと表示されない部分も描画処理が走ってしまいパフォーマンスの問題が出てきます。
LazyColumn
はその名の通り、表示するまで描画処理が走りません。
こんな関数です。
@Composable
fun LazyColumn(
modifier: Modifier! = Modifier,
state: LazyListState! = rememberLazyListState(),
contentPadding: PaddingValues! = PaddingValues(0.dp),
reverseLayout: Boolean! = false,
verticalArrangement: Arrangement.Vertical! = if (!reverseLayout) Arrangement.Top else Arrangement.Bottom,
horizontalAlignment: Alignment.Horizontal! = Alignment.Start,
flingBehavior: FlingBehavior! = ScrollableDefaults.flingBehavior(),
userScrollEnabled: Boolean! = true,
content: (@ExtensionFunctionType LazyListScope.() -> Unit)?
): Unit
使ってみます。
@Composable
private fun ListTitles(contents: List<Pair<String, String>>) {
LazyColumn {
items(contents.size) { index ->
val content = contents[index]
ListTitle(title = content.first, body = content.second)
{
}
}
}
}
@Preview(
uiMode = Configuration.UI_MODE_NIGHT_YES,
showBackground = true
)
@Composable
fun ListTitlesDarkPreview() {
SampleComposeTheme {
val mocks = (1..100).map {
Pair("This is a title$it", "Detail Content$it")
}
ListTitles(contents = mocks)
}
}
Scaffold
Scaffoldコンポーザブル関数
は、Material Designの基本的なレイアウト構造を提供する関数です。TopAppBar、BottomAppBar、FloatingActionButton、DrawerなどのMaterialコンポーネントを配置する場所を提供します。コンポーザブルを配置する箱を用意して、必要なものをそこに入れていくというもので、Slotと呼ばれています。
インターフェイスは以下です。
@Composable
fun Scaffold(
modifier: Modifier! = Modifier,
scaffoldState: ScaffoldState! = rememberScaffoldState(),
topBar: (@Composable () -> Unit)? = {},
bottomBar: (@Composable () -> Unit)? = {},
snackbarHost: (@Composable (SnackbarHostState) -> Unit)? = { SnackbarHost(it) },
floatingActionButton: (@Composable () -> Unit)? = {},
floatingActionButtonPosition: FabPosition! = FabPosition.End,
isFloatingActionButtonDocked: Boolean! = false,
drawerContent: (@Composable @ExtensionFunctionType ColumnScope.() -> Unit)? = null,
drawerGesturesEnabled: Boolean! = true,
drawerShape: Shape! = MaterialTheme.shapes.large,
drawerElevation: Dp! = DrawerDefaults.Elevation,
drawerBackgroundColor: Color! = MaterialTheme.colors.surface,
drawerContentColor: Color! = contentColorFor(drawerBackgroundColor),
drawerScrimColor: Color! = DrawerDefaults.scrimColor,
backgroundColor: Color! = MaterialTheme.colors.background,
contentColor: Color! = contentColorFor(backgroundColor),
content: (@Composable (PaddingValues) -> Unit)?
): Unit
今回はTopAppBar
コンポーザブルを設定しています。
@Composable
private fun ListTitles(contents: List<Pair<String, String>>) {
Scaffold(
topBar = {
TopAppBar(
title = { Text("My TopAppBar") },
)
}
) {
LazyColumn {
items(contents.size) { index ->
val content = contents[index]
ListTitle(title = content.first, body = content.second)
{
}
}
}
}
}
これで完成です。
全体のソース
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
SampleComposeTheme {
Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colors.background) {
MyContentView()
}
}
}
}
}
@Composable
fun MyContentView() {
val mocks = (1..100).map {
Pair("This is a title$it", "Detail Content$it")
}
ListTitles(contents = mocks)
}
@Composable
private fun ListTitles(contents: List<Pair<String, String>>) {
Scaffold(
topBar = {
TopAppBar(
title = { Text("My TopAppBar") },
)
}
) {
LazyColumn {
items(contents.size) { index ->
val content = contents[index]
ListTitle(title = content.first, body = content.second)
{
}
}
}
}
}
@Composable
private fun ListTitle(
title: String,
body: String,
onClick: () -> Unit,
) {
Surface(
modifier = Modifier.clickable { onClick() },
shape = MaterialTheme.shapes.medium, elevation = 1.dp,
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(all = 8.dp)
) {
Image(
painter = painterResource(R.drawable.ic_launcher_background),
contentDescription = "Contact profile picture",
modifier = Modifier
.size(40.dp)
.clip(CircleShape)
.border(1.5.dp, MaterialTheme.colors.secondary, CircleShape)
)
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 8.dp, vertical = 16.dp),
) {
Text(
text = title,
style = MaterialTheme.typography.subtitle2,
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = body,
style = MaterialTheme.typography.body2,
)
}
}
}
}
@Preview(
uiMode = Configuration.UI_MODE_NIGHT_NO,
showBackground = true
)
@Composable
fun DefaultPreview() {
SampleComposeTheme {
MyContentView()
}
}
@Preview(
uiMode = Configuration.UI_MODE_NIGHT_YES,
showBackground = true
)
@Composable
fun MyContentViewDarkPreview() {
SampleComposeTheme {
MyContentView()
}
}
@Preview(
uiMode = Configuration.UI_MODE_NIGHT_YES,
showBackground = true
)
@Composable
fun ListTitlesDarkPreview() {
SampleComposeTheme {
val mocks = (1..100).map {
Pair("This is a title$it", "Detail Content$it")
}
ListTitles(contents = mocks)
}
}
@Preview(
uiMode = Configuration.UI_MODE_NIGHT_YES,
showBackground = true
)
@Composable
fun ListTitleDarkPreview() {
SampleComposeTheme {
ListTitle("This is a title", "Detail Content", {})
}
}
ソーシャル経済メディア NewsPicks メンバーの発信を集約しています。公式テックブログはこちら→ tech.uzabase.com/archive/category/NewsPicks
Discussion