Flutter はじめの一歩

はじめに自動的に作られるカウンターアプリを丁寧に解説

mono 
Flutter 🇯🇵

--

本記事は、このPublicationの中でも特に初学者向けに書かれています。主なターゲット層は以下のような方です。

  • JavaScript・PHPなどとっつきやすいWeb系の言語は見よう見まねで触ったことがあるがきちんとは理解できてはいない(プログラムを1行も書いたこと無いレベルの場合はFlutterの前にまず何かとても簡単な入門書をまず一冊挟んだ方が良いです)
  • Flutterを触り立て(環境構築は済んでいる)

以前、以下の呟きをしましたが、例えばデザイナー本業の方がFlutterも活用できるようになると幅が広がると思い、また公式ドキュメントにもその旨が書かれていました。

とはいえ、元々バリバリコードを書けてしまうようなごく一部のデザイナーを除いて、やはりFlutterを使いこなせるようになるには(UIレイアウト周りに絞ったとしても)けっこう障壁が高いとは思うので、本記事がその補助となれば幸いです。Flutterを(ほぼ)触ったこと無いエンジニアにも役立つ内容にはなっていると思います。

個人的事情として、今業務の一環でそういう教育のトライしていることがこの記事を書く大きな動機となっています(本記事も教材にします)。

本記事では、プロジェクト生成直後に作られるカウンターアプリを題材にFlutterについて説明していきます。

こちらにアクセスするとブラウザからも実行できます(Chrome推奨)

かなり詳しく説明していきますが、Widget(UIレイアウトの設計図)を組み合わせてUIの階層構造(HTMLタグの入れ子に近い)を構築していくというのが本質であり、その他細かいところは初めは分からなくても良いと思います。以降、Widgetで組んだUIの階層構造を「Widgetツリー」と呼びます。

こんな風に、サンプルプロジェクトとシミュレーターと本記事を並べながら説明を読むと理解しやすい作りになっています。また、小見出しの先頭のアルファベットは、ソースコードのコメントと対応しています。

それでは、1行目から見ていきます。

A. パッケージの import

import 'package:flutter/material.dart';

まず、以下のマテリアルデザイン用のUIコンポーネントを使えるように上のように import 文を書きます。

iOSっぽいデザインにしたい場合、以下が用意されていますが、とりあえずマテリアルデザインに統一した方が楽かつ簡単です。

B. main関数

void main() {
runApp(MyApp());
}

Android Studioの場合、上のツールバーから、このいずれかのアイコンをクリックすると、アプリが実行されますが、まず初めにこの main 関数が呼ばれます。

左: 普通に実行 | 右: デバッグ実行(プログラムを途中で停止・観察できて便利)

通常、 main.dart 内の main 関数が呼ばれますが、それは上のツールバーで main.dart がデフォルト選択されているからであって、違うファイルを選択することもできます。

expr syntax / arrow syntax

main 関数は以下のように短く書くこともできますし、元々の自動生成されるひな形コードはその書き方になっています。

void main() => runApp(MyApp());

これは見た目が少し違うだけで全く同じように動作します。日本語では、エクスプレッション構文・アロー構文などと呼ぶはずです。

また、main() の後ろくらいにカーソルを置いてから、Option + Enterを押すと次のような選択肢が出るので2つ目を選ぶと書き換えてくれます。

Option + Enterというキーボードショートカットは、Android Studioでその文脈に応じて便利な機能を呼び出したい時に多用し、機能名としてはクイックアクションなどと呼びます。

また、この書き方は、1つの式だけで完結するような関数でしか使えず、例えば次のように2つ以上の式が含まれている場合には変換不可能( => で書けない)です。

void main() {
print('Hello');
runApp(MyApp());
}

() {} ではなく () => で書かれた関数は次のようなメリットがあります。

  • 少しコンパクトに書けてスッキリする
  • 他の処理をせずに1つの結果だけを返す処理であることが分かりやすい

慣れないうちは、前者の () {} の書き方だけにしても良いですが、 () => の書き方もよく出てくるので少なくとも同じ意味だと分かるようにはしましょう。

Flutter関係なく純粋なDartプログラムとしても実行可能

まず前提として、FlutterアプリはDartというプログラミング言語ですべて記述します。Web開発の場合は、HTML・CSS・JavaScriptを使い分けますが、それをすべてDart言語1つで完結します。

HTML・CSSでの記述がどうFlutterコードに対応するのかの対比は、以下のドキュメントが分かりやすいです。

Styling and aligning text より (上がHTML・CSS、下がDartコード)

そのDartをFlutterフレームワークとは関係なく、純粋なプログラミング言語として記述・実行することももちろん可能です。

いろいろやり方はありますが、Flutter初学者が手っ取り早く純粋なDartコードとして実行したい場合は、 次の手順がおすすめです。

  1. lib フォルダを右クリックして、New → Dart Fileと選択

2. 次のように program など適当な名前を指定

3. 以下のようにmain関数と、内部の処理として print という与えた文字列を出力する関数を呼ぶコードを記述

void main() {
print('Hello World 🐶');
}

そのファイルを右クリックして RunDebug を押すと、

次のように、下の Run タブか Debug タブに出力結果(Hello World 🐶)が出力されます。

Flutterアプリを書きながらDartを学ぶのもありですが、Dartだけ切り離して学ぶと課題がシンプルになって学びやすいことも多いので、適宜使い分けましょう。

Flutterアプリを起動できる状態に戻すには、上のツールバーからそのソースコードの記述してある main.dart に戻しましょう。

きちんとしたやり方

上記のやり方は正規のお行儀の良いやり方ではないので、本番コードではやらないようにしましょう(逆に言うと学習のためのプロジェクトなら全く問題ないです)。

きちんとしたDartプロジェクトの作り方は、色々やり方はありますが、Visual Studio Code(VS Code)のコマンドパレットで以下のように打って、Console Applicationなど作るのがおすすめです。

ちなみに、このコマンドの裏側ではstagehandという、Dartプロジェクトのひな形作成ツールが動いています。

DartPadというブラウザ上でDartコードを動かせる環境もあって、手軽に試したりコード共有したりするのには便利ですが、ローカル環境を用意した方が色々快適に勉強が捗るはずです。

C. runApp関数

void main() {
runApp(MyApp());
}

上述の main 関数内では、 runApp 関数が呼ばれています。 MyApp はWidget で、とりあえず根元のWidget(下の図の Widget に相当するもの)をそこに指定するとFlutterアプリが起動してそのWidgetツリーを解釈してUIが表示される、と理解すれば充分です。

Everything’s a widget より

FlutterのUIはすべてWidgetの組み合わせで構築されます

runApp の詳細がもし気になる場合は、commandキーを押しながら runApp 部分をクリックすると、中身の処理を確認できます(この処理以降を理解するのは難易度高いです)。

void runApp(Widget app) {
WidgetsFlutterBinding.ensureInitialized()
..scheduleAttachRootWidget(app)
..scheduleWarmUpFrame();
}

Flutterフレームワークのソースコードもすべて公開されているため、気になったところはこうやって確認することができるのもFlutterの魅力の1つです(例えばiOSのUIKitなどは確認不可能なため具体的な実装は挙動から想像するしかありません)。

D. StatelessWidgetを継承したクラス (MyApp)

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}

FlutterのUI構築の1番の基本はStatelessWidgetを継承したクラスの組み合わせです。名前の通り、Stateless(状態を持たず静的)なWidget(UIコンポーネント)です。

使い方はシンプルで以下のようにします。

  1. StatelessWidgetを継承( extends )したクラスを作成
  2. buildメソッドでそのUI構築に必要なWidgetを組み合わせて組んだWidgetツリーを return で返す
class 自作Widgetのクラス名 extends StatelessWidget {
@override
Widget build(BuildContext context) {
// Widgetを返す
}
}

通常、自分で丸ごと書かずに、 st あたりまで打つとこのようにひな形生成のための候補一覧が表示されるので、 stless を選んでEnterキーを押すと、右のようなコードが自動生成されるのでクラス名とbuildメソッドの実装を埋めていきます。

runApp 関数に根元のWidgetとして指定した MyApp Widgetでは、build メソッド内で、MaterialApp Widgetを返しています。根本部分でMaterialAppを返すのはマテリアルデザインのアプリを作るときのお作法です。

home にはMyHomePage Widget(次に説明します)を指定してますが、これがアプリ起動直後に表示されるページに相当します。

アプリ起動中に、右のFlutter InspectorからWidgetsツリーの階層構造(≒UIの階層構造)を確認すると次のようになっているのが確認できますが、まさにこの MyAppbuild メソッドで組み合わせたWidgetの入れ子構造がそのまま反映されています。

HTMLのタグっぽく表現すると大体次のようなイメージで、MyAppクラスのbuildメソッドではDartコードでそれと同じものを表現しているだけで、あまり難しく考える必要はないです。

<MyApp>
<MaterialApp>
<MyPhomePage title="Flutter Demo Home Page" />
</MaterialApp>
</MyApp>

E. StatefulWidgetを継承したクラス (MyHomePage)

class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);

final String title;

@override
_MyHomePageState createState() => _MyHomePageState();
}

MyHomePageは上述のStatelessWidgetとは違って、StatefulWidgetとなっています。これは名前の通り、Stateful(状態を持っていて動的)なWidget(UIコンポーネント)です。初めは理解が難しめなので、きちんと理解するのはとりあえずStatelessWidgetで静的なレイアウトを組むのに慣れてからの方が良いかもしれません。

このカウンターアプリの場合、ボタンが押される度に数字が増えて行きますが、それが”Stateful”な部分で、そのためにStatefulWidgetを使っています。

使い方は、以下の通りです。上の例では title を受け取っていますが、それはStatefulWidgetとは直接関係ないため、後ほどまた触れます。

  1. StatefulWidgetを継承( extends )したクラスを作成
  2. createStateメソッドで “State” を返す
class 自作Widgetのクラス名 extends StatefulWidget {
@override
_MyHomePageState createState() => _MyHomePageState();
}

2の createState がよく分からないと思いますが、次に説明します。

F. Stateを継承したクラス

class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;

void _incrementCounter() {
setState(() {
_counter++;
});
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.display1,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}

ちょっと大きなクラスなので、いくつかに分けて説明して行きます。

まず、このクラスは、上述のStatefulWidgetを継承したMyHomePageクラスがcreateStateで返すクラスです。

StatefulWidgetはそれ自体はほとんどスカスカで、実体はcreateStateで返すStateにほぼ集約されていて、Stateは次の機能を持ちます。

  1. 状態を保持・更新する
  2. StatelessWidgetと同様にbuildメソッドでWidgetツリーを返す
class _StatefulWidgetのクラス名State extends State<StatefulWidgetのクラス名> {
// このようにクラスのフィールドとして状態を保持する
int _counter = 0;

@override
Widget build(BuildContext context) {
// 状態を使いつつ組んだWidgetを返す
}
}

これも自分で丸ごと書かずに、 stf あたりまで打つと出てくる候補の中から stful を選んでEnterキーを押すと、次のようにひな形が自動生成されるのでクラス名とStateのbuildメソッドを埋めて行きます。

StatefulWidgetとStateが分かれている理由

buildメソッドを持つのがStatefulWidgetではなくStateとなっている理由はStatefulWidgetのbuildメソッドのドキュメントに記されています。簡単に言うと、アプリケーション開発者がStatefulWidgetを継承したWidgetを扱いやすくするためですが、とりあえずそういうもんだと割り切ってしまっても問題ないです。

G. 状態の保持と更新

class _MyHomePageState extends State<MyHomePage> {
// G. 状態の保持と更新
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
// 以下buildメソッドなど
//...
}

カウンターアプリでは、 以下の操作によってカウントを上げています。

  • _counter というフィールドでカウント数(状態)を保持
  • _incrementCounter メソッド内で setState メソッドで包みながら _counter を増やす

このように、状態の保持と更新ができるのがStateクラスの大きな特徴です。

setStateで状態の更新をUIに反映させる

単に _counter++; を実行するだけだと、Stateの _counter の数字は上がりますが、それがUIには反映されません(Hot Reload実行などすると反映されますが)。

値の更新とともに、それをUIに伝えたい場合は、次のようにsetStateで囲むお作法があります。

setState(() {
// ここに状態の更新処理を書く
});

Flutterフレームワークに対して、「次の画面更新タイミングで今のStateの状態を元にUI更新して」という命令を意味します。

H. _MyHomePageStateのbuildメソッド

class _MyHomePageState extends State<MyHomePage> {
// G. 状態の保持と更新
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
// I. _counterの表示
Text(
'$_counter',
style: Theme.of(context).textTheme.display1,
),
],
),
),
// J. ボタン操作に応じて_counterを増やす
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}

いったん細かいことは気にせず、「 build メソッドで何かWidget組み合わせてWidgetツリーを返しているなあ」程度にざっくり見てください。このbuildメソッドの結果は次のUI全体に相当します。

I. _counterの表示

Text(
'$_counter',
style: Theme.of(context).textTheme.display1,
),

上述のStateのフィールドの _counterText Widgetを使って表示しています。UIとしては次の部分に相当していて、+ボタンを何回か押して _counter3 の瞬間であれば次のように表示されます。

J. ボタン操作に応じて_counterを増やす

floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
),

FloatingActionButtononPressed に上で説明した _incrementCounter メソッドを設定します。

onPressed は、ボタンが押された時に呼ばれ、そこに引数無しで戻り値void(無し)のメソッドを設定します。上の例のように_incrementCounterメソッドを直接指定することもできますが、ベタに書くと次のようにも書けます。

floatingActionButton: FloatingActionButton(
onPressed: () {
_incrementCounter();
}
,
tooltip: 'Increment',
child: Icon(Icons.add),
),

先述のアロー関数( => )を使って次のようにも書けます。

floatingActionButton: FloatingActionButton(
onPressed: () => _incrementCounter(),
tooltip: 'Increment',
child: Icon(Icons.add),
),

_incrementCounterメソッドでは上述の通りsetStateを使って _counter の数値を増やしているのでそれが即座にUIに伝わって数字が増えて行きます。

ここでちょっとややこしいState周りの説明はひと段落なので、次は _MyHomePageStateのbuildメソッドでのレイアウトについて説明していきます。今回は変化するカウント表示が絡むのでStatefulWidgetのStateのbuildメソッドに書かれていますが、以降の内容はStatelessWidgetのbuildメソッドでも同様です。

K. ページはScaffoldで組む

class _MyHomePageState extends State<MyHomePage> {
// 省略
// ...
// H. _MyHomePageStateのbuildメソッド
@override
Widget build(BuildContext context) {
// K. ページはScaffoldで組む
return Scaffold(
appBar: AppBar(...),
body: Center(...),
floatingActionButton: FloatingActionButton(...),
);
}
}

一部省略すると、_MyHomePageStateのbuildメソッドは上のようになっています。

根元のScaffoldは、FlutterにおいてマテリアルデザインベースでUIを組む時にページごとにそれで囲うのがお約束ごとのようなものです。

上述のMaterialApp Widgetと組み合わせて以下のように使うのがほぼ決まりごとです。

L. AppBar

appBar: AppBar(
title: Text(widget.title),
)

Scaffoldはマテリアルデザインのアプリページ土台なので、AppBar(iOSではナビゲーションバーと呼ぶ)を簡単に設定することができます。

M. bodyでページの中身をレイアウト

body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.display1,
),
],
),
),

Scaffoldのbodyにはページの中身に対応するWidgetツリーを記述します。この場合、次のようなWidgetの組み合わせになっています。

  1. Center: 子要素をセンター寄せ
  2. Column: 複数の子要素群を縦に並べる
  3. Text 2つ: Columnで縦に並べられる対象

インスペクターで見ると視覚的により分かりやすいかもしれません。シミュレーターでの描画結果を見るとWidgetの組み合わせ指定通りのレイアウトになっていることが分かります。

他にはどんなWidgetがある?

今回、bodyの中で使っているのはたった3種類のWidgetですが、Flutterでは豊富なWidgetが用意されていて、それらを適切に組み合わせるのが肝です。初めは次のドキュメントで主要なWidgetの使い方を体系的に学ぶのをお勧めします。

また、公式YouTubeチャンネルWidget of the weekというプレイリストで毎週いろいろなWidgetが紹介されているので、これも全話観ましょう。

理想的には、すべてのWidgetを把握しておくのが望ましいので、少し慣れてきたら以下の一覧など眺めて知らないものがあったら調べてみたりすると良いです。

説明をスキップした細かい箇所をフォロー

以上の流れの中で説明しにくかった箇所についてフォローしていきます。

title の値の受け渡し

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
// コンストラクター
MyHomePage({Key key, this.title}) : super(key: key);

// 受け取った文字列の入れ物
final String title;
}

使い方は、以下の通りです。上の例では title を受け取っていますが、それはStatefulWidgetとは直接関係ないため、後ほどまた触れます。

上の説明で、 title の受け渡しの説明を省略したので、まずはそれをフォローします。

まず、 titleMyHomePage クラスのフィールドです。MyHomePage クラスのインスタンスは MyHomePage() で出来ますが、そこで title に設定したい値(この場合”Flutter Demo Home Page”という文字列)を受け取れるようにその引数付きのコンストラクターを用意しています。

引数部分で this.title と書くことでフィールドへの代入まで自動的に行ってくれます。明示的に書くと次のようになりますが上の短縮形を使うのが普通です。

class MyHomePage extends StatefulWidget {
// コンストラクター
MyHomePage({Key key, String title})
: this.title = title,
super(key: key);

// 受け取った文字列の入れ物
final String title;
}

また、今回のサンプルでは、この title_MyHomePageState から Text(widget.title) として利用されています。Stateは対応する(自身を createState した)Widgetにアクセスできます。

N. Theme

Flutterで統一的なデザインをするには、Theme の扱いが重要になってきます。

このサンプルでは根元のMaterilAppのthemeで指定している部分です。

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
primarySwatch: Colors.blue,
)
,
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}

例えば、この primarySwatch: Colors.blueprimarySwatch: Colors.red に変えた後にHot Reloadを実行すると、次のようにAppBarとFloatingActionButtonの色が変わります。サンプルアプリ側のコードではそれぞれに直接色を指定してないですが、それぞれWidgetツリーの上位で与えられたテーマを元に配色するようになっているからです。

また、カウンターの数字の部分では次のようにTextのstyleに Theme.of(context).textTheme.display1 を指定していますが、これも見て分かる通りThemeが関係する部分です。

Text(
'$_counter',
style: Theme.of(context).textTheme.display1,
),

MaterialApp Widget内でマテリアルデザインに沿った各種TextStyleのセットを自動割り当てされているので、そのデフォルトのスタイルの中から選んであてたい場合は上のように指定できます。

また、例えば次のように少し大きめ・太字にしたいとして、

この部分だけの特別な表示なら以下でも良いですが、

Text(
'$_counter',
style: TextStyle(
fontSize: 72,
fontWeight: FontWeight.bold,
)
,
),

アプリ内の各所で同じスタイルを設定したい類いのものであれば、次のようにthemeとして設定しておけば、

Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
textTheme: TextTheme(
display1: TextStyle(
fontSize: 72,
fontWeight: FontWeight.bold,
),
),
)
,
);
}

表示する箇所でそれを次のように参照することでそのスタイルを適用できて統一的なデザインで組めるようになります。

Text(
'$_counter',
style: Theme.of(context).textTheme.display1,
),

Themeの仕組みをきちんと理解するには InheritedWidget の理解も大事になってきてこれはFlutterフレームワークの大事な要素なのですが、けっこう難しいので慣れてきたタイミングで以下など読んで勉強してみてください。

クラス名やフィールド名の先頭の _ (アンダースコア)は何?

多くのプログラミング言語での private キーワードに相当し、外部からのアクセスを制限するものです。クラス名・フィールド名・メソッド名の先頭に付けますが、このサンプルでは _MyHomePageState クラス・ _incrementCounter()_counter が利用例です。

Dartの場合、基本的に1ファイル = 1ライブラリとなっていて、 _ を使うことでファイル外(ライブラリ外)からアクセスできないように隠せます。

例えば、次のような a.dart ファイルがあった時、

// a.dart
class _A1 {}
class A2 {
String _text;
}

b.dart ファイルから、それらを使おうとすると、 _ の有無に応じて次のようになって、NGのパターンではコンパイルエラーになって実行できません。

// b.dartimport 'a.dart';// NG
final a1 = _A1();
// OK
final a2 = A2();
// NG
print(a2._text);

カウンターサンプルではすべて1ファイルに含まれているので、実は _ は意味を成していないですが、実際のアプリではファイル外からのアクセスが必要なもの以外はなるべく隠すようにします。

ドキュメントとしては、以下などに書かれている内容です。

Libraries and visibility
The import and library directives can help you create a modular and shareable code base. Libraries not only provide APIs, but are a unit of privacy: identifiers that start with an underscore (_) are visible only inside the library. Every Dart app is a library, even if it doesn’t use a library directive.

https://dart.dev/guides/language/language-tour#libraries-and-visibility

Dart基礎

何か1つでもプログラミング言語に慣れていれば、Dartはかなりとっつきやすいです。Flutter書きながらたまに直感的に仕様を推測できないところだけ調べたり程度でもかけてしまうと思います。

とはいえ、それだけだと多少知識に抜けが出てしまったりするので、次などに一通り目を通すと良いです。

一方、例えばちょっとJavaScript触ったことある程度以下の知識だと、初めは分からないことが多くて苦労するかもしれません🤔ざっと調べたところ、このあたりが比較的容易な日本語記事・本でした。

Dart入門 — Dartの要点をつかむためのクイックツアー

このあたりも理解できない場合、他の言語でも良いので超入門書から学ぶしかない気がします🤔 Swiftあたりは型があって比較的簡潔という点でDartとまあまあ似ていてかつ人気言語で情報も多いので、Amazonなどのレビューを見て優しそうな本を選んでみてください🐶

あるいは、まずはStatelessWidgetとそのbuildメソッドの利用に徹して静的なレイアウトを組むことだけに割り切ればDart言語の知識はあまり必要ないため、とりあえずそうしてDartの勉強は先延ばしにしてしまうのもありだと思います👌

以上、カウンターアプリを元に、Flutter基礎をなぞりました。(プログラミング自体も含めて)初学者をターゲットにしたつもりでしたが、どうしてもだんだん難しくなってきてしまった気もします🤔

ただ、必ずしも初めからすべて理解する必要はなく、たくさん書いたり調べたりしながら、少しずつ分からないところが減っていけば充分だと思います。そのためには楽しく学習できることが一番大事なので、分からない点にはあまり悩まず前向きに取り組んでみてください。

本記事と合わせて、以下なども読むとFlutter学習・開発がより捗ると思います🐶

--

--