LayerX エンジニアブログ

LayerX の エンジニアブログです。

【特別対談】 Flutterエンジニアの今オレ x iOSエンジニアの過去オレ

こんにちは。バクラク申請・経費精算チームでモバイルエンジニアをしている @_chocoyama です。社内のラジオ好きコミュニティに属しているのですが、自分の推し番組を紹介したところ誰にも刺さらず、コミュニティに属しているのにソロ活動している今日このごろです。

この記事はLayerXテックアドカレ2023の29日目の記事です、前回は Tomoaki さんが「バクラクのAI-OCRを支える性能モニタリングの仕組み #LayerXテックアドカレ - LayerX エンジニアブログ」を書いてくれました。 本日の記事では、Flutterアプリを開発している現在の私(以降、今オレ)と、iOSネイティブアプリを開発をしていた過去の私(以降、過去オレ)が対談した内容となっています。

Flutterに対してふわっとしたイメージしかないネイティブアプリエンジニアの皆さんの参考になると幸いです。

ご挨拶

過去オレ:こんにちは、chocoyamaです。本日はどうぞよろしくお願いします。

今オレ:こんにちは、chocoyamaです。本日はどうぞよろしくお願いします。
過去オレさんはFlutterの解像度が低い部分も多いと思いますので、今日は何でも聞いてください。

過去オレ:ありがとうございます、それでは自己紹介から。私は2023年6月現在、iOSのネイティブアプリエンジニアをしております。UIはSwiftUIで作っており、Flutterはちょこちょこ触ったことがある程度です。

今オレ:私の方も簡単に。現在はバクラク申請・経費精算のモバイルアプリ開発をFlutterで進めております。現行アプリがWebViewのガワアプリのため、ほぼフルスクラッチで書いている状態でまだ未リリースです。

なぜFlutterにしたのか

過去オレ:早速ですが、なぜFlutterを採用したのですか?

今オレ:現状のチーム状況やプロダクトの複雑性を考慮して選定しました。明日チームメイトの @kikuchy さんから関連記事が出る予定なので、是非そちらも参考にしてみてください。

過去オレ:気になります。今オレさんの世界では明日記事が読めるのに、自分は待たないといけないの辛いです。

今オレ:たしかに過去オレさんが記事を読めるのは半年後ぐらいですね(笑)。楽しみに待っていてください!

Flutter開発の所感

過去オレ:実際に開発を始めてみて、どんな使い心地ですか?

今オレ:コードを書き始めたのは8月中旬ごろなのですが、とても生産性が高いと感じています。開発中のプロダクトは複雑性が高く必要な実装量も膨大であるため、複数プラットフォームの実装を1ソースで行えるメリットは非常に大きいですね。

過去オレ:言語がDartである点やフレームワーク・エディタに至るまでiOS開発とは環境が大きく異なると思いますが、その辺りは問題なかったですか?

今オレ:はい、大きな問題にはなりませんでした。SwiftUIとFlutterは宣言的UIで実装するという根本の部分が同じであるため、「どう作るか」の設計思考はそのまま使えています。SwiftUIに限らずJetpack ComposeやReact, Vueの経験者であれば、比較的学習コストが低いのではないでしょうか。

アーキテクチャ

過去オレ:iOSアプリを作る時はTCAやMVVMあたりが多い気がしますが、Flutterだとどんなアーキテクチャで開発していますか?

今オレ:イメージ的には、「MVVM」と「グローバルStateを扱うReducer的な仕組み」を組み合わせている感じでしょうか。それぞれflutter_hooksRiverpodという技術を駆使して実装しています。

過去オレ:どういった使い分けをしているのですか?

今オレ:flutter_hooksは「UI単体のローカルStateを扱う場合」に利用し、Riverpodは「複数のUIを跨ぐグローバルなStateを扱う場合」に利用しています。詳細な解説は割愛しますが、以下のようなイメージですね。

状態管理の構成

(flutter_hooksのカスタムフックのサンプルコード)

/// SomePage単体でのみ利用するローカルStateの定義
typedef SomePageState = ({
  int count,
});

/// SomePage単体で発生する状態変化イベントの定義
typedef SomePageAction = ({
  VoidCallback onTapIncrement,
  VoidCallback onTapDecrement,
});

/// flutter_hooksの機能を活用して状態の保持や変更を行う
({SomePageState state, SomePageAction action}) useSomePage() {
  final count = useState(0);
  return (
    state: (count: count.value),
    action: (
      onTapIncrement: useCallback(
        () => count.value += 1,
        const [],
      ),
      onTapDecrement: useCallback(
        () => count.value -= 1,
        const [],
      ),
    )
  );
}

/// 定義したカスタムフックを呼び出す
class SomePage extends HookWidget {
  const SomePage({super.key});

  @override
  Widget build(BuildContext context) {
    final (:state, :action) = useSomePage();

    return Scaffold(
      body: Center(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            Text("count: ${state.count}"),
            ElevatedButton(
              onPressed: action.onTapIncrement,
              child: const Text("increment"),
            ),
            ElevatedButton(
              onPressed: action.onTapDecrement,
              child: const Text("decrement"),
            ),
          ],
        ),
      ),
    );
  }
}

(Riverpodを扱ったのサンプルコード)

/// グローバルで扱うStateの定義とProviderの作成
///(状態変化の方向を1方向にするため、直接Stateにはアクセスできないようにしている)
@freezed
class UserState with _$UserState {
  factory UserState({
    @Default(User()) User user,
  }) = _UserState;
}

final _userStateProvider =
    AutoDisposeStateProvider<UserState>((ref) => UserState());

/// ReadonlyなStateを参照するためのProvider定義
@riverpod
UserState userReader(UserReaderRef ref) => ref.watch(_userStateProvider);

/// Stateを変更するための純粋関数
void setUserModifier(
  Reader read, {
  required User user,
}) =>
    read(_userStateProvider.notifier).update(
      (state) => state.copyWith(user: user),
    );

/// 定義したProviderを呼び出す
class SomePage extends ConsumerWidget {
  const SomePage({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final state = ref.watch(userReaderProvider);
    return Scaffold(
      body: Center(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            Text("name: ${state.user.name}"),
            ElevatedButton(
              onPressed: () =>
                  setUserModifier(ref.read, user: const User(name: "updated")),
              child: const Text("update"),
            ),
          ],
        ),
      ),
    );
  }
}

今オレ:ちなみに、UIアーキテクチャについてはチームメンバーの yohei さんが書いた FlutterアプリにおけるUI Component Architecture #LayerXテックアドカレ - LayerX エンジニアブログ も参考にしてみてください。

過去オレ:また読めるの半年後じゃないですか…。

SwiftとDart

過去オレ:言語についても聞かせてください。Swiftは最近でもConcurrencyやMacroの追加などもあり進化し続けています。ここからDartにスイッチすると機能落ちしませんか?

今オレ:正直、Swiftほど高機能ではないとは思っています。ただ、今年リリースされたDart3では表現の幅が大きく広がっているので、かなり開発はしやすくなっています。

過去オレ:そうなんですね、気に入っている機能はありますか?

今オレ:Records型, Sealed class, Pattern Matchの追加などは気に入っています。Swiftでは「enumのcaseごとに特有の値を持たせ、switchで網羅チェックをする」という対応をよくしていたのですが、Dartでもこれと同等のことが可能になっています。

(Swiftでのサンプルコード)

struct SomeImmutableValue {
    let intValue: Int
    let stringValue: String
}

enum SomeEnum {
    case a(SomeImmutableValue)
    case b
    case c
}

func main() {
    let someImmutableValue = SomeImmutableValue(intValue: 0, stringValue: "")
    let someInstanceA = SomeEnum.a(someImmutableValue)
    let hoge: SomeImmutableValue? = switch someInstanceA {
    case .a(let value): value
    case .b, .c: nil
    }
    print(hoge)
}

(Dart3を活用したサンプルコード)

/// 値型を簡潔に定義できる
typedef SomeImmutableValue = ({
  int intValue,
  String stringValue,
});

/// sealed classの定義
sealed class SomeSealedClass {
  const SomeSealedClass._();
}

/// sealed classをimplementsしたクラスを定義
class SomeClassA implements SomeSealedClass {
  const SomeClassA(this.value);
  final SomeImmutableValue value;
}
class SomeClassB implements SomeSealedClass {}
class SomeClassC implements SomeSealedClass {}

void main() {
  // インスタンスの生成
  final someImmutableValue = (intValue: 0, stringValue: "hoge");
  final someInstanceA = SomeClassA(someImmutableValue);

  // selaed classをswitchすると対象のクラスを全て網羅しないとコンパイルエラーになる
  // (Swiftでswitch enumするときのアレ!)
  // case文でパターンマッチさせて個別の処理をすることもできるし、下記のように値を直接返却することも可能
  final value = switch (someInstanceA) {
      SomeClassA(value: final value) => value,
      SomeClassB() || SomeClassC() => null,
  };
  print(value); // (valueA: 0, valueB: hoge)
}

過去オレ:Dartも日々進化しているんですね。

今オレ:そうですね。 Announcing Dart 3. 100% sound null safety. Records… | by Michael Thomsen | Dart | Medium は5月に発表されている記事なので、過去オレさんも参考にしてみてください。

過去オレ:この記事は読める!

Flutterの辛いところは?

過去オレ:では別の話題で、Flutter開発で辛いと感じる部分はありますか?

今オレ:コード自動生成への依存が多いのが個人的には辛いですね。Diffが大きくなったりbuild_runnerのwatchを実行しておく面倒さは感じます。あと、実装コードがSwiftUIと比べるとゴチャつくというのもありますね。

過去オレ:SwiftUIではXcode Previewsによって、アプリを実行せずに対象ファイルを開くだけで表示確認ができる便利な機能もありますよね。Flutterだとこういった機能はないですか?

https://developer.apple.com/jp/xcode/swiftui/

今オレ:残念ながらないですね。開発時はHot Reloadにより効率的に実装を進められますが、ファイルから直接表示確認することはできません。

過去オレ:そこはSwiftUIの方が使いやすそうですね。

今オレ:そうですね、ただ代替手段としてWidgetbookというツールを導入しました。これは実装済みWidgetのカタログアプリを生成できるツールで、Web開発者向けにはStorybookにあたるものといえばわかりやすいでしょうか。

過去オレ:便利そうですね、WidgetのUIを確認したくなったら自動生成されたアプリを起動すればいいと。

今オレ:そうなんです。ただ、確認したい時に都度アプリをビルドするのはめんどくさいですよね。そのため、mainブランチマージ時に自動でGitHub Pagesにホスティングするようにしました。

Widgetbookを使って動作させているWebアプリ

過去オレ:そんなことができるんですか!

今オレ:はい、FlutterはWeb用に動かすこともできるので、こういった手段が取れちゃいます。

過去オレ:共通UIコンポーネントを確認したいときに、Webからさっと触れるの素晴らしいですね。

OS最適化された体験は作れる?

過去オレ:最後にもう一点、FlutterはGoogleがメンテナンスしているのでiOSに最適化された体験を作りづらい印象があるのですがどうですか?

今オレ:ネイティブ実装と比べてしまうと作りづらいとは思います。デザインシステムはMaterial Designベースですしね。Cupertinoを活用してiOSらしいUIにもできますが、開発・デザイン両面でコストは増加するので、チームでは原則Material Designベースで作ることを決めていました。

過去オレ:パフォーマンス面はどうですか?

今オレ:最近はiOSの120Hz対応なども入ったり、ネイティブ実装と遜色なくなってはいます。ただ、Flutter特有のパフォーマンスを落としてしまう実装はあるので注意は必要です。

過去オレ:ネイティブ機能との連携はどうでしょうか?

今オレ:多くのケースでは、pub.devを検索すればプラットフォームを抽象化してくれるパッケージが見つかります。最悪自作する必要はありますが、MethodChannelとpegeonを使って型安全な連携もできるので、大きな懸念要因にはならないかなと考えています。

過去オレ:AppExtensionを作る場合はプロセスが本体アプリと切り離されるので、最悪Flutterで書いたコードを二重実装することになったりしませんか?

今オレ:その点は私も懸念していました。調査したところ、FlutterEngineをAppExtension&本体アプリ上で直接動作させ、Dartの実装をSwift/Kotlinから呼び出せることは確認できています。

過去オレ:そんなことも!つまりFlutter側のAPI通信やビジネスロジックなどを、Swiftコードから呼び出せるんですね。

今オレ:はい、可能でした。こちらについては別途アウトプットしたいなと思っています。

おわりに

過去オレ:本日はお話ありがとうございました。ぼんやり感じていたFlutterへの不安感が払拭された気がします。

今オレ:ネイティブアプリをメインに開発していると、クロスプラットフォーム技術の採用に慎重になっちゃいますよね。しかし今のところは大きく困っていることは無く、むしろとても良い開発者体験のもと進められています。同じように不安感がある方や、LayerXのプロダクト&開発に興味がある方などいらっしゃれば、カジュアル面談も待っていますので是非お声がけください。

jobs.layerx.co.jp

LayerXのモバイルアプリ開発について話しましょう!


過去オレ:ありがとうございます。同僚にも話してみますが、参加できるのが半年後ぐらいなのでちょっと先すぎますね(笑)

今オレ:そうですね(笑)。未来で待ってます。


採用情報 → jobs.layerx.co.jp

カジュアルナイト → jobs.layerx.co.jp/casual-night