GameWith Developer Blog

GameWith のエンジニア、デザイナーが技術について日々発信していきます。

Flutter開発の真実:クロスプラットフォームの光と影 #Flutter #GameWith #TechWith

目次

はじめに

GameWithのクライアントアプリチームでリーダーをしているkyamです。

元々はiOSエンジニアとして活動していましたが2018年頃から社内・社外でFlutterを活用したモバイルアプリやWebサービスをリリースしてきました。
Flutter開発を個人的に始めてから約6年経ち、本記事ではFlutter開発者として積み重ねてきた経験と知見を共有し、特に自身として様々な点で苦労したFlutter Webに焦点を当てながら、その可能性・実用性と現実的な課題について掘り下げていきます!
(※この記事ではFlutterを利用したアプリ・Web開発においての設計や技術的な内容に焦点をあてたものではなく、個人の経験においてFlutterを利用してきての感想がメインです。)

Flutter開発6年間の実践知識

Flutterの進化と共に

2018年にバージョン1.0がリリースされて以来、Flutterは目覚ましい進化を遂げてきました。初期のころは「React Nativeの対抗馬」として語られることが多かったFlutterですが、今やクロスプラットフォーム開発の最有力候補として多くの企業に採用されています。

Flutterの採用状況
※ サイバーエージェント社でのFlutter採用状況

I/O Extended 2023 - Flutter 活用事例 - Speaker Deck より引用させていただいています。

こちらは2023年時点での画像ですが、実際にサイバーエージェント社では新規にアプリを作るものに関しては、ほぼFlutterを採用しているそうです。 その他、様々な企業のモバイルエンジニアの方とお話しすることもあるのですが、Flutterを使い始めたという声もよく聞きます。 特にベンチャー企業でFlutterを利用するケースがかなり増えてきているなと個人的には感じます。
(一方でReact Nativeを採用する事例も時折耳にすることも増えた気がしますが、React Nativeに関しては本記事では割愛します)

弊社でFlutterを選んだ最大の理由に関しては下記のブログにもまとめておりますので、良かったら読んでみてください。 tech.gamewith.co.jp

実務での実感:Flutterの真の価値

一般的にFlutterを開発する上でのメリット・デメリット(課題)に関しては下記のものがあげられると思います。

メリット:

  • 開発速度の向上: 特にUI構築においては、従来のネイティブ開発と比較して2〜3倍の速度で開発可能
  • メンテナンスコストの削減: iOSとAndroidのコードベースを一元管理できることで、バグ修正や機能追加の工数が大幅に削減
  • チーム編成の柔軟性: プラットフォーム専門知識を持たない開発者でも参画しやすく、人材リソース配分の自由度が高い

デメリット:

  • ネイティブ機能へのアクセス: 特殊なデバイス機能や最新OS機能へのアクセスには、プラグイン開発やネイティブコード統合が必要
  • アプリサイズの肥大化: 基本的なアプリでも初期サイズが大きくなりがち
  • 学習曲線: Dartという言語とウィジェットベースの考え方に慣れるまでに時間がかかる

上記が一般的にFlutterのメリット・デメリットでよく見かける感想やまとめだと思います。

個人での感想レベル

個人的な感覚だと、EditorがXcodeやAndroid Studioに縛られずCursor (VSCode) で書けるのがかなり大きいです。

www.cursor.com

上記Cursorを利用しつつAIライクな開発ができるのもFlutterの良い点です。

Cursorに関しては弊社のエンジニアが感想を書いたブログもありますのでまだCursorに馴染みがない方はこちらも良かったら目を通してみてください。 tech.gamewith.co.jp

開発速度の向上に関しては、少なくともUI開発においては自分はSwift(UIKit)で書くよりもかなり早く書くことができます。 プロジェクトによって、Storyboardを採用していたりXibでUIを組んでいたり、違いがあると思いますが自分は元々Swiftでのアプリ開発も AutoLayoutをコードで書くのが好きだったので合っているのもあるかもしれません。

以前はUIの微調整のためにビルド→実行→確認のサイクルを繰り返していたため、イテレーション回数が多く時間がかかっていましたが FlutterではHot ReloadやHot Restartも相まって、ほぼリアルタイムで変更を確認できるので助かっています。
(※ 開発速度の比較は一般的な傾向であり、当然ながら開発者の習熟度やプロジェクトの性質によって異なります。自分は直近SwiftUIを殆ど触れていないので、FlutterじゃなくてSwiftUIとXcodeでも今はこんな感じで爆速にプレビューが機能して開発できるよ!などといったものがあったら、是非教えてください!)

実際にFlutterでアプリ開発のみを行う場合はかなりノンストレスだなと感じるくらい開発体験は良いです。 iOS, Androidでのプラットフォーム間の差異で苦しんだこともパッと浮かばないくらいにはないです。

Flutterのバージョンアップの際にAndroidでビルドが通らなくなり build.gradle 関連や settings.gradle 関連の更新に時々苦戦するくらいです。 (Androidエンジニアの方は逆にXcode側の設定で少し苦戦することがあるかもしれません)

ベンチャー企業におけるFlutter採用

次にベンチャー企業などのFlutter採用理由ですが、やはり単純に人員にかけるコストを削減できることも大きいのかなと個人的には思います。 その企業のフェーズによりますが、極論一人でiOS・Android・Webの開発ができるのでそれぞれのエンジニアを3人採用せずとも1人でも開発自体は行うことができます。 (これは個人の能力などあらゆる事情を総合的に考慮する必要があるので一概には言えません。一人だとレビューが、とかそういった事情は一旦抜きにして開発Onlyで見た時の話です。)
勿論必要なコミュニケーションコストも減るので会社の初期フェースでプロダクトを作る上で少人数でFlutterを採用しクロスプラットフォーム開発を行うのは、理にかなっているかなという気はします。

開発中の典型的な障壁とその解決策

Flutter Webは、当初は実験的な位置づけでしたが、現在では安定版として多くのプロジェクトで採用されています。 しかし、管理画面や社内ツールでの利用が主流であり、一般消費者向けのサービスとしての活用例はまだ少ないのが現状です。

Flutter Web特有の課題

1. レンダリングエンジンによるUI崩れ

Flutter Webにはhtmlcanvaskitskwasm の3つのレンダリングエンジンがあります:

エンジン メリット デメリット
html ・初期ロード速度が速い
・メモリ使用量が少ない
・プラットフォーム間の一貫性が低い
・高度な描画機能に制限あり
canvaskit ・モバイルとの描画一貫性が高い
・すべての描画機能をサポート
・初期ロード時間が長い
・メモリ使用量が多い
skwasm ・canvaskitより小さいバンドルサイズ
・より高速な初期ロード
・ネイティブに近いパフォーマンス
・メモリ効率が良い
・比較的新しい技術
・一部ブラウザでの互換性問題
・まだ発展途上の部分がある

ただ現在は、htmlは非推奨になり、基本的には canvaskit を使うことが多いかなと思います。 当時 html を使っていた頃は、上手くグラデーション付きの文字が表示されなかったり、日本語入力の場合のみTextFieldのCursorの位置がずれるなど細かい点で色々問題がありました。

※ 最新の Flutter Version 3.29.2 ではついに html は削除されています。

CanvasKitでGoogleFontなどといったカスタムフォントを利用する際に「Tofu問題」が発生する場合には、事前にフォントをDLするなどして対応も可能です。

    await GoogleFonts.pendingFonts([
      GoogleFonts.notoSans(),
    ]);

アプリでは問題がないが、Web(デスクトップでは問題ないが、モバイルブラウザだとUI崩れ)だと起きるなど、Flutter Webで定期的に発生する気がします。 そういった場合はまだまだFlutter Webの情報は少ないので、なかなかすぐに解決できなかったりします。ここが個人的に時間を浪費する部分です。

2. Web・アプリなど特定のプラットフォームでのビルドエラー

殆どのケースでは、コード内の分岐でアプリとWebの場合分けができます。

ex. 各種SNSログインのケース

  Future<UserCredential> signInWithTwitter() async {
    final twitterProvider = TwitterAuthProvider();
    if (kIsWeb) {
      return await _auth.signInWithPopup(twitterProvider);
    } else {
      return await _auth.signInWithProvider(twitterProvider);
    }
  }

必要に応じて、kIsWeb などで分岐してあげれば良いですが、これが使えないケースもあります。

例えば下記のようなファイルを作成しWebで実行するとビルドエラーになります。

import 'dart:io'; // webでは dart.ioが利用できないのでエラーになる
import 'package:core/gen/assets.gen.dart';
import 'package:extended_image/extended_image.dart';
import 'package:flutter/material.dart';

class ProfileSettingAvator extends StatelessWidget {
  const ProfileSettingAvator({
    super.key,
    required this.filePath,
  });

  final String filePath;

  @override
  Widget build(BuildContext context) {
    return CircleAvatar(
      backgroundImage: ExtendedImage.file(File(filePath)).image,
      child: Assets.icons.imageAddWhite.svg(
        width: 32,
        height: 32,
      ),
    );
  }
}

逆に、Webのために dart:html を利用しているクラスをアプリで実行するとエラーになります。

これの解決策としては、Conditional Importing(条件付きインポート)や用途によってはこの問題に事前に対応しているクロスプラットフォーム用のPackage(後述する universal_html など)を導入することになります。 詳しくは公式ドキュメントにも記載されています。

Creating packages | Dart

これを利用することで複数ファイルを作る必要はありますが、それぞれのプラットフォームに適した処理を書くことができ、 各プラットフォームでのビルドエラーも防ぐことができます。

3. FlutterのVersionアップによる問題がWebで見つかりやすい

Flutterは定期的にVersionアップされます。これらのアップデートを分かりやすく分類すると、主に3種類に分けられます。

  1. パッチアップデート (例: 2.3.02.3.1 )
    バグ修正や小さな改善が中心で、互換性を維持した基本的には安全なアップデート

  2. マイナーアップデート (例: 2.3.02.4.0 )
    新機能の追加や改善を含むが、基本的な互換性は維持されることの多いアップデート

  3. メジャーアップデート (例: 2.3.03.0.0 )
    フレームワークの根幹に関わる大きな変更や破壊的変更も含む場合がある大規模なアップデート

メジャーアップデートに限らずマイナーアップデートでも各Versionアップによって、よりiOS本来のUIを実装できるCupertino系のWidgetが追加・更新されたり、 外部Packageに頼らずに済むようなWidget (ex. CarouselView) が追加されたり、Web開発をする上で嬉しいテキスト選択 関連のWidgetの改善がされたり、レンダリングエンジンが更新されパフォーマンスが上がったり、Flutter開発者にとっては嬉しく更新内容を見るだけでアップデートしたくなるような内容が多いです。

docs.flutter.dev

しかし、安易にVersionアップに飛びつくと様々な問題に遭遇することもあります。 直近苦しんだIssueとして記憶にあるのが、特に下記2つです。

github.com github.com

これはiOS18系列でモバイルブラウザでスクロールが効かなくなる問題が、3.24 系列で発生し、 修正された 3.27 系列にあげようとすると 3.27 では TextField にバグがあり入力中に内部的にクラッシュしキーボードが強制的に下がるというものでした。 色々この辺りのIssueは同時期にたくさん建ち、GoogleはFlutter Webをテストしていない・見捨てているなど色々なコメントも散見されました。苦笑

アプリではあまり遭遇したことはないですが、実際WebはDesktop(Widndows or Mac etc..)ブラウザやモバイルブラウザで挙動が変わることもあるためテストも大変です。

ここに関しては解決策というわけではないですが、常にFlutterのIssueをチェックして最新の情報を追うようにすると解決しやすいです。

パッケージ紹介

次は今まで自分が様々なプロジェクトを担当してきた中で、実際に導入したことや個人で利用したことのあるアプリパッケージを紹介します。 記載がないものでこれ良かったよなどおすすめのものがあれば是非教えて欲しいです!

Network関連

設計方針はプロジェクトによってまちまちだと思うのですがクリーンアーキテクチャなどにおけるリポジトリパターンを採用する際に、 dioretrofitriverpod を用いてコードの保守性・テスト容易性・拡張性を向上させることができます。

特にREST APIを利用するプロジェクトでは、この組み合わせが非常に効果的です。 retrofit を使うことでAPI呼び出しをインターフェースとして宣言的に記述でき、SwaggerなどのAPIドキュメントを見ながら開発を進めやすくなります。 エンドポイントの追加や変更があっても、対応するメソッドを追加・修正するだけで済むため、APIの進化に合わせた開発がスムーズに行えます。 また、コード生成によってHTTPリクエストの実装詳細を気にせず、ビジネスロジックに集中できる点も大きな利点に感じます。

以下はGitHub APIを使った実装例です。

final githubApiDataSourceProvider = Provider.autoDispose(
  (ref) => GithubApiDataSource(ref.watch(dioProvider)),
);

@RestApi(baseUrl: "https://api.github.com")
abstract class GithubApiDataSource {
  factory GithubApiDataSource(Dio dio) => _GithubApiDataSource(dio);

  /// ユーザー情報取得
  @GET('/users/{username}')
  Future<UserResponse> fetchUser({
    @Path('username') required String username,
  });

  /// リポジトリ一覧取得
  @GET('/users/{username}/repos')
  Future<RepositoriesResponse> fetchRepositories({
    @Path('username') required String username,
    @Query('sort') String? sort,
    @Query('per_page') int? perPage,
  });

  /// スター付きリポジトリ一覧取得
  @GET('/users/{username}/starred')
  Future<RepositoriesResponse> fetchStarredRepositories({
    @Path('username') required String username,
    @Query('sort') String? sort,
    @Query('per_page') int? perPage,
  });
}
final githubRepositoryProvider = Provider.autoDispose<GithubRepository>(
  (ref) => GithubRepositoryImpl(
    githubApiDataSource: ref.watch(githubApiDataSourceProvider),
  ),
);

abstract class GithubRepository {
  Future<User> fetchUser({
    required String username,
  });
  
  Future<List<Repository>> fetchRepositories({
    required String username,
    RepositoriesSortType? sortType,
    int? perPage,
  });
  
  Future<List<Repository>> fetchStarredRepositories({
    required String username,
    RepositoriesSortType? sortType,
    int? perPage,
  });
}

class GithubRepositoryImpl implements GithubRepository {
  GithubRepositoryImpl({
    required this.githubApiDataSource,
  });

  final GithubApiDataSource githubApiDataSource;

  @override
  Future<User> fetchUser({
    required String username,
  }) async {
    final response = await githubApiDataSource.fetchUser(
      username: username,
    );
    return response.user;
  }

  @override
  Future<List<Repository>> fetchRepositories({
    required String username,
    RepositoriesSortType? sortType,
    int? perPage,
  }) async {
    final response = await githubApiDataSource.fetchRepositories(
      username: username,
      sort: sortType?.value,
      perPage: perPage,
    );
    return response.repositories;
  }

  @override
  Future<List<Repository>> fetchStarredRepositories({
    required String username,
    RepositoriesSortType? sortType,
    int? perPage,
  }) async {
    final response = await githubApiDataSource.fetchStarredRepositories(
      username: username,
      sort: sortType?.value,
      perPage: perPage,
    );
    return response.repositories;
  }
}

実際の開発では、エラーハンドリングのためにResult型(Either型など)を戻り値に使うことが多いですが、上記はシンプルな例として紹介しています。 リポジトリパターンを採用すると定型的なコード(ボイラープレート)が増えると言われていましたが、最近のCursorなどのAI搭載エディタの進化により、この問題も大きく軽減されていると感じます。
適切な設計に基づいてコードを書く際も、タブキーを押すだけで多くのコード入力が自動補完されるため、開発効率を落とさずに堅牢な設計を実現できるようになりました。

REST APIではなく、GraphQLを使うパターンの場合は、graphql_fluttergraphql_codegen の組み合わせが使いやすかったです。
当時は artemis を使うケースもありましたが、こちらは現在サポートが終了しているのでferry で代替しているケースもあるそうですが、自分はこちらは使ったことがないので使っている方は是非使用感など教えてください!

UI関連

  • flutter_svg
    • SVG画像をサービス内で表示するのに利用。アプリ・Web問わず利用可。
SvgPicture.asset(Assets.images.imgSample);

flutter_gen と共に利用することで、画像などのリソースに安全にアクセスすることができます。

iOSエンジニアだと、お馴染みの R.swiftflutter_gen のようなものです。

ネットワークから取得した画像をキャッシュして表示するものです。

こちらも基本的には cached_network_image と同様の使い道になるので、どちらかを採用しているケースが多いと思います。

昔は cached_network_image がWEBをサポートしていなかったので、extended_image を採用していましたが 今はWebでも問題なく動作するので自分は cached_network_image を利用しています。

その他利用したことのあるパッケージ

下記に羅列したものに関して、一つ一つ利用用途や、感想などお答えできますので導入に悩んでいたり使い勝手で聞いてみたいことがあったら是非コメントしてください!
例えば flutter_in_appwebviewflutter_webview で悩んでいる人もいたりするかもしれませんが、それぞれのメリット・デメリットなど。

UI系

メディア処理系

ファイル・画像処理系

ネットワーク・API系

WebView

課金

RevenuCat も使ってみたいですがまだ導入したことはありません。 www.revenuecat.com

ルーティング

デバイス・プラットフォーム系

アプリ設定系

その他

3rdParty関連

Firebase関連

Flutter Web特化パッケージ

アプリでも利用できるものも含みますが、特にFlutter Webのために導入したことのあるパッケージです。

クロスプラットフォームの互換性担保という観点では便利です。 通常の dart:html はWebプラットフォームでのみ動作しますが、universal_html はモバイル、デスクトップなど全プラットフォームで動作するので プラットフォーム交友の条件分岐などが不要になるのと、これを採用する際も既存の dart:html コードを package:universal_html/html.dart に置き換えるだけで利用可能なので楽です。

Flutter Webにてストリームを取得して、データを読み取ろうとした際に一括での読み取りになり苦戦しました。 遭遇する場面としては例えばOpenAIのAPIを叩いてストリームでデータを表示しようとする際などが想定できます。 その際にアプリではスムーズに表示されるが、Flutter Webだとスムーズに表示されず一括で応答が表示されることがありました。 (過去の関連Issue)

下記はサンプルですが、このような分岐が当時必要でした。

  Future<http.ByteStream> getStream(http.Request request) async {
    final fetchClient = FetchClient(mode: RequestMode.cors);
    final response = await fetchClient.send(request);
    return response.stream;
  }

Webの場合

if (kIsWeb) {
        request.persistentConnection = false; // Important for large data
        var stream = getStream(request);
        stream.asStream().listen(
          (event) {
            event
                .transform(const Utf8Decoder())
                .transform(const LineSplitter())
                .listen(
              (dataLine) {
       // データが流れる

通常のアプリの場合

        final client = http.Client();
        final response = await client.send(request);
        if (response.statusCode == HttpStatusCodes.ok) {
          final buffer = StringBuffer();
          final subscription = response.stream
              .transform(const Utf8Decoder())
              .transform(const LineSplitter())
              .listen((dataLine) {
      // データが流れる

アプリ・Web関係なくダウンロード処理を簡単に導入できるのでオススメです。

SEOのためのメタタグを簡単に追加できるパッケージです。 GoRouterと連携して使うこともできます。

final router = GoRouter(
  routes: [
    GoRoute(
      path: '/',
      builder: (context, state) {
        if (kIsWeb) {
          MetaSEO meta = MetaSEO();
          meta.ogTitle(ogTitle: 'トップページ');
          meta.description(description: 'トップページの説明');
        }
        return const HomePage();
      },
    ),
    GoRoute(
      path: '/about',
      builder: (context, state) {
        if (kIsWeb) {
          MetaSEO meta = MetaSEO();
          meta.ogTitle(ogTitle: '会社概要');
          meta.description(description: '会社概要ページの説明');
        }
        return const AboutPage();
      },
    ),
  ],
);

以上実際に利用したことがあったりWeb特有のPackageの簡単な紹介でした! (個人開発レベルだとまだまだ色々触っていますのでFlutterエンジニアの方情報交換したいです!)

その他の開発上の運用について

アプリに関してはたくさん情報が溢れていると思うので今回は割愛し、Flutter Webでの運用について簡単に紹介します。

例えばFirebase HostingにデプロイしたFlutter Webアプリケーションに対してアクセス制限を設定したいケースがあると思います。

Flutter Webアプリケーションに対するアクセス制限は、Firebase Functionsを活用することで柔軟に実装できます。
例えば開発環境や限定公開サイトではBasic認証、特定のドメインに対してはIP制限を適用するなど、用途に応じた設計も可能です。

1. Basic認証の実装例

const functions = require("firebase-functions/v1");
const express = require("express");
const basicAuth = require("basic-auth-connect");

const app = express();

// 環境変数からユーザー名とパスワードを取得
const getAuthCredentials = () => {
  const env = process.env.GCLOUD_PROJECT;
  let user, pass;

  if (env === "your-dev-project") {
    user = functions.config().auth.dev_user;
    pass = functions.config().auth.dev_pass;
  } else if (env === "your-prod-project") {
    user = functions.config().auth.prod_user;
    pass = functions.config().auth.prod_pass;
  }

  return { user, pass };
};

// Basic認証の適用
const { user, pass } = getAuthCredentials();
app.all(
  "/*",
  basicAuth((inputUser, inputPass) => {
    return inputUser === user && inputPass === pass;
  })
);

app.use(express.static(__dirname + "/web/"));
exports.app = functions.region("asia-northeast1").https.onRequest(app);

環境変数の設定方法

Firebase Functionsの環境変数は以下のコマンドで設定します:

  • 開発環境用
$ firebase functions:config:set auth.dev_user="username" auth.dev_pass="password" --project your-dev-project
  • 本番環境用
$ firebase functions:config:set auth.prod_user="username" auth.prod_pass="password" --project your-prod-project

2. IP制限の実装

特定のドメインに対してIP制限を適用したい場合、以下のような実装が可能です。 下記は外部でIPを設定したい場合ですが、ローカルで設定する場合は環境変数を用いてIPのリストを設定するのが良いかと思います。

const functions = require("firebase-functions/v1");
const express = require("express");
const requestIp = require("request-ip");
const crypto = require('crypto');

const app = express();

/**
 * システム認証用のトークンを生成する関数
 */
function generateSystemAuthToken(systemAuthSalt) {
  const timestamp = Math.floor(Date.now() / 1000).toString();
  const data = `${systemAuthSalt}${timestamp}`;
  const hash = crypto.createHash('sha256').update(data).digest('hex');
  return `Bearer ${hash}.${timestamp}`;
}

/**
 * APIを使用してIPアドレスが許可されているか確認する関数
 */
async function checkIpAllowed(domain, ipAddress) {
  try {
    // 環境変数からシステム認証用のソルトを取得
    const systemAuthSalt = functions.config().system?.auth_salt;
    if (!systemAuthSalt) {
      console.error("システム認証用のソルトが設定されていません");
      return false;
    }

    const authToken = generateSystemAuthToken(systemAuthSalt);
    const apiUrl = 'https://api.example.com/v1/system/check-ip';

    const response = await fetch(apiUrl, {
      method: 'POST',
      headers: {
        'Authorization': authToken,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        domain: domain,
        ip_address: ipAddress
      })
    });

    if (!response.ok) {
      throw new Error(`API request failed with status ${response.status}`);
    }

    const data = await response.json();
    return data.flag === true;
  } catch (error) {
    console.error('IP確認APIエラー:', error);
    return false;
  }
}

// 特定のプロジェクトの特定ドメインの場合のみIP制限を適用
if (process.env.GCLOUD_PROJECT === "your-restricted-project") {
  app.all("/*", async (req, res, next) => {
    // x-forwarded-hostヘッダーからホスト名を取得
    const forwardedHost = req.headers["x-forwarded-host"] || req.hostname || "";

    // x-forwarded-forヘッダーから最初のIPを取得
    let clientIp;
    if (req.headers["x-forwarded-for"]) {
      clientIp = req.headers["x-forwarded-for"].split(",")[0].trim();
    } else {
      clientIp = requestIp.getClientIp(req);
    }

    // 特定のホスト名の場合のみIP制限を適用
    if (forwardedHost === "restricted.example.com") {
      try {
        const isAllowed = await checkIpAllowed(forwardedHost, clientIp);
        
        if (isAllowed) {
          next();
        } else {
          res.status(403).send("Access Denied");
        }
      } catch (error) {
        console.error("IPチェックエラー:", error);
        res.status(500).send("Internal Server Error");
      }
    } else {
      next();
    }
  });
} else {
  // 他のプロジェクトはBasic認証を適用
  const { user, pass } = getAuthCredentials();
  app.all(
    "/*",
    basicAuth((inputUser, inputPass) => {
      return inputUser === user && inputPass === pass;
    })
  );
}

app.use(express.static(__dirname + "/web/"));
exports.app = functions.region("asia-northeast1").https.onRequest(app);

この辺りはFirebase Hostingを想定している前提で書きましたが、どちらもfunctionsを活用する場合には、 firebase.json の設定を変更する必要があります。

例えば本番環境など特に制限をかけない場合は下記の様なケースが多いですが、

    {
      "target": "prod",
      "public": "build/web",
      "ignore": ["firebase.json", "**/.*", "**/node_modules/**"],
      "rewrites": [
        {
          "source": "**",
          "destination": "/index.html"
        }
      ],
      "headers": [
        {
          "source": "**",
          "headers": [
            {
              "key": "Cache-Control",
              "value": "max-age=0,must-revalidate,public" // 任意に設定
            }
          ]
        }
      ]
    },

functionsを経由する場合は、rewrites 部分を変更する必要があるので注意です。

    {
      "target": "dev",
      "public": "public",
      "ignore": ["firebase.json", "**/.*", "**/node_modules/**"],
      "rewrites": [
        {
          "source": "**",
          "function": "app"
        }
      ],
      "headers": [
        {
          "source": "**",
          "headers": [
            {
              "key": "Cache-Control",
              "value": "max-age=0,must-revalidate,public"
            }
          ]
        }
      ]
    },

URL Strategyに関する注意点

docs.flutter.dev

後は、usePathUrlStrategy(); を使うとURLから#タグを消すことができますが、 functions経由のアプリケーションで#タグを消すと上手く動作しなかったりすることもあるのでこの辺も最初のはまりポイントかなと思います。

まとめ:Flutterの光と影

色々脱線しましたが、本記事のテーマであったクロスプラットフォーム対応のFlutterの光と影に戻ります。

クロスプラットフォーム対応のFlutterの光(強み)

  • 生産性: 開発の高速化
  • 継続的な進化: GoogleのバックアップやFlutterコミュニティによる着実な改善

クロスプラットフォームの対応のFlutterの影(課題)

  • 成熟度: 一部の領域(特にWeb)ではまだ発展途上。アプリとWebのメンテナンスの両立は大変。

6年間のFlutterを用いたクロスプラットフォーム開発を通じて実感したのは、「万能ではないが、適材適所で非常に強力」ということです。 少なくとも一般的なモバイルアプリをFlutterで提供する分には全く問題がないと思いますし、むしろ最高の選択だと個人的には考えています。 ですがWebサポートまで含めると現状少し効率や、Webサービス自体の品質が多少落ちるなという感覚があります。

特にFlutter Webはまだ実践的な情報が少なく、問題が発生した際のトラブルシューティングやベストプラクティスの情報収集が難しいことがあります。 StackOverflowやGitHubのIssueを探っても解決策が見つからないケースも少なくなく、独自に解決策を見つけなければならない状況に直面することもあります。 これはモバイルアプリ向けのFlutterと比較すると、コミュニティの知見の蓄積がまだ途上段階だなと感じます。

しかし、これらの課題も年々FlutterのVersionアップの度に改善されており、Flutter Webの将来にも期待しています。 実際、大規模なアプリケーション開発においても、Flutterは十分な選択肢となっており、自分が担当してきた社内・社外におけるFlutterのプロジェクトでも サービスの開発・運営を進行する上で大きな問題なく上手くいっていると感じています!

最後に

Flutter開発を成功させるための大切なことを紹介させてください!

コミュニティに貢献する姿勢を持つ: 問題に遭遇したら、解決策をコミュニティに共有し、オープンソースの成長に貢献する。

実際にFlutterのissueは日々25-30件程建てられています。
特に最新のFlutterのVersionが公開された後などはissueも多く建てられるため、自分が遭遇した問題に同じように悩むFlutter開発者も多いです。

GitHubのIssue上で様々な意見交換をするだけでも有益で誰かの助けになることも多いです! またこういった行動を行うことで、Flutterに対する興味関心も増していき、Flutter力も付いていくと思います。

今回Flutterのクロスプラットフォームの光と影というテーマでブログを書かせていただきましたが、個人的にはほとんどです!!
理由として、今まで述べてきたように開発効率の大幅な向上、コードの一元管理によるメンテナンス性の高さ、そして何より日々進化し続けるエコシステムにあります。

github.com

こちらのFlutter Roadmapによると、Google社員以外のContributorがGoogle社員の数を上回っているそうです。 そしてWeb platformの2025年を見ると一層期待が持てます。

確かにWeb対応にはまだまだ課題もありますが、それを上回るメリットがFlutterにはあると実感しています。 Flutterが提供する価値は、現代のアプリ開発において非常に強力な武器になりますし、自分自身ももうiOSエンジニア(Swift)といえないくらい、Flutterに浸かっています。苦笑

今後もFlutter開発について定期的に情報を発信できればと思いますので、GameWith Developer Blogを今後とも宜しくお願いします。

そしてGameWithではエンジニアを絶賛募集中です!
サーバーエンジニアやフロントエンジニアの方、AIに興味がある方や、Unityでの開発に興味がある方もお気軽にカジュアル面談をお申し込みください!

github.com


この記事が、皆さんのFlutter開発の一助となれば幸いです。ご質問や、もっとこのテーマで掘り下げて欲しいなどご意見がありましたら、是非コメント欄でお待ちしております!