🌱

Dart/Flutterを簡潔に書くための豆知識

に公開

どの言語でも、プログラムは大抵短い方が読みやすいです。
今回は、開発に役立つと思われる簡潔に書くtipsを紹介します。
ちなみにDartでは、インライン系の書き方が少ないですね(それも、分かりづらいからでしょうけど...)。

インラインスプレッド

リストの中でリストを展開できます。

final baseArray = [1, 2, 3];
final result = [0, ...baseArray, 4];
void main(){
  print(result);  // [0, 1, 2, 3, 4]
}

https://dart.dev/language/collections#spread-element

式で副作用

これは、Dart3.0で追加されたRecord型を活用して、副作用ををつける方法です。
(value1, value2).$1とすると、value1が取得される一方で、value2も評価されることを利用しています。
意図が理解しづらいのがデメリットで、一番上の書き方が一番わかりやすい気もします。

void main(){
    func();
    print("hello");
}
String func(){
    print("func called");
    return "returnValue";
}
void main() => print((func(), "hello").$2);
String func(){ 
  print("func called");
  return "returnValue";
}
void main() => print((() => func() ?? 'hello')());
String? func() {
  print('func called');
  return null;
}
追記:戻り値がないとできませんでした

すみません、戻り値がvoidだとこんなエラーになるようです。戻り値のある関数でテストしていて見逃していました。

compileNewDDC
main.dart:1:29: Error: This expression has type 'void' and can't be used.
void main() => print((() => A() ?? 'hello')());
                            ^
main.dart:1:44: Error: This expression has type 'void' and can't be used.
void main() => print((() => A() ?? 'hello')());
void main(){
    func();
    print("hello");
}
void func(){
    print("func called");
}
void main() => print((func(), "hello").$2);
void func() => print("func called");

これを使えば、Javascriptの、voidも表せます(使おうと思ったことあったっけ)。
https://dart.dev/language/records

式で副作用2

無名で複雑なことをするときはこちらの方が読みやすいかもしれません。

void main(){
    print(((){func();return "hello";})());
}
void func(){
    print("func called");
}
func called
hello

非同期で無名で処理を実行する時も便利です。

void main() async {
    print(
      await (
        () async => Future.delayed(
          Duration(seconds: 2), 
          ()=>2,
        ),
      )()
    );
}

これを使えば、FutureBuilderも...

FutureBuilder(
  future: (() async {
    await Future.delayed(Duration(seconds: 2));
    return "done";
  })(),
  builder: (context, snapshot) {
    if (snapshot.connectionState == ConnectionState.waiting) {
      return CircularProgressIndicator();
    }
    return Text(snapshot.data.toString());
  },
)

状況によっては、カスケード記法が使えることがあります。

class MyAction {
  String _value;

  MyAction(this._value);

  MyAction func() {
    print("func");
    return this;
  }

  String getValue() {
    return _value;
  }
}

void main() {
  String myValue = (MyAction("retValue")..func()).getValue();

  print(myValue); // func
  // retValue
}

(MyAction("retValue")..func())の括弧がないと.getValueがカスケード記法の..funcの続きになって

compileNewDDC
main.dart:17:20: Error: A value of type 'MyAction' can't be assigned to a variable of type 'String'.
 - 'MyAction' is from 'package:dartpad_sample/main.dart' ('/tmp/dartpadONIKJC/lib/main.dart').
  String myValue = MyAction("retValue")..func().getValue();
                   ^

になることに注意して下さい。
...結論、カスケード記法が使えるときはそれが良くて、次に、普通に行を変えて書けるなら、Minifyツールでない限りそれが読みやすく、開発中などにprintする必要があって文として書くのが大変なら無名関数呼び出し、が良いと思います。

三項演算子

これは、インラインの定番ですね。式で、条件がtrueの時Aになり、falseの時Bになります。

final condition = 5 > 3; // true
print(condition 
        ? "5 is greater than 3" 
        : "5 is equal to or less than 3" )

条件を増やすときは、入れ子にする必要があります。

final condition1 = 5 > 3; // true
final condition2 = 5 == 3; // false
void main() => print(
  condition1
      ? "5 is greater than 3"
      : (condition2 ? "5 is equal to 3" : "5 is less than 3"),
);

リストで条件

Flutterでこんな書き方してませんか?

children: [
    isAuth ? Text("Alex") : SizedBox() // 三項演算子
]

これは、パフォーマンス的にあまり良くないのと、ウィジェット以外で使えないので、代わりに以下のように書きましょう。

import "package:flutter/material.dart";

final bool isAuth = true;

void main() {
  runApp(
    MaterialApp(
      home: Scaffold(
        body: Column(children: [
            if (isAuth) Text("Alex"),
        ]),
      ),
    ),
  );
}

マップでもこの表現が使えます。

final age = 17;
final mapObject = {
  "username": "Taro",
  if (age >= 18) "email": "test@example.com",
};
void main() {
  print(mapObject);
}

これは、リストで使える表現です。
https://dart.dev/language/collections#if-element

アロー関数(無名関数)

これも定番ですが、一応紹介しておきます。型定義は知らない人もいたかもしれません。

// define type
typedef JoinStrIntFunc = String Function(String, int);
final JoinStrIntFunc joinStrInt2 = (s, i) => "$s$i";

// write inline
final String Function(String, int) joinStrInt1 = (s, i) => "$s$i";

// without arrow function
void func(s, i){
  return "$s$i";
}
final void Function(String, int) joinStrInt3 = func;

Null合体演算子

void main(){
  final name = "alex";
  print(name ?? "No information");
}

カスケード記法

これは、メソッドの副作用を使うとき、連続して書ける記法です。

class Player {
  int hp = 10;
  void run(){
    hp -= 1;
  }
  void eat(){
    hp += 1;
  }
}
void main(){
  final StringBuffer buffer = StringBuffer()
    ..write('Hello')
    ..write(' ')
    ..write('World!');
  print(buffer); // Hello World!

  final Player player = Player()
    ..run()
    ..run()
    ..run()
    ..hp += 2;
    ..eat();
  print(player.hp); // 10
}

awaitは使えないことに注意して下さい(使いたいこと多いのに...)

class MyService {
  Future<void> init() async {
    print('Initializing...');
    await Future.delayed(Duration(seconds: 1));
    print('Initialized');
  }

  void doSomething() {
    print('Doing something');
  }
}

Future<void> main() async {
  final service = MyService()
    ..await init()
    ..doSomething();
}
compileNewDDC
main.dart:15:7: Error: Unexpected token 'await'.
    ..await init()
      ^^^^^

Switch式

Dart3.0の新機能です。

final value = 2;
final result = switch (value) {
  1 => "one",
  2 => "two",
  3 => "three",
  _ => "other",
};
void main() {
  print(result); // two
}

これで、FutureBuilderも簡潔に書けますね。

FutureBuilder(
  future: Future.delayed(Duration(seconds: 2), () => 2),
  builder: (context, snapshot) => switch (snapshot.connectionState) {
    ConnectionState.waiting => CircularProgressIndicator(),
    ConnectionState.done => snapshot.hasError
                              ? Text(snapshot.error.toString())
                              : Text(snapshot.data.toString()),
    _ => throw UnimplementedError(),
  },
)

https://dart.dev/language/branches#switch-expressions

文字列テンプレート

文字列の中に変数を埋め込むことができます。自動で文字列に変換してくれるので便利です。

void main() {
  var name = 'Taro';
  var age = 20;
  print('Hi, I\'m $name and next year I\'ll be ${age + 1}.');
}
Hi, I'm Taro and next year I'll be 21.
class Person {
  const Person({required this.name, required this.age});
  final String name;
  final int age;
  
  
  String toString(){
    print("toString called");
    return "$name ($age)";
  }
}
void main(){
  final person = Person(name: "alex", age: 5);
  print("Person: $person");
}
toString called
Person: alex (5)

Assert

開発中にのみ条件がfalseの時エラーになり、実行が中断されます。通常のtry catchでキャッチできます。

void setAge(int age) {
  assert(age >= 0);
  assert(age <= 100, "age: too big");
}

https://dart.dev/language/error-handling#assert

entries

mapの値を取得する方法です。

var map = {'a': 1, 'b': 2};
void main() {
  map.entries.forEach((e) => print('${e.key}:${e.value * 2}'));
}
// Result:
// a:2
// b:4

ちなみに、mapを使うとこのように書けます。

var map = {'a': 1, 'b': 2};
var doubled = map.map((k, v) => MapEntry(k, v * 2));
void main(){
  print(doubled); // {a: 2, b: 4}
}

enum拡張

EnumExtensionで新しいプロパティを追加できます。

enum Status { idle, loading, error }

extension StatusLabel on Status {
  String get label => {
    Status.idle: 'Idle',
    Status.loading: 'Loading',
    Status.error: 'Error',
  }[this]!;
}

void main(){
  print(Status.idle.label); // "Idle"
}

assertを使わないassert

throwは式として使えるので、三項演算子を使って実現できます。

void setSpeed(int value) =>
    value >= 0 && value <= 100
        ? speed = value
        : throw ArgumentError('Invalid speed: $value');

なぜか、rethrowは式として使えないんですよね...
特に、switch式で不便さを感じます。

takeWhile

リストやイテラブルの要素を、条件がtrueの間だけ取得したい場合に使います。
takeWhileは、最初に条件がfalseになった時点で、それ以降の要素は無視します。

void main() {
  final numbers = [1, 2, 3, 4, 5, 1, 2];
  final result = numbers.takeWhile((n) => n < 4);
  print(result.toList()); // [1, 2, 3]
}

whereとの違いは、whereは全ての要素に条件を適用しますが、takeWhileは途中で打ち切ります。

void main() {
  final numbers = [1, 2, 3, 4, 5, 1, 2];
  final result = numbers.where((n) => n < 4);
  print(result.toList()); // [1, 2, 3, 1, 2]
}

Never型

Never error() => throw Exception('Error occured!');

void main(){
  final int? age = null;
  if (age == null) error();
  print(age + 3);
}

Never型でないと、

main.dart:6:13: Error: Operator '+' cannot be called on 'int?' because it is potentially null.
  print(age + 3);

のようになってしまいます。
ただ、Enumでは使えないので注意です。Union型が早く出てくれれば解決しそうですが...

Never error() => throw Exception('Error occured!');

enum StatusCode { ok, notFound, forbidden }

void main() {
  final statusCode = StatusCode.ok;
  if (statusCode == StatusCode.ok) error();
  print(switch (statusCode) {
    StatusCode.notFound => "HTTP Error 404: Not Found",
    StatusCode.forbidden => "HTTP Error 403: Forbidden",
  });
}

https://github.com/dart-lang/language/issues/83
https://github.com/dart-lang/language/issues/2711

Function.apply

関数を引数の配列を使って呼び出すことができます。
使う場面は少ないですが、動的に引数を渡す必要があるときはこれを使うので、覚えておいて損はなさそうですね。

int sum(int a, int b) => a + b;

int addWithNamed({required int a, required int b}) => a + b;

void main() {
  final result = Function.apply(sum, [1, 2]); // 3
  print(result); // 3

  // Named arguments can be passed as a map in the third parameter
  final namedResult = Function.apply(
    addWithNamed,
    [],
    {#a: 3, #b: 4},
  ); // 7
  print(namedResult); // 7
}

Symbol

動的にメソッドを呼び出したりするためのものです。

assert(Symbol("foo") == Symbol("foo"));
assert(Symbol("foo") == #foo);
assert(identical(const Symbol("foo"), const Symbol("foo")));
assert(identical(const Symbol("foo"), #foo));
assert(Symbol("[]=") == #[]=);
assert(identical(const Symbol("[]="), #[]=));
assert(Symbol("foo.bar") == #foo.bar);
assert(identical(const Symbol("foo.bar"), #foo.bar));
The created instance overrides Object.==.

https://api.dart.dev/dart-core/Symbol/Symbol.html

import 'dart:mirrors';

class MyClass {
  void sayHello() {
    print('Hello!');
  }
}

void main() {
  var obj = MyClass();

  // Create a mirror of the object
  var mirror = reflect(obj);

  // Create a Symbol from method name
  var methodName = #sayHello;

  // Invoke method via Symbol
  mirror.invoke(methodName, []);
}

コンストラクタでassert

ブロックボディなしで、コンストラクタ内でassertすることができます。

class User {
  final String name;
  const User(this.name) : assert(name != '');
}

void main(){
  print(User("Taro").name);
  print(User("Alex").name);
  print(User(""));
}
Uncaught Error, error: Error: Assertion failed: file:///tmp/dartpadAQNKXJ/lib/main.dart:3:28
name != ''
is not true

なぜ、TaroAlexが表示されないのかは、要調査ですね...

範囲のIterable

Iterable.generateを使ってできます。0から第一引数-1まで繰り返されることに注意。

final list = List.generate(5, (i) => i * i); // [0, 1, 4, 9, 16]

Abstractfactory

実は、Abstractのクラスにも、コンストラクタを定義することができます。

abstract class Animal {
  factory Animal(String type) {
    if (type == 'dog') return Dog();
    else return Cat();
  }
}

class Dog implements Animal {}
class Cat implements Animal {}

void main(){
  // print(Dog("dog")); // error
  print(Animal("dog"));
}

Strict mode

これは、簡潔に書くための豆知識と言えるかどうか微妙ですが、厳密にわかりやすく書くための機能です。

(Project name)/analysis_options.yaml
analyzer:
  language:
    strict-casts: true
    strict-inference: true
    strict-raw-types: true

https://dart.dev/tools/analysis#enabling-additional-type-checks

Null aware operators

??=という演算子です。デフォルト値を設定する、つまりnullの時だけ代入するという意味があります。

void main() {
  String? value = null;
  if (value == null) {
    value = "default value";
  }
  print(value); // "default value"
}

これを、

void main() {
  String? value = null;
  value ??= "default value";
  print(value); // "default value"
}

こんなに短くかけてしまいます。
lateだと

void main() {
  late String? value;
  if (value == null) {
    value = "default value";
  }
  print(value); // "default value"
}
compileNewDDC
main.dart:3:7: Error: Late variable 'value' without initializer is definitely unassigned.
  if (value == null) {
      ^^^^^

もちろん、エラーになりますが、この記法を使っても、

void main() {
  late String? value;
  value ??= "default value";
  print(value); // "default value"
}
compileNewDDC
main.dart:3:3: Error: Late variable 'value' without initializer is definitely unassigned.
  value ??= "default value";
  ^^^^^

エラーになることに注意して下さい。
...あまりlateを使うことはなさそうですね。
https://dart.dev/resources/dart-cheatsheet#null-aware-operators

代入の式

お馴染みの変数代入は式としても使えます。

void main() {
  String var1 = "value";
  print(var1 += "-");
  print(var1 += "newValue");
}
void main() {
  String var1 = "value";
  print(var1 = "newValue");
}

ちなみに、下だけ、linterThe value of the local variable 'var1' isn't used.と言われてしまいます。
使ってるのに、、ん?+=にするとならなくて、これだけだと意味がないので、やっぱり賢いですね。

Discussion