シンプルなスマホUIを実装する(Flutter):その1
概要
マルチプラットフォーム開発フレームワークの1つであるFlutterを利用してスマホUI(スマホアプリ)を開発してみます。
マルチ開発フレームワークはReactNativeとFlutterが2大巨頭で、日本ではFlutter、海外ではReactNativeが優勢らしいです。当社はReactNativeを利用していますが、外部案件だとFlutterを利用することもあるので最低限の知識はつけておきましょう。
前提
- 環境
- macOS
- Visual Studio Codeがインストールされている
- かつFlutter機能拡張がインストールされており、flutter doctorコマンドで問題が無い状態
- Android Studioがインストールされており、シミュレータが利用できる
- Xcodeがインストールされており、シミュレーターが利用できる
- 知識
- できれば「シンプルなスマホUIを実装する(Android Native)」の学習が完了していること
- できれば「シンプルなスマホUIを実装する(iOS Native)」の学習が完了していること
準備
必要な雛形を生成する
- VSCodeにてコマンドパレットを開き、Flutter: New Project を入力・選択します
- Empty Applicationを選択
- 適切な保存場所選択してください
- こだわりがなければホームディレクトリを選んでおいてください。サポートの際にわかりやすいので
- Project Nameを聞かれるので入力してください
- ここではsimple-mobile-flutterとしています
動作確認
プロジェクトの雛形が問題無く生成され実行可能か一度動作確認しておきます。
以下の操作を行います。
- 動作確認する環境を指定
- VSCode右下にあるプラットフォーム選択メニューを選択し「Start iOS Simulator」を選択します
- iPhoneのシミュレータが起動するはずです(標準ではmacOSになっている)
- メニューがよくわからない人は手動でiPhoneシミュレータを起動しておいてもらってもいいです
- VSCode右下にあるプラットフォーム選択メニューを選択し「Start iOS Simulator」を選択します
- アプリの起動(デバッグ開始)
- VSCodeのメニューからRun -> Start Debuggingを選択します
- Select Debuggerの選択メニューが開くので、Dart & Flutterを選択してください
シミュレータにHello World!と表示されればOKです。
雛形の構成を確認する
プロジェクトディレクトリの1階層目は以下のようになっているはずです。
いろいろ多いですが、実装に利用するのはlibディレクトリとpubspec.yamlくらいです。
- libディレクトリ
- 実装ファイル
- pubspec.yaml
- ライブラリ等の設定で利用
.
├── analysis_options.yaml
├── android
├── flutter_application_1.iml
├── ios
├── lib
├── linux
├── macos
├── pubspec.lock
├── pubspec.yaml
├── README.md
├── web
└── windows
では、libディレクトリの中身を見てみます。
.
└── main.dart
main.dartファイルが1つあるだけです。Flutterのブートはこのmain.dartが実行されるだけとなります。
追加のファイルを作成する
では、実装に利用するファイルを作成します。ディレクトリ等は特に作成せずmain.dartと同じ階層に以下のファイルを作成します。
- bottomtab.dart
- ボトムタブを含む全体を構成するファイル。これをmain.dartから呼び出す想定
- home.dart
- Home画面
- contact.dart
- Contact画面。Formを実装する想定
touch lib/bottomtab.dart lib/home.dart lib/contact.dart
必要なライブラリをインストールする
API連携で利用する通信用ライブラリ(http)を設定しておきます。
ライブラリの追加はプロジェクトフォルダ直下にあるpubspec.yamlを編集して行います。
以下を追加します。記述後、自動的にライブラリが読み込まれます。
name: flutter_application_1
description: "A new Flutter project."
publish_to: 'none'
version: 0.1.0
environment:
sdk: ^3.8.1
dependencies:
+ http: any
flutter:
sdk: flutter
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^5.0.0
flutter:
uses-material-design: true
実装
では実装していきます。
基本構造を実装する
本アプリはHome画面とContact画面の2画面を持ち、また、その画面はボトムタブで切り替え可能としたいので、その基本構成から実装してみます。
先にタブで表示する対象となるHomeとContactから実装してみます。
lib/home.dart
ただHomeと表示するだけの実装です。
なお、コメントとして// dart format offを追加しています。
これはVSCodeのFlutter機能拡張フォーマッタが効くのを一時的に無効化するための記述なので、標準のフォーマッタを使いたい人は無理に記述する必要はありません。
ここでは可読性向上のために入れた改行が無効化されてしまうので入れています。
import 'package:flutter/material.dart';
// dart format off
class Home extends StatelessWidget {
const Home({super.key});
@override
Widget build(BuildContext context) {
return Center(
child: Text("Home"),
);
}
}
lib/contact.dart
現状ただContactと表示するだけの実装ですが、Contactは動的に値を保持・操作するので状態を管理可能なStatefulWidgetとして実装します。
あらためてhome.dartをみてもらうとStatelessWidgetを継承しているかと思います。
import 'package:flutter/material.dart';
// dart format off
class Contact extends StatefulWidget {
const Contact({super.key});
@override
State<Contact> createState() => _ContactState();
}
class _ContactState extends State<Contact> {
@override
Widget build(BuildContext context) {
// 全体をColumnでレイアウト
return Center(
child: Text("Contact"),
);
}
}
StatefulWidgetを利用するとき、
- StatefulWidgetを定義
- 実際にStateを担うクラスを紐づける
- Stateを管理や表示を担うクラスを定義
という記述は慣用句的に出てくるので慣れておきましょう。
技術的にはStatefulWidgetがイミュータブル(変更不可)のため、状態を別クラスで管理するという仕組みに基づいています。
bottomtab.dart
では、BottomTabを実装してみます。
BottomTab自体状態を管理するのでStatefulWidgetになっており、実際の機能実装は_BottomTabStateが担います。
主な実装手順やポイントは以下の通りです。
- _selectedTabでどの(何番目の)Tabが選択されたかを管理
- _screens配列に表示予定の画面(Widget)を代入
- _onItemTappedで_selectedTabへの代入値を切り替え
- body: _screens[_selectedTab]で画面を表示
- bottomNavigationBarで実際のボトムタブを設定
import 'package:flutter/material.dart';
// 画面インポート
import './home.dart';
import './contact.dart';
// dart format off
class BottomTab extends StatefulWidget {
const BottomTab({super.key});
@override
State<BottomTab> createState() => _BottomTabState();
}
class _BottomTabState extends State<BottomTab> {
// どのタブが選択されているかを保持(indexは0から開始)
int _selectedTab = 0;
// Widgetの配列(切り替えに利用)
final List<Widget> _screens = [Home(),Contact()];
// Tabがタップされたときの動作を定義(後で代入)
void _onItemTapped(int index){
setState((){
_selectedTab = index;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: _screens[_selectedTab],
bottomNavigationBar: BottomNavigationBar(
items: [
BottomNavigationBarItem(icon: Icon(Icons.home), label: "Home"),
BottomNavigationBarItem(icon: Icon(Icons.mail), label: "Contact")
],
currentIndex: _selectedTab,
onTap: _onItemTapped,
)
);
}
}
main.dart
では、main.dartを編集して全体が連携して動作するようにします。
と言っても実装したBottomTabを読み込み表示するだけです。
import 'package:flutter/material.dart';
import './bottomtab.dart';
void main() {
runApp(const MainApp());
}
class MainApp extends StatelessWidget {
const MainApp({super.key});
// dart format off
@override
Widget build(BuildContext context) {
return const MaterialApp(
home: BottomTab(),
);
}
}
ここまで記述ができたら一度動作確認を行ってください。
各ファイルを実装する
では基本構造ができたので各ファイルを仕上げていきます。
lib/home.dart
Homeはスタティックな内容なので淡々と書いていきます。
全体をColumnで囲み、さらに各要素をContainerで囲み縦に配置していきます。
import 'package:flutter/material.dart';
// dart format off
class Home extends StatelessWidget {
const Home({super.key});
@override
Widget build(BuildContext context) {
// 全体をColumnでレイアウト
return Column(
children: [
//hero
Container(
color: Color(0xFFAAAAAA),
width: double.infinity,
height: 200,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(height: 40), // spacer
Text("ヒーローエリア",
style: TextStyle(color: Colors.white, fontSize: 18)),
Text("ヒーロエリアのキャッチコピー",
style: TextStyle(color: Colors.white))
],
),
),
//services
//service_a
SizedBox(height: 20),
Container(
color: Color(0xFFAAAAAA),
width: MediaQuery.of(context).size.width * 0.8,
height: 100,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text("サービスA",
style: TextStyle(color: Colors.white, fontSize: 16)),
Text("サービスAの説明",
style: TextStyle(color: Colors.white)),
],
),
),
//service_b
SizedBox(height: 20),
Container(
color: Color(0xFFAAAAAA),
width: MediaQuery.of(context).size.width * 0.8,
height: 100,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text("サービスB",
style: TextStyle(color: Colors.white, fontSize: 16)),
Text("サービスBの説明",
style: TextStyle(color: Colors.white)),
],
),
),
//service_c
SizedBox(height: 20),
Container(
color: Color(0xFFAAAAAA),
width: MediaQuery.of(context).size.width * 0.8,
height: 100,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text("サービスC",
style: TextStyle(color: Colors.white, fontSize: 16)),
Text("サービスCの説明",
style: TextStyle(color: Colors.white)),
],
),
),
],
);
}
}
記述が終わったら動作確認してください。
サービスをコンポーネント化することも考えましたが、一旦ベタ書きで。
lib/contact.dart
Contactは少々複雑なので順を追って実装してみます
基本レイアウト
レイアウトの基本はHomeと同じですがFormはFormコンポーネントがあるのでそれを利用します。
入力項目はTextFormFieldで対応します。
import 'package:flutter/material.dart';
// dart format off
class Contact extends StatefulWidget {
const Contact({super.key});
@override
State<Contact> createState() => _ContactState();
}
class _ContactState extends State<Contact> {
@override
Widget build(BuildContext context) {
// 全体をColumnでレイアウト
return Column(
children: [
//hero
Container(
color: Color(0xFFAAAAAA),
width: double.infinity,
height: 120,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(height: 40), // spacer
Text("お問い合わせフォーム",
style: TextStyle(color: Colors.white, fontSize: 18)),
Text("お気軽にお問い合わせください",
style: TextStyle(color: Colors.white))
],
),
),
// form
Padding(
padding: EdgeInsets.all(30),
child: Form(
child: Column(
children: [
// title
SizedBox(height: 10),
TextFormField(
decoration: InputDecoration(
labelText: "お問い合わせタイトル",
border: OutlineInputBorder(),
),
),
// email
SizedBox(height: 30),
TextFormField(
decoration: InputDecoration(
labelText: "Email",
border: OutlineInputBorder(),
),
),
// message
SizedBox(height: 30),
TextFormField(
decoration: InputDecoration(
labelText: "お問い合わせ内容",
border: OutlineInputBorder(),
),
minLines: 3,
maxLines: 5,
keyboardType: TextInputType.multiline,
),
// button
SizedBox(height: 30),
SizedBox(
width: double.infinity,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Color(0xFF333333),
foregroundColor: Color(0xFFFFFFFF),
),
onPressed: (){},
child: Text("送信")),
),
],
),
),
),
],
);
}
}
ここまでで一旦レイアウトに関する記述は完了です。
一旦動作確認をお願いします。
長くなるので記事を分けます。
その2に続く。
Discussion