フロントエンドエンジニアが始めるFlutter
はじめに
業務ではVue/NuxtやSvelteをTypeScriptで書いているフロントエンドエンジニアです。
最近はフロントエンド以外の領域を勉強していて、今朝Rustのthe Bookを読み終わりました。
今回はFlutterの話をします。
Dartの歴史とGoogleの野望
Dartは2011年にGoogleのカンファレンスで大々的に発表されました。2008年にChromeを発表し、Webブラウザにおけるシェアを伸ばしつつあったGoogleは、ブラウザ上で動的なインタラクションを可能とするJavaScriptそのものを書き換えようとしたのです。というのも当時のJavaScriptは、クラスやモジュールを取り入れようとしたECMAScript 4(JavaScript 2.0)が放棄されたことから明らかなように、MicrosoftやYahoo!などレガシーサイトを擁する企業の抵抗により先進的な機能を取り入れられずにいました。これらの機能を取り入れたDartは、当時としては先見的とも言える仕様を備えていました。
しかし当時は混迷極まるJS方言の戦国時代でもありました。Adobe FlashやMicrosoft Silverlightなどのプラグインが台頭し、AdobeはActionScript、MicrosoftはJScriptなどの方言を独自実装したりしていました。当時人気のあったWebフレームワークのRuby on Railsでは、CoffeeScriptというRubyライクなAltJSが好んで使われました。ようやく2015年にES6が標準化された際、プラグインが次第に廃止されていくとともにフロントエンド開発の方向性はBabelによるトランスパイルへと進みました。このような流れの中で次第にAltJSとしての地位を固めていったのがTypeScriptです。
結局JavaScriptを置き換えようというGoogleの野望は受け入れられるはずもなく、2015年にはChromeへの採用を断念しています。2017年にはGoogle社内でさえ標準言語にTypeScriptが採用され、Dartは2018年にCodementorの「最も学ぶ価値のないプログラミング言語」1位という不名誉さえ受けました。Googleにおいても世間的にも、Dartは忘れられた言語になったかに見えました。
しかしDartは完全には忘れ去られていませんでした。2018年にGoogleはDart 2.0、そしてFlutterを発表します。FlutterはiOSアプリとAndroidアプリを同時に開発できるモバイルプラットフォーム開発用のフレームワークとして紹介され、その記述言語には新しいDartが採用されていました。このアイデアは新しいもの好きなモバイルデベロッパーの興味を惹き、わずかではありますが実プロダクトにも採用されていきます。
しかしより興味深いのは、FlutterがPCやWeb上でも動作するアプリも開発対象とする「クロスプラットフォーム開発キット」へと変貌を遂げたことです。FlutterはGoogleがかつて断念したかに見える「DartによるWeb体験」を、ブラウザへの標準搭載とは異なる形で実現しようとしているかに見えます。
それだけではありません。ChromeやAndroidの開発を続けていた裏で、Googleは謎のOS「Fuchsia」を開発していたことが2016年8月に明らかになりました。2021年にGoogleは自社製のスマートディスプレイ「Nest Hub」への搭載をもってFuchsiaを正式にリリースしますが、このFuchsiaで動作するアプリを開発するために使われていた開発プラットフォームが他ならぬFlutterだったのです。
2021年現在で世界のスマートフォン市場シェア一位であるSamsungが、将来的に採用OSをAndroidからFuchsiaに乗り換えるという噂もまことしやかに囁かれています。こうした流れに他社も追随するとしたら、その時にはFlutterがクロスプラットフォームの主要な開発言語になっているかもしれません。これが実現されたとき、最初にDartを開発した時のGoogleの野望は異なる形で達成されたことになるでしょう。
Flutterの機能とそれを支える思想
Flutterの機能はしばしばReact Nativeと比較されます。実際、FlutterはReact Nativeに強い影響を受けて開発されました。例えば宣言的UIの採用、さまざまな最適化による差分更新などです。
その上でFlutterはネイティブ開発により最適化されています。ビルドされたReact Nativeアプリの実体がJavaScriptで端末上でのコンパイルを必要とするのに対し、Flutterアプリはネイティブコードにコンパイルされており、描画処理も端末のGPUを使って直接行います。当然ながらパフォーマンスは一般にFlutterがReact Nativeを上回ります。
開発者体験はどうでしょうか。Dartによる堅牢な静的型付けやコンパイル時の型チェックはもちろん、基本的なUIや機能がウィジェットとしてビルトインされています。とりわけWeb開発でも利用されることの多いMaterial UIがデフォルトで採用されており、UIフレームワークとしての強みを最大限に生かすことができます。もしあなたがオブジェクト指向UIデザインにもとづくアプリ開発を理想としているのなら、Flutterはそれにうってつけのフレームワークと言えるでしょう。
これは私感ですが、JavaScriptを元にしたTypeScriptを使って、ReactというWeb用のフレームワークを元にしたReact Nativeでネイティブアプリを書くというのは、設計思想としてかなり無理をしているように感じられます。それに対してクロスプラットフォーム向けに一から設計されたFlutter、およびJavaScriptを置き換えることを目標に開発されたDartの組み合わせは、ネイティブアプリを開発するのにより適していると思われれました。
Flutterを使ってどういったことができるでしょうか。Flutter Webで紹介されているこちらのプロダクトとその動画が参考になるかもしれません。
YouTubeのvideoIDが不正です
FlutterではiOSやAndroidのモバイルアプリを同時に開発できるだけでなく、デスクトップアプリやWebアプリも開発できます。さらにそれらは右クリックメニューや共有といったプラットフォーム特有のアクションにも対応することができます。カメラなどハードウェアへのアクセスやWebにおけるRTLの対応など、現時点では難しいことや未解決の課題もまだまだあるのですが、基本的なアプリケーションであればFlutterとMaterial UIの組み合わせで完結しそうな気がします。
これらのアプリを同一ソースで開発できるという利点ももちろんですが、それよりもむしろWebとネイティブで一貫した体験ができる、ということが強調されているように感じられます。
エコシステム
JavaScriptとDartのエコシステムを比較してみましょう。
JavaScriptのnpmにあたるCLIはpubで、npm install
は pub get
に当たります。
しかしFlutter開発ではむしろ flutter run
や flutter build
といったコマンドを使うことが多いでしょう。
DartやFlutterのパッケージは pub.dev で配信されており、 pubspec.yaml
ファイルでバージョン管理されます。
型
型はCやJavaなどと同じ前置で、コロンづけで後置するTypeScriptとは異なります。
数は整数 int
と浮動小数点数 double
の二種類です。符号の有無は区別しません。
配列は List<String>
とは書けますが、 String[]
とは書けません。
Map
はJavaScriptの Object
に近いですが、JSONへの変換は Object
ほど使い勝手が良くありません。
エコシステム
フロントエンド開発を始めるにあたってはnpm、Node.js、Yarnなどをインストールすることが多いですが、FlutterではFlutter SDKを入れた上で、各プラットフォームの環境を整える必要があります。
flutter doctor
のチェック項目を全部OKにするには、Chrome、Android Studio、Xcode、CocoaPodsをインストールすることになります。
Android Studioは統合開発環境は、Xcodeは開発者ツールですが、それぞれAndroidとiOSのエミュレータがついてくるので、IDEと統合してこれらエミュレータ上で動作検証をすることができます。
CocoaPodsはiOS開発で使われるCocoaプロジェクトの依存関係マネージャーです。
ただしFlutterはWebブラウザでも動くので、これらを入れなくてもChrome上で開発はできます。
サクッと動かてみたい人にはこういう環境もあります。
実装
それではFlutterプロジェクトの実際のコードを見てみましょう。これは flutter create my_app
時に自動生成されるボイラープレートです。
// Copyright 2018 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
Widget build(BuildContext context) {
return MaterialApp(
title: 'Welcome to Flutter',
home: Scaffold(
appBar: AppBar(
title: const Text('Welcome to Flutter'),
),
body: const Center(
child: Text('Hello World'),
),
),
);
}
}
モジュールシステムはES6とあまり変わりません。
void main() {}
で囲まれた箇所が実行時に走るコードです。
runApp()
でFlutterアプリを実行しています。
MyApp()
がアプリの実体で、 StatelessWidget
というウィジェットを継承しています。これは状態を持つ StatefulWidget
などに対照されるもので、それ以上書き変わることがないため const
つきで代入されています。
key
はスクリーンを一意に特定するために指定される必要があります。
ウィジェットは普通 build
メソッドを持っており、これをオーバーライドすることで実際の要素を記述します。ここではコンテキストを引数に取り、 MaterialApp()
でMaterial UIフレームワークを利用しています。 home
プロパティの Scaffold()
オブジェクトにより、画面上部のバーなどを手軽に表現することができます。
別のボイラープレートの例を見てみましょう。
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(primarySwatch: Colors.blue),
home: MyHomePage(title: 'Flutter Demo Home Page'));
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key? key, required this.title}) : super(key: key);
// This widget is the home page of your application. It is stateful, meaning
// that it has a State object (defined below) that contains fields that affect
// how it looks.
// This class is the configuration for the state. It holds the values (in this
// case the title) provided by the parent (in this case the App widget) and
// used by the build method of the State. Fields in a Widget subclass are
// always marked "final".
final String title;
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
// This call to setState tells the Flutter framework that something has
// changed in this State, which causes it to rerun the build method below
// so that the display can reflect the updated values. If we changed
// _counter without calling setState(), then the build method would not be
// called again, and so nothing would appear to happen.
_counter++;
});
}
Widget build(BuildContext context) {
// This method is rerun every time setState is called, for instance as done
// by the _incrementCounter method above.
//
// The Flutter framework has been optimized to make rerunning build methods
// fast, so that you can just rebuild anything that needs updating rather
// than having to individually change instances of widgets.
return Scaffold(
appBar: AppBar(
// Here we take the value from the MyHomePage object that was created by
// the App.build method, and use it to set our appbar title.
title: Text(widget.title)),
body: Center(
// Center is a layout widget. It takes a single child and positions it
// in the middle of the parent.
child: Column(
// Column is also a layout widget. It takes a list of children and
// arranges them vertically. By default, it sizes itself to fit its
// children horizontally, and tries to be as tall as its parent.
//
// Invoke "debug painting" (press "p" in the console, choose the
// "Toggle Debug Paint" action from the Flutter Inspector in Android
// Studio, or the "Toggle Debug Paint" command in Visual Studio Code)
// to see the wireframe for each widget.
//
// Column has various properties to control how it sizes itself and
// how it positions its children. Here we use mainAxisAlignment to
// center the children vertically; the main axis here is the vertical
// axis because Columns are vertical (the cross axis would be
// horizontal).
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('You have pushed the button this many times:'),
Text('$_counter', style: Theme.of(context).textTheme.headline4)
])),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons
.add)) // This trailing comma makes auto-formatting nicer for build methods.
);
}
}
こちらでは MaterialApp
の home
として MyHomePage
が指定されています。 MyHomePage
は StatefulWidget
を継承していますが、これは State<MyHomePage>
を継承する _MyHomePageState
と一緒に定義されています。 _MyHomePageState
の内部では _counter
とともに _incrementCounter
関数が定義され、これが呼び出されるたびに setState()
が発火することで _MyHomePageState
が再ビルドされ、結果 MyHomePage
が再描画されるという仕組みになっています。呼び出しているのは floatingActionButton
の onPressed
メソッドで、これは Icons.add
というMaterial UIのアイコンをそのまま使用しています。
上記のコードを見ても分かる通り、Flutterは基本的にオブジェクト指向のフレームワークです。Flutterではこれらのオブジェクトをウィジェットと呼称するので、ウィジェット志向と言ってもいいかもしれません。こうしたアプローチは今や関数型のパラダイムが一般的となったフロントエンド開発から見れば前時代的なものに感じられるかもしれません。しかしモバイルアプリがその画面スペースの制約上それほど多くの機能やスタイルを要求しないとすれば、これらがウィジェットとして標準的に提供されることは実装の手間を軽減してくれるかもしれません。
さて、今度はスタイリングについてCSSと比較しながら見ていきましょう。
<div class="greybox">
Lorem ipsum
</div>
.greybox {
background-color: #e0e0e0; /* grey 300 */
width: 320px;
height: 240px;
font: 900 24px Roboto;
display: flex;
align-items: center;
justify-content: center;
}
code:style.dart
var container = Container( // grey box
child: Center(
child: Text(
"Lorem ipsum",
style: bold24Roboto,
),
),
width: 320,
height: 240,
color: Colors.grey[300],
);
WebページではCSSスタイルシートとして定義されているスタイルは、FlutterではUIコンポーネントとして表現されています。すなわち、 Container
は高さや幅や背景色だけを指定し、マークアップ的な要素を持ちません。 Center
についても同様です。 Text
だけがマークアップとしての意味合いを持ち、その属性として style
が直接指定されています。
UIをコンポーネントとして定義するこうしたアプローチは、divなどの無駄なマークアップ要素を排除する一方で、ネストがどんどん深くなるという懸念を有しています。Webであればマークアップ要素とスタイルは分離されているので、入れ子構造が深くなっても可読性がそこまで下がりませが、FlutterではスタイルもUIコンポーネントとしてウィジェットに組み込まれており、それぞれが引数として子要素を取るので、結果として可読性が下がる傾向にあると感じられます。
ただしこうしたスタイリングの問題点は、今後 Mix()
変数が導入されることで解消される可能性があります。
import 'package:mix/mix.dart';
final squareMix = Mix(
height(150),
width(150),
);
// Use in a Box widget
Box(
mix: squareMix,
child: Child(),
);
状態管理
フロントエンド開発においてそうであるのと同様に、Flutter開発においても状態管理は大きな課題です。状態を持つウィジェットとしては先に挙げた StatefulWidget
の他にコンポーネントを横断する InheritedWidget
がありますが、実プロジェクトではこれを適切に設計したProviderや、その改良版であるRiverpodなどをはじめ、いくつかの状態管理ライブラリが採用されています。React開発ではおなじみのReduxのほか、BLoCパターンやMobXなど、それぞれが得意分野を異にしています。
教材
Flutterの教材は日本語であまり提供されていません。基本的には英語ドキュメントを読んでいくことになるでしょう。
本項で述べているような基礎的な知識はFlutter公式ドキュメントで習得できます。またここにはチュートリアルやクックブック、各領域のデベロッパーに向けての比較記事など、これだけで多くの情報が得られます。
ドキュメントを読むのが億劫であれば、Youtube公式チャンネルをご参照ください。いくつかの基礎的な内容の動画に加えて、「Flutter Widget o the Week」では毎週ウィジェットを紹介してくれます。
より実践的な開発を志すなら、私個人はFlutter Apprenticeを利用しました。レシピアプリの製作を通して、レイアウトの構築やタブのあるスクリーンの作り方、APIやFirestoreからデータ取得や更新の実装、さらにPlay StoreやApp Storeへのリリースなど、実プロダクトの開発に必要な知識や技能を習得することができます。
これから始める方は、UdemyのFlutter講座で頭一つ抜けた評価を誇るAngela Yu博士の教材がおすすめです。GoogleのFlutter開発チームが監修しており、Flutterを使ってできることを一通り体験することができます。
将来性
最後にFlutterとフロントエンド開発の将来性について個人的な見立てを述べます。先にSamsungに関する噂を取り上げたように、今後AndroidというモバイルOSがFushciaというクロスプラットフォームOSに取って替わるという可能性があります。またAppleにベンダーロックインされているiOSの開発者は今後も仕様の変更に振り回されることでしょう。こうしたプラットフォーム内外の変遷をフレームワーク単位で吸収しうるFlutterは、長期的に見るとこうしたモバイル開発を含むクロスプラットフォーム開発の選択肢となりえます。もっともその実現度はFlutter開発チームおよびコントリビュータたちの努力、そしてGoogleの経営方針にかかっているのですが。
Webのフロントエンド開発は、しばらくReact/NextJSの一強時代に入るでしょう。TypeScript対応に遅れを取ったVue/NuxtJSがこの先これらに肩を並べる可能性はそれほど高くなさそうです。Svelte/SvelteKitはその軽量さと書きやすさ、コンパイルの速さなどから、中小規模のWebアプリケーションではReact同等のシェアを獲得していくポテンシャルがあるように思われます。またRails 7.0の再起やLitをはじめとするWeb Componentsの動きなど、そもそもJSでフロントエンドを開発するというパラダイムが今後突き崩されていくことも考えられなくはないでしょう。
フロントエンドの統合ツールチェーンを目指したRomeがJavaScriptでの実装を断念し、Rustで書き直す決断をしたことに代表されるように、より専門的なWebアプリケーションや関連ツールは次第にRustおよびそのWASMとの組み合わせに取って変わられる可能性があります。あるいはDenoがTypeScriptの使用をやめたように、TypeScriptも型推論とドキュメンテーション以上の相対的価値を提供できないと今後判定されていくかもしれません。とはいえWeb開発の仕事は今後もなくなることはないでしょうし、JavaScriptはしばらくの間そのためのもっとも開発しやすい言語であり続けるでしょう。ですからフロントエンドエンジニアがFlutterその他の開発プラットフォームを今のうちから学んでおくことは、エンジニアとして生き残る上で多少のリスクヘッジにつながるのではないでしょうか。
参考文献
- 世界のプログラミング言語(33) 一度は挫折したDart言語が最近モバイル開発で右肩上がりに https://news.mynavi.jp/techplus/article/programinglanguageoftheworld-33/
- さよならAndroid?サムスンが将来的にAndroidから「フクシア」OSに乗り換えるとの噂 https://smhn.info/202201-samsung-may-change-os-from-android-to-fuchsia
- なぜGoogleはFlutterを開発したのか(次期OS・Fuchsiaに絡む壮大な野望) https://minpro.net/why-google-develop-flutter
- 元Googleエンジニアのメンターによる講義を公開 ──トヨタ自動車が実践する「Flutter」研修の内容とは? https://techplay.jp/column/1516
- Flutter for web developers https://docs.flutter.dev/get-started/flutter-for/web-devs
Discussion