🎯

Dart&Flutterのコードを改善する42の簡単な方法

2022/04/22に公開
3

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)な言語の特徴の一つです。

5. リスト(配列)、Set, Map の中で、if文が使えます。これを Collection-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 の表示、非表示の分岐にも使われます。

6. ミュータブルな変数は、Cascade Operator を使用して書き換えられます

final path = Path();
path.moveTo(0, 0);
path.lineTo(0, 2);

final path = Path()
  ..moveTo(0, 0)
  ..lineTo(0, 2);

チェーンにして書けるので便利です。Custom Painter ではよく使います。

7. 例外処理で on 句によって例外の型ごとに分岐ができます

try {
    await authService.signInAnonymously();
} on PlatformException catch (e) {
    // PlatformException の処理
} catch (e) {
    // その他の例外処理
}

キャッチ アンド リリースはダメ、ゼッタイ。

8. 例外処理でtrycatchの両方で行う処理はfinally ブロックを使います

try {
    setState(() => _isLoading = true);
    await authService.signInAnonymously();
    // setState(() => isLoading = false)
} catch (e) {
    // setState(() => isLoading = false)
} finally {
    setState(() => _isLoading = false);
}

DRY。(洗練されたクリアな味、辛口。)

9. デバックのDX向上のために、クラスに toString() を実装できます

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() をすでに持っている。

10. ?? オペレーターで、値がnull の場合のフォールバックな値を用意します

const restaurant = {
    'name': 'Pizza Luigi',
    'cuisine': 'Italian',
};

final numRating = restaurant['numRatings'] ?? 0;

JavaScriptではNull合体演算子(Nullish Coalescing)といい、nullundefinedの場合も含む。

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+$');
}

トリプルスラッシュでコメントを書くことで、クラス名のホバーでドキュメンテーションを確認できます。

14. クラスの hashCode, ==, toString() の実装を自動生成したいですか? Equatable パッケージが使えます

デフォルトでは、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]

シャローコピーとは、メモリ上にある実体(データ)そのもののコピーではなく、参照のみをコピーすること。(破壊的でない)

16. null でないときだけ callback を実行したい。?.call() が使えます

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 と役割は同じです。

17. call メソッドを実装すると、関数のように呼び出し可能になります

class PasswordValidator {
    bool call(String password) {
        return password.length > 10;
    }
}

void main() {
    final validator = PasswordValidator();
    validator('test') // false
    validator('test1234') // false
    validator('whatchamacallit') // true
}

18. 日時に関して人に優しいAPIが欲しいですか? extensionsが使えます

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 などを使うのがよいでしょう。便利メソッドが多数あります。

19. 複数のFutureを同時に実行したいですか? Future.waitが使えます

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

20. パッケージ内のAPIを選択的にインポートしたいですか? showhide が使えます

// 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も使ってやってください。

21. 他のパッケージと名前の衝突を避けるには import as が使えます

import 'dart:convert';
import 'package:http/http.dart' as http;

Future<int> getLocationId(String city) async {
    final res = await http.get('...');
}

譲り合ってご利用ください

22. 小数点以下の number フォーマットにはtoStringAsFixed(n)が使えます

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"

26. 一度だけインスタンス化できるクラス(別名シングルトン)が必要ですか? Private named constructor で静的インスタンス変数を使用します

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 ってこと

28. is-a 関係のクラスは継承を使います

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を実装するときに使用する。

30. Null Safety な方法で Map を反復処理する必要がありますか? .entriesが使えます

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');
//}

31. ウェブとネイティブプラットフォームで異なる実装が必要ですか? conditional imports を使用します

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'

Dart Documentation

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

33. computed な変数を扱う必要がありますか?ゲッターとセッターを使いましょう。

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 => '飯田';

34. 即時にリターンする Future を返す必要がありますか? Future.valueを使用します

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)

35. コードを実行する前に強制的に遅延させたいですか? Future.delayedを使用します

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);
    })
}

36. Timer.periodicを使用して、指定された期間で繰り返すタイマーを作成します

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();
}

periodicperiod の形容詞で 定期的、周期的という意味。

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)
}

37. 列挙型をintまたはその逆に変換する必要がありますか? .indexvalues[i]を使用します

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..."); // おいマジか
}

logger | Dart Package

40. Flutter DevToolsを使用して、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

42. throw しかできない関数が欲しいですか? Never を使用します

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 で充分です。この文脈なら、ディープコピーが正しいと思います。

sekitatssekitats

まずコメントありがとうございます。

結論から言うと、Spraed Operator は shallow copyです。

15はコード上では確かに新しい配列を作っていますが、中身が数字の配列(String, int, double はimmutable)なのでコピー元に影響がないです。

厳密には再帰的にコピーすることが Deep copyです。

ディープコピーとは、mutableなオブジェクトのリストに対してコピー処理を再帰的に行うこと
https://qiita.com/kasa_le/items/fc5379ba15e6193f37f8#comment-eb36b0256f257366cf5a

https://twitter.com/ntaoo/status/1267020825795821569

(シャローコピーは)新たな実体を生成するがコピー元を参照している部分がある

https://qiita.com/agajo/items/a145ff44e22f52a62066#新たな実体を生成しないものはシャローコピーと呼ばないことにします

Spraed Operator は一部参照を含んだコピーが行われるので Shallow copyです。

https://suragch.medium.com/cloning-lists-maps-and-sets-in-dart-d0fc3d6a570a#:~:text=that easy%2C though.-,Making a shallow copy,-Dart collections don’t

ちなみに、a=b は参照渡しです。