Upgrade to Pro — share decks privately, control downloads, hide ads and more …

複雑なUI実装の壁を越えるための考え方事例紹介 (iOS/Android間で実装を合わせるヒント)

複雑なUI実装の壁を越えるための考え方事例紹介 (iOS/Android間で実装を合わせるヒント)

7月23日に開催された「ししとうLT #4 -壁LT会-」での登壇資料になります。

これまでに何度か過去の登壇で行ってきた「iOS/Android両方を効率良く進めるための観点の紹介」の総集編になります。

Fumiya Sakai

July 23, 2024
Tweet

More Decks by Fumiya Sakai

Other Decks in Technology

Transcript

  1. 自己紹介 ・Fumiya Sakai ・Mobile Application Engineer アカウント: ・Twitter: https://twitter.com/fumiyasac ・Facebook:

    https://www.facebook.com/fumiya.sakai.37 ・Github: https://github.com/fumiyasac ・Qiita: https://qiita.com/fumiyasac@github 発表者: ・Born on September 21, 1984 これまでの歩み: Web Designer 2008 ~ 2010 Web Engineer 2012 ~ 2016 App Engineer 2017 ~ Now iOS / Android / sometimes Flutter
  2. この様な画面実装を題材として処理を考えてみます 検索結果画面表示の中に広告作品を一定の規則性を持たせて表示する場合 検索結果 … 広告結果 … この画面での表示規則: 検索結果表示内に広告が ミックスされた形になる。 1.検索結果は4行2列で並ぶ

    2.広告結果は1行2列で並ぶ 必ず規則が担保できるか? 1ページあたりの最大数: 1.検索結果は32件表示 2.広告結果は8件表示 ① 検索結果32件/広告結果5件: (1)広告結果<検索結果÷4 or (2)広告結果+検索結果=(奇数) の場合 ② 検索結果9件/広告結果3件: UICollectionViewやRecyclerViewのでの並び順を実現する際に綺麗 に見せるための調整が必要になる場合もある。 検索結果画面は表示処理時にページネーションを伴う場合が多い。 1.ページの終端に到達した場合 2.検索結果は少ないが広告結果が多く取得できた場合 実装時に考慮したいポイントと考え方: Array<T>やList<T>で表示対象データを調整しやすい形にしたい iOSのDiffableDataSourceを組み立てる様にAndroid側も整えたい
  3. 表示対象のDataSourceに対する処理方針を決定する 検索結果と広告結果を上手に分割&マージをして調節する処理がポイント Cell要素 or ViewHolder要素1つ分に表示したいデータを準備する際の考え方: (1) 現在検索結果に表示される作品が広告結果を含めて奇数個の場合には、検索結果作品を1つ埋める処理をして、 できるだけ広告結果が1行2列に並ぶ配慮をする 条件に合致した場合は、Nページ当たりの検索結果を格納する配列から先頭要素を取得&削除を実行し、その要素を並べ替え結 果を格納する配列の先頭に追加する。

    (2) 1行2列広告作品、その次に4行2列検索結果作品、以降繰り返しとなる様に対象データを分割する 1.検索結果配列:[[検索結果8個分], [検索結果8個分], ... ] 2.広告結果配列:[[広告結果2個分], [広告結果2個分], ... ] 3.1.&2.で求めた結果でchunkedした組の個数が多い方を全体のLoop処理回数に設定する (3) 最大Loop回数の1行2列広告作品、その次に4行2列検索結果作品を追加するための処理を実行する
  4. 題意を満たす並び順を実現する処理部分の抜粋 取得できたデータに対してchunk処理を利用した後に表示データへ変換する chunkedSearchProductsGroup .getOrNull(loopIndex)?.let { … 追加処理 … } (Kotlin)

    chunked(size: Int): https://kotlinlang.org/api/latest/jvm/ stdlib/kotlin.collections/chunked.html Kotlinの場合は下記の様になる: Swift ⇔ Kotlin内で内部処理ロジックを読み替えられそうな余地を探す chunk処理がこの表示をするポイント: (Swift) chunks(ofCount: Int): Array要素を指定個数のかたまり分割する
  5. iOS・Androidではどの様な方針を取り得るか? UIKit・SwiftUIでも方針は異なる&Android View・Jetpack Composeでも同様 iOS: UIKit/SwiftUI・Android: Android View/Jetpack Compose で実装する際のアイデアをまとめてみる

    (1) iOS (2) Android UIKitを利用する場合 Jetpack Composeを利用する場合 UICollectionViewLayoutを継承した独自クラスの実装 UICollectionViewCompositionalLayoutを活用した実装 構成方針が全く異なっていた LazyColumnとPaging3を利用した実装…?? Grid型のLayoutを実装したOSS等を活用した実装…?? 普段から扱い慣れていたのでイメージがすぐ掴めた 🌾 UICollectionViewCompositionalLayoutを利用 最初は全くイメージが湧かずに困惑してしまった 😗 特にPaging3での実装を理解するのに苦戦した
  6. まずはAndroidのStaggeredGridLayoutManagerで考える Pinterestの様なWaterfallGrid型のLayoutが実現可能なので使えるかを検証 結論: StaggeredGridLayoutManagerだけでは実現が難しそうであった… 🙆 OK 🙅 NG 列の指定が基準になる点がポイント val

    staggeredGridLayoutManager = StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL) recyclerView.layoutManager = gridlayoutManager 1列目 2列目 列が定まらない ❌ 第1引数: 行数
  7. 実はこう考えてみると心が軽くなりました 3パターンのSectionがある形だと考えてみてはどうだろうか? 1つのSectionでのLayoutと考えてしまった事が原因でした: 1 Section have 3 items. … Section

    … Item この様に1つのSection単位で考える様にすると考えやすくなると思います。 この様に考えるメリット UICollectionViewCompositionalLayoutを利用した形での実装方針は立てやすい。 3個入りでLayoutが異なる3パターンのSectionと捉える(最悪3個で1つのCell要素と考えるのもOK) 宣言的UI(SwiftUI・Jetpack Compose)だともっと考えやすくなる 部品構造自体もSwiftUI・Jetpack Composeを利用する事で直感的に組み立てられる。 ログ送信等でデータ取得順番が必要な場合はView表示用で利用するObjectに予め格納すれば良い。
  8. Section要素の概要とレイアウトを組み立てる際の概要 全体で見ると難しそうだが要素を丁寧に分解してみると考えやすくなる 表示要素3つを引数で渡す想定で考えてみると良い: Jetpack Compose階層構造: ConstraintLayout - AsyncImage (左側の大きな画像) -

    HorizontalDivider (縦線をPaddingに合わせて) - AsyncImage (右上側の小さな画像) - Divider (横線をPaddingに合わせて) - AsyncImage (右下側の小さな画像) (参考) SwiftUI階層構造: HStack (with Padding) - AsyncImage (左側の大きな画像) - VStack (with Padding) - AsyncImage (右上側の小さな画像) - AsyncImage (右下側の小さな画像) この様に1つの要素内に6 つの部品があると考える イメージを持つと良い。 https://fvilarino.medium.com/creating-a- dynamic-grid-in-jetpack-compose-35f6cb71fd55 https://www.youtube.com/watch?v=txQqhuSZ9xQ SwiftUI 2.0 Compositional Layout - Instagram Feed Layout - SwiftUI Tutorials Width・Height 計算は別途必要
  9. Paging3を利用したPagination処理と組み合わせる Paging3を利用したアーキテクチャの概要とデータ取得処理を結合する箇所 Screen View 公式Documentで推奨しているPaging3利用時のArchitecture: https://developer.android.com/topic/libraries/architecture/paging/v3-overview ViewModel UseCase PagingSource Repository

    Infrastructure 自分が経験した際のArchitecture概要: UseCase内部でPagingSourceを 初期化する様な形にする。 ※実装ポイント① ※個人的に事前に知っておくと良さそうな部分 1. データ取得処理を実際に実行する場所はどこ? PagingSouce内部でRepositoryクラスの処理を実行する。 Pagination処理はPagingSouceにお任せするイメージ。 2. Flow<PagingData<T>>部分の処理は何をしている? 取得したPagingをFlow型で返す&Paging処理の設定をする。 PagingSourceで取得結果と広 告結果をMergeして返却する。 ※実装ポイント② ViewModel内ではFlow<PagingData<t>>の作成処理をする。 画面では.collectAsLazyPagingItems()で取得して利用する。 ※実装ポイント③
  10. class FoodPhotoViewModel( private val useCase: FoodPhotoUseCase ): ViewModel() { …

    ※今回はpaginationに関する処理を抜粋しています override val foodPhotoStream: Flow<PagingData<FoodPhoto>> by lazy { Pager(PagingConfig(pageSize = 24, initialLoadSize = 24)) { useCase.foodPhotoPagingSource() }.flow.cachedIn(viewModelScope) … } Paging3を利用したPagination処理例と表示関連部分 Section単位でのList要素がFlowで返却される&Section値でレイアウトが決定 (参考1) https://tech.pepabo.com/2021/10/18/android-paging3/ (参考2) https://speakerdeck.com/ticktaku77/shi-jian-paging-3 @Composable fun CatListScreen(viewModel: CatViewModel) { val pagingItems = viewModel.foodPhotoStream.collectAsLazyPagingItems() SwipeRefresh( state = pagingItems.loadState.refresh is LoadState.Loading, onRefresh = { pagingItems.refresh() } ) { LazyColumn { // Section毎のView要素を表示する … ※以降にエラー発生時のHandling処理を追加する } } } Section: 0 Section: 1 Section: 2 Section: 3 ① LeadingLarge ② TrailingLarge ③ SmallSet 表示データを3つ格納した List要素を更にListに格 納したものが返却される Sectionを選出基準はどの様に定める? ① Section / 4 = X とする。 ② Y = X % 2 の結果で判定する。 Y = 0 Y = 1
  11. PagingSource内部で実行するデータ取得〜Merge処理 取得結果と広告結果を取得した後に特定Index値の部分を広告結果に置き換える Kotlinの「chunked(size: Int)」を利用する事でSection用データを作成する: (1) まずは従来通りの写真取得結果 + 広告結果をRequest Coroutineを利用して表示に必要なデータを取得する。 (2)

    Page番号(現在何Page目か?)を基にして該当Index値を広告作品に置換 (3) 広告作品を混ぜた一覧List要素をchunkedする ※1. 広告結果が取得できない時は、そのまま写真取得結果を表示する。 ※2. 複数のRequestが必要な場合には、ZipないしはCombineLatest等を活用する。 奇数Page番号 偶数Page番号 Section要素内の格納順番と取得できた写真取得結果のIndex値を考慮する ※1. 24件より少なく該当Indexがない場合は置換処理は実施しない 1Page分の表示内容リスト:[[取得結果3個分], [取得結果3個分], … ] ※1. 取得結果3個分の中に広告作品が混ざっている形となっている
  12. まとめ iOS/Androidの両方を実装を見比べて見ると、実はそう遠くない部分もある 今後ともメインはiOSに置きながらもAndroidについてもアウトプットができる様に頑張ります。 1. 言語・コンポーネント・アーキテクチャの違い等はあるけど類似点・共通点を探してみる: 実装のイメージが類似している場合には実装方針を見抜くチャンスと捉える様にしています。個人的にはこれまでの経験の中か ら「実装のテイストが似ている部分」を見つけ出す様にするために、現在は両方進んでコードを読む様にしています。 2. iOS/Android間で明らかに考え方が異なる部分は念入りに仕様調査をする: 以前の業務でも苦戦した部分はDIコンテナ関連処理と動画再生関連部分でした。両方進んでコードを読み進めていく際に、考え

    方が大きく異なる部分については、サンプル実装をしているOSS等をヒントに基本理解を進める様にしています。 3. 業務で活用しているGraphQLの利用も個人的には大きな後押しになっている: GraphQLやApolloを業務内で活用している事に加えて、最近では積極的にRailsコードも読む機会が増えました。サーバーとの通 信部分等を一緒に考える余地を増やしていく様にする事で、より生産性を高めてスピードアップにも繋がると考えています。