💠

Compose Multiplatformを既存プロジェクトに部分適用する方法について

2023/10/25に公開

はじめに

こんにちは。
株式会社アイスタイルで@cosmeアプリのAndroidエンジニアをしている鈴木と申します。
最近は、激辛にハマって鬼殺しという唐辛子を何でもかけて食べています。

今回は、Compose Multiplatform[1]を@cosmeアプリにて、既存の画面で部分適用することがあったため似たようなケースで参考になるのではないかと思い、急遽筆を取ることといたしました。
ここでいう部分適用というのは、画面全体ではなく画面中の一部のViewにのみ適用する手段のことを指します。
Compose Multiplatformを既存のプロジェクトにて、一部のViewだけでも適用したいと考えている皆様に、役立つ情報であればと思います。(この記事の情報は、2023/10時点での情報です。)

なお、Compose Multiplatformを既存プロジェクトに取り組んだ際の全体設計やハマったポイントなどについても記事にしています。もしご興味あれば、こちらもご覧になっていただければ幸いです。

https://zenn.dev/istyle/articles/eca6ccf62ae759

既存プロジェクトに部分的に組み込む方法

既存プロジェクトに組み込む方法としては、次の3点を実装することで実現できます。

  1. Shared側にて、Viewまでの実装を一通り作成する
  2. Android側にて、ComposeViewを利用して、Sharedで作成したViewをそのまま利用して表示する
  3. iOS側にて、UIHostingControllerを利用して、Sharedで作成したViewをそのまま利用して表示する

Shared側実装

Shared側での実装は、組み込みたいViewをComposeで組み込むだけです。
サンプルでは、単純な画像とテキストのColumnを作成して表示するViewを作ります。
今回は、Kamel[2]というライブラリを使用して画像を表示するサンプルを作成します。
サンプルコードは次の通りです。

@Composable
fun SampleScreen() {
  Column(
    modifier = Modifier.fillMaxSize()
  ) {
    KamelImage(
      asyncPainterResource("https://www.example.com/image.png"),
      contentDescription = "",
      onLoading = {
        ProgressIndicator()
      },
      onFailure = {
        Image(
          painter = painterResource(
            imageResource = MR.images.error_img
          ),
          contentDescription = "",
          modifier = modifier
        )
      },
    )
    Spacer(modifier = Modifier.height(8.dp))
    Text(
      text = "test"
    )
  }
}

KamelImageを利用する場合、画像の通信中の表示はonLoading内で、通信中に表示したいComposableを記載します。ProgressIndicatorは自作したローディング表示用のComposableになります。前半で紹介した記事内にて、詳細を記載しておりますので、ここでは割愛します。
また、Error時の表示は、onFailureが呼ばれ、ここにエラー時に表示したいComposableを記載します。今回はMoko Resources[3]を併用してエラー時の表示を実現しています。
プロダクト実装で、よくある通信して画像を表示し、エラーの場合はアプリ側で持っているローカル画像を表示するという流れをKamel + Moko Resourcesで実現できるため、現状の画像周りの処理はこの組み合わせがオススメです。

Android側に受け渡す部分は、androidMainのMain.ktにて次のようにしてください。

@Composable
fun SampleView() = SampleScreen()

iOS側に受け渡す部分は、iOSMainのMain.ktにて次のようにしてください。

@Composable
fun SampleViewController() = ComposeUIViewController { SampleScreen() }

Android側実装

Androidで利用する場合は、通常のComposeを利用する場合とそう変わりません。
今回は既存プロジェクトで部分的に組み込みたい場合かつxmlでレイアウトを構築している場合を想定していますので、ComposeViewを利用することで実現していきます。

サンプルコードは次のとおりです。まずは、xml側です。

<LinearLayout
  android:layout_width="match_parent"
  android:layout_height="wrap_content"
  android:orientation="vertical">
  ...
  <androidx.compose.ui.platform.ComposeView
    android:id="@+id/test_view"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    />
  ...
</LinearLayout>

サンプルでは、LinearLayoutで書かれていますがConstraintLayoutでもRelativeLayoutでも問題なく使用できます。ComposeViewをただそのまま追加するだけです。

今回はRecyclerViewのAdapter内部で利用するような場合を想定しているので、その場合に部分的に利用する際のKotlinのコードは次のようになります。

binding.testView.setContent {
  SampleView()
}

通常のComposableを部分利用する場合と同じで、ComposeViewのsetContent内部に先で紹介したComposableをそのまま記載するのみです。

iOS側実装

iOSで利用する場合は、UIHostingControllerを利用します。UIKitを利用していても、UIHostingControllerを併用することで容易にCompose Multiplatformで作成したViewを利用することができます。
今回は、UITableViewにてセルを一つ追加したい場合を想定しています。
StoryboardでUITableViewCellを作成し、対応したクラスを作成します。
サンプルコードは次のとおりです。

import UIKit
import SwiftUI
import Shared

class RecommendAppViewCell: UITableViewCell {

    @IBOutlet private weak var mainView: UIView!

    override func awakeFromNib() {
        super.awakeFromNib()
        let host = UIHostingController(rootView: SampleView())
        mainView.addSubview(host.view)
        // Add AutoLayout Setting
    }
}

ここで出てきているSampleViewは、この後に説明するSwiftUIで作成したViewになります。
UIHostingControllerを利用して、UIViewであるmainViewにaddSubViewで追加します。
あとはよしなにAutoLayout周りの設定をするのみです。

次に、SwiftUIでShared側にて作成したComposableを表示する部分を作成します。サンプルコードは次のとおりです。

import SwiftUI
import Shared

struct SampleView: View {
    var body: some View {
        SampleViewControllerRepresentable()
    }
}

struct SampleViewControllerRepresentable: UIViewControllerRepresentable {
  func makeUIViewController(context: Context) -> UIViewController {
    return MainKt.SampleViewController()
  }
  func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
  func dismantleUIViewController(_ uiViewController: UIViewController, coordinator: ()) {}
}

makeUIViewControllerにて、Shared側でiOSに受け渡す部分として作成したCompsable関数を呼び出しViewControllerとして返します。初回に実行したい処理がある場合は、ここで一緒に行うのが良いです。
SwiftUI側も単純に表示するのみです。Compose Multiplatformを利用していても通常のUIKit + SwiftUIを組み込む方法と変わらず利用することができます。

おわりに

いきなり画面全体で適用していくのが難しい場合であっても、一部のViewにCompose Multiplatformを組み込むことは可能です。まずは、部分的に利用していきたい方もいらっしゃるのではないかと思い、この記事が誰かの参考になれば幸いです。

この記事を読んで、Compose Multiplatformを使った開発に興味を持った方、もしくは開発に参加したい方は、下記の応募リンクからご応募いただけます。一緒に素晴らしいプロジェクトを作っていきませんか?ご応募お待ちしております。

Androidエンジニア

https://open.talentio.com/r/1/c/isytyle_career/pages/43022

iOSエンジニア

https://open.talentio.com/r/1/c/isytyle_career/pages/43019

脚注
  1. iOS、Android、デスクトップ、ウェブといった複数のプラットフォームにて、Jetpack Composeを利用できる仕組みです。(https://www.jetbrains.com/ja-jp/lp/compose-multiplatform/) ↩︎

  2. Kamelは、現時点でCompose Multiplatformに対応している非同期メディア読み込みライブラリです。(https://github.com/Kamel-Media/Kamel) ↩︎

  3. Moko Resourcesは、現時点でCompose Multiplatformに対応しているローカルリソース周りをサポートしているライブラリです。アプリ側で持っている画像の表示や、文字列、フォント、ファイルなどへのアクセスを容易にしてくれます。現状プロダクトの要件によって変わってくるとは思いますが、画像の表示での利用が便利です。(https://github.com/icerockdev/moko-resources) ↩︎

株式会社アイスタイル

Discussion