こんにちは、テルプロです!
「FlutterでのAPI通信のやり方がわからない」とお悩みではないでしょうか?
本記事ではそんな悩みを解決していきます!
- FlutterでのAPI通信のやり方をサンプルで理解できる
- Riverpod + freezed + dioの使い方がわかるようになる
- コードを公開しているので、自分の環境で確かめることができる
API通信(dio)でお天気アプリを作る
事前準備
パッケージをインストール
今回使用するパッケージは以下の通りです。
dependencies:
build_runner: ^2.1.8
dio: ^4.0.4
flutter:
sdk: flutter
flutter_keyboard_visibility: ^5.2.0
flutter_riverpod: ^1.0.3
freezed: ^1.1.1
freezed_annotation: ^1.1.0
json_serializable: ^6.1.5
dev_dependencies:
flutter_lints: ^1.0.0
flutter_test:
sdk: flutter
プロジェクト構成
リポジトリ構成
完成イメージ
本アプリは2画面のみの構成になります。1画面目で入力した都市のお天気情報をAPIから取得し、2画面目で取得した内容を表示するというシンプルなアプリです。
今回はOpenWeatherApiを使用します。アプリを作り始める前にサインインをし、appIdを押さえておいてください。
OpenWeatherApi:https://openweathermap.org/current
それでは、Riverpod + freezed + dioを用いて上記のアプリを作っていきましょう!
ソースコード
main.dart
import 'package:clima/config/config.dart';
import 'package:clima/view/input_page.dart';
import 'package:clima/view/result_page.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
void main() {
runApp(const ProviderScope(child: MyApp()));
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Clima',
theme: ThemeData(
appBarTheme: kAppBarTheme,
scaffoldBackgroundColor: kScaffoldBackgroundColor,
inputDecorationTheme: InputDecorationTheme(
border: kTextFieldStyle,
focusedBorder: kTextFieldStyle,
enabledBorder: kTextFieldStyle,
),
),
routes: {
'/': (context) => InputPage(),
'/result': ((context) => const ResultPage()),
},
);
}
}
model / weather.dart
まず初めに、APIから取得したい内容のクラスを作成していきましょう。今回は気温、体感気温、湿度、気圧という4つの天気情報を取得していきます。
クラスの作成にはfreezedを使用しています。freezedを使用することで、不変クラスを作成することができます。
import 'package:flutter/foundation.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
// build_runnerを使うことで自動生成されるファイル
part 'weather.freezed.dart';
part 'weather.g.dart';
// 天気情報モデル
@freezed
class Weather with _$Weather {
factory Weather({
int? timezone,
int? id,
String? name,
WeatherMain? main,
}) = _Weather;
factory Weather.fromJson(Map<String, dynamic> json) =>
_$WeatherFromJson(json);
}
// 詳しい詳細情報
@freezed
class WeatherMain with _$WeatherMain {
factory WeatherMain({
double? temp,
double? feels_like,
double? temp_min,
double? temp_max,
int? pressure,
int? humidity,
}) = _WeatherMain;
factory WeatherMain.fromJson(Map<String, dynamic> json) =>
_$WeatherMainFromJson(json);
}
freezedでクラスを作成したら、下記のコマンドを回してください。
flutter pub run build_runner watch --delete-conflicting-outputs
これにより、ファイルが自動生成されます。–delete-conflicting-outputsを加えることで、クラスに変更が加えられる度に再生成されます。
modelフォルダが上記のようになっていれば問題ありません。これで取得したいデータを保持するクラスを作成することができました。
service / weather_api_client.dart
次に、APIを呼び出しデータを取得するためのクラスを作っていきます。
APIの取得にはdioを使用しています。
import 'dart:convert';
import 'package:clima/models/weather.dart';
import 'package:dio/dio.dart';
// APIを呼び出し、データを取得する
class WeatherApiClient {
Future<Weather?> fetchWeather(String? location) async {
final dio = Dio();
const appId = '4b0e4756a7f3015c0d08c2f0263c224a&units=metric';
final url =
'https://api.openweathermap.org/data/2.5/weather?q=$location&appid=$appId';
var response = await dio.get(url);
if (response.statusCode == 200) {
try {
return Weather.fromJson(response.data);
} catch (e) {
print(e);
throw e;
}
}
}
}
これで、APIを取得するためのクラスを作成することができました。
repository / repository.dart
APIを取得するためのメソッドをrepositoryから呼び出します。メソッドを切り分けることで、APIを取得するためのメソッドだと理解しやすくなります。
import 'package:clima/models/weather.dart';
import 'package:clima/services/weather_api_client.dart';
// APIを取得するためのメソッドをRepositoryから呼び出す
class Repository {
final _api = WeatherApiClient();
dynamic fetchWeather(String? location) async {
return _api.fetchWeather(location);
}
}
これで、APIを取得するためのメソッドをrepositoryに切り分けることができました。
view_model / provider.dart
次に、取得したAPIの状態を非同期で管理できるようにしていきます。状態管理にはRiverpodを使用しています。
今回は、非同期処理を行うのに適しているFutureProviderを使用していきます。
import 'package:clima/models/weather.dart';
import 'package:clima/repositories/repository.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
// 都市名の状態を管理するためのProviderを作成
final cityNameProvider = StateProvider((ref) => 'Tokyo');
// Repository(APIの取得)を管理するためのProviderを作成
final repostitoryProvider = Provider((ref) => Repository());
// APIの取得を非同期で管理するためのProviderを作成
final dataProvider = FutureProvider.autoDispose<Weather>((ref) async {
// Repositoryのインスタンスを取得
final repository = ref.read(repostitoryProvider);
// 都市名の状態を監視
final cityName = ref.watch(cityNameProvider.notifier);
// 都市名を組み込み、APIを取得する
return await repository.fetchWeather(cityName.state);
});
これにより、APIの取得を非同期で行えるようになりました。
view / input_page.dart
最後に、お天気アプリのUI(見た目)を作っていきます。
まずは、1画面目の「都市名を入力するView」です。
import 'package:clima/components/button_widget.dart';
import 'package:clima/config/config.dart';
import 'package:clima/repositories/repository.dart';
import 'package:clima/view_models/provider.dart';
import 'package:flutter/material.dart';
import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class InputPage extends ConsumerWidget {
final controller = TextEditingController();
@override
Widget build(BuildContext context, WidgetRef ref) {
// Providerの監視
final cityName = ref.watch(cityNameProvider.notifier);
return KeyboardDismissOnTap(
child: Scaffold(
body: Center(
child: SingleChildScrollView(
reverse: true,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text('Clima', style: kTitleTextStyle),
const SizedBox(height: 20.0),
Image.asset(
'assets/icon.png',
width: 160,
height: 160,
),
const SizedBox(height: 30.0),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 40.0),
child: TextFormField(
controller: controller,
onChanged: (value) {
cityName.state = value;
print(cityName.state);
},
keyboardType: TextInputType.emailAddress,
textAlign: TextAlign.center,
decoration: InputDecoration(
hintText: '都市名を入力してください',
hintStyle: kTextFieldLabelStyle,
labelText: 'City Name',
labelStyle: kTextFieldLabelStyle,
fillColor: Colors.white,
filled: true,
focusedBorder: kTextFieldStyle,
enabledBorder: kTextFieldStyle,
),
style: kTextFieldLabelStyle,
),
),
const SizedBox(height: 20.0),
ButtonWidget(
label: '検索',
press: () {
// 都市名をURLに組み込む
Repository repository = Repository();
repository.fetchWeather(cityName.state);
Navigator.pushNamed(context, '/result');
},
),
],
),
),
),
),
);
}
}
view / result_page.dart
最後に、2画面目の「取得したAPI情報を反映させるView」です。
FutureProviderのwhenメソッドを使用することで、Viewの反映、ローディング時、エラー時の3つの処理を書くことができるようになっています。
import 'package:clima/components/add_info.dart';
import 'package:clima/components/button_widget.dart';
import 'package:clima/config/config.dart';
import 'package:clima/models/weather.dart';
import 'package:clima/repositories/repository.dart';
import 'package:clima/view_models/provider.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class ResultPage extends ConsumerWidget {
const ResultPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
// dataProviderの監視
final asyncValue = ref.watch(dataProvider);
// cityNameProviderの監視
final cityName = ref.watch(cityNameProvider.notifier);
return Scaffold(
appBar: AppBar(title: const Text('Clima')),
body: Center(
child: asyncValue.when(
loading: () => const CircularProgressIndicator(),
error: (e, _) => Text(e.toString()),
data: (data) => Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
padding: const EdgeInsets.all(20.0),
decoration: kCityNameContainerStyle,
child: Text(cityName.state, style: kCityNameTextStyle),
),
const SizedBox(height: 60.0),
Column(
children: const [
Text('Additional Information', style: kAddInfoTextStyle),
SizedBox(height: 20.0),
Divider(color: Colors.black54, height: 1.0),
],
),
const SizedBox(height: 10.0),
Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
AddInfo(
label: '気温', value: data.main!.temp.toString() + '°'),
AddInfo(
label: '体感気温',
value: data.main!.feels_like.toString() + '°',
),
],
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
AddInfo(
label: '湿度',
value: data.main!.humidity.toString() + '%',
),
AddInfo(
label: '気圧',
value: data.main!.pressure.toString() + 'hPa',
),
],
),
const SizedBox(height: 30.0),
ButtonWidget(
label: '更新',
press: () async {
ref.refresh(dataProvider);
},
),
],
),
],
),
),
),
);
}
}
大変お疲れ様でした!以上でアプリは完成です!
今回ご紹介したアプリ全体のソースコードはこちらです。再利用に用いる「components」と「config」も記載されています。
よろしければ、ご参考にどうぞ。
GitHub:https://github.com/terupro/clima
まとめ
今回は、API通信(dio)でお天気アプリを作る方法をご紹介しました。
dioを使うことで、API通信を楽に実装することができます。ご紹介したサンプルを参考に、自分で色々と試してみてください。
▼以下では、私の実体験に基づいて「Flutterの効率的な勉強法」の具体的な手順を詳しく解説しています。ぜひ参考にしてみてください!
最後までご覧いただきありがとうございました。ではまた!