Dart/Flutterを簡潔に書くための豆知識
どの言語でも、プログラムは大抵短い方が読みやすいです。
今回は、開発に役立つと思われる簡潔に書くtipsを紹介します。
ちなみにDartでは、インライン系の書き方が少ないですね(それも、分かりづらいからでしょうけど...)。
インラインスプレッド
リストの中でリストを展開できます。
final baseArray = [1, 2, 3];
final result = [0, ...baseArray, 4];
void main(){
print(result); // [0, 1, 2, 3, 4]
}
式で副作用
これは、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も表せます(使おうと思ったことあったっけ)。
式で副作用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);
}
これは、リストで使える表現です。
アロー関数(無名関数)
これも定番ですが、一応紹介しておきます。型定義は知らない人もいたかもしれません。
// 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(),
},
)
文字列テンプレート
文字列の中に変数を埋め込むことができます。自動で文字列に変換してくれるので便利です。
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");
}
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拡張
Enum
にExtension
で新しいプロパティを追加できます。
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",
});
}
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 overridesObject.==
.
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
なぜ、Taro
、Alex
が表示されないのかは、要調査ですね...
Iterable
範囲のIterable.generate
を使ってできます。0
から第一引数
-1
まで繰り返されることに注意。
final list = List.generate(5, (i) => i * i); // [0, 1, 4, 9, 16]
Abstract
のfactory
実は、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
これは、簡潔に書くための豆知識と言えるかどうか微妙ですが、厳密にわかりやすく書くための機能です。
analyzer:
language:
strict-casts: true
strict-inference: true
strict-raw-types: true
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
を使うことはなさそうですね。
代入の式
お馴染みの変数代入は式としても使えます。
void main() {
String var1 = "value";
print(var1 += "-");
print(var1 += "newValue");
}
void main() {
String var1 = "value";
print(var1 = "newValue");
}
ちなみに、下だけ、linter
にThe value of the local variable 'var1' isn't used.
と言われてしまいます。
使ってるのに、、ん?+=
にするとならなくて、これだけだと意味がないので、やっぱり賢いですね。
Discussion