Atrae Tech Blog

People Tech Companyの株式会社アトラエのテックブログです。

Yenta Android版にJetpack Composeを導入しました

こんにちは!ビジネス版マッチングアプリYentaのエンジニアをしている@bull です。

私は2021年7月にアトラエにジョインし、YentaのAndroid版の開発・運用・保守を担当しています。

YentaのAndroid版では2021年11月から各画面を少しずつJetpack Composeで書き換えています。

そこで導入に至った経緯について説明していこうと思います!

※導入から暫く経ちますが、新機能との兼ね合いもあり、最近は着手ができていないです

Jetpack Compose導入に至った経緯

Yentaの機能が増えていくにつれ、フラグメントの切り替え周りが複雑になっていたり、Viewの変更が複雑になってきているという状況でした。

そこで、保守の観点でリファクタリングを行わないといけないと判断しました。

その1つとしてJetpack Composeを導入するかどうかという話が出てきました。

アトラエではWebのプロダクトが多いため、ReactやFlutterなどの宣言型UIに慣れた人の割合が多くいます。

Greenでは、より生産性の高い技術組織になることを考え、Webフロントとモバイルアプリの実装を行き来できるようにモバイルアプリの開発にFlutterを導入しました。

atraetech.hatenablog.com

Yentaでも同様に、お互いの行き来がしやすく、Greenのように生産性の高いエンジニア組織になるため、WebフロントとAndroidの開発を両方とも宣言的UIで行えるよう、このタイミングでJetpack Composeの導入を決断しました。

導入してみてよかったこと

Viewの切り替えが楽

これまでは以下のように画面(Fragment)の表示・非表示でremoveしてaddするという操作を行っていました。

   private fun switchFragment(state: PageState) {
        val transaction = childFragmentManager.beginTransaction()

        var mainFragment = childFragmentManager.findFragmentByTag(MainFragment.TAG) as? MainFragment
        if (state == PageState.MAIN) {
            if (mainFragment == null) mainFragment = MainFragment.newInstance()
            transaction.add(R.id.fragment_container, mainFragment, MainFragment.TAG)
        } else mainFragment?.let {
            transaction.remove(it)
        }

        var signUpFragment =
            childFragmentManager.findFragmentByTag(SignUpFragment.TAG) as? SignUpFragment
        if (state == PageState.SIGN_UP) {
            if (signUpFragment == null) signUpFragment = SignUpFragment.newInstance()
            transaction.add(R.id.fragment_container, signUpFragment, SignUpFragment.TAG)
        } else signUpFragment?.let {
            transaction.remove(it)
        }

        var loginFragment = childFragmentManager.findFragmentByTag(LoginFragment.TAG) as? LoginFragment
        if (state == PageState.LOGIN) {
            if (loginFragment == null) loginFragment = LoginFragment.newInstance()
            transaction.add(R.id.fragment_container, loginFragment, LoginFragment.TAG)
        } else loginFragment?.let {
            transaction.remove(it)
        }
    }

これをJetpack Composeでは、removeしなくても呼び出したいComposableを呼び出すだけで良いので直感的な画面の切り替えが可能になりました。

when (screen) {
    PageState.MAIN -> {
        MainScreen(
            …
        )
    }
    PageState.SIGN_UP -> {
        SignUpScreen(
            …
        )
    }
    PageState.SIGNED_IN -> {
        LogInScreen(
            …
        )
    }
}

Preview、LiveEditが便利

Preview、LiveEditはとても便利です。

これまでViewを確認をしたい場合は基本的にアプリをBuildしてViewの確認をしていたと思います。

xmlでも確認できると思いますが、xmlによっては確認できないものもあるでしょう。

Preview機能を使えば、アプリ全体をbuildしなくてもUIの確認ができるのも魅力の一つです。 1つのファイル内でいくつものPreviewをつけることも可能なので、様々なパターンを網羅したい場合にも役に立ちます。

最近ではLive LiteralsからLiveEditに変更になりました。 これらはbuildせずとも変更を即座に適応してくれるホットリロード機能です。

Live Literalsでは文字の大きさ変更、テキスト変更くらいしか対応していなかったのですが、Live Editでは、コンポーネントを配置した時にも反映されるようになりました。 これを使うことにより、効率良いUIの構築が可能になるでしょう。

備考:Android Developers, Twitter

Viewが以前より隠蔽された

これまではActivity、Fragmentごとにパーツをそれぞれ作っており、ViewBindingを用いてActivity、Fragment内で値をセットしていました。

この場合、Viewを直接いじれてしまうので、どのプロパティをいじるのかView側で制御できませんでした。 Jetpack Composeに変換すれば、引数でPropsを渡すことが可能になるため、引数でどのプロパティをいじりたいか指定することが可能になります。

それにより、Activity、Fragmentからむやみにコンポーネントを変形することができない状態となり、保守性が担保されるようになります。

つまり、Activity、Fragmentからは引数を渡してあげるだけでコンポーネントを作成できるのです。

@Composable
fun SignUpLayout(
    onClickTermDetail: (uri: String) -> Unit,
    onClickPolicyDetail: (uri: String) -> Unit,
    buttonContents: @Composable () -> Unit
) {
    WelcomeDescription(stringId = R.string....)
    HeightSpacer(dp = 24.dp)
    buttonContents()
    HeightSpacer(dp = 24.dp)
    LoginHelpDescription(onClickTermDetail, onClickPolicyDetail)
    HeightSpacer(dp = 36.dp)
}

また、Atomic Designのように階層に分けてコンポーネントを分けて依存関係を片方向にすることで、一つ当たりのコンポーネントの責務も少なくなりました。

その結果、Activityが知ることは必然的に少なくなり、コンポーネントの記述量も減って開発しやすくなりました。

ネストを考える必要がない

Compose ではネストされたレイアウトを効率的に処理できるため、このようなレイアウトを利用した複雑な UI 設計が可能になります。Android View ではパフォーマンス上の理由でネストされたレイアウトを避ける必要がありましたが、Compose ではこの点が改善されています。

引用:AndroidDevelopersより

この文章より、これまでxmlを用いていた時はパフォーマンスのことを考え、ConstraintLayout、RecyclerViewなどの重たいレイアウトのネストを避けて実装していましたが、その懸念がなくなるのは助かります。

備考:パフォーマンスとビュー階層 | Android デベロッパー

結果的にパフォーマンスも少しずつですが改善してきています。

苦労したところ

ドキュメントが少ない

やはり、xmlと比べてドキュメントの数が少ないので苦労しました。

Android Developersにない情報は、Twitterを駆使して探したり、Youtubeで動画を探したりもしました。 大抵の場合、海外の英語ドキュメントに書かれているので、日本語の検索でヒットしなかった時は英語で調べてみることをお勧めします。

コンポーネント再生成のタイミング

どのタイミングでコンポーネントが再生成されるのかといった再コンポーズの理解を深めていかないと、無駄な処理が走ってしまってJetpack Composeの良さを活かせないです。

これまでの考え方でいくと親のコンポーネントで更新処理が走ると、子のコンポーネントも更新処理が走ると一見思うでしょう。

でも実際は更新が行われたコンポーネントのみ更新されるため、子のコンポーネントの再コンポーズはスキップされます。 そして、親のコンポーネントで変更した値を引数として子に渡している場合は再コンポーズされます。

例えば、以下のような親と子のコンポーネントがあるとします。

fun NameBox(){
    var name by remember { mutableStateOf("")}
    var description by remember { mutableStateOf("")}
    var birthday by remember { mutableStateOf("")}

    Button1(onClick = {name += "Name "})
    Button2(onClick = {birthday += "BirthDay "})

    NameText(text = name)
    DescriptionText(text =description)
}

NameBox()を呼び出すと全てのコンポーザブルが再コンポーズされると思いきや、 変更が加わったコンポーネントのみが再コンポーズされます。

詳細を言うと、nameに変更が加わった場合は、Button1とNameTextが再コンポーズされ、 descriptionに変更があった場合、DescriptionTextだけが再コンポーズされます。

このように無駄なところで再コンポーズをし、パフォーマンスを下げないようにするという部分に気を遣いました。

つい最近では、再コンポーズまたはスキップした頻度をLayout Inspectorから確認できるようになったので有効活用しようと思います。 興味ある方は以下からキャッチアップしてみてください。

備考:Get recomposition counts

細かい部分

やはり、実装していて何度も気づくことがたくさんありました。 最初にAndroid Developersなどを熟読するべきだったと後悔しました。

以下は最初から気づいておけば良かったと思った一部です。

  • Element関数はModifier型の引数を持ち、名前をmodifierで、最初のオプショナルパラメータでなければならない
  • 再コンポーズは可能な限りスキップする
  • StateとEventを明確に分ける
  • UI ロジックと UI 要素の状態を管理する状態ホルダーを明確に分ける
  • ViewModel インスタンスを他のコンポーザブルに渡してはいけない
  • 拡張性を備えた最小単位のコンポーネント設計

Jetpack Composeの学習について

Jetpack Composeの学習としてどうすれば良いか迷う人がいると思います。 そこで一例として、私の勉強方法を紹介します。

私は以下の手順で勉強しました。

  • Android Developersを見る
  • Sampleアプリに目を通す
  • 作ってみる
  • 課題にぶつかったらググる

※あくまで私の勉強方法です

Android Developersを見る

やはり、公式のAndroid Developersはとても分かりやすいです。 Jetpack Composeの知識としては以下のサイトを参考にしました。

最近ではCodelabも始まったので目を通してみると良いかもしれません。

Jetpack Compose CodeLab

Sampleアプリに目を通す

Sampleアプリについては有名どころで言うと以下になると思います。

作ってみる

やはり実践あるのみです。 やらなければ分からないことも多いと思うので、試しにアプリを作成した方が近道だと思います。 ただし、やる前に基本的な部分は押さえておきたいので、先ほど説明したこの2つは読んでおくとスムーズかと思います。

課題にぶつかったらググる

最近では情報は増えてきましたが、やはり情報量は少ない方です。 特に日本語ですと記事の量はかなり少ないので、検索する際は英語で調べることをオススメします。

初心者あるあるとして

  • ググるにもどう調べれば良いかが分からない
  • 言いたいことを言葉にできない

このようなことがあると思います。 そんな時の私の解決方法は以下の通りです。

  • 何を実行したいのかを詳細に記述して検索
  • 実現したいものをクラス名(パーツ)とJetpack Composeをセットにして検索
    • 例: BottomNavigationViewのJetpack Composeはあるのか
  • Logcatを読んでエラーの原因の説明文を検索
  • stack overflow、TwitterGitHubのIssueで検索

ぜひ参考にしてみてください。

まとめ

現在のyentaでは新しい施策が多く控えているため、時間がある時に少しずつ移行を進めています。

既存機能の改修では、xmlでレイアウトが組まれている部分が多く、そこをJetpack Composeへリプレイスすると、複雑な仕様の理解しなければならず、またコード量が純粋に多くなってしまうため、新規で作る画面やシンプルな画面での実装しています。

いきなり全画面の導入は難しいかもしれませんが、今後新規でアプリを作る場合は全画面導入を視野に入れているので、少しずつ移行を進めていこうと思います。

Jetpack Composeの移行には私が紹介した他にもまだまだ価値があります。 小さな画面でも良いので徐々に進めていくのもアリかと思いますので、 まだCompose化を進めていない方は是非検討してみてください!

最後までお読みいただきありがとうございました!