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

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

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

Duplicate class対応:Google Play Coreライブラリの分割

内容

androidx.navigation関連のライブラリを2.6.0にあげたところDuplicate classエラーが発生した。

> A failure occurred while executing com.android.build.gradle.internal.tasks.CheckDuplicatesRunnable
   > Duplicate class com.google.android.play.core.common.IntentSenderForResultStarter found in modules jetified-core-1.10.3-runtime (com.google.android.play:core:1.10.3) and jetified-core-common-2.0.2-runtime (com.google.android.play:core-common:2.0.2)
     Duplicate class com.google.android.play.core.common.LocalTestingException found in modules jetified-core-1.10.3-runtime (com.google.android.play:core:1.10.3) and jetified-core-common-2.0.2-runtime (com.google.android.play:core-common:2.0.2)
     Duplicate class com.google.android.play.core.common.PlayCoreDialogWrapperActivity found in modules jetified-core-1.10.3-runtime (com.google.android.play:core:1.10.3) and jetified-core-common-2.0.2-runtime (com.google.android.play:core-common:2.0.2)
     Duplicate class com.google.android.play.core.listener.StateUpdatedListener found in modules jetified-core-1.10.3-runtime (com.google.android.play:core:1.10.3) and jetified-core-common-2.0.2-runtime (com.google.android.play:core-common:2.0.2)
     Duplicate class com.google.android.play.core.splitcompat.SplitCompat found in modules jetified-core-1.10.3-runtime (com.google.android.play:core:1.10.3) and jetified-feature-delivery-2.0.1-runtime (com.google.android.play:feature-delivery:2.0.1)
     Duplicate class com.google.android.play.core.splitcompat.SplitCompatApplication found in modules jetified-core-1.10.3-runtime (com.google.android.play:core:1.10.3) and jetified-feature-delivery-2.0.1-runtime (com.google.android.play:feature-delivery:2.0.1)
以下略

依存関係をチェックすると、Google PlayのCoreライブラリが関係していることが判明。Google Playのライブラリはどこかの時点で機能ごとに分割されたらしい。これまで通りにcoreライブラリをそのまま使っていたが、別ライブラリが依存関係を更新した時点で競合が発生する(した)ので注意。

対応

参考ページを確認し、coreライブラリを削除して使っている機能だけ選んで追加する

-    implementation("com.google.android.play:core:1.10.3")
-    implementation("com.google.android.play:core-ktx:1.8.1")
+    implementation("com.google.android.play:feature-delivery-ktx:2.1.0")
+    implementation("com.google.android.play:app-update-ktx:2.1.0")

参考

developer.android.com

PagingDataのテストデータを作成する

内容

Jetpack ComposeでPagingライブラリのレスポンスであるFlowを使ったComposableを Preview、テストしたい。どうすればよいか。

対応

Version 3.0.0-alpha04 から PagingData.from(List<T>) が使えるようになったのでこれを使う。

// 本の一覧があるとする
val books: List<Book> = listOf(Book(title="クラインの壺"), Book(title="利己的な遺伝子"))

// PagingData化
val booksData: PagingData<Book> = PagingData.from(books)

// Flow<PagingData>化
val booksFlow: Flow<PagingData<Book>> = flowOf(booksData)

// LazyPagingItems化
val pagingItem: LazyPagingItems<Book> = booksFlow.collectAsLazyPagingItems()

感想

ちゃんとComposableのプレビューが表示されることは確認済み。 これでComposableのPreviewやテストで任意のテストデータ(リスト)から作成できるようになった。 これはかなり便利。

参考

stackoverflow.com