【Flutter】StateNotifierProviderを使って任意のタイミング&非同期でUIを更新する【riverpod】

ネコニウム研究所

PCを利用したモノづくりに関連する情報や超個人的なナレッジを掲載するブログ

【Flutter】StateNotifierProviderを使って任意のタイミング&非同期でUIを更新する【riverpod】

2024-5-9 | ,

FlutterのパッケージriverpodStateNotifierProviderを使って任意のタイミング&非同期でUIを更新したい!

概要

今回の記事では、FlutterのパッケージriverpodStateNotifierProviderを使って任意のタイミング&非同期で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.yamldependencies:に下記のような感じで追加して

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)),
                  ),
                );
              }),
            ]),
          ),
        ),
      ),
    );
  }
}

StateNotifierProviderAsyncValueを併用することで任意のタイミング&非同期でUIを更新することができる。ちょいとコードが複雑になる感触。

まとめ(感想文)

定期的、または、任意のタイミングで更新する必要があるケースに使えるかもね!