はじめに
この記事は、Flutterでページ間、Widget間で簡単にデータをシェアする方法についてまとめたものです。
この記事は、複雑なデータの受け渡し(BLoCパターン,ScopedModel,Redux)には言及しません。
コンストラクタを用いたデータの受け渡し
最も一般的なコンストラクタを用いたページ間でのデータの受け渡しについて説明します。
データクラスの定義
はじめに、いくつかのプロパティを持ったClassを定義します。
class Data {
String text;
int counter;
String dateTime;
Data({this.text, this.counter, this.dateTime});
}
コンストラクタを通してデータを渡す
上で定義したDataクラスのインスタンスをあるページ(PageOne)から別なページ(PageTwo)へ受け渡す場合を想定します。
一つ目のページで、Dataクラスのインスタンスを宣言し初期化します。
ElevatedButtonのonPressedでNavigator.pushをコールし、SecondPageへ遷移します。
この時、SecondPage内のコンストラクタでDataクラスのオブジェクトを受け取ります。
Data Class
class Data{
String text;
int num;
Data({required this.num,required this.text});
}
ElevatedButton
ElevatedButton(
child: const Text('Send data to next page'),
onPressed: () {
Navigator.push(context, MaterialPageRoute(
builder: (context) => SecondPage(data: data)
)
);
},
),
SecondPage
class SecondPage extends StatelessWidget {
final Data data;
const SecondPage({Key? key, required this.data}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Constructor - second page')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(data.text,style: const TextStyle(fontSize: 18,fontWeight: FontWeight.bold)),
const SizedBox(height: 20),
Text('${data.num}')
],
),
),
);
}
}
コードを見てわかる通り、やるべきことはDataタイプのインスタンスをfinalで宣言し、コンストラクタに追加するだけです。dataインスタンスはFirstPageで初期化された通りに、SecondPageで表示されています。
データ受け渡しと受け取り
データを次のページに渡して何らかの処理を行い、初めのページに戻したい場合もあると思います。
このときは、Navigator.popにデータを渡すことで、初めのページにデータを戻すことができます。
この処理は、基本的に次の2ステップで実現できます。
1.Navigator.pushをasync functionで覆い、Navigator.popが実行されるのをawaitする。
2.二つ目のページで、Navigator.popに対してデータを渡し、初めのページに戻る。
ElevatedButton
ElevatedButton(
child: const Text('Send data to next page'),
onPressed: () => _secondPage(context,data),
),
async Function
void _secondPage(BuildContext context,Data data) async {
final dataFromSecondPage = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => SecondPage(data: data)
)
) as Data;
//Here you have the data from SecondPage
data.text = dataFromSecondPage.text;
data.num = dataFromSecondPage.num;
}
SecondPageからのデータ受け渡し
ElevatedButton(
onPressed: () => {
changeParameter(),
//data back to FirstPage
Navigator.pop(context, data)
},
child: const Text('Back')
)
InheritedWidget
コンストラクタを用いてデータの受け渡しを行うのは、1階層差のWidget間では非常に便利です。
一方で、複数の階層に渡ってデータの受け渡しを行いたいとき、コンストラクタを用いた方法では記述するコードの量が増え、データの動きが複雑になります。
複数階層のWidget間でデータの受け渡しを行いたいときに便利なのが、InheritedWidgetです。
InheritedWidget
class InheritedDataProvider extends InheritedWidget {
final Data data;
InheritedDataProvider({
Widget child,
this.data,
}) : super(child: child);
@override
bool updateShouldNotify(InheritedDataProvider oldWidget) => data != oldWidget.data;
static InheritedDataProvider of(BuildContext context) => context.inheritFromWidgetOfExactType(InheritedDataProvider);
}
InheritedWidgetクラスを継承し、受け渡しを行いたいデータをWidgetツリーに追加することで、of関数を用いてDataクラスのインスタンスにアクセスすることができるようになります。
InheritedDataProvider
InheritedDataProvider(
child: InheritedDataWidget(),
data: data,
),
IngeritedDataProvider
class InheritedDataWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
final data = InheritedDataProvider.of(context).data;
return Container(
child: Column(
children: <Widget>[
Text(‘Parent’),
Text(‘${data.text}’),
InheritedDataWidgetChild()
],
),
);
}
}
このInheritedDataWidgetChild以下のWidgetでも、of関数を用いることでデータにアクセスすることができます。
IngeritedDataProvider
class InheritedDataWidgetChild extends StatelessWidget {
@override
Widget build(BuildContext context) {
final data = InheritedData.of(context).data;
return Container(
child: Column(
children: <Widget>[
Divider(),
Text(‘Child’),
Text(‘${data.text}’),
InheritedDataWidgetGrandchild()
],
),
);
}
}
InheritedWidget以下のWidgetツリーのどこでも、同様の方法でデータにアクセスすることができます。
この方法の問題点は、ChildWidgetがデータに変更を加えても、変更を加えたWidgetより上位のWidgetに対してデータの変更を反映できない点にあります。
Singletons
ページ間やWidget間でデータの受け渡しを行う方法には、global singletonを使うというものもあります。
まず、singletonクラスを作り、値を追加します。
singletons/appdata.dart
class AppData {
static final AppData _appData = new AppData._internal();
String text;
factory AppData() {
return _appData;
}
AppData._internal();
}
final appData = AppData();
appDataのインスタンスにアクセスしたいファイルにおいて、appdata.dartをインポートします。
import ‘../singletons/appdata.dart’;
class AppDataPage extends StatefulWidget {
@override
AppDataPageState createState() {
return new AppDataPageState();
}
}
class AppDataPageState extends State<AppDataPage> {
final textController = TextEditingController();
@override
Widget build(BuildContext context) {
textController.text = appData.text;
return Scaffold(
appBar: AppBar(
title: Text(‘AppData PageOne’),
),
body: Container(
padding: EdgeInsets.all(12.0),
alignment: Alignment.center,
child: Column(
children: <Widget>[
TextField(
controller: textController,
decoration: InputDecoration(
labelText: ‘Text’,
hintText: ‘Insert some text’,
border: OutlineInputBorder()),
onChanged: (text) {
appData.text = text;
},
),
Divider(),
RaisedButton(
child: Text(‘PageTwo’),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => SecondPage()),
);
},
),
],
),
),
);
}
}
CallBacks
二つのWidget間でデータを受け渡しするときには、CallBacksを用いるのも有効です。
二つ目のWidgetで、テキストデータと現在時刻を取得し、ElevatedButtonのonPressedでCallBack関数に値を渡します。
一つ目のWidgetでは、setStateを用いてCallBack関数に渡された値を取得します。
callbacks.dart
class CallbacksWidget extends StatelessWidget {
final Function(String dateTime) onChangeDate;
final Function(String text) onChangeText;
CallbacksWidget({this.onChangeDate, this.onChangeText});
@override
Widget build(BuildContext context) {
return Container(
child: Column(
children: <Widget>[
Container(height: 6.0),
TextField(
decoration: InputDecoration(
labelText: ‘Text’,
hintText: ‘Insert some text’,
border: OutlineInputBorder()),
onChanged: (value) {
onChangeText(value);
},
),
Container(height: 6.0),
ElevatedButtonButton(
child: Text(“GetTime”),
onPressed: () {
var dateTime = DateFormat(“dd/MM/yyyy — HH:mm:ss:S”).format(DateTime.now());
onChangeDate(dateTime);
},
),
],
),
);
}
}
CallBack関数はFunction型として宣言され、コンストラクタで受け取られます。
final Function(String dateTime) onChangeDate;
final Function(String text) onChangeText;
CallbacksWidget({this.onChangeDate, this.onChangeText});
onChangedにおいて、値は一つ目のWidgetで定義されたonChangeText関数に渡されています。
同様のことが、ElevatedButtonでも起きており、onPressedにおいてonChangeDate関数に値が渡されています。
@override
Widget build(BuildContext context) {
return Container(
child: Column(
children: <Widget>[
Container(height: 6.0),
TextField(
decoration: InputDecoration(
labelText: ‘Text’,
hintText: ‘Insert some text’,
border: OutlineInputBorder()),
onChanged: (value) {
onChangeText(value);
},
),
Container(height: 6.0),
ElevatedButton(
child: Text(“GetTime”),
onPressed: () {
var dateTime = DateFormat(“dd/MM/yyyy — HH:mm:ss:S”).format(DateTime.now());
onChangeDate(dateTime);
},
),
],
),
);
}
Callback関数に渡された値を受け取る方法はsetStateまたはstreamsを用います。
setState
CallbacksWidget(
onChangeDate: (newdate) {
setState(() {
dateTime = newdate;
});
},
onChangeText: (newtext) {
setState(() {
text = newtext;
});
},
),
streams
CallbacksWidget(
onChangeDate: (newdate) {
streamedTime.value = newdate;
},
onChangeText: (newtext) {
streamedText.value = newtext;
},
),
/Riverpod
RiverpodはFlutterの状態管理パッケージです。
RiverpodはFlutterでよく使われているproviderパッケージを開発している人が、providerパッケージで問題のある部分を改良したものです。
この記事ではProviderとproviderを区別しています。
Providerは、providerパッケージのことではなくてRiverpodにおけるProviderクラスのことを指すものとします。
今回はRiverpodのみを紹介します。
Riverpodの使い方
Riverpodでは、以下のようにグローバルなfinal値として定義することが一般的です。
Providerは完全にimmutableなのでグローバルに定義しても問題は起こりません。ここでrefというオブジェクトは、他のProviderにアクセスしたり、破棄の時の処理をいれるために使えます。
今回は、StateProvider は、内部に state_notifier を使っているため、state を変更するだけで変更が通知されるようになっています。
final myProvider = StateProvider((ref) => 0);
Providerは StateProvider / ChangeNotifierProvider / StreamProvider / StateNotifierProvider などがあります。
Provider を使用するには、メイン関数内でMyApp()を ProviderScope で覆う必要があります。
void main() {
runApp(ProviderScope(child: MyApp()));
}
build メソッドの中で Provider を使って myProvider の値を取得します。
class MyHome extends HookWidget {
@override
Widget build(BuildContext context) {
final int _myValue = Provider(myProvider).state;
...
onPressed の中などで myProvider の状態を変更します。
それには context.read メソッドを使います(ここは provider パッケージと同じです)。stateを変更することで、自動的に変更通知が行われます。
floatingActionButton: FloatingActionButton(
onPressed: () {
context.read(myProvider).state++;
},
),
Riverpodを使うと、はじめにProviderScopeでWidgetTreeを覆うことで、Global変数としてWidgetTree内のどこからでも値にアクセスし、変更を加えることができます。
また、直感的に書くことができ、コードの量が非常に少なくて済むのも魅力の一つだと思います。
おわりに
ページ間やWidget間でのデータの受け渡しは、Navigator.pushによるものの解説が多く、他の方法を探そうとしたときにとても苦労しました。
そこで、簡単にデータの受け渡しを実装する方法についてまとめてみました。
この記事が、僕と同じように困っているFlutter初心者の方の助けに慣れば幸いです。