シンプルな壁紙対応時計を開発する
こんにちはnasustです。
Flutter入門者向けにシンプルな壁紙対応時計の開発を解説します。 入門向けなので簡単な実装となっています。
なお「 初回作成時のサンプルのカウントアッププログラムを読み解く 」で重複する内容は解説しません。
解説に使用するソースコードは以下で公開されています。
Flutterプロジェクトを作成
Flutterのアプリを作成するにはプロジェクトを作成します。 プロジェクトの作成方法はいくつかあります。
コマンドライン
以下のコマンドを実行するとプロジェクトが作成されます。
flutter create -i swift -a kotlin --org com.nasust flutter_clock
-i と -a
iOSとAndroidのプログラム言語を設定します。
-i
はObjectCまたはSwiftが指定出来ます。
-a
はJavaまたはkotlinが指定出来ます。
—org
KotlinのパッケージとiOSのbundle identifierを設定します。
com.nasust
は自分の組織のものに書き換えてください。
私の場合はnasust.comというドメインを取得しているので、それを利用しています。
指定しないとcom.example
になってしまうので必ず指定するようにしましょう。
VS Code
VS Codeのコマンドパレットを開いてFlutter: New Project
を実行します。
シンプルに作成出来ますが、コマンドラインの-i
,-a
,--org
が設定出来ないです。
Android Studio
Start a new Flutter project
を選択します。
{{< img src=“fluttercreateandroid_studio.png” scale=0.8 >}}
Android Studioのプロジェクト作成はウィザード形式で 内容を簡単に設定できるので一番お勧めです。
main.dart
lib/main.dartを編集します。
hello worldが表示されるサンプルコードになっています。 これを以下のように編集します。
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'widgets/clock_widget.dart';
import 'widgets/wallpaper_widget.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 Clock',
theme: ThemeData.dark(),
home: _ClockHomePage(),
);
}
}
class _ClockHomePage extends StatelessWidget {
final _key = new GlobalKey<WallpaperWidgetState>();
_pickImage() async {
var imageFile = await ImagePicker.pickImage(source: ImageSource.gallery);
if (imageFile != null) _key.currentState.imageFile = imageFile;
}
Widget build(BuildContext context) {
return Scaffold(
body: GestureDetector(
behavior: HitTestBehavior.opaque,
child: Stack(
children: <Widget>[
Container(child: WallpaperWidget(key: _key)),
Container(child: Center(child: ClockWidget())),
],
),
onTap: () => _pickImage(),
),
);
}
}
class MyApp extends StatelessWidget
ルートのWidgetです。ダークテーマを適用しています。
最初のページは_ClockHomePage
を指定しています。
class _ClockHomePage extends StatelessWidget
StatelessWidget
を継承している_ClockHomePage
です。
壁紙と時計の処理は子のWidgetにしているのでStatelessWidget
にしました。
_ClockHomePage
をStatefulWidget
にして壁紙と時計を一個のWidgetで実装することもできますが、時間の更新、壁紙の変更などで画面全体のWidgetツリーを更新する事になってしまうので負荷が掛かります。
Widgetツリーとは、FlutterのWidgetはツリー状の親子関係で管理されています。 StatefulWidgetの状態が更新されると子のWidgetが再ビルドの対象になります。
負荷を掛けないようにするには状態の変更で、Widgetツリーの再ビルドの対象を最小限にした方が良いです。
StatefulWidgetのパフォーマンスについては 「StatefulWidget class - widgets library - Dart API」 のPerformance considerationsで解説されております。
DevToolsでWidgetツリーの構造が確認できます。
body: GestureDetector
画面をタッチしたら壁紙を選択する画面を表示したいので、 GestureDetectorでタッチイベントを拾っています。
see also: GestureDetector class - widgets library - Dart API
child: Stack
壁紙と時計を重ねたいのでStackを使用します。StackはレイアウトのWidgetです。
Stack
には壁紙を表示するWallpaperWidget
と時計を表示するClockWidget
を配置しています。
それぞれのWidgetは、Container
とCenter
でレイアウトを調整しています。
see also: Stack class - widgets library - Dart API
see also: Container class - widgets library - Dart API
see also: Center class - widgets library - Dart API
DevToolsでDebug Paintを実行するとレイアウトが確認できます。 このようにFlutterはレイアウトのWidgetで簡単に配置できます。
壁紙の設定処理
onTap: () => _pickImage()
で壁紙の画像を選択する画面を表示して、
選択された画像を壁紙に設定しています。
_pickImage() async {
var imageFile = await ImagePicker.pickImage(source: ImageSource.gallery);
if (imageFile != null) _key.currentState.imageFile = imageFile;
}
ImagePicker.pickImage(source: ImageSource.gallery)
は
画像を選択する画面が表示されます。画像が選択されると画像ファイルが返されます。
このメソッドはpub.devで公開されているプラグインを使用しています。
このプラグイン使用するには、以下のようにpubspec.yamlを編集します。
dependencies:
image_picker: ^0.6.3
VS CodeまたはAndroid Studioで開発している場合は、記述して保存すると自動的にダウンロードが始まって使用可能になります。
asyncとawaitについて
_pickImage() async
とawait ImagePicker.pickImage
と記述されているのは、
これらの処理を非同期で行う為です。
ImagePicker.pickImage
で画像が返されるタイミングは選択した後になります。その戻り値はFuture<File>
になっています。
Futureは「画像が選択される」ようなイベントをthenメソッドに指定したコールバックで処理できます。
see also: Future class - dart:async library - Dart API
壁紙を設定する処理を Futureのthenメソッドでイベントをコールバックする場合のコードは以下の通りになります。
ImagePicker.pickImage(source: ImageSource.gallery).then((imageFile){
if (imageFile != null) _key.currentState.imageFile = imageFile;
});
Future<File>
のthenメソッドで指定しているコールバックは画像を選択されたタイミングで呼ばれます。
このようにコールバックであるのは非同期で処理する為です。 Flutterはシングルスレッドで動作しています。 もし逐次処理で画像が選択されるまで待つならばアプリ全体が停止します。
この「画像が選択された」のようなイベントをコールバックで処理する仕組みはFlutterがイベントループで動作している為です。
FlutterのようなGUIのアプリはユーザの操作など沢山のイベントが発生します。 無限ループでイベントをキューに貯めて、対応したコールバックで処理をします。 Android,iOS,Window,MacなどのGUIは、この形式で動作しています。 後、GUIじゃないですがNode.jsもイベントで動作しています。
偶にGUIが固まって、その状態で色々操作してから固まったのが解除されると一気に操作されるのは、 キューに操作のイベントが溜まっているからですね。
イベントループについては、「 The Event Loop and Dart (翻訳) - Qiita 」に詳しく解説されています。
そしてasync、awaitについてですが、Futureのthenメソッドとコールバックの処理を簡潔に書ける書式です。 コールバックの問題点は、コールバックの中にコールバックを書くと分かり難くなる問題があります。 1つは2つならともかく、多重のコールバックになるとソースが見辛くなりバグの元になりやすいです。
それを解決したのがasync、awaitです。
var imageFile = await ImagePicker.pickImage(source: ImageSource.gallery)
は、コールバックをFutureのthenメソッドを隠蔽して一行で逐次処理っぽく書けるので分かりやすいです。
final _key = new GlobalKey() と _key.currentState.imageFile = imageFile
_key
の型はGlobalKey<WallpaperWidgetState>()
です。
この_key
をWallpaperWidget
のコンストラクターで名前付き引数key
に指定すると、
_key.currentState
でWallpaperWidget
の状態管理するState
クラスを継承したWallpaperWidgetStateにアクセスできます。
この方法でWidgetに壁紙を設定しています。
GlobalKey
とはWidgetを識別します。このようにGlobalKey
で関連付けると、
簡単にWidgetとStateを取得できます。
see also: GlobalKey class - widgets library - Dart API
clock_widget.dart
libs/widgets/clock_widget.dartは以下の通りです。 時計を表示します。
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:intl/intl.dart';
class ClockWidget extends StatefulWidget {
ClockWidget({Key key}) : super(key: key);
ClockWidgetState createState() => ClockWidgetState();
}
class ClockWidgetState extends State<ClockWidget> {
var _nowTime = DateTime.now();
final _dateFormat = new DateFormat.Hms();
void initState() {
super.initState();
_initTimer();
}
void _initTimer() {
Timer.periodic(Duration(milliseconds: 33),
(Timer timer) => setState(() => _nowTime = DateTime.now()));
}
Widget build(BuildContext context) {
var text = _dateFormat.format(_nowTime);
var fontSize = MediaQuery.of(context).size.width * 0.15;
return Stack(
children: <Widget>[
Text(
text,
style: TextStyle(
fontSize: fontSize,
foreground: Paint()
..style = PaintingStyle.stroke
..strokeWidth = 4
..color = Colors.black),
),
Text(
text,
style: TextStyle(
fontSize: fontSize,
),
)
],
);
}
}
class ClockWidget extends StatefulWidget
このWidgetは時計という状態を持っているので、StatefulWidget
を継承して実装しています。
StatefulWidget
はUIの一部が動的に変化する場合に便利です。
createState
でClockWidgetState
を作成して返しています。
see also: StatefulWidget class - widgets library - Dart API
class ClockWidgetState extends State
時計のテキストを作成するState
を継承しています。
see also: State class - widgets library - Dart API
var _nowTime = DateTime.now();
DateTime型の現在の時間のプライベート変数です。
see also: DateTime class - dart:core library - Dart API
final _dateFormat = new DateFormat.Hms();
DateFormat型のプライベート変数です。 DateFormatはDateTimeをフォーマットに従って文字列に変換できます。
see also: DateFormat class - intl library - Dart API
DateFormatはpub.devで公開されているDart Packageです。
使用するにはpubspec.yamlを以下の様に記述します。
dependencies:
intl: ^0.16.0
intlは国際化対応に便利なパッケージです。
void initState()
このメソッドはオーバーライドして実装しています。
initState()
はWidget作成時に呼ばれます。
_initTimer
メソッドを実行しています。
void _initTimer()
ここでは時計の為の時間の_nowTime
を
30フレームで現在時刻に更新するタイマー登録しています。
void _initTimer() {
Timer.periodic(Duration(milliseconds: 33),
(Timer timer) => setState(() => _nowTime = DateTime.now()));
}
タイマーはTimerクラスで登録できます。
see also: Timer class - dart:async library - Dart API
タイマーで実行する関数は、setState
メソッド内で実行しています。
setState
メソッドは引数のコールバックを指定した関数は同期的に実行して、状態変化をFlutter UIのフレームワークに通知します。指定したコールバックで状態変化を実装します。
see also: setState method - State class - widgets library - Dart API
Widget build(BuildContext context)
時間のテキストのUIを作成しています。
var fontSize = MediaQuery.of(context).size.width * 0.15
は画面サイズに合わせてフォントサイズを計算しています。
see also: MediaQuery class - widgets library - Dart API
画面に文字を表示するにはTextのWidgetを使用します。
see also: Text class - widgets library - Dart API
Stack(
children: <Widget>[
Text(
text,
style: TextStyle(
fontSize: fontSize,
foreground: Paint()
..style = PaintingStyle.stroke
..strokeWidth = 4
..color = Colors.black),
),
Text(
text,
style: TextStyle(
fontSize: fontSize,
),
),
]
)
Stack
で同じ文字を重ねて表示しているのは、アウトラインに黒の枠を表示する為です。
1個目のforeground引数のPaintで黒の枠を描画しています。
see also: Paint class - dart:ui library - Dart API
1個目のTextは枠以外が透明になりますので、2個目のTextで文字の塗りつぶしをしています。 重ねることで白塗りの黒枠の文字が表示されます。
現状、Flutterで文字に枠を表示される方法は、この様にStack
とPaint
を使用するか、文字に影を付けるShadow
を使用する方法があります。
see also: Shadow class - dart:ui library - Dart API
see also: Missing Text outline / stroke / border feature · Issue #24108 · flutter/flutter
wallpaper_widget.dart
libs/widgets/wallpaper_widget.dartは以下の通りです。 壁紙を表示します。
import 'dart:io';
import 'package:flutter/widgets.dart';
class WallpaperWidget extends StatefulWidget {
WallpaperWidget({Key key}) : super(key: key);
State<WallpaperWidget> createState() {
return WallpaperWidgetState();
}
}
class WallpaperWidgetState extends State<WallpaperWidget> {
Image _image;
set imageFile(File imageFile) {
setState(() {
_image = Image.file(imageFile,fit: BoxFit.cover);
});
}
Widget build(BuildContext context) {
return Container(
constraints: BoxConstraints.expand(),
child: _image,
);
}
}
WallpaperWidget extends StatefulWidget
壁紙の状態を持っていますので、StatefulWidget
を継承しています。
createState()
でWallpaperWidgetState
を作成しています。
set imageFile(File imageFile)
画像を設定するプロパティです。
プライベート変数であるImage _image
に画像を設定しています。
Image.file(imageFile,fit: BoxFit.cover)
のfit: BoxFit.cover
は
fitさせる方法を指定しています。
see also: Image class - widgets library - Dart API
see also: BoxFit enum - painting library - Dart API
Widget build(BuildContext context)
画像を内包したContainer
を返しています。
constraints: BoxConstraints.expand()
は、_imageをContainer
の大きさに合わせて拡大縮小させます。
see also: constraints property - Container class - widgets library - Dart API
see also: BoxConstraints class - rendering library - Dart API
まとめ
Flutter解説用に壁紙対応時計アプリを開発しました。今回の解説で注目すべき点は以下の通りです。
- pub.devでプラグインを簡単に導入できる
StatelessWidget
とStatefulWidget
を組み合わせて負荷を最小限にするasync
,await
で非同期の処理を行えるGlobalKey
でWidget
とState
を簡単に取得できる
これらの機能を利用することで、iOSやAndroidと比べるとシンプルに開発できました。 フレームワーク、宣言型UIとdartのスクリプト言語という特徴でiOSやAndroidのアプリ開発より少ない工程で実装できるのがFlutterの良いところです。