gikyu's memo

気が向いたら書いていきます?

2023年にやったJetpackComposeの対応を少しだけ紹介します

去年に引き続き、アドベントカレンダーの季節がやってきてました! ということでこれは 🎅GMOペパボエンジニア Advent Calendar 2023 - Adventar の16日目の記事です。 昨日は minne の同僚のnobuさんの記事でした。クリスマスツリーすごい、業務とは全然違ったことをやってるし、何よりプログラミングで楽しんでいて刺激になりました!(語彙力なくてすみません)。

hibinokoto.hatenadiary.jp

てことで、自分は何を書こうか色々迷っていたのですが、今年はAndoridエンジニアになって2年目でminneのいくつかの画面を JetpackCompose で実装したので、JetpackCompose 関連でやった作業をの内容を少〜し紹介いたします。😂

どんなものやったのか

フォロー一覧画面の書き換え

minneではユーザーのフォローリストを確認できる画面があります。 フォローしているユーザーとフォローされているユーザーの一覧を確認できる画面で、タブがあり、タブを切り替えるとユーザーの一覧を切り替えることができる画面になっています。 イメージとしてはこちらのような感じです。

Image from Gyazo

すでにAndroidViewで実装されているもので、 TabRowTabHorizontalPagerを組み合わせて書き換え実装を行いました。

プロダクトコードから簡略化されてはいますが、だいたい大枠としては以下のようなコードになりました。

private enum class Tab(val tabName: String) {
    TAB1("tab1"),
    TAB2("tab2"),
}

@Composable
fun HogeListScreen(
    state: State = rememberState(),
    coroutineScope: CoroutineScope = rememberCoroutineScope(),
    modifier: Modifier = Modifier.fillMaxSize()
) {

    val pagerState = rememberPagerState(initialPage = Tab.TAB1.ordinal)

    Column(modifier = modifier) {
        TabRow(
            selectedTabIndex = pagerState.currentPage,
            backgroundColor = Color.LightGray,
            contentColor = Color.Yellow,
        ) {
            Tab.values().map { it.tabName }.forEachIndexed { index, tabName ->
                Tab(
                    text = { Text(text = tabName, fontWeight = FontWeight.Bold) },
                    selected = pagerState.currentPage == index,
                    onClick = {
                        coroutineScope.launch {
                            pagerState.animateScrollToPage(index)
                        }
                    },
                    selectedContentColor = Color.Red,
                    unselectedContentColor = Color.Blue
                )
            }
        }

        HorizontalPager(
            count = Tab.values().size,
            state = pagerState
        ) { pager ->
            when (pager) {
                Tab.TAB1.ordinal -> {
                    UserListSection(
                        users = state.users,
                        onRowClick = { state.onUserClick(it.id) },
                        modifier = modifier
                    )
                }

                Tab.TAB2.ordinal -> {
                    UserListSection(
                        users = state.users,
                        onRowClick = { state.onUserClick(it.id) },
                        modifier = modifier
                    )
                }
            }
        }
    }
}

少しポイントを紹介

①Tabの中の選択・未選択状態でのインジケーターや文字色の設定はTabRowとTabで別々に設定

TabRow(
            selectedTabIndex = pagerState.currentPage,
            backgroundColor = Color.LightGray,
            contentColor = Color.Yellow,
        ) {
            Tab.values().map { it.tabName }.forEachIndexed { index, tabName ->
                Tab(
                    text = { Text(text = tabName, fontWeight = FontWeight.Bold) },
                    selected = pagerState.currentPage == index,
                    onClick = {
                        coroutineScope.launch {
                            pagerState.animateScrollToPage(index)
                        }
                    },
                    selectedContentColor = Color.Red,
                    unselectedContentColor = Color.Blue
                )
            }
        }

Tab 全体の色は backgroundColor で設定し、選択された際のインジケーターの色は contentColor で設定します。 選択時の Tab の文字色は Tabの selectedContentColor に Color を設定し、逆に未選択時のTab の文字色は Tab の unselectedContentColor に Color を設定します。 最初、TabRow の contentColor で選択・未選択時の色の設定ができると思っていたのですが、TabRow 内で定義されたTabで直接文字色を設定する必要があり少し迷ったのでこちらは注意です。

②Tabタップ時にそのTabに遷移する方法

TabRow では、PagerState に保持されたindex値を変更することで、タブの切り替えが可能です。 rememberPagerState で PagerState を作成し、その際 initialPage を設定することで初期Indexを決定できます。 Tab を enumで定義して、以下のように enum の index を取得して、initialPage として渡しています。

val pagerState = rememberPagerState(initialPage = Tab.TAB1.ordinal)

そして、タップの切り替えは pagerState を animateScrollToPage で切り替えます。 animateScrollToPage はsuspend関数なので、coroutineScope を呼び出す必要があり、scope内で animateScrollToPage を呼び出します

coroutineScope.launch {
    pagerState.animateScrollToPage(index)
}

③HorizontalPagerでページを切り替える。

HorizontalPager も pagerState をページの切り替えに使用するので、TabRow で使用した Pager State を渡します。 Horizontal Pager 上の pagerState の index の切り替えは、自動でやってくれるようなので特に Tab のように手動で切り替えるようなことをする必要はありません。 切り替えた際に PagerScope として、page の Index 値が返ってくるので、その index 値で表示したい View を宣言してあげます。

HorizontalPager(
            count = Tab.values().size,
            state = pagerState // こちらにTabRowで使用したPagerStateを渡す。
        ) { pager ->
            when (pager) {
                Tab.TAB1.ordinal -> { 
                      // ここにindexごとに表示したいViewを宣言する
                 }
                Tab.TAB2.ordinal -> { 
                      // ここにindexごとに表示したいViewを宣言する
                 }
            }
        }

AndroidView だったらこの View を作るのにやることが多くて大変ですが、JetpackCompose だと本当に簡単にできて感動です。🥹

SwipeRefresh の置き換え作業

SwipeRefreshって?

Accompanist で提供されてた SwipeRefresh の置き換え作業。 SwipeRefresh(Swipe to Refresh)っていうのはこんなやつ。よく見たことあると思いますが、画面の上から下にスワイプすることで画面が更新されるやつです。

Image from Gyazo

そして、Accompanist が何か少し補足すると、Accompanist は、Jetpack Compose をサポートする補助ライブラリの集まりです。 Jetpack Compose はまだ新しく、Android の従来の UI フレームワークのいくつかの機能は公式にはまだ提供されていません。 Accompanist は、このギャップを埋めます。つまり、Android View システムで使われていた機能を Jetpack Compose 内で簡単に使用できるようにするための橋渡しをしてくれるのが、Accompanist です。

Accompanist の Swipe Refresh を確認すると、deprecated の記載があり、公式から提供されたので、そちらを使ってくださいって記載がある、そして丁寧に migration guide も書いてあります。

google.github.io

今までのコードはこんな感じ

シンプルにリフレッシュしたい View を SwipeRefresh でラップしているだけの実装になってます。 すごいわかりやすい、実装ですよね。

val viewModel: MyViewModel = viewModel()
val isRefreshing by viewModel.isRefreshing.collectAsState()

SwipeRefresh(
    state = rememberSwipeRefreshState(isRefreshing),
    onRefresh = { viewModel.refresh() },
) {
 // リフレッシュしたいView
    LazyColumn {
        items(30) { index ->
            // TODO: list items
        }
    }
}

MigrationGuideを確認してみる

そして、migration guide に載っているコードはこちら。今まで SwipeRefresh を宣言してラムダの中にリフレッシュさせたい View を記述していたのが、Box を宣言して、refreshState を Box の modifier に渡して、 さらに PullRefreshIndicator を宣言する形に変わってます。

val viewModel: MyViewModel = viewModel()
val refreshing by viewModel.isRefreshing

val pullRefreshState = rememberPullRefreshState(refreshing, { viewModel.refresh() })

// Box に pullRefreshState を渡している
Box(Modifier.pullRefresh(pullRefreshState)) {

    LazyColumn(Modifier.fillMaxSize()) {
        ...
    }

    // こちらも新たに追加されてた
    PullRefreshIndicator(refreshing, pullRefreshState, Modifier.align(Alignment.TopCenter))
}

Composable 関数を定義する

今まで SwipeRefresh と宣言していた部分に Box と PullRefreshIndicator を新たに宣言するのは大変そう(めんどくさい)、かつこれで Swipe to Refresh できるのかわかりづらい。だったら新たに SwipeRefresh という Composable 関数を宣言すれば差分もそこまでなくできそうってことで改めてこんな Composable 関数を作りました。何にも難しいことはしていないです。

@OptIn(ExperimentalMaterialApi::class)
@Composable
fun SwipeRefresh(
    refreshing: Boolean,
    onRefresh: () -> Unit,
    indicatorColor: Color = Color.gray,
    indicatorModifier: Modifier = Modifier,
    modifier: Modifier = Modifier,
    content: @Composable () -> Unit,
) {

    val refreshState = rememberPullRefreshState(
        refreshing = refreshing,
        onRefresh = onRefresh
    )

    Box(modifier = modifier.pullRefresh(refreshState)) {

        content()

        PullRefreshIndicator(
            refreshing = refreshing,
            state = refreshState,
            contentColor = indicatorColor,
            modifier = indicatorModifier.align(Alignment.TopCenter),
            scale = true
        )
    }
}

で最終的に使う時はこんな感じ、変わったのは、SwipeRefresh の引数が state から refreshing に変わったぐらい。そのまま SwipeRefresh を使えていい感じです!

val viewModel: MyViewModel = viewModel()
val isRefreshing by viewModel.isRefreshing.collectAsState()

SwipeRefresh(
    refreshing = isRefreshing,
    onRefresh = { viewModel.refresh() },
) {
    LazyColumn {
        items(30) { index ->
            // TODO: list items
        }
    }
}

どんどん公式の実装が出ており、Accompanist もDeprecatedが増えてきたので、どんどん更新していきたいですね!🚀(どんどん多いな)

最後に

てことで、今年実装した Jetpack Compose 関連の紹介でした。 まだまだ Jetpack Compose で実装した画面はたくさんあるので、紹介できたらよかったのですが、書いてると結構なボリュームになりそうなのでまたどこかで紹介します!

最近は新規の画面は基本的に Jetpack Compose で実装しており、だんだんと慣れ書くのが楽しくなってきたので、もっと Jetpack Compose と仲良くなって、既存の画面も Jetpack Compose に置き換えていきたいと思っています。

明日の17日目の記事は一体どんな内容なのか楽しみです!

minneのAndroid開発者になるために知っておくと良さそうなこと(リンク集)

この記事はGMOペパボエンジニア Advent Calendar 2022の22日目の記事になります!

自分は今年の3月からiOSエンジニアから転身してGMOペパボAndroidエンジニアとしてminneを開発させてもらってます。 minneのAndroidエンジニアとして活躍するべく、まだまだ勉強することは山積しているのですが、自分と同じように新たにAndroidエンジニアをやってみたい、それもminneを開発したいって方を対象に 知っていると良さそう、かつ個人的にお世話になったものをリンク集にしてまとめてみました。

はじめに

まず2022年現在のminneのandoridの開発環境がまとまっているので、こちらを一読ください! こちらを見ればminneのAndroidエンジニアになるためにはどんな技術を学べば良いかサクッとわかるかと思います!

tech.pepabo.com

Android関連の情報リソース

Andridエンジニアになるには当然、Androidに関して知る必要がありますよね? Andridエンジニア目指したことがある人にとっては知ってて当然のものではあるとおもいますが、 以下から有益情報を得ることができるので参考にどうぞ。

developer.android.com

  • AndroidDagashi
    Android関連のニュースを得ることができます。 RSS リーダーなどを使って常に最新情報を取得できるようにしておくのをオススメします。

androiddagashi.github.io

  • DroidKaigi
    DroidKaigiはAndroidのカンファレンスで、毎年開催されており、オンラインやオフラインでAndroidエンジニア同士のコミュニケーションの場になっています。 Andorid開発のその時ホットな技術トピックなど知ることができます。

droidkaigi.jp

  • Now in Android
    KotlinJetpack Composeで完全に構築されたAndroidアプリ。Androidの設計と開発をベストプラクティスに従っており、開発者にとって役立つリファレンスとなることを目的としている。とのこと。 有志により開発されているアプリで、Github上からリポジトリが見れるので、開発に貢献してみたい、モダンなアーキテクチャのアプリを見てみたい方は一度確認してみると良いと思います。 また最近Google Play Storeからダウンロードできるようになったので、ぜひ触ってみてください

github.com

Android開発を学べるコンテンツ

Andoidを学ぶのに有益なコンテンツをまとめてみました。 実際に手を動かして学べるものも多いので、ぜひ見てください。

  • Android Developersのトレーニングコース
    Android Developersにハンズオン形式のトレーニングコースがあります。 レベル毎にプログラミング初心者から経験者まで学べる形になっており、内容もかなり充実しています。

developer.android.com

  • Codelab
    小規模なアプリを作成するプロセスや、既存のアプリに新しい機能を追加するプロセスなどが学べます

codelabs.developers.google.com

  • Android DevelopersのYouTube
    Android DevelopersのYouTubeチャンネルです。新たな機能がでた際に、動画を通して学べるので登録しておくと便利です。

www.youtube.com

  • PhilippLacknerさんのYouTube
    Androidの開発の技術トピック(DataBinding、ConstraintLayout、Kotlin、AAC (Android Architecture Components)、Jetpack、Kotlin Coroutineなど)からTipsまで幅広く学べるチャンネルです。 英語にはなりますが、字幕などを使えばわからないとこともないので、ぜひ視聴してみてください。

www.youtube.com

  • モケラボ株式会社のYouTubeチャンネル
    Android開発のYouTube動画は英語のコンテンツが多いですが、こちらは日本語でAndroid開発の基礎が学べます。 初心者でCodelab触ったけど、よくわからなかったって方におすすめです。

www.youtube.com

  • Kodeco
    モバイル開発を学ぶのに特化した学習コンテンツのプラットフォームです。 サブスクリプション形式で少し値段が高めではありますが、書籍や動画など優良なコンテンツが豊富なので、ぜひ一度見てみてください。

www.kodeco.com

minneで使われているフレームワーク・ライブラリを知る

minneで使われているフレームワーク・ライブラリの一部を共有します。

Kotlin Coroutine

minneではKotlin 内での非同期処理には Coroutines を用いており、マストで知るべき技術トピックになります。

Android での Kotlin コルーチン  |  Android デベロッパー  |  Android Developers

Kotlin の Coroutine を概観する - Qiita

Dagger Hilt

minneではDI(Dependency Injection)用のライブラリとしてDagger Hiltを利用しています。 DIはプログラミング初心者の方だと概念から理解するのに少しとっつきにくいかもしれないですが、 単体テストが容易になるなど、何かと便利なので、これもマストで知るべき技術トピックになります。

Android アプリでの Hilt の使用  |  Android デベロッパー  |  Android Developers

Dagger Hilt (DevFest 2020 資料) - Qiita

Jetpack Compose

新規の画面はJetpack Composeで実装することを、開発方針として掲げており、 既存画面もJetpack Composeに書き換えていく予定なので、こちらもマストで知るべき技術トピックです

Android Compose のチュートリアル  |  Android デベロッパー  |  Android Developers

HTTP リクエス

minne では REST API では Retrofit2 を利用していますが、今後 Web APIは GraphQL で実装することを開発方針としており、 こちらも理解しておくべき技術トピックになります。

柔軟、堅牢なアプローチを提供するGraphQL minne AndroidがFacebook製クエリ言語を採用した理由 - ログミーTech

Epoxy

minneではRecyclerViewを使ってはいますが、Adapter などに独自で処理を実装する必要があり、実装する上で面倒な処理も多いためそれを解消してくれる Epoxyというライブラリも使っています。 新たな画面はJetpack Composeを用いることが望ましいとしていますが、Epoxyで実装されている画面もまだまだ多く理解しておくとベターです。

複雑な画面をRecyclerViewで作るEpoxy - Android - まっせぎ blog

minneで採用されているアーキテクチャを知る

minneでは、基本的にMVVMを採用しており、minneのアーキテクチャに関する記事があるので、 ぜひそちらをご確認ください。

Android アーキテクチャ コンポーネント  |  Android デベロッパー  |  Android Developers

Android Meetup イベントレポート - Pepabo Tech Portal

デザインに関して

Material Design

マテリアルデザインとは、Googleが推奨する明確なガイドラインが定められたデザインであり、 「見やすく、直感的に操作できるWebページ・サービス」を作ることを目的としており、新規画面はマテリアルデザインを踏襲した画面になるので、 エンジニアも知るべき物になります。

マテリアルデザインとは?歴史とトレンドから考える今後のUIデザイン - WEB改善事例集(GMOソリューションパートナー株式会社)

Figma

minneではデザインツールとしてFigmaを使用しており、 デザイナーさんとのコミュニケーションツールになるのでエンジニアも使い方を知っておくのがベターです。

エンジニアのための Figma 知識

CI / CD

※追記予定

最後に

どうだったでしょうか? こうしてみると結構知っておくべきことが多いなぁって思います。 リンクを並べただけですが、minneのAndoridエンジニアとして9ヶ月ほどやってきて役に立った記事や情報などを共有させていただきました。 これからAndroidアプリ開発を始めたい方、特にminneのAndroid開発をしたいって方の参考になれば幸いです。 また、minneの開発にもし興味がありましたらこちらの採用ページもご覧ください。