JetpackComposeのリストでスクロール位置を保持する

こんにちはid:numanuma08です。Jetpack Composeしていますか?ときどきApply Code Changesをするとそのままアプリがクラッシュする現象も発生していますが(スタックトレースには何もでない)、AndroidのUI生成・管理が非常にやりやすいですね。

今回はJetpack Composeのリストでスクロール位置を保持しようと思ったけどできなくてハマっていた事例を紹介します。

何故かスクロール位置が保持されない

Jetpack Composeのドキュメントを見るとColumnやRowをスクロールさせるときrememberScrollStateをModifierに渡せば画面遷移や再コンポーズが実行されてもスクロール位置が保持されると書いてあります。またLazyColumnやLazyRowはrememberLazyListStateを引数に渡すと同様にスクロール位置が保持されます。ちなみに、rememberLazyListStateはデフォルト引数で指定されています。

developer.android.com

現在、Jetpack Composeの学習のために作っているアプリでもこの仕組を使って非同期に取得したデータを画面にLazyColumnで表示、スクロール位置を保持しようと思ったのですがなぜか画面遷移後にスクロール位置がリセットされてしまっていました。

ちなみに表示しているデータはIMDBが公開しているデータセットにOMDBから取得したポスター画像データです。

www.imdb.com

www.omdbapi.com

この画面は縦方向にジャンル情報をLazyColumnで、横方向にそのジャンルの映画タイトルをネストさせたLazyRowで表示しています。

f:id:numanuma08:20210903203637p:plain

結局何がおかしいのかよくわからず、データ構造をイジったり色々こねくり回していましたがどうにか解決できました。

リストの高さをfixさせる必要がある

画面遷移のたびに再コンポーズが発生しますが、そのとき動的にレイアウトを生成するため高さが後から決定しますが高さが不定の状態ではスクロール位置への移動ができないようです。そのため、今回のコードは次のようになりました。

// ViewModelからStateFlowを通知する
val genres by viewModel.allGenres().collectAsState()

LazyColumn {
  items(genres) { genre ->
    // ViewModelからStateFlowを通知するが、ViewModel内でキャッシュしたものを利用する
    val posters = viewModel.postersInGenre(genre).collectAsLazyPagingItems()
    // リストの行で描画する要素の高さを決定しておく
    Colum(modifier = Modifier.height(240.dp)) {
      Text(genre.title)
      LazyRow {
        items(posters) { poster ->
          // ポスター画像を表示
        }
      }
    }
}
      }

動作はこんな感じになります。

振り返ってみれば大したこと無い内容でしたが、ハマっているときは全く前に進まなくて焦っていました。