Closed37

Flutterでアプリを作るまで(自分用勉強メモ)

とっきゃーとっきゃー

計算系の演算子で、 ~/ が地味に便利だなーと思った。
たまにシステムを開発していて、割り算で整数値を出したい、という時があるので、それがネイティブに演算子として存在しているので、便利過ぎて泣いた;;

print("10 / 6 = ${10 / 6}");
// -> 10 / 6 = 1.6666666666666667

print("10 ~/ 6 = ${10 ~/ 6}");
// -> 10 ~/ 6 = 1
とっきゃーとっきゃー

あと、個人的な感想として、私自身がもともとJavaをガッツリやっていたので、それに文法が似ているところが多く、すんなり受け入れそうだな、と感じた。
Javaの古き良き文法+今どき流行りの文法がいい感じに混ざった感じ。

とっきゃーとっきゃー

あ、ちなみに、FlutterSDKは以下のリンクを見てインストールした。
https://flutter.dev/docs/get-started/install

「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.
とっきゃーとっきゃー

代入演算子で ??= が地味に便利だなぁ。
格納先の変数が null かどうかで値を入れる/入れないをif文で条件分岐させて入れていたので、こういう地味だけどかゆいところに手が届く〜みたいな文法好きです。

とっきゃーとっきゃー

例外の処理については、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 というクラスがあるが、もっと便利な公式パッケージがあるので、それを使ってみることに。

公式だが標準では入っていないので、コマンドを使ってパッケージをダウンロードする。

  1. pubspec.yaml を作成
  2. name: <パッケージ名> を追記
  3. 以下のコマンドを実行
$ dart pub add http

すると、 dependencies が以下のようになる。

pubspec.yaml
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)通信の処理の実装が便利になった。

openweathermap.dart
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ドキュメント
https://openweathermap.org/current

Dartのhttpパッケージ
https://pub.dev/packages/http

とっきゃーとっきゃー

都道府県→市区町村の情報をどうやって取得するか調べてみたが、無料&登録なしでできるものだとここが良さそうと思い、利用してみることにした。

http://geoapi.heartrails.com/

郵便番号からも探せるとのことなので、結構便利かもしれない。
また、OpenWeatherMapへ渡す際、緯度・経度が必要になるが、それもこのAPIから取得できるから、相性も良いかな。

とっきゃーとっきゃー

実装にあたり、「オニオンアーキテクチャ」でやってみようと思う。

前からクリーンアーキテクチャが気になっていて色々なものを読んでいたが、なんとなーくの概念はわかりつつ、実装に落とすとなるとどのようにすれば分からず明け暮れていたら、オニオンアーキテクチャが良さそうで、具体例も上がっているところもちらほら。

まあそもそも、ドメイン駆動設計自体もうっすらとしか知らないので、ここから勉強を始めないといけないのかもだけど。

参考:
https://qiita.com/little_hand_s/items/2040fba15d90b93fc124

とっきゃーとっきゃー

オニオンアーキテクチャなどを利用するのであれば、DI(Dependency Injection:依存性注入)は必須。

コンストラクタを使い、利用する際に外部から差し込めるようにするのもよいが、やはり完全に切り離したいな、と思い、DIのライブラリを導入してみることに。

https://pub.dev/packages/get_it
https://qiita.com/kabochapo/items/fcdbd6f7bcf69a91df83

それなりにFlutter(Dart)を使っている人たちの中でも有名っぽいので、これを使ってみようと思う。

とっきゃーとっきゃー

JSONのデコード・エンコードについては、Flutter(というか、Dart?)にリフレクションの機能がないので、他の言語にあるような方法はできないのね。

手動で実装をするか、コードジェネレーター的なものを使って自動で変換できるようにするか、の2択ということらしい。

https://docs.flutter.dev/development/data-and-backend/json

シンプルなら手動で XXXX.fromJson()XXXX.toJson() のメソッドを作るのが簡単かな。

後者の方法は、classにアノテーションをつけ、コマンドを実行するとコードが自動生成されるらしい。
watcherなんかも用意されているので、変更を検知→コードを生成 のながれを自動的にすることは可能か。
→ どこかで実行を忘れたー!みたいなことは起こりそうではある...

とっきゃーとっきゃー

Flutter公式の Serializing JSON using code generation libraries を参考に json_serializable パッケージを使って実現しようとしたが、 build_runner を走らせても全然生成されない...

https://docs.flutter.dev/development/data-and-backend/json#serializing-json-using-code-generation-libraries

一応、GithubのIssueにも私と同じ事例が報告されている。

https://github.com/google/json_serializable.dart/issues/1125

何が問題なのか、これからもう少し調査しなければ...

とっきゃーとっきゃー

Flutter公式の Serializing JSON using code generation libraries を参考に json_serializable パッケージを使って実現しようとしたが、 build_runner を走らせても全然生成されない...

これ、解決しそう...!

$ dart run build_runner build

と実行する場合、ルート配下にある lib 下(そのサブディレクトリも含む)のdartファイルしか読み込んでくれない!

私のディレクトリ構成だと、 /src/domain/model 下に変換してほしいModelクラスのあるdartファイルがあるので、これを何かオプションを渡して変換してくれるように設定すればいけるかもしれない。

公式ドキュメントか、ソースコードをじっくり見てみるか。

とっきゃーとっきゃー

ソースコードを全て lib 配下に入れることで、無事 build_runner を実行すると *.g.dart を生成してくれるようになった。

一件落着!
また再び、サンプルアプリを作っていこう。

だんだんとDartの文法などが分かってきたぞ...!

とっきゃーとっきゃー

json_serializablejson_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練習用に作っていたアプリだが、だんだん形にもなってきて、もうすぐ一通りの完成となりそうなので、リポジトリを公開してみることにした。

まだコード自体恥ずかしい状態ではあるが、まあ何かの参考になればと...

https://github.com/mltokky/jp_weather_information

とっきゃーとっきゃー

会社でも自社アプリを作ろう!みたいなプロジェクトでFlutterの採用を決めたので、今後はよりFlutterへの関わりが増えそう。

なんせ、既に公開済みのiOS/AndroidアプリをFlutterへ移行する、ということなので、Swift/Kotlinで作る場合との比較とかできるかも?

とっきゃーとっきゃー

こちらは少し穴が空いたが、会社のiOS/Androidネイティブアプリ(Kotlin or Swift)をFlutterへ移行中。

BLoCパターン+flutter_blocを使って作ってみよう!ということで進めている。
今だと Riverpod とかもあるが、とりあえずは1つのパターンで実装を進め、後で別の方が良いとなれば移行もありだね。

とっきゃーとっきゃー

flutter_bloc

https://pub.dev/packages/flutter_bloc

StateとEventは自分で必要な分だけclassを作成し、あとはBlocに管理してもらう。

最初、StateとEventの数だけ実装するのはめんどくさいかなーと思ったが、それさえ実装してしまえばBlocを継承したクラス内で外部から受信したEventでStateを切り替えくれて、管理してくれるので、自前でBLoCパターンを作るよりも遥かに楽。

Blocクラスの実装も、基本は何かイベントを受信した時にどういう処理をし、その結果によってどのStateをemitするのかを指定するだけでOK。

とっきゃーとっきゃー

UIの構成に困ったら、 Expanded を使え!

と書いたが、使える場所は限られてましたね。
主に RowColumnFlex でしか使えないとのこと。

なるほどなぁ..

とっきゃーとっきゃー

会社では realmIsar に変更することが決まった。
まあそこまで大きなデータを入れるわけでもないので、パフォーマンス的には変わらないとは思うが。。。

ただ、これまでネイティブでそれぞれ作成していた時の各OS版のものからFlutter版への移行がそのままではできず、realmを使い続けるのがかなり大変そうだった。
そのため、Isarに移行しデータのマイグレーションを実装することにした。

せっかくFlutterで作り直すのだから、Dartで書けたほうがいいよね、ていうのもあり。

とっきゃーとっきゃー

そろそろネタ切れ感があるのと、だいぶDart/Flutterが定着してきた。
何か共有したい場合は記事にしたいなと思っているので、こちらのスクラップがクローズかな。

このスクラップは2023/06/02にクローズされました