今年1月からFlutterのWebアプリケーションを開発している孫と申します。
一つのFlutterアプリで様々なプラットフォームに提供するためには適応性(Adaptive)と反応性(Responsive)を考慮する必要があります。適応性(Adaptive)と反応性(Responsive)は何なのか、そして何が違うかを解説いたします。
※ YouTubeの「アダプティブとレスポンシブの比較 | Decoding Flutter」の内容を元に編集しました。
FlutterはAndroid, iOS, Web, Windows, Mac, Linux appなど様々なプラットフォームでプログラムを動かすことが可能です。
そのため、各プラットフォームで適切に表示し、適切に動かすためには以下の点を考慮する必要があります。

- フレキシブルな画面サイズ
- 様々な入力方法
- プラットフォーム別の機能
上記の項目を考慮し、下記のように様々な画面のサイズと様々なプラットフォームの組み合わせを条件式で処理することもできなくはないですが、その方法をしっかり守るのは結構難しいです。
if (Platform == foldable) { } else if (Platform == web) { } else if (Platform == desktop) { } else if (Platform == iPhone) { } else if (Platform == android) { } else if (Platform == tabletAndroid) { } else if (Platform == desktopWithTouchscreen) { } else if (Platform == iPhoneButSmallAndNotchless) { }
なぜなら二つの概念が混在しているからです。
- 適応性(Adaptive)
- プラットフォーム別の機能に応じて機能の変更すること
- 反応性(Responsive)
- 利用できる画面に応じて見え方を変更すること
適応性(Adaptive)
適応性とはプラットフォームが提供できる機能にFlutterアプリが対応することを意味します。
例えば、ユーザーの位置を把握したい場合、いろんなプラットフォームからのGPS APIを理解し使用するのはかなり難しいです。
その際アプリはそれぞれのAPIに対応する必要があります。
一番簡単な方法はプラグイン(Plugin)を使って各プラットフォームのGPS APIを抽象化することです。
そのプラグインでは各プラットフォームからGPS情報を引っ張ってくる一つのDartパッケージです。
例として baseflowが公開しているGeolocator パッケージを見てみましょう~!
// geolocator.dart static Future<Position?> getLastKnownPosition( {bool forceAndroidLocationManager = false}) => GeolocatorPlatform.instance.getLastKnownPosition( forceLocationManager: forceAndroidLocationManager);
Dartパッケージコードは位置を得るためのインタフェースを提供するだけで、ほとんど何もしません。
// Geolocator's FusedLocationClient.java public void getLastKnownPosition( PositionChangedCallback positionChangedCallback, ErrorCallback errorCallback) { fusedLocationProviderClient .getLastLocation() .addOnSuccessListener( positionChangedCallback::onPositionChanged); /* ... clipped for brevity ... *./ }

位置情報を取得するためAndroid, iOS, MacまたはWindowからもらうか、 JavaScriptの相互運用が適用されるWebの場合、関連APIを直接呼び出すこともあります。
これで一つの懸念点が解決できます。
反応性(Responsive)
それぞれのプラットフォームには、多くの画面サイズがあります。反応性とは利用できる画面に応じて見え方が変えること、つまりレスポンシブデザインの概念に該当します。
Flutterでは様々な画面のサイズや機能に対して部品の配置や表示を多様に変更することができます
final screenWidth = MediaQuery.of(context).size.width; if (screenWidth > 1200) { return UltraWideLayout(); } else if (screenWidth > 600) { return WideLayout(); } else if (screenWidth > 300) { return NarrowLayout(); } else { return UltraNarrowLayout(); }
一番直接的な方式は、MediaQueryを利用して、フルスクリーンサイズを取得し、その画面サイズよって異なるレイアウトを構成することです。Pixel数を元により多くの情報を見せたり、少ない情報を見せたり調節します。
MediaQueryでは親のWidget(ツリーの高い方)のbuildメソッドに含まれることが多いかと思います。
子のWidgetのレイアウトは、LayoutBuilderを使ってください。
// LayoutBuilder child: LayoutBuilder( builder: (context, constraints) { if (constraints.maxWidth > 1200) { return UltraWideLayout(); } else if (constraints.maxWidth > 600) { return WideLayout(); } else { return NarrowLayout(); } }, ),
LayoutBuilderでは親のconstraints に変化がある場合、builderメソッドが再び呼び出され、レイアウトをアップデートする機会を持てます。
ここでのメリットはMediaQueryみたいな全体画面ではなく親Widgetを元にしてレイアウトを構築することです。
💡 MediaQueryとLayoutBuilderの大きい違いは、MediaQueryは親Widgetのサイズではなく、画面全体のコンテキストを使うのに対し、LayoutBuilderは親Widgetの最大幅と高さを使える点です。
これで懸念点が解決できました!
画面サイズに対してResponsiveを考慮したレイアウト作りはできましたが、Flutterの場合、様々なプラットフォームに対応しようと思うと、同じUIとデザインで表示することは難しいですよね。
例えば、スクロールやボタン表示順番などをプラットフォームの規約によって処理する必要がある時がありますが、その際はTheme.of(context).platform を使ってみてください。
// Theme.of(ctx).platform Widget _buildConfirmationButtons(BuildContext context) { Widget confirm = Button(label: "OK"); Widget cancel = Button(label: "Cancel"); if (Theme.of(context).platform == TargetPlatform.iOS) { return Row(children: [confirm, cancel]); } else { return Row(children: [cancel, confirm]); } }
Theme.of(count).platformではレスポンシブ対応のため、アプリの見た目がAndroidかiOSかを確認することが可能です。
💡 Flutterでは見た目を上書きできる機能があります。 例えばAndroid実機やエミュレータでiOS Widgetの挙動を確認することができます。
逆に今動いている実機環境がAndroidかiOSかそれともWindowsかを確認するためにはdart:io.Platform を使ってください。
dart:io.Platform では適用性のために、アプリが起動している実機がAndroidかiOSかを区別してくれます。プラットフォームの特化機能を処理する時にはこちらをよく使われてます。
最後
もっと劇的な変化を作りたい場合もありますね。
例えばナビゲーションやバックボタンを設定したり、全てプラットフォームからMaterialで表示するかどうかを決定したり、プラットフォームの特化デザインやカスタマイズデザインを使うかを決定することです。
LayoutBuilderとプラグインでは一部の課題は解決してくれますが、さらに活用するためにはもっと勉強する必要があります。