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

同じ様なUIをiOS/Android間で合わせるヒントNo.2

 同じ様なUIをiOS/Android間で合わせるヒントNo.2

Swift/Kotlin愛好会 #51での登壇資料になります。

今回も前回と同様にiOS/Android間で類似した様な実装を進めていく上で、いくつかの実例を元にしてiOS/Android両方を効率良く進めるための観点・ヒントや着想を得るポイントとなり得る部分に関して簡単に紹介できればと思います。

今回もLayout処理にまつわるロジックや考え方の部分について、Androidアプリ開発での事例を基に一見すると難しそうな画面構築に関するポイントを整理しています。

※過去の登壇内容も参考にして頂けると幸いです。

● 同じ様なUIをiOS/Android間で合わせるヒント
https://speakerdeck.com/fumiyasac0921/androidjian-dehe-waseruhinto

Fumiya Sakai

April 26, 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. 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での実装を理解するのに苦戦した
  3. まずはAndroidのStaggeredGridLayoutManagerで考える Pinterestの様なWaterfallGrid型のLayoutが実現可能なので使えるかを検証 結論: StaggeredGridLayoutManagerだけでは実現が難しそうであった… 🙆 OK 🙅 NG 列の指定が基準になる点がポイント val

    staggeredGridLayoutManager = StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL) recyclerView.layoutManager = gridlayoutManager 1列目 2列目 列が定まらない ❌ 第1引数: 行数
  4. 実はこう考えてみると心が軽くなりました 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に予め格納すれば良い。
  5. 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 計算は別途必要
  6. 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()で取得して利用する。 ※実装ポイント③
  7. 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
  8. 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個分の中に広告作品が混ざっている形となっている
  9. OnScrollListener利用時と比較してのメリット CoroutineやFlowの対応はもとより、正確な読み込み状態に応じた処理が可能 OnScrollListener スクロールの最下部へ到達し た際に次のデータを読み込む 処理を実行。 PagingSourceに読み込みや状態管理を委譲できる: OnScrollListenerを利用時は自分で状態管理が必要 ※1. 場合により、onScrollStateChangedやonScrolled等も利用する。

    ※2. 判定条件を算出するための値の関係性が難しい場合もある。 (参考1) https://hiropoppo.hatenablog.com/entry/kotlin_recipe/endless override val foodPhotoStream: Flow<PagingData<FoodPhoto>> by lazy { Pager(PagingConfig(pageSize = 24, initialLoadSize = 24)) { useCase.foodPhotoPagingSource() }.flow.cachedIn(viewModelScope) } Flowとの相性はOnScrollListenerより良い: (参考2) https://devblog.thebase.in/entry/2021/09/01/110000 (参考3) https://techblog.yahoo.co.jp/entry/2023022130414554/
  10. 補足: UICollectionViewCompositionalLayout利用時 private func createExampleSectionLayouts() -> NSCollectionLayoutSection { // 1.

    Itemのサイズ設定 // MEMO: 全体幅2/3の正方形を作るために左側の幅を.fractionalWidth(0.67)に決める let twoThirdItemSet = NSCollectionLayoutItem(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.67), heightDimension: .fractionalHeight(1.0))) twoThirdItemSet.contentInsets = NSDirectionalEdgeInsets(top: 0.5, leading: 0.5, bottom: 0.5, trailing: 0.5) // MEMO: 右側に全体幅1/3の正方形を2つ作るために高さを.fractionalHeight(0.5)に決める let oneThirdItem = NSCollectionLayoutItem(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalHeight(0.5))) oneThirdItem.contentInsets = NSDirectionalEdgeInsets(top: 0.5, leading: 0.5, bottom: 0.5, trailing: 0.5) // MEMO: 1列に表示するカラム数を2として設定し、Group内のアイテムの幅を1/3の正方形とするためにGroup内の幅を.fractionalWidth(0.33)に決める let oneThirdItemSet = NSCollectionLayoutGroup.vertical(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.33), heightDimension: .fractionalHeight(1.0)), repeatingSubitem: oneThirdItem, count: 2) // 2. Groupのサイズ設定 // MEMO: leadingItem(左側へ表示するアイテム1つ)とtrailingGroup(右側へ表示するアイテム2個のグループ1個)を合わせる let group = NSCollectionLayoutGroup.horizontal(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalHeight(0.33)), subitems: [twoThirdItemSet, oneThirdItemSet]) // 3. Sectionのサイズ設定 let section = NSCollectionLayoutSection(group: group) // MEMO: HeaderとFooterのレイアウトを決定する let headerSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(44)) let header = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: headerSize, elementKind: UICollectionView.elementKindSectionHeader, alignment: .top) section.boundarySupplementaryItems = [header] section.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 0, bottom: 16, trailing: 0) return section } ※この形になります Item/Group作成に注意
  11. まとめ iOS/Androidの両方を実装を見比べて見ると、実はそう遠くない部分もある 今後ともメインはiOSに置きながらもAndroidについてもアウトプットができる様に頑張ります。 1. 言語・コンポーネント・アーキテクチャの違い等はあるけど類似点・共通点を探してみる: 実装のイメージが類似している場合には実装方針を見抜くチャンスと捉える様にしています。個人的にはこれまでの経験の中か ら「実装のテイストが似ている部分」を見つけ出す様にするために、現在は両方進んでコードを読む様にしています。 2. iOS/Android間で明らかに考え方が異なる部分は念入りに仕様調査をする: 以前の業務でも苦戦した部分はDIコンテナ関連処理と動画再生関連部分でした。両方進んでコードを読み進めていく際に、考え

    方が大きく異なる部分については、サンプル実装をしているOSS等をヒントに基本理解を進める様にしています。 3. AndroidのPaging3内の処理やレイアウト改修を通じた知見と収穫: Paging3を利用した際には、自前でこの部分を実装する必要があるiOSとは大きく異なる点を押さえておく事で、大体の工数把握 や実装前のデザイナーとの調整等の場面で役立つ場面もあったので、この様な部分は引き続き大切にしていきたいです。