Studyplus Engineering Blog

スタディプラスの開発者が発信するブログ

DroidKaigi 2022にスポンサー&参加&登壇しました

こんにちは。 久々に朝の電車に乗りました。モバイルクライアントグループ テックリードの若宮(id:D_R_1009)です。

DroidKaigi 2022お疲れ様でした! 10月5日から7日にかけて、フルで楽しむことができました。 まさかお会いできるとは、という方なんかともお話しできて、とてもエネルギーに満ちています。


今回「Add-to-appの戦い方」と題しまして、スタディプラスで運用しているAdd-to-appの知見を発表しました。 本ブログでは、セッションの補足として、直面した課題に対応している方法などを紹介します。

Add-to-appの戦い方

Day 2の16:05〜16:30のセッションに登壇しました。 セッションの様子については、DroidKaigiのYoutubeチャンネル上で公開される予定です。

droidkaigi.jp

speakerdeck.com

スタディプラスとFlutter、Add-to-app

セッション内で紹介している通り、スタディプラスはFlutterを比較的長く採用している企業です。

speakerdeck.com

tech.studyplus.co.jp

そして、2021年末から提供を始めた「Studyplusブック読み放題」にて、Add-to-appを本番環境で採用しました。

info.studyplus.co.jp

また、つい先日提供を開始した機能においても、Add-to-appを採用しています。

info.studyplus.co.jp

このような背景があるため、Add-to-appの採用や活用に関して、経験を積むことができました。 セッションでは、この経験をもとに、Add-to-appの"良い活用"について紹介しています。

Add-to-appのメリット、デメリット

Add-to-appを活用すると、既存のアプリケーションを維持したまま、AndroidとiOSに新機能を追加できます。 複数のプラットフォームに対して、少ない開発コストで、高速にリリースできる仕組みは非常に魅力的です。 こうした、開発速度と既存アプリケーションへの影響の少なさが、Add-to-appのメリットと言えます。

一方でFlutterを採用する以上に、Add-to-appを採用することは技術的に難しい挑戦をすることになります。 これが、Add-to-appを安易にお勧めできないデメリットです。

docs.flutter.dev

go_router によるrouting

セッションで紹介した通り、Flutter Engineのキャッシュ利用はほぼ必須です。 このため、Add-to-appで開く画面を、Add-to-appの外部から制御する必要があります。

スタディプラスでは、この問題をMethod Channelとgo_routerで実現しています。


前提として、スタディプラスでは現在、Add-to-appで表示するべき画面が2つあります。 ここでは、仮に moviemusic の2つの機能を表示する処理を書きます。

まず、下記のようなroutesを作ります。

final routes = [
  GoRoute(
    path: '/movie',
    pageBuilder: (context, state) => MaterialPage(
      key: state.pageKey,
      child: const MovieScreen(),
    ),
  ),
  GoRoute(
    path: '/music',
    pageBuilder: (context, state) => MaterialPage(
      key: state.pageKey,
      child: const MusicScreen(),
    ),
  ),
];

続いて、このrouteを持つRouterをProvideし、Method Channelを実装するクラスが参照できるようにします。

final routerProvider = Provider(
  (_) => GoRouter(
    routes: routes,
  ),
);
final moduleRepositoryProvider = Provider(
  (ref) => ModuleRepository(
    router: ref.watch(routerProvider),
  ),
);
class ModuleRepository {
  ModuleRepository({
    required this.router,
  }) {
    _channel.setMethodCallHandler(_handleMessage);
  }
  final _channel = const MethodChannel('com.example.app/module');
  final GoRouter router;
  @override
  void dispose() {
    _channel.setMethodCallHandler(null);
    super.dispose();
  }
  Future<dynamic> _handleMessage(MethodCall call) async {
    switch (call.method) {
      case 'route':
        router.go('${call.arguments}');
        break;
      /// 他の処理を実装
    }
  }
}

あとは、Method Channelで外部から「今表示したい」画面をリクエストできるようにします。 Method Channelのやりとりを行うクラスに、下記のようなメソッドを追加するイメージです。

    private fun navigateToMovie() {
        channel.invokeMethod("route", "/movie")
    }
    private fun navigateToMusic() {
        channel.invokeMethod("route", "/music")
    }
    private func navigateToMovie() {
        methodChannel.invokeMethod("route", arguments: "/movie")
    }
    private func navigateToMusic() {
        methodChannel.invokeMethod("route", arguments: "/music")
    }

これらのメソッドを呼び出した後に、Add-to-appの画面へ遷移することで、表示したい画面を切り替えられます。 なおiOSの場合には、Method Channelへのリクエスト後にdelayが必要かもしれません。 Androidでは問題が起きにくいのですが、iOSでは画面の切り替え前に処理をした場合、1frame程度のdelayを入れないと反映が完了していないことがありました。 細かくOSの違いが影響するので、十分な動作チェックをしましょう。

SwiftPMによるxcframeworksの結合

Add-to-appにおいて、Flutterのモジュールをネイティブに読み込ませる方法はいくつかあります。 しかし、環境変数をAdd-to-appのコードの中で利用する場合、利用できる仕組みは限られてきます。

Add-to-appの運用において、一番厄介な問題は「検証環境と本番環境のリクエスト先サーバー切り替え」です。

Flutterにおける、debugrelease はネイティブアプリケーションの文脈における「開発環境」と「本番環境」ではありません。 Flutterのコードをデバックするための、ビルドモードの違いと解釈した方が良いものです。 このため、Add-to-appのビルド時に debugrelease のどちらかを選択することは、開発環境と本番環境のどちらを向いているのかと一致しません。

Flutterのアプリケーションの場合、この処理はビルド時の引数に dart-define を指定することで解決します。 しかし、Add-to-appではこの処理が最近になって実装されたものであり、改善の余地がいくつかあるものになります。

github.com

今回は、iOSのプロダクトにおける dart-define の扱い方を紹介します。


iOSでは flutter build ios-framework を利用することで、dart-define の指定ができます。 このため、導入ドキュメント的には、BかCのoptionを選択することになります。

docs.flutter.dev

しかし、iOSアプリケーションではSwift Package Managerの採用をおこなっていることも多く、CocoaPodsへの依存を強めたくはありません。 Option Cを選択すると、「Add-to-app内でも参照しているライブラリは、全てCocoaPodsで管理する」必要が生じます。 CocoaPodsはビルドに時間がかかりがちなため、すでにSwift Package Managerへ移行し切っているプロジェクトでは、少し悲しい気持ちになります。

そこで、今回はSwift Package Managerのbinary targetを利用する方法を紹介します。

developer.apple.com

まずAdd-to-appを、module_flutterというディレクトリで開発します。 そして、Swift Package Managerとして認識できるように、Package.swift ファイルを作成します。

/
  Studyplus.xcodeproj
  /Studyplus
    /App
  /module_flutter
    Package.swift
    create_framework.sh
    pubspec.yaml
    pubspec.lock
    /lib

まず、frameworkを作成するための create_framework.sh を次のように指定します。

#!/bin/sh -x
rm -rfv build
flutter pub get
flutter build ios-framework --output=build --no-debug --no-profile

このようにすると、releaseビルドでbuildディレクトリ内に最新のframeworkが作成されるようになります。 ディレクトリが指定されているので、次のような設定を Package.swift に記載します。

// swift-tools-version:5.3
import PackageDescription
let items = [
    "App",
    "firebase_auth",
    "firebase_core",
//    "FirebaseAuth",
//    "FirebaseCore",
//    "FirebaseCoreDiagnostics",
//    "FirebaseCoreInternal",
    "Flutter",
    "FlutterPluginRegistrant",
    "FMDB",
//    "GoogleDataTransport",
//    "GoogleUtilities",
//    "GTMSessionFetcher",
//    "nanopb",
    "shared_preferences_ios",
]
let package = Package(
    name: "module_flutter",
    defaultLocalization: "en_us",
    platforms: [
        .iOS(.v11)
    ],
    products: [
        .library(
            name: "module_flutter",
            targets: items
        ),
    ],
    dependencies: [
        .package(url: "https://github.com/firebase/firebase-ios-sdk.git", .exact("9.5.0")),
    ],
    targets: items.map({ name in
        Target.binaryTarget(
            name: name,
            path: "build/Release/\(name).xcframework"
        )
    })
)

あとは、XcodeのSwift Package Managerにて、/module_flutter/Package.swift を認識させればOKです。 なお、一部のライブラリ(FirebaseAuthなど)はネイティブアプリケーション側でも利用しているため、依存関係を再整理するためにコメントアウトしています。 重なっているライブラリがある場合、Swift Package Managerが複数の依存があるときに解決ができなくなるため、注意してください。

おわりに

簡単ではありますが、セッションについてコード的に紹介しました。 Add-to-appを採用することがあれば、ぜひ参考にしていただければと思っています。

Add-to-appは非常に便利なツールですが、上記のような工夫が必要になります。 これらの対応は参考にできる情報が少なく、手探りで検討が必要になりがちです。 Add-to-appのメリットとデメリットについて、本ブログが把握の助けになれば幸いです。