前人未踏の領域へ Androidアプリ開発編

Androidアプリ開発に関する調査メモ置き場。古い記事にはアプリ以外も含まれます。

Jetpack Compose:TopAppBarとPullToRefreshContainerがうまく連動しない

課題

androidx.compose.material3:material31.2.0 にしたところ、TopAppBarPullRefreshがうまく連動して動作しなくなった(TopAppBarDefaults.exitUntilCollapsedScrollBehaviorの場合に下にスクロールしてもTopAppBarが降りてこない)。どうすればよいか。

対応

1.2.0 よりmaterial3にpulltorefreshパッケージが追加された。その影響か挙動が変わってしまったようだ。

実装方法が色々とあると思うのでポイントのみ。nestedScrollConnection を2つ使うことになるが、順番が影響する。pullToRefreshState.nestedScrollConnectionscrollBehavior.nestedScrollConnectionよりも先に記述するのがポイント。

// TopAppBar向け
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
// PullToRefresh向け
val pullToRefreshState = rememberPullToRefreshState()

Scaffold(
modifier = Modifier
            .nestedScroll(pullToRefreshState.nestedScrollConnection)
            .nestedScroll(scrollBehavior.nestedScrollConnection)
)

Scaffold じゃなくても PullToRefreshを囲んでいるBoxのModifierに書いても動作した。何かしら違いはあるかも。

感想

nestedScrollをチェインして書けるの知らなかったし、順番も気にしたことなかったな。気づけて良かった。

Jetpack Compose:特定の条件のときだけClickableにしたい

内容

あるコンポーサブルをにclickable属性を付与したいが、条件によってはoffにしたい。
クリックイベントを空 {}にしてもタップエフェクトが発生してしまう。どうすればよいか。

対応

  • Modifier.clicable()のenabled パラメータを使う
@Composable
fun SomeText(onClick: (() -> Unit)? = null) {
    Row(modifier = Modifier.clickable(enabled = onClick != null){ onClick?.invoke() }) {

    }
}

@Composable
fun OtherText(enabled: Boolean, onClick: (() -> Unit) = {}) {
    Row(modifier = Modifier.clickable(enabled = enabled){ onClick() }) {

    }
}

enabledがfalseになっていればclickアクションは反応せず、タップエフェクトも発生しない。

注意点

この方法だと反応(Ripple Effect)はしなくなるもののイベントそのものは拾ってしまうので 親のComposable側でclickableを定義してあっても無視されてしまう。

それを避けたい場合は run関数などを使って非nullの場合にだけclickableを定義する方法がある。

Box(
        modifier = modifier.fillMaxWidth().run {
            onClick?.let {
                clickable { onClick() }
            } ?: this
        }
    ) {
}

というかもうこうしてModifier.clickableでもnullを許容しちゃえばいい気がしてきた。

fun Modifier.clickable(onClick: (() -> Unit)?): Modifier {
    return run { onClick?.let { clickable(onClick = it) } ?: this }
}

その他

昔書いたコード(上の注意点)が良くなかったと思い修正しようとしたが記事が見当たらなかった上に クリックイベント拾わないケースに気づいて結局完全にはなくせないことが判明。

Jetpack Compose:Tab, Pagerでページが変更されたことを検知する

内容

TabLayoutとPagerの組み合わせはAndroidではお約束だが、Tab(TabRow, TabColumn)とPager(HorizontalPager, VerticalPager)それぞれのComposableにはページが変更された、といういい感じの関数をセットする方法がない。どうすればよいか。

対応

Tab側

TabはTabコンポサーブル自身がonClickを受け取るようになっているのでそれを使う。タブが再選択されたとかは自分で制御するしかなさそうだ。

// 実装例
TabRow(){
    Tab(onClick = {
                  // タブがクリックされた
         }
    )
}

Pager側

Pager側のイベント検知はPagerStateの状態変化をウォッチすることで行う。

   val pagerState: PagerState = rememberPagerState(initialPage) { pageCount }
    LaunchedEffect(pagerState) {
        snapshotFlow { pagerState.currentPage }.collect { page ->
            // ページが(スワイプなどで)変更された
        }
    }

リファクタリング(Tab)

TabはTabが選択されたのと再選択を検知できるようにしてみる

// 適当。大体こんな感じで
@Composable
TabRowWrapper(
    pagerState: PagerState,
    onTabSelect: (Int) -> Unit = {},
    onTabReselect: (Int) -> Unit = {},
){
  TabRow(indicator = { tabPositions ->
            TabRowDefaults.Indicator(Modifier.pagerTabIndicatorOffset(pagerState, tabPositions))
      }){
      Tab(onClick = {
                    if (pagerState.currentPage == index) {
                        onTabReselect(index)
                    } else {
                        onTabSelect(index)
                    }
                }
      )
  }
}

呼び出し側でpagerState.animateScrollToPageしてあげる。 まあこの処理自体はTabRowWrapperに直接書いてもいいかもしれない

TabRowWrapper(
    onTabSelect = { position ->
                    coroutineScope.launch {
                        pagerState.animateScrollToPage(position)
                   }
    }
)

リファクタリング(Pager)

各ScreenのComposableに上記を記述するのは 面倒なのでPagerStateを生成する処理をまとめて呼べるようにしてみる

@Composable
fun createPagerState(
    initialPage: Int = 0,
    onTabChange: (Int) -> Unit = {},
    pageCount: Int = 0
): PagerState {
    val pagerState: PagerState = rememberPagerState(initialPage) { pageCount }
    LaunchedEffect(pagerState) {
        snapshotFlow { pagerState.currentPage }.collect {
            onTabChange(it)
        }
    }
    return pagerState
}

これで呼び出し側は1行で書けるようになった。

val pagerState: PagerState = createPagerState(0, onTabChange, pageCount)

参考

developer.android.com