Flutterのパッケージriverpod
のStateNotifierProvider
を使って任意のタイミング&非同期でUIを更新したい!
概要
今回の記事では、Flutterのパッケージriverpod
のStateNotifierProvider
を使って任意のタイミング&非同期でUIを更新する手順を掲載する。
仕様書
環境
- Android Studio Giraffe | 2023.2.1 Patch 2
- Flutter 3.19.6
- http: 1.1.0
- flutter_riverpod: 2.5.1
手順書
インストール編とコード編の2部構成です。
インストール編
ターミナルでコマンドを実行するか
flutter pub add http flutter_riverpod
pubspec.yaml
のdependencies:
に下記のような感じで追加して
dependencies:
http: ^1.1.0
flutter_riverpod: ^2.5.1
ターミナルでflutter pub get
する。
flutter pub get
コード編
過去の記事に載せたOpen-Meteoという天気予報のAPIと通信して1週間分の天気予報を表示するコードを使う。
TextButton
をタップするとAPIから情報を再取得してTable
を更新するよう修正した例。
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:http/http.dart' as http;
void main() {
runApp(
MaterialApp(
home: Scaffold(
body: const StateNotifierProviderSample1()
)
)
);
}
class WeatherInfo {
late List<double> temperature2mMax;
late List<double> temperature2mMin;
late List<String> time;
late List<int> weatherCode;
WeatherInfo(
{required this.temperature2mMax,
required this.temperature2mMin,
required this.time,
required this.weatherCode});
}
final weatherInfoStateNotifierProvider =
StateNotifierProvider<WeatherInfoStateNotifier, AsyncValue<WeatherInfo>>(
(ref) {
return WeatherInfoStateNotifier();
});
class WeatherInfoStateNotifier extends StateNotifier<AsyncValue<WeatherInfo>> {
WeatherInfoStateNotifier() : super(const AsyncValue.loading()){
update();
}
Future<WeatherInfo> update() async {
try {
final response = await http.get(
Uri.parse(
'https://api.open-meteo.com/v1/forecast?latitude=35.6785&longitude=139.6823&daily=weather_code,temperature_2m_max,temperature_2m_min&timezone=Asia%2FTokyo'),
);
debugPrint('response.statusCode: ${response.statusCode}');
debugPrint('response.headers: ${response.headers}');
debugPrint('response.body: ${response.body}');
if (response.statusCode == 200) {
final Map<String, dynamic> responseJson = jsonDecode(response.body);
state = AsyncValue.data(WeatherInfo(
temperature2mMax:
List<double>.from(responseJson['daily']['temperature_2m_max']),
temperature2mMin:
List<double>.from(responseJson['daily']['temperature_2m_min']),
time: List<String>.from(responseJson['daily']['time']),
weatherCode: List<int>.from(responseJson['daily']['weather_code']),
));
}
} catch (e) {
debugPrint(e.toString());
throw Exception(e.toString());
}
throw Exception('Failed to load weather info');
}
}
class StateNotifierProviderSample1 extends StatelessWidget {
const StateNotifierProviderSample1({super.key});
@override
Widget build(BuildContext context) {
return ProviderScope(
child: SafeArea(
child: Center(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(mainAxisSize: MainAxisSize.min, children: <Widget>[
Consumer(builder:
(BuildContext context, WidgetRef ref, Widget? child) {
List<String> headers = [
'Date',
'Max Temp (°C)',
'Min Temp (°C)',
'Weather Code'
];
final asyncValue = ref.watch(weatherInfoStateNotifierProvider);
return asyncValue.when(
data: (data) => Table(
border: TableBorder.all(),
columnWidths: {
for (int i = 0; i < headers.length; i++)
i: const FlexColumnWidth(),
},
children: [
TableRow(
decoration: BoxDecoration(color: Colors.grey[300]),
children: headers.map((header) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
header,
textAlign: TextAlign.center,
style:
const TextStyle(fontWeight: FontWeight.bold),
),
);
}).toList(),
),
...List.generate(data.time.length, (index) {
return TableRow(
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
data.time[index],
textAlign: TextAlign.center,
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
data.temperature2mMax[index].toString(),
textAlign: TextAlign.right,
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
data.temperature2mMin[index].toString(),
textAlign: TextAlign.right,
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
data.weatherCode[index].toString(),
textAlign: TextAlign.right,
),
),
],
);
}),
],
),
loading: () => const CircularProgressIndicator(),
error: (error, stack) => Text(
'Error: $error',
style: const TextStyle(color: Color(0xFFFF0000)),
),
);
}),
Consumer(builder:
(BuildContext context, WidgetRef ref, Widget? child) {
final asyncValue = ref.watch(weatherInfoStateNotifierProvider);
return asyncValue.when(
data: (data) => TextButton(
style: TextButton.styleFrom(
backgroundColor: Colors.blue,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(4)),
),
padding: const EdgeInsets.fromLTRB(0, 0, 0, 0),
),
onPressed: () =>
ref.read(weatherInfoStateNotifierProvider.notifier).update(),
child: const Padding(
padding: EdgeInsets.fromLTRB(0, 0, 0, 0),
child: Text("UPDATE",
style:
TextStyle(fontSize: 16.0, color: Colors.white)),
),
),
loading: () => const CircularProgressIndicator(),
error: (error, stack) => Text(
'Error: $error',
style: const TextStyle(color: Color(0xFFFF0000)),
),
);
}),
]),
),
),
),
);
}
}
StateNotifierProvider
とAsyncValue
を併用することで任意のタイミング&非同期でUIを更新することができる。ちょいとコードが複雑になる感触。
まとめ(感想文)
定期的、または、任意のタイミングで更新する必要があるケースに使えるかもね!