モチベーション
皆様、Flutter Riverpodの.family
についてただ単にProviderを利用する際に引数を与えられるだけと思っていませんでしたか(私もつい最近までそう思っていました)。それにしても何故family
と名前がついているのか?今まで疑問でしたが、最近解消しましたので記載します。
.familyについて
Riverpodの.family
は正式ドキュメントに使い方や解説がありますが、使用時に引数を指定することが可能にします。
例としては以下のように.family
を使用したProviderを定義して。(引用元)
final messagesFamily = FutureProvider.family<Message, String>((ref, id) async {
return dio.get('http://my_api.dev/messages/$id');
});
使用する際に以下のように引数にid
を与えることができます。通常の関数のように使用時に引数を指定することが可能です。
Widget build(BuildContext context, WidgetRef ref) {
final response = ref.watch(messagesFamily('id'));
}
このまま関数のように使うのは便利ですが、一意の値を取得することもできます。
.familyで一意の値を取得する
正式ドキュメントの冒頭に書いてありますが、用途として引数が識別するIDとなり一意のプロバイダを取得することが可能です。そのことをサンプルコードを用いて紹介します
一意のプロバイダーを取得する例
サンプルコード全体
import 'package:riverpod/riverpod.dart';
// Providerで返すclassを定義
class Target {
}
// Provider.familyを定義。引数に関係なく同じ値のインスタンスを返す
final targetProviderFamily = Provider.family<Target,int> ((ref, arg){
return Target(); // 引数に関係なく同じ値のインスタンスを返す
});
void main() {
final container = ProviderContainer();
// 同じ引数=0を指定
final t1 = container.read(targetProviderFamily(0));
final t2 = container.read(targetProviderFamily(0));
// 同じインスタンスのためtrueにになる
print(t1 == t2); // true
// 異なる引数=1を指定
final t3 = container.read(targetProviderFamily(1));
// t1とt3の比較ではfalseを返す
print(t1 == t3); // false
}
コードの詳細
.familyの定義
以下のtargetProviderFamily
は引数に関係なくTarget()
のインスタンスを返します。ここで注意はこの例では引数に関係ないインスタンスを返している事です
// Provider.familyを定義。引数に関係なく同じ値のインスタンスを返す
final targetProviderFamily = Provider.family<Target,int> ((ref, arg){
return Target(); // 引数に関係ないインスタンスを返す
});
.familyの引数が同じ値の場合
次にmain
の中で同じ引数=0
をプロバイダに指定した値を取り出して==
にて比較します。この時の結果はtrue
になります。これは引数が同じ場合は同じTarge()
のインスタンスを返すことを意味します。
// 同じ引数=0を指定
final t1 = container.read(targetProviderFamily(0));
final t2 = container.read(targetProviderFamily(0));
// 同じインスタンスのためtrueにになる
print(t1 == t2); // true
詳しく言うと、Target() == Target()
は異なるインスタンスとなるためfalse
になるはずですが、true
になるのは、同じ引数では再度インスタンスを作成することなく一意のインスタンスを再利用していることを意味します。
.familyの引数が異なる値の場合
上記に追加で以下の部分では異なる引数=1
を指定した値と前回の引数=0
で所得した値を比較するとfalse
になります。
// 異なる引数=1を指定
final t3 = container.read(targetProviderFamily(1));
// t1とt3の比較ではfalseを返す
print(t1 == t3); // false
異なる引数では異なるインスタンスになることを意味します。
サンプルコードから
.family
は引数を苗字代わりに一意の値を取得することが可能です。この特性を利用して無駄にインスタンスを立てることなく再利用することが可能になります。
引数の注意点
引数にint
やString
のようにリテラルの値を使う場合は一意の値を返しますが、引数にObjectを使う場合は注意が必要です。Objectが同じ引数と確認するためにはObjectのhashCode
と==
OperatorをOverrideする必要があります。
hashCode
と==
OperatorをOverride例
import 'package:riverpod/riverpod.dart';
// 引数となるClass
class Arg {
final String value;
const Arg(this.value);
// override == operator
@override
bool operator == (Object other) {
if (identical(this, other)) {
return true;
}
if (other is Arg) {
return runtimeType == other.runtimeType && value == other.value;
} else {
return false;
}
}
// override hasCode
@override
int get hashCode => value.hashCode;
}
class Target {}
final targetProviderFamily = Provider.family<Target,Arg> ((ref, arg){
return Target();
});
void main() {
final container = ProviderContainer();
// 同じ引数=0を指定
final t1 = container.read(targetProviderFamily(const Arg('Zero')));
final t2 = container.read(targetProviderFamily(const Arg('Zero')));
// 同じインスタンス返す
print(t1 == t2); // true
// 異なる引数=1を指定
final t3 = container.read(targetProviderFamily(const Arg('One')));
// 異なるインスタンスになる
print(t1 == t3); // false
}
引数となるObjectおClassでhashCode
と==
OperatorをOverrideすればリテラルの時と同様に使うことができます。
しかしhashCode
と==
OperatorをOverrideするのは面倒なのでequatableを使うともっと簡単に書けます。
最後に
Riverpodの.family
には何故family
と名前がついているのか?なんとなくなく理解できたかと思います。
よく使うProviderには.family
を利用して再利用できるようにすればアプリのリソース効率につながります。
.family
の弱点としては、引数の数は1個の制限があります。しかし複数値を利用した場合の解決策としては正式ドキュメントの中で先ほどのequatableやtupleを使うことで可能です(私は引数にClass定義するのは面倒なのでtupleをよく利用しています)。