Flutterでアプリを作るまで(自分用勉強メモ)
まずは、Dart言語について学ばないと、ということで、公式サイトの言語ツアーを見てサンプルコードを書いてみる。
計算系の演算子で、 ~/
が地味に便利だなーと思った。
たまにシステムを開発していて、割り算で整数値を出したい、という時があるので、それがネイティブに演算子として存在しているので、便利過ぎて泣いた;;
print("10 / 6 = ${10 / 6}");
// -> 10 / 6 = 1.6666666666666667
print("10 ~/ 6 = ${10 ~/ 6}");
// -> 10 ~/ 6 = 1
あと、個人的な感想として、私自身がもともとJavaをガッツリやっていたので、それに文法が似ているところが多く、すんなり受け入れそうだな、と感じた。
Javaの古き良き文法+今どき流行りの文法がいい感じに混ざった感じ。
あ、ちなみに、FlutterSDKは以下のリンクを見てインストールした。
「Get the Flutter SDK」と書いてある章のすぐ下にあるzipファイルダウンロードのボタンをクリックしてFlutter SDKをダウンロード&解凍、指示通りの位置へ展開する。
$ mkdir ~/development
$ cd ~/development
$ unzip ~/Downloads/flutter_macos_2.8.1-stable.zip
↑ development
というディレクトリはおそらくデフォルトで存在しないと思うので、ディレクトリを生成するコマンドを追加
また、 .zshrc
に以下の一文を追記し、どこでも flutter
コマンドを使えるようにする。
export PATH=~/development/flutter/bin:$PATH
追記後、 source .zshrc
を実行し、上記の設定を今のターミナルに反映する。
最後に flutter
コマンドを実行し、正常にヘルプが表示されることを確認する。
$ flutter
Manage your Flutter app development.
Common commands:
flutter create <output directory>
Create a new Flutter project in the specified directory.
flutter run [options]
Run your Flutter application on an attached device or in an emulator.
Usage: flutter <command> [arguments]
Global options:
-h, --help Print this usage information.
-v, --verbose Noisy logging, including all shell commands executed.
If used with "--help", shows hidden options. If used with "flutter doctor", shows additional
diagnostic information. (Use "-vv" to force verbose logging in those cases.)
-d, --device-id Target device id or name (prefixes allowed).
--version Reports the version of this tool.
--suppress-analytics Suppress analytics reporting when this command runs.
Available commands:
Flutter SDK
bash-completion Output command line shell completion setup scripts.
channel List or switch Flutter channels.
config Configure Flutter settings.
doctor Show information about the installed tooling.
downgrade Downgrade Flutter to the last active version for the current channel.
precache Populate the Flutter tool's cache of binary artifacts.
upgrade Upgrade your copy of Flutter.
Project
analyze Analyze the project's Dart code.
assemble Assemble and build Flutter resources.
build Build an executable app or install bundle.
clean Delete the build/ and .dart_tool/ directories.
create Create a new Flutter project.
drive Run integration tests for the project on an attached device or emulator.
format Format one or more Dart files.
gen-l10n Generate localizations for the current project.
pub Commands for managing Flutter packages.
run Run your Flutter app on an attached device.
test Run Flutter unit tests for the current project.
Tools & Devices
attach Attach to a running app.
custom-devices List, reset, add and delete custom devices.
devices List all connected devices.
emulators List, launch and create emulators.
install Install a Flutter app on an attached device.
logs Show log output for running Flutter apps.
screenshot Take a screenshot from a connected device.
symbolize Symbolize a stack trace from an AOT-compiled Flutter app.
Run "flutter help <command>" for more information about a command.
Run "flutter help -v" for verbose help output, including less commonly used options.
ひとまず、サンプルコードを書いたリポジトリを用意。
ここに、Dart言語を学んだ際に使用したサンプルソースコードを置いていこうかな。
代入演算子で ??=
が地味に便利だなぁ。
格納先の変数が null
かどうかで値を入れる/入れないをif文で条件分岐させて入れていたので、こういう地味だけどかゆいところに手が届く〜みたいな文法好きです。
条件文やループ文などは、Javaとかとほぼ文法同じですね。
プログラミングの勉強を始めた時は、Javaとかを学んでいたから、この辺は馴染みのある文法でありがたや。
例外の処理については、Javaとかとほぼやり方は同じだが、一部書き方が違う。
特定の例外のキャッチ
try {
var a = "test";
int.parse(a);
} on FormatException {
print("test: throw FormatException");
}
出力
test: throw FormatException
また、Errorオブジェクトとは別に、StackTraceを引数で取れるというのがDartとJavaとの違いですね。
try {
int.parse("oo001122");
} catch (e, s) {
print("error: \n$e");
print("stack trace: \n$s");
} finally {
print("finally process");
}
出力
error:
FormatException: Invalid radix-10 number (at character 1)
oo001122
^
stack trace:
#0 int._handleFormatError (dart:core-patch/integers_patch.dart:130:5)
#1 int._parseRadix (dart:core-patch/integers_patch.dart:141:16)
#2 int._parse (dart:core-patch/integers_patch.dart:102:12)
#3 int.parse (dart:core-patch/integers_patch.dart:64:12)
#4 main (file:///Users/<username>/Repo/dart-sample/src/exception.dart:19:9)
#5 _delayEntrypointInvocation.<anonymous closure> (dart:isolate-patch/isolate_patch.dart:281:32)
#6 _RawReceivePortImpl._handleMessage (dart:isolate-patch/isolate_patch.dart:184:12)
finally process
だいぶしばらくぶりとなってしまった...(式の準備もありつつ、仕事もありつつ、風邪もひきつつ...)
まあ、あまり言語勉強に執着していると肝心のFlutterに移れないので、そこそこしたらFlutter編に移りますかね。
Named Constructor
いいですね〜〜〜。
Kotlinの拡張関数みたいな書き方。(ちょっと概念は違いますが)
class Rectangle {
int x = 0;
int y = 0;
int width = 0;
int height = 0;
Rectangle.Copy(Rectangle origin) {
this.x = origin.x;
this.y = origin.y;
this.width = origin.width;
this.height = origin.height;
}
}
non-defaultなコンストラクタの実行前?に親クラスのコンストラクタ、もしくは値の初期化ができる書き方があった...!
class Rectangle {
int x = 0;
int y = 0;
int width = 0;
int height = 0;
Rectangle.create(int x, int y, int width, int height) {
this.x = x;
this.y = y;
this.width = width;
this.height = height;
}
}
class ColorRectangle extends Rectangle {
int color = 0xffffff;
// `:` に続けて、親クラスのコンストラクタ(名前付き含む)を呼び出す記述をする
ColorRectangle.create(int x, int y, int width, int height, int color) : super.create(x, y, width, height) {
this.color = color;
}
}
また、自身のコンストラクタを呼ぶ、ということもできるので、
class Hoge {
int x;
int y;
Hoge(this.x, this.y);
// ↓↓↓
Hoge(int x) {
this(x, 0);
}
}
と書かずとも、
class Hoge {
int x;
int y;
Hoge(this.x, this.y);
// ↓↓↓
Hoge(int x) : this(x, 0);
}
とできるのか。。。
本当にJavaにかゆいところに手が届く〜といった感じになってて、ますます気に入ってきた。
なるほど、Immutableなclassというのも作れるのか。
コンストラクタの修飾子として const
を追加、また、初期化する変数は必ず final
をつけるのが条件。
ちゃんとコンパイル時にImmutableが保証されているということか。
class ImmutableRectangle {
final int x, y, width, height;
const ImmutableRectangle(this.x, this.y, this.width, this.height);
}
あまり使う場面は多くなさそうなイメージ。
Getter
文法
<返す値の型> get <プロパティ名> => <値を返す式>;
例
class Rectangle {
int x, y, width, height;
// Getter
int get left => x;
int get right => x + width;
int get top => y;
int get bottom => y + height;
}
Setter
文法
set <プロパティ名>(<引数>) => <引数を使ったセット操作>;
例
class Rectangle {
int x, y, width, height;
// Setter
set left(int left) => x = left;
set right(int right) => x = right - width;
set top(int top) => y = top;
set bottom(int bottom) => y = bottom - height;
}
SwiftのようなExtensionができるのもびっくり!
Dartはかなり多機能な言語なんだなぁ...
文法
extension <extension名> on <拡張する既存クラス> {
<戻り値の型> <メソッド名>(<引数定義>) {
// 実装
}
}
例
extension ManipulationString on String {
String shiftCharacter(int shift) {
return this.runes.map((e) => String.fromCharCode(e + shift)).join();
}
}
Genericsも搭載されている。
しかも、ListやSet、Mapの生成の書き方が、想定よりも随分と省略されている。
Dartだとこれがデフォルト?
var strList = <String>["start"]; // create List<String>
trList.add("test");
trList.add("test");
trList.add("test");
trList.addAll(["test1", "test2", "test3"]);
print("strList: $strList");
var strSet = <String>{"start"}; // create Set<String>
strSet.add("test");
strSet.add("test");
strSet.add("test");
strSet.addAll({"test1", "test2", "test3"});
print("strSet: $strSet");
var strMap = <String, String>{"init": "start"}; // create Map<String, String>
strMap["key1"] = "test1";
strMap["key2"] = "test2";
strMap["key3"] = "test3";
print("strMap: $strMap");
出力
strList: [start, test, test, test, test1, test2, test3]
strSet: {start, test, test1, test2, test3}
strMap: {init: start, key1: test1, key2: test2, key3: test3}
また、クラスやメソッドの定義にもKotlinのような書き方でGenericsが利用できる。
もう少しDart言語自体に慣れておきたいので、もうちょっと実用的なものを作ってみよう。
まずは、コマンドラインツールとして、通信する処理を含めたものを作ってみることにする。
OpenWeatherMapのAPIを使って、現在地の天気を取ってみることに。
dart:io
パッケージ内に HttpClient
というクラスがあるが、もっと便利な公式パッケージがあるので、それを使ってみることに。
公式だが標準では入っていないので、コマンドを使ってパッケージをダウンロードする。
-
pubspec.yaml
を作成 -
name: <パッケージ名>
を追記 - 以下のコマンドを実行
$ dart pub add http
すると、 dependencies
が以下のようになる。
name: 'dart_sample'
environment:
sdk: '>=2.10.0 <3.0.0'
dependencies:
http: ^0.13.4
sprintf: ^6.0.0
※ environment
はVSCodeを使っている過程で追加された
※ sprintf
は以下のソースコードで使用するため、後で追加した
こうすると、HTTP(S)通信の処理の実装が便利になった。
import 'dart:io';
import 'package:http/http.dart' as http;
import 'package:sprintf/sprintf.dart';
const OPEN_WEATHER_MAP_API_KEY = '<API KEY>';
const OPEN_WEATHER_MAP_DOMAIN = 'api.openweathermap.org';
void main(List<String> args) async {
try {
// Open Weather Mapへ通信開始
var response = await http.get(Uri.https(OPEN_WEATHER_MAP_DOMAIN, "/data/2.5/weather", {
'appid': OPEN_WEATHER_MAP_API_KEY,
'lon': '139.7649361',
'lat': '35.6812362',
}));
if (response.statusCode != 200) {
print(sprintf("error -- %d: %s\n%s", [response.statusCode, response.reasonPhrase, response.body]));
exit(1);
}
print(sprintf("result: %s\n", [response.body]));
} catch (e, s) {
print("catch error: " + e.toString());
print(s.toString());
exit(1);
} finally {
print("--- END ---");
}
}
Output
result: {"coord":{"lon":139.764,"lat":35.6767},"weather":[{"id":803,"main":"Clouds","description":"broken clouds","icon":"04n"}],"base":"stations","main":{"temp":290.97,"feels_like":290.79,"temp_min":286.62,"temp_max":291.95,"pressure":1019,"humidity":76},"visibility":10000,"wind":{"speed":2.57,"deg":60},"clouds":{"all":75},"dt":1651860595,"sys":{"type":2,"id":268395,"country":"JP","sunrise":1651866204,"sunset":1651915897},"timezone":32400,"id":1857654,"name":"Marunouchi","cod":200}
参考
OpenWeatherMapのAPIドキュメント
Dartのhttpパッケージ
都道府県→市区町村の情報をどうやって取得するか調べてみたが、無料&登録なしでできるものだとここが良さそうと思い、利用してみることにした。
郵便番号からも探せるとのことなので、結構便利かもしれない。
また、OpenWeatherMapへ渡す際、緯度・経度が必要になるが、それもこのAPIから取得できるから、相性も良いかな。
実装にあたり、「オニオンアーキテクチャ」でやってみようと思う。
前からクリーンアーキテクチャが気になっていて色々なものを読んでいたが、なんとなーくの概念はわかりつつ、実装に落とすとなるとどのようにすれば分からず明け暮れていたら、オニオンアーキテクチャが良さそうで、具体例も上がっているところもちらほら。
まあそもそも、ドメイン駆動設計自体もうっすらとしか知らないので、ここから勉強を始めないといけないのかもだけど。
参考:
オニオンアーキテクチャなどを利用するのであれば、DI(Dependency Injection:依存性注入)は必須。
コンストラクタを使い、利用する際に外部から差し込めるようにするのもよいが、やはり完全に切り離したいな、と思い、DIのライブラリを導入してみることに。
それなりにFlutter(Dart)を使っている人たちの中でも有名っぽいので、これを使ってみようと思う。
JSONのデコード・エンコードについては、Flutter(というか、Dart?)にリフレクションの機能がないので、他の言語にあるような方法はできないのね。
手動で実装をするか、コードジェネレーター的なものを使って自動で変換できるようにするか、の2択ということらしい。
シンプルなら手動で XXXX.fromJson()
と XXXX.toJson()
のメソッドを作るのが簡単かな。
後者の方法は、classにアノテーションをつけ、コマンドを実行するとコードが自動生成されるらしい。
watcherなんかも用意されているので、変更を検知→コードを生成 のながれを自動的にすることは可能か。
→ どこかで実行を忘れたー!みたいなことは起こりそうではある...
Flutter公式の Serializing JSON using code generation libraries
を参考に json_serializable
パッケージを使って実現しようとしたが、 build_runner
を走らせても全然生成されない...
一応、GithubのIssueにも私と同じ事例が報告されている。
何が問題なのか、これからもう少し調査しなければ...
Flutter公式の Serializing JSON using code generation libraries を参考に json_serializable パッケージを使って実現しようとしたが、 build_runner を走らせても全然生成されない...
これ、解決しそう...!
$ dart run build_runner build
と実行する場合、ルート配下にある lib
下(そのサブディレクトリも含む)のdartファイルしか読み込んでくれない!
私のディレクトリ構成だと、 /src/domain/model
下に変換してほしいModelクラスのあるdartファイルがあるので、これを何かオプションを渡して変換してくれるように設定すればいけるかもしれない。
公式ドキュメントか、ソースコードをじっくり見てみるか。
あー、そもそも、DartやFlutterには標準で推奨しているディレクトリ構成があるんだ。
これに従ってやるのが基本で、 build_runner
もそれ前提になってるのかな。
設定を見つけようとしたけどなかなか無かったので、ひとまずソースコードは lib/src
下にいれるようにしてみよう。
ソースコードを全て lib
配下に入れることで、無事 build_runner
を実行すると *.g.dart
を生成してくれるようになった。
一件落着!
また再び、サンプルアプリを作っていこう。
だんだんとDartの文法などが分かってきたぞ...!
json_serializable
と json_annotation
パッケージを使ってModelクラスを定義するのはいいが、ボイラープレートなコードをクラスごとに用意するのはめんどくさいね〜。
import 'package:json_annotation/json_annotation.dart';
part 'test.g.dart';
(fieldRename: FieldRename.snake)
class Test {
String field1;
int field2;
Test(this.field1, this.field2);
factory Test.fromJson(Map<String, dynamic> json) => _$TestFromJson(json);
Map<String, dynamic> toJson() => _$TestToJson(this);
}
特に、 fromJson()
と toJson()
を毎回コピペでもやっていくのがめんどくさい。
やはりこういう時はボイラープレート部分も自動生成とかすべきかな、と思い、 code_builder
を使ってなんとか自動生成できないかを検討中。
Flutter 3.0がリリースされたのでアップグレードを実施し、dartのバージョンも上がったのだが、よくよくみたら arm64
に対応していた!!
$ dart --version
Dart SDK version: 2.16.2 (stable) (Tue Mar 22 13:15:13 2022 +0100) on "macos_x64"
↓
$ dart --version
Dart SDK version: 2.17.1 (stable) (Tue May 17 17:58:21 2022 +0000) on "macos_arm64"
仕事場ではM1 Macを使っているので、これは感動!
そして、中途半端すぎてDart練習用に作っていたアプリだが、だんだん形にもなってきて、もうすぐ一通りの完成となりそうなので、リポジトリを公開してみることにした。
まだコード自体恥ずかしい状態ではあるが、まあ何かの参考になればと...
会社でも自社アプリを作ろう!みたいなプロジェクトでFlutterの採用を決めたので、今後はよりFlutterへの関わりが増えそう。
なんせ、既に公開済みのiOS/AndroidアプリをFlutterへ移行する、ということなので、Swift/Kotlinで作る場合との比較とかできるかも?
こちらは少し穴が空いたが、会社のiOS/Androidネイティブアプリ(Kotlin or Swift)をFlutterへ移行中。
BLoCパターン+flutter_blocを使って作ってみよう!ということで進めている。
今だと Riverpod とかもあるが、とりあえずは1つのパターンで実装を進め、後で別の方が良いとなれば移行もありだね。
flutter_bloc
StateとEventは自分で必要な分だけclassを作成し、あとはBlocに管理してもらう。
最初、StateとEventの数だけ実装するのはめんどくさいかなーと思ったが、それさえ実装してしまえばBlocを継承したクラス内で外部から受信したEventでStateを切り替えくれて、管理してくれるので、自前でBLoCパターンを作るよりも遥かに楽。
Blocクラスの実装も、基本は何かイベントを受信した時にどういう処理をし、その結果によってどのStateをemitするのかを指定するだけでOK。
UIの構成に困ったら、 Expanded
を使え!
UIの構成に困ったら、 Expanded を使え!
と書いたが、使える場所は限られてましたね。
主に Row
や Column
、 Flex
でしか使えないとのこと。
なるほどなぁ..
会社のスマホアプリでローカルDBといえば realm
といった感じで活用しているが、調べると Isar
というのもあるみたいだね。
独自DBらしく、高速で軽い、と。
また、SQLiteを使うのであれば、 Drift
が良さそう?
Floor
というのもあった!
これはよく見る実装方法な気がしている。
会社では realm
→ Isar
に変更することが決まった。
まあそこまで大きなデータを入れるわけでもないので、パフォーマンス的には変わらないとは思うが。。。
ただ、これまでネイティブでそれぞれ作成していた時の各OS版のものからFlutter版への移行がそのままではできず、realmを使い続けるのがかなり大変そうだった。
そのため、Isarに移行しデータのマイグレーションを実装することにした。
せっかくFlutterで作り直すのだから、Dartで書けたほうがいいよね、ていうのもあり。
そろそろネタ切れ感があるのと、だいぶDart/Flutterが定着してきた。
何か共有したい場合は記事にしたいなと思っているので、こちらのスクラップがクローズかな。