私個人はバックエンドエンジニアですが、1つのシステムを開発しようとすると当然ながらバックエンドのみで完結するものではなく、何らかの形でフロントエンドの開発を必要とします。
個人・社内向けのツールや試作アプリなどの簡易な開発においても、フロントエンドは必要になる訳です。
バックエンドをLaravelで制作する場合、フロントエンドの実装方法として最初に上がるのはbladeテンプレートを使用する方法になりますが、昨今ではLaravelの外部インターフェースをAPI方式で提供しているので、今更通常のWeb方式に先祖返りはしたくありません。
現在当社におけるフロントエンド制作の主要技術であるNuxtを使用することも考えましたが、既に先行して当該技術を習得した人間が複数存在する状態で、あたらめて個人的に相応の学習コストを掛けることは何となくもったいないです。
上記以外の選択肢としては、現在会社として新規に取り入れようとしている技術であるFlutterがあります。
Flutterはモバイルアプリの開発を主目的とするもので、現在進行中の開発もその用途でのものですが、Webアプリの開発も可能です。
社内の習熟度もまだまだこれからと言ったところで、学習コストを掛ける価値もありそうです。
と言うことで、個人的にもFlutterにチャレンジしてみることにしました。
今回は初回と言うこともあって、極シンプルにトップページとサブページの2つの静的ページを持ち、相互に行き来ができるだけの機能を実装したいと思います。
これにより以下のような点が学習できると思っています。
- Flutterにおけるプログラムの書き方の基礎
- ファイルの分割と関連付け方法
- 画面遷移方法
- ビルド結果を実際にWebサーバに展開し、動作可能とする方法
Flutterとは
そもそもFlutterとはどのようなものでしょう。
ChatGPT(OpenAI提供)に聞いてみようかと思いましたが、せっかくなので同じGoogleが開発したBardに聞いてみました。
Flutterは、Googleが開発したオープンソースのモバイルアプリ開発フレームワークです。Dart言語を使用して開発され、Android、iOS、Web、Linux、macOS、Fuchsiaなど、さまざまなプラットフォームでアプリケーションを開発することができます。
Flutterの特徴は、以下の通りです。
・クロスプラットフォーム開発が可能
・高速で美しいUIを作成できる
・ホットリロード機能により、コードの変更を即座に反映できる
・オープンソースで、無料で利用できる
Flutterは、モバイルアプリ開発の効率化と品質向上に大きく貢献するフレームワークです。現在、多くの企業や個人開発者によって利用されており、今後もその人気は高まっていくと予想されます。
後半、若干自画自賛気味ですかね。
先に示したように、今回の目的はWebアプリのフロントエンドを制作することですが、将来的にはモバイルアプリ開発にも使用できるという点では魅力的です。
NuxtのようにHTML、CSS、Javascriptの技術に基づいている訳でもないので、フロントエンドエンジニアにアドバンテージがある訳でもなさそうです。
画面の構造やスタイルの記述に関してもクラス(Widget)生成時のパラメータで指定するような書式になっており、Nuxtと比較してプログラム色が強いため、よりプログラマー寄りのバックエンドエンジニアがチャレンジするには良い題材かと思います。
なお、上記説明にもあったように、Flutterはあくまでフレームワークの名称であり、プログラミング言語としてはDartと言うものになります。LaravelとPHPの関係と同様です。
よって、Flutter関連技術に言及する場合、厳密にはFluttter固有の仕様とDartとしての仕様を区別すべきかもしれません。
ただ、バックエンドに関しても特定の仕様に言及する場合に、それがLaravel固有のものか、PHPとして共通的なものかを特に気にせず「Laravelではこのように書く」と表現しますので、Flutterに関してもそのようにしたいと思います。
開発環境構築
Flutterのインストールや設定に関してはネット情報を参考にしてください(最新の情報に準じてもらう方が確実ですので)。
エディタに関しては、個人的にはVim派なのですが、今回はVSCodeを使いたいと思います(個人的には初使用です)。
FlutterプラグインをインストールしておくとWidget名やパラメータの候補を出してくれたり、間違いや不適切な書式に関する警告や改善のヒントを表示してくれたりしますので、大変便利です。
Webアプリの実行環境としては従来通りVirtualbox+Vagrantで作成した仮想マシン上にUbuntu+VirtualminでWebサーバ仮想ホスト環境を構築し、仮想ホストのドキュメントルート配下にWebアプリを配置することにします。
なお、VirtualboxではホストOS(Mac)とファイル共有ができるため、Mac環境でのFlutterのビルド結果が展開されるフォルダを仮想マシンと共有し、ドキュメントルート配下の適切なパスにリンクしておくことで、いちいちファイルを移動させる必要がなくなります。
Flutterプロジェクト生成
前述したようにVirtualbox上の仮想マシンと共有された環境にFlutterプロジェクトを生成します。
具体的には、コマンドを使用して以下のように生成できます。
# flutter create <プロジェクト名>
なお、VSCodeにFlutterプラグインがインストールされている状態であれば、VSCode側からプロジェクト生成ができます。この辺の操作に関してはネット参照と言うことで。
蛇足ながら、当該Flutterプロジェクトに関して生成されたファイル群はgitで世代管理+共有できるようにしておくと良いです。
また、今回はWebアプリを開発したいと言うことで、追加で以下のコマンドを実行しておく必要があります。
# flutter config --enable-web
なお、プロジェクトを生成した段階でエントリポイントとなる「lib/main.dart」には簡単なサンプルプログラムが既に記述されています。したがって、プロジェクト生成直後に開発環境においてテスト実行してみたり、ビルドした結果をWebサーバ経由でブラウザから使用してみたりすることも可能です。
アーキテクチャ
上記までで開発の準備が整いましたが、安易に作業に着手せず、この段階でしっかりアーキテクチャに関して考えておきたいと思います。
なお、アーキテクチャに関しては以下の通り(再びBardさんの見解)。
プログラムにおけるアーキテクチャとは、プログラムの構造や構成、設計指針、およびそれらの相互関係を示すものです。アーキテクチャは、プログラムの開発、保守、拡張において重要な役割を果たします。
アーキテクチャは、プログラムの目的や用途、利用者、開発環境、運用環境など、さまざまな要因を考慮して設計されます。アーキテクチャが適切に設計されていると、プログラムは効率的かつ効果的に実行され、保守や拡張が容易になります。
アーキテクチャの設計は、プログラム開発の初期段階で行うことが重要です。アーキテクチャが設計されていないと、プログラムの開発が遅れたり、コストがかさんだりする可能性があります。また、アーキテクチャが設計されていないと、プログラムの保守や拡張が困難になる可能性があります。
アーキテクチャは、プログラムの開発、保守、拡張において重要な役割を果たします。アーキテクチャが適切に設計されていると、プログラムは効率的かつ効果的に実行され、保守や拡張が容易になります。
ただ、Flutter自体をこれから学習していこうとしている身で、現時点において将来を見通したアーキテクチャ設計ができるとは思えません。
現時点では、今回の目標である「2つの静的ページから構成されるアプリ」での必要性に限定してアーキテクチャを考えたいと思います。
アーキテクチャを真面目に考えるとなかなかに奥が深そうですが、単純には以下の2点かと思っています。
- どのように機能分割するか
- 分割した機能をどのようにファイル化し、配置するか
先に示したようにプロジェクト生成直後は「lib/main.dart」1つのみが生成されており、その中に複数のクラスが定義されています。
しかし、実際の開発では実装されるクラスも多く、1つのファイルに全ての処理が詰め込まれていると言うことは考え難いです。
シンプルに考えて、画面単位でクラス(ファイル)は分割すべきでしょうし、1画面に関する記述内容が大きく、複雑になるようであれば適宜分割したくなると思われます。
と言うことで、まずは以下のような構造を考えたいと思います(ネット情報なども参考にしています)。
lib
+-- ui
+-- screens
+-- components
「screens」配下には各画面に相当するファイルを格納します。
また、まとまった処理や複数箇所で共有できる処理などは別クラスとして切り出して、クラスのサイズをコンパクトにしたいと思います。
そのように画面から切り出された部分的な処理を「components」に格納します。
処理内容
前述の方針に従い、今回は以下のようにファイル分割しました。
lib
+-- main.dart
+-- ui
+-- screens
| +-- top.dart
| +-- sub.dart
+-- components
+-- my_app_bar.dart
「lib/main.dart」はエントリポイントです。
「lib/ui/screens/top.dart」ではトップページに関する処理を定義します。
「lib/ui/screens/sub.dart」ではサブページに関する処理を定義します。
「lib/ui/components/my_add_bar.dart」ではトップ、サブの両ページで共有されるAppBar(画面上部に表示されるヘッダ的な部分)の生成に関する処理を定義します。
各処理に関しては以降で詳しく触れていきます。
エントリポイント(lib/main.dart)
エントリポイントである「lib/main.dart」の内容は以下の通りです。
import 'package:flutter/material.dart';
import 'package:sample1/ui/screens/top.dart';
import 'package:sample1/ui/screens/sub.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'sample1 app',
home: const TopPage(),
routes: {
"/top": (context) => const TopPage(),
"/sub": (context) => const SubPage(),
},
);
}
}
「main」関数内で「runApp」を実行し、MyAppを呼び出しています。
MyApp内では「build」メソッドによりMaterialAppを生成しています。この結果、Widget全体で構成されるWidgetツリーのルートはMaterialAppとなるようです。
MaterialAppはGoogleさん提唱の「マテリアルデザイン」に準じてアプリ全体に共通のテーマやナビゲーションなどを適用できるようにするものです。
アプリ全体を包含する位置付けと言うことでMaterialAppはルートWidgetであることが必要となる訳ですね。
MaterialApp生成時のパラメータには様々な情報を指定できるようですが、上記ではシンプルに以下の3つを指定しています。
title | アプリのタイトルと言うことで、特にWebアプリにおいてはhead内titleの値として使用されます |
home | アプリ起動後の最初のページに当たるWidgetを指定します |
routes | アプリの画面遷移のルートを指定します |
特に「routes」では各画面のパスと関連するWidgetの対応付けを行っています。
また、ここで呼び出している「TopPage」「SubPage」クラスは別ファイルで定義されているため、それらを参照するよう指定しているのが下記の部分です。
import 'package:sample1/ui/screens/top.dart';
import 'package:sample1/ui/screens/sub.dart';
上記のように「lib/main.dart」の内容はエントリポイントとしてルートWidgetであるMaterialAppを生成するところまでにしておくのが機能分割的に良さそうです。MaterialAppのパラメータの指定を除けば、本処理は全てのプロジェクトにおいて、ほぼ共通かつ固定の処理と言って良いでしょう。
ちなみに、MaterialApp以外でも同様にアプリ全体のテーマ等を制御するWidgetはあるようですが、とりあえずFlutterの定番として今後も特に必要のない限りMaterialAppを採用したいと思います。
なお、上記でさりげなく使用(継承)しているStatelessWidgetですが、文字通り状態を持たないWidgetとのことです。
ただ、「状態」に関する理解が現時点では不十分なため、今回は深くは触れません。
今まで各種サンプルロジックを見てきた限りではStatelessWidgetは上記のようにbuildメソッドで他のWidgetを生成することのみに徹しているようなので、とりあえずはこの程度の用法を固定的に用いることができるようにしておけば問題なさそうです。
トップページ(lib/ui/screens/top.dart)
トップページ(lib/ui/screens/top.dart)の内容は以下の通りです。
import 'package:flutter/material.dart';
import 'package:sample1/ui/components/my_app_bar.dart';
class TopPage extends StatelessWidget {
const TopPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: const MyAppBar(title: "Topページ"),
body: Column(
children: [
const Text("Topページです"),
ElevatedButton(
onPressed: () => Navigator.of(context).pushNamed("/sub"),
child: const Text('Subページへ'),
)
],
),
);
}
}
エントリポイントの際にも触れたように、StatelessWidgetを継承したクラスとしてbuildメソッドにより画面に相当するWidget(Scaffold)を生成しています。
ScaffoldはFlutterにおける標準的な画面レイアウト構築用のWidgetです。
ここでは「appBar」「body」の2つのパラメータのみ指定しています。
appBarにはページのヘッダ部に該当する表示を行うWidgetを指定します。
通常は「AppBar」というWidgetを直接指定するのですが、今回は複数ページで共通の生成処理として「MyAppBar」という独自のWidgetを使用しています。
MyAppBarは別ファイルで定義されているため、その参照も指定します。
import 'package:sample1/ui/components/my_app_bar.dart';
bodyには当該画面を構成する各種Widgetを指定しています。
Columnは内包するWidgetを縦並びに表示するWidgetです。「children」パラメータとして表示するWidgetを指定します。
Textは文字列を表示するWidgetです。
ElevatedButtonはボタンを表示するWidgetで、ここではサブページへ移動するボタンを定義しています。「onPressed」パラメータにはボタンが押された時の挙動を指定します。「child」パラメータにはボタンの表示内容を指定します(ここでは「Subページへ」という文字列を表示しています)。
なお、ページの移動に関する処理は以下のような書式になっています。
Navigator.of(context).pushNamed("/sub")
Flutterのルーティングはスタック的な考え方でできており、スタックの最上位に配置された画面が表示されます。
よって、ページ遷移する場合は、スタックの最上位に移動先の画面を配置(push)することになります。
元の画面に戻る際はスタックの最上位の画面を除去することで、一つ前の画面が最上位になり、表示されることになる訳です。
サブページ(lib/ui/screens/sub.dart)
サブページ(lib/ui/screens/sub.dart)の内容は以下の通りです。
import 'package:flutter/material.dart';
import 'package:sample1/ui/components/my_app_bar.dart';
class SubPage extends StatelessWidget {
const SubPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: const MyAppBar(title: "Subページ"),
body: Column(
children: [
const Text("Subページです"),
ElevatedButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('戻る'),
)
],
),
);
}
}
基本構造はトップページと変わりありません。
ボタンが押された時の挙動はトップ画面に戻るようになっています。
先ほど触れたように、「戻る」操作はスタックの最上位から現在の画面を取り除く操作(pop)になる訳です。
Navigator.of(context).pop()
共通部品(lib/ui/components/my_app_bar.dart)
先に紹介した2つのページの共通部品として、AppBarを生成するクラスを定義しています。
import 'package:flutter/material.dart';
class MyAppBar extends StatelessWidget implements PreferredSizeWidget {
final String title;
const MyAppBar({Key? key, required this.title}) : super(key: key);
@override
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
@override
PreferredSizeWidget build(BuildContext context) {
return AppBar(title: Text(title));
}
}
コンストラクタにおいて、パラメータとして指定された「title」を同名のプロパティとして保持するようにしています。
const MyAppBar({Key? key, required this.title}) : super(key: key);
今まで示してきたように、通常のStatelessWidgetではbuildによりWidgetを生成しますが、本クラスのbuildではAppBarを返す必要があり、AppBarはPreferredSizeWidgetクラスなので、buildの処理もそのように書き換えます。
また上記関係で、クラス自体の定義でPreferredSizeWidgetを継承(implements)する必要があります。
加えて以下の処理も必要となります。
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
と書いてきましたが、正直なところ上記処理はネット情報やChatGPTさんの教えに従ったもので、意味を十分理解できていないです。
実はネット情報でもAppBar生成の共通化に関する情報はあまり発見できませんでした。
HTMLで考えれば、各ページ共通のヘッダ部は独立したファイルとしておき、各ページから参照するようにしておくのは常套手段だと思うので、この辺があまり考慮されていないように思われる点は謎です。
ビルド
上記各ファイルを生成した上で、ビルドを実施します。
# flutter build web --base-href /sample/
「flutter build web」でWebアプリとしてのビルドが行われます。
一点注意が必要なのは、単に「flutter build web」と実行した場合、ビルドされたWebアプリは展開先となるサイトのドキュメントルート直下に配置されることを想定した構造になる点です。
もし、ドキュメントルート直下ではなくサブディレクトリ以下に配置したい場合は、上記のように「–base-href」オプションを使用して、Webアプリが配置されるディレクトリのドキュメントルートからの相対パスを指定する必要があります(上記例では、ドキュメントルート配下の「sample」ディレクトリに配置されることを想定しています)。
この時、指定するパスの最初と最後はスラッシュ(/)である必要があります。
まとめ
上記で、とりあえずはメイン、サブの2つのページを行き来できるアプリがWeb環境で実行できるようになりました。
今回は、確信犯的にFlutter(Dart)の書式(シンタックス)に関する説明をかなり端折っています。
これは、書式よりも全体としての構造(あくまでシンプルなものではありますが)の方に着目すべきと思ったからです。
PHPの解説書の多くでは、HTMLの中にPHPの書式を埋め込むような形のサンプルを提示していたりします。
無論、それでPHPの書式自体は説明できるのですが、実際にPHPでシステムを開発する際にはそのような書き方はほとんどしません。むしろPHPとHTMLはテンプレートエンジンを使用して分離するのが普通でしょう。
「説明」のための利便性から、むしろ間違った例示をしているように思います。
また、実際のシステム設計にはトップダウン的な視点が必要だと思っています。この中に、本記事の途中でも触れた「アーキテクチャ」の検討なども含まれてきます。
しかし、プログラミング言語に関する解説の大半は、まずは細部・局所の処理に言及し、それらが組み合わされて最終的には1つのシステムが形成可能となる、と言うボトムアップ的な発想でできているように思います。
個別の機能や書式に言及する必要性からある程度止むを得ないとは思うものの、何となく分かった気になるだけで実用性が感じられない、モヤっとした知識を習得するだけになってしまうような印象です。
上記のような懸念から、「我流Flutter学習」のアプローチとしては細部に拘らずに、全体的な把握と実用性に重点を置いて進めていきたいと思っています。
学習すべき内容はまだまだ山積みですが、とりあえずは第一歩を踏み出したと言うことで、今後少しずつ精進していきたいと思います。