Dart&Flutterのコードを改善する42の簡単な方法
Dartのコードを改善するTips42選 (※元ネタは Google Developer Expert Andrea さんの code with andrea から。)を翻訳しました。
そのままの転載は不味いので私のコメントを添えています。
すでに他の言語の経験がある方にも、Dart脱初心者にも、これからお休みになる方、そしてお目覚めの方にも有益な内容となっています。
Dart & Flutter Easy Wins 1-7
Dart & Flutter Easy Wins 8-14
Dart & Flutter Easy Wins 15-21
Dart & Flutter Easy Wins 22-28
Dart & Flutter Easy Wins 29-35
Dart & Flutter Easy Wins 36-42
※注意2020年8月ころの記事のため内容が古い場合もあり得ますご了承ください。
1. var より final より const を優先する
const favorite = 'I like javascript very much.';
final newFavorite = favorite.replaceAll('javascript', 'dart');
var totalSpaces = 0;
for (var i = 0; i < newFavorite.length; i++) {
final c = newFavorite[i];
if (c == ' ') {
totalSpaces++;
}
}
print('Counted $totalSpaces'); // Counted 4
再代入しない変数はconst
を使いましょう
2. 安全なコードのために type annotation(型指定)が使えます
const cities = <String>['Tokyo', 'Osaka', 15];
// => The element type 'int' can't be assigned to the list type 'String'.
リストに型書きましょう。
_
が使えます
3. 使わない引数は MateriaoPageRoute(
// builder: (context) => DetailsPage();
builder: (_) => DetailPage();
)
アンダースコア派? アンダーバー派? 私はアンスコ派
4. dart の関数は第一級オブジェクトなので、引数に関数を直接代入できます
void main() {
const values = [1, 2, 3];
// values.map((value) => square(value));
values.map(square).toList();
}
int square(int value) {
return value * value;
}
引数に関数を渡すことができるのは第一級オブジェクト(first class)
な言語の特徴の一つです。
Collection-if
と言います
5. リスト(配列)、Set, Map の中で、if文が使えます。これを const addRatings = true;
final avgRating = 4.5;
final numRatings = 5;
final restaurant = {
'name': 'Pizza Luigi',
'cuisine': 'Italian',
if (addRatings) ...{
'avgRating': avgRating,
'numRatings': numRatings,
},
};
print(restaurant);
Map の中で突如として現れる if文。でもDartでは間違いではありません。
Widget の表示、非表示の分岐にも使われます。
Cascade Operator
を使用して書き換えられます
6. ミュータブルな変数は、final path = Path();
path.moveTo(0, 0);
path.lineTo(0, 2);
final path = Path()
..moveTo(0, 0)
..lineTo(0, 2);
チェーンにして書けるので便利です。Custom Painter
ではよく使います。
on
句によって例外の型ごとに分岐ができます
7. 例外処理で try {
await authService.signInAnonymously();
} on PlatformException catch (e) {
// PlatformException の処理
} catch (e) {
// その他の例外処理
}
キャッチ アンド リリースはダメ、ゼッタイ。
try
とcatch
の両方で行う処理はfinally
ブロックを使います
8. 例外処理でtry {
setState(() => _isLoading = true);
await authService.signInAnonymously();
// setState(() => isLoading = false)
} catch (e) {
// setState(() => isLoading = false)
} finally {
setState(() => _isLoading = false);
}
DRY。(洗練されたクリアな味、辛口。)
toString()
を実装できます
9. デバックのDX向上のために、クラスに class Point {
const Point(this.x, this.y);
final num x;
final num y;
String toString() => '($x, $y)';
}
void main() {
const a = Point(2, 3);
print(a); // => (2, 3)
// toString() がなかった場合 => Instance of 'Point' が返る
}
つまり、すべてのクラスは toString()
をすでに持っている。
??
オペレーターで、値がnull
の場合のフォールバックな値を用意します
10. const restaurant = {
'name': 'Pizza Luigi',
'cuisine': 'Italian',
};
final numRating = restaurant['numRatings'] ?? 0;
JavaScriptではNull合体演算子(Nullish Coalescing)
といい、null
と undefined
の場合も含む。
11. 複数行の文字列を使用して、テキストの大きなブロックを表します
print("""
花屋の店先に並んだ
いろんな花を見ていた
ひとそれぞれ好みはあるけど
どれもみんなきれいだね
""")
JavaScript のテンプレートリテラルと同じで改行コードがいらない。
12. 文字列リテラルには区切り文字としてシングルクォート、ダブルクォートが使えます。バックスラッシュで特殊文字をエスケープするか、raw 文字列を使用します
print("Today I'm feeling great!");
print('Today I\'m feeling great');
print("She said: \"Hello Tim\"");
print('She said: "Hello Tim"');
print(r'C:\Windows\system32');
文字列の前にr
を付けると raw 文字列になる。raw 文字列の中では\
は特別な意味を持たない。
13. トリプルスラッシュでドキュメンテーションコメントが作られます
/// Simple email validator.
///
/// Can be used to perform basic client-side validation
class SimpleEmailValidator extends RegexValidator {
SimpleEmailValidator() : super(r'^\S+@\S+\.\S+$');
}
トリプルスラッシュでコメントを書くことで、クラス名のホバーでドキュメンテーションを確認できます。
hashCode
, ==
, toString()
の実装を自動生成したいですか? Equatable
パッケージが使えます
14. クラスの デフォルトでは、2つのオブジェクトが同じインスタンスである場合、==
はtrueを返します。しかし、
class Person {
final String name;
Person(this.name);
}
final person = Person('Tom');
print(person == Person('Tom')); // false <== インスタンス同士の比較が false
print(person); // Instance of Person
2つのインスタンスを比較できるようにするために==
,hashCode
をオーバーライドする必要があります。より詳しくは、Dart Documentationをチェックしてください。
class Person {
const Person(this.name);
final String name;
bool operator ==(Object other) =>
identical(this, other) ||
other is Person &&
runtimeType == other.runtimeType &&
name == other.name;
int get hashCode => name.hashCode;
}
Equatable packageを継承することでこれを簡単に解決できます。
import 'package:equatable/equatable.dart';
class Person extends Equatable {
final String name;
Person(this.name);
List<Object> get props => [name];
bool get stringify => true;
}
final person = Person('Tom');
print(person == Person('Tom')); // true
print(person); // Person(Tom)
それなのに僕ら人間はどうしてこうも比べたがる?
15. リスト(配列) や collection をシャローコピーするには spread 演算子が使えます
const list = [1, 2, 3];
final copy = [...list]; // shallow copy
// 以下も同様
// final copy2 = List.from(list);
// final copy3 = []...addAll(list);
copy[0] = 0;
print(list); // [1, 2, 3]
print(copy); // [0, 2, 3]
シャローコピーとは、メモリ上にある実体(データ)そのもののコピーではなく、参照のみをコピーすること。(破壊的でない)
null
でないときだけ callback
を実行したい。?.call()
が使えます
16. class CustomDraggable extends StatelessWidget {
const CustomDraggable({Key key, this.onDragCompleted});
final VoidCallback onDragCompleted;
Future<void> _dragComplete() async {
onDragCompleted?.call();
// こうしなくていい
// if (onDragCompleted !== null) {
// onDragCompleted();
// }
}
Widget build(BuildContext context) {}
}
JavaScriptの optional chaining と役割は同じです。
call
メソッドを実装すると、関数のように呼び出し可能になります
17. class PasswordValidator {
bool call(String password) {
return password.length > 10;
}
}
void main() {
final validator = PasswordValidator();
validator('test') // false
validator('test1234') // false
validator('whatchamacallit') // true
}
extensions
が使えます
18. 日時に関して人に優しいAPIが欲しいですか? extension IntX on int {
Duration get seconds => Duration(seconds: this);
Duration get minutes => Duration(minutes: this);
Duration get hours => Duration(hours: this);
}
void main() {
print(5.seconds);
}
オレオレメソッドを生やすよりは、dartx などを使うのがよいでしょう。便利メソッドが多数あります。
Future
を同時に実行したいですか? Future.wait
が使えます
19. 複数のclass CovidAPI {
Future<int> getCases() => Future.value(1000);
Future<int> getRecovered() => Future.value(100);
Future<int> getDeaths() => Future.value(10);
}
void main() async {
final api = CovidAPI();
final values = await Future.wait([
api.getCases(),
api.getRecovered(),
api.getDeaths(),
]);
print(values); // [1000, 100, 10]
}
DartでPromise.all
するにはFuture.wait
。
show
と hide
が使えます
20. パッケージ内のAPIを選択的にインポートしたいですか? // Stream だけ使いたい
import 'dart:async' show Stream;
// StreamController 以外を使いたい
import 'dart:async' hide StreamController;
void main() {
const data = [1, 2, 3];
final stream = Stream.fromIterable(data); // ok
final controller = StreamController(); // =>
// the function 'StreamController' isn't defined
}
StreamController
も使ってやってください。
import as
が使えます
21. 他のパッケージと名前の衝突を避けるには import 'dart:convert';
import 'package:http/http.dart' as http;
Future<int> getLocationId(String city) async {
final res = await http.get('...');
}
譲り合ってご利用ください
toStringAsFixed(n)
が使えます
22. 小数点以下の number フォーマットにはconst x = 12.34567;
print(x.toString()); // 12.34567
print(x.toStringAsFixed(3)); // 12.346
print(x.toStringAsPrecision(3)); // 12.3
print(x.toStringAsExponential(3)); // 1.235e+1
e+1
のeはexponentiation
「累乗」「べき乗」「指数」の意。 1.235e+1=12.35
23. 知ってましたか? Dartは文字列の乗算をサポートしています
for (var i = 1, i <= 5; i++) {
print('⚡️' * i);
}
// ⚡️
// ⚡️⚡️
// ⚡️⚡️⚡️
// ⚡️⚡️⚡️⚡️
// ⚡️⚡️⚡️⚡️⚡️
Rubyでもn文字以上の文字列
のようなテストコードで使われます。
24. コンストラクタが一つでは不十分ですか? 名前付きコンストラクタが使えます
class Complex {
final double re;
final double im;
Complex(this.re, this.im);
Complex.real(this.re) : im = 0;
Complex.imaginary(this.im) : re = 0;
Complex.identity() : re = 1, im = 0;
}
25. deserializeには static method より factory named constructor を優先します
class Item {
final String itemId;
final double price;
Item({require this.itemId, required this.price});
factory Item.fromJson(Map<String, dynamic> json) {
return Item(
itemId: json['itemId'] as String,
price: json['price'] as double,
)
}
// static Item fromMap(Map<String, // dynamic> json) {
// return Item(
// itemId: json['itemId'] as // String,
// price: json['price'] as // double,
// )
// }
}
factory constructor について詳しいことが書かれています。The difference between a "factory constructor" and a "static method"
Private named constructor
で静的インスタンス変数を使用します
26. 一度だけインスタンス化できるクラス(別名シングルトン)が必要ですか? class Singleton {
Singleton._(); // ← Private named constructor
static final instance = Singleton._();
}
シングルトンとは、そのクラスのインスタンスが1つしか生成されないことを保証するデザインパターンのことである。
Typescript のシングルトン
class Singleton {
private static instance: Singleton;
private constructor() {}
static getInstance() {
if (!Singleton.instance) {
Singleton.instance = new Singleton();
}
return Singleton.instance;
}
}
ヒノノニトンは日野自動車が商品展開している2トントラック「デュトロ」のキャッチコピー。
27. 一意なアイテムの collection が必要ですか? リスト(配列)よりも Set を使います
final smapSet = { '中居', '香取', '草なぎ' '稲垣', '木村', '中居'};
final smapList = ['中居', '香取', '草なぎ', '稲垣', '木村', '中居'];
print(smapSet); // {中居, 香取, 草なぎ, 稲垣, 木村}
print(smapList); // [中居, 香取, 草なぎ, 稲垣, 木村, 中居]
もともと特別な Only one ってこと
is-a
関係のクラスは継承を使います
28. enum Action { eat, run, swim }
abstract class Animal {
Set<Action> get actions;
String toString() => '$runtimeType actions: $actions'
}
class Dog extends Animal {
Set<Action> get actions => { Action.eat, Action.run }
}
class Shark extends Animal {
Set<Action> get actions => { Action.eat, Action.swim }
}
void main() {
print(Dog()); // Dog actions: {Action.eat, Action.run}
print(Shark()); // Shark actions: {Action.eat, Action.swim}
}
is-a
関係とはDog is a Animal
が成り立つ関係のこと。is-a
が成り立つとき継承が可能になる。
has-a
の関係であるとき、クラスを部品に見立てて組み合わせる。これをコンポジションという。たとえば、自動車はエンジンをもつ a car has a engine.
29. 抽象クラスの継承では、すべての抽象メソッドはオーバーライドされなければならず。他のメソッドはオーバーライドできる。
implements
した場合は、すべてのメソッドがオーバーライドされなければならない。
enum Action { eat, run, swim }
abstract class Animal {
Set<Action> get actions;
void breathe() { print("breathing..."); }
}
class Dog extends Animal {
// extends: no need to override `breathe()`
// extends: can override `actions`
Set<Action> get actions => { Action.eat, Action.run }
}
class MockDog implements Animal {
// implements: must override `breathe()`
void breathe() { print("pretending to breathe...")}
// implements: must override `actions`
Set<Action> get actions => { Action.eat, Action.run }
}
implements
はJavaではinterface
を実装するときに使用する。
.entries
が使えます
30. Null Safety な方法で Map を反復処理する必要がありますか? const activities = <String, double>{
'Blogging': 10.3,
'Youtube': 30.6,
'Courses': 75.8,
}
// Prefer this
for (var entry in activities.entries) {
final formatted = entry.value.toString();
print('${entry.key}: $formatted');
}
// for (var key in activities.keys) {
// final formatted = activities[key]!.toString();
// print('$key: $formatted');
//}
conditional imports
を使用します
31. ウェブとネイティブプラットフォームで異なる実装が必要ですか? import 'copy_to_clipboard_stub.dart'
// dart:html
if (dart.library.html) 'copy_to_clipboard_web.dart'
// dart.io
if (dart.library.io) 'copy_to_clipboard_non_web.dart'
32. ジェネリックで関数の型を宣言する必要がありますか? typedefを使用します
typedef ItemWidgetBuilder<T> = Widget Function(BuildContext context, T item);
class ListItemBuilder<T> extends StatelessWidget {
const ListItemBuilder({required this.items, required this.itemBuilder});
final List<T> items;
final ItemWidgetBuilder<T> itemBuilder;
...
}
東和薬品の typedef
computed
な変数を扱う必要がありますか?ゲッターとセッターを使いましょう。
33. class Temperature {
Temperature._({this.celsius});
factory Temperature.celsius(double degrees)
=> Temperature._(celsius: degrees);
factory Temperature.fahrenheit()
=> Temperature._(celsius: (degrees - 32) / 1.8)
double celsius;
double get fahrenheit => celsius * 1.8 + 32;
set fahrenheit(double degrees) => celsius = (celsius: (degrees - 32) / 1.8)
}
void main() {
final temp = Temperature.celsius(30);
print('${temp.fahrenheit.toStringAsFixed(0)}F'); // getters
temp.fahrenheit = 90; // setters
print('${temp.celsius.toStringAsFixed(0)}C');
}
これは飯田という文字列を返すゲッターズ(ゲッターにする必要ない)
String get iida => '飯田';
Future.value
を使用します
34. 即時にリターンする Future を返す必要がありますか? abstract class APIService {
Future<int> getFollowersCount(String userId);
}
class MockAPIService implements APIService {
Future<int> getFollowersCount(String userId)
=> Future<int>.value(100);
}
JavaScriptでいう、Promise.resolve(100)
Future.delayed
を使用します
35. コードを実行する前に強制的に遅延させたいですか? abstract class APIService {
Future<int> getFollowersCount(String userId);
}
class MockAPIService implements APIService {
Future<int> getFollowersCount(String userId) async {
await Future.delayed(Duration(seconds: 2));
return 100;
}
}
TypeScriptで書くとこんな感じ↓。Dartの方がスッキリしてます。
function getFollowersCount(userId: string): Promise<number> {
return new Promise((resolve, _) => {
setTimeout(() => {
resolve(100)
}, 2000);
})
}
Timer.periodic
を使用して、指定された期間で繰り返すタイマーを作成します
36. Timer _timer;
int _counter = 10;
void initState() {
super.initState();
_timer = Timer.periodic(
const Duration(seconds: 1),
(timer) => setState(() {
if (_counter < 1) {
timer.cancel();
} else {
_counter--;
}
})
)
}
void dispose() {
_timer.cancel();
super.dispose();
}
periodic
は period
の形容詞で 定期的、周期的
という意味。
JavaScript (React)で書くとこんな感じ
let timerId = null;
this.state = {
_counter: 10
}
function updateCount() {
timerId = setInterval(() => {
if (_counter < 1) {
clearInterval(timerId);
timerId = null;
} else {
this.setState({
_counter: _counter--;
})
}
}, 1000)
}
.index
とvalues[i]
を使用します
37. 列挙型をintまたはその逆に変換する必要がありますか? enum Move { up, down, left, right };
void main() {
final move = Move.left;
print(move.index); // 2
print(Move.values[3]) // Move.right
}
38. bool値がfalseの場合、assertsを使用してプログラムの実行を中断します
void submitRating(String url, double rating) {
assert(url.startsWith('https'));
assert(
rating >= 0 && rating <= 5,
'Ratings must be between 0 and 5' // 任意のメッセージ
)
}
void main() {
submitRating('https://my.api.com', 6);
}
assert()
でbool値がfalseの場合、任意のエラーを出力する
39. アプリに適したロガーが必要ですか? ロガーパッケージを使用します
import 'package:logger/logger.dart';
void main() {
Logger.level = Level.warning;
final logger = Logger();
logger.v("Verbose log");
logger.d("Debug log");
logger.i("Info log");
logger.w("Warning log");
logger.e("Error log");
logger.wtf("What the..."); // おいマジか
}
40. Flutter DevToolsを使用して、Flutterアプリを検査およびプロファイリングします
41. 匿名関数は変数に割り当てたり、他の関数に引数として渡すことができます
void main() {
// 匿名関数を sayHi 変数に代入
final sayHi = (name) => 'Hi! $name';
welcome(sayHi, 'Andrea');
}
void welcome(String Function(String) greet, String name) {
// 匿名関数を greet 引数で受け取る
print(greet(name));
print('Welcome to this course');
}
No.4 と同様に Dart は第一級オブジェクトであるため、匿名関数を変数に代入したり関数を引数にわたすことが可能。
JavaScript ほど見栄えはよくない。とくに引数 String Function(String) greet
Never
を使用します
42. throw しかできない関数が欲しいですか? Understanding null safety | Dart
Never wrongType(String type, Object value) {
throw ArgumentError('Expected $type, but was ${value.runtimeType}.');
}
class Point {
Point(this.x, this.y);
final double x, y;
bool operator ==(Object other) {
if (other is! Point) wrongType("Point", other);
return x == other.x && y == other.y;
}
}
void main() {
final a = Point(1, 2);
final b = 'not a point';
print(a == b); // Expected Point, but was String.
}
等価演算子をオーバーライドして、オリジナルなArgumentError
を返す関数を定義しています。TypeScriptと同様、throw は Neverを返す
まとめ
気づいた方がいるかもしれません。この記事は「世界にひとつだけの花」がテーマです。
Discussion
気になったのでコメントします。15. は「シャローコピー」ではなく「ディープコピー」だと思います。
スプレッド演算子で要素を展開し新しい配列を作ることで、メモリ領域が全く別の新しい配列ができます。shallow copyであれば、
a=b
で充分です。この文脈なら、ディープコピーが正しいと思います。まずコメントありがとうございます。
結論から言うと、Spraed Operator は shallow copyです。
15はコード上では確かに新しい配列を作っていますが、中身が数字の配列(String, int, double はimmutable)なのでコピー元に影響がないです。
厳密には再帰的にコピーすることが Deep copyです。
Spraed Operator は一部参照を含んだコピーが行われるので Shallow copyです。
ちなみに、a=b は参照渡しです。
そういうことなんですね、理解が深まりました。
返信ありがとうございましたー