こんにちわ いせきです。 今日も元気にコードを書いています。
今回は、APIを使ってアプリを使ってみたいけど「複雑そうだな。。。」や「難しくて自分にはできないよ」と思う駆け出しエンジニアがいるのではないでしょうか。僕もそうでした!!
上記の方や復習した方に簡単にできると思えるような方法を共有したいと思います。
はじめに
今回使用するAPIです。こちらは、ゆめみの研修課題で使用するAPIとなっています。
この課題の中のissuesの中にある「Session2」をやってみようと思います。
やること
・Reload ボタンをタップして天気予報を取得する
・天気予報を画面に表示する
導入
README.md
に書いてある通りに導入していこうと思います。
1、pubspec.yamlに記載する。
Flutterファイル構造を見てみる
- .dart.tool
- .idea
- android
- build
- ios
- lib
- test
- gitignore
- .metadata
- analysis_option.yaml
- pubspec.lock
- pubspec.yaml //このファイルです〜
- README.md
...(ここまでとしておきます。)
pubspec.yaml
のファイルを開いて、整理しつつ、yumemi_weather
を導入してみましょう。
pubspec.yaml
の内容をコピペする際にはインデントに注意してください
場合によっては、Pub get
できない場合があります。
name: sample_app #自分が作成したアプリの名前
description: A new Flutter project.
publish_to: 'none'
version: 1.0.0+1
environment:
sdk: '>=2.18.4 <3.0.0'
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^1.0.2
#この位置に追加する。
yumemi_weather:
git:
url: https://github.com/yumemi-inc/flutter-training.git
ref: main
path: packages/yumemi_weather
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^2.0.0
flutter:
uses-material-design: true
dependencies
とdev_dependencies
のどっちを入れたらいいのかなと思う方もいると思うのでわかりやすく表にします。この詳しい説明や使用例などは、別の記事で書こうと思います。
dependencies | dev_dependencies |
---|---|
自分の作っているプロジェクト(パッケージ)を使う人にとっても必要となるパッケージを設定する場合 | 自分のプロジェクト(パッケージ)の開発段階でのみ必要となる場合 |
どうでしょうか?綺麗になったと思います。そして、yumemi_weather
が追加されましたね。
これで、Pub get
を押してみてください。
これでAPIのPackageを使えるようになりました。やっていきましょう!!!
yumemi_weatherでできること
では、実際にyumemi_weather
は何ができるかを確認します。
ファイルを確認する
import 'dart:convert';
import 'dart:io';
import 'dart:math';
class Request {
Request({
required this.area,
required this.date,
});
final String area;
final DateTime date;
factory Request.fromJson(Map<String, dynamic> json) {
return Request(
area: json['area'] as String,
date: DateTime.parse(json['date'] as String),
);
}
}
class Response {
Response({
required this.weatherCondition,
required this.maxTemperature,
required this.minTemperature,
required this.date,
});
final WeatherCondition weatherCondition;
final int maxTemperature;
final int minTemperature;
final DateTime date;
Map<String, dynamic> toJson() {
final dateText =
'${date.year.padLeft(4)}-${date.month.padLeft(2)}-${date.day.padLeft(2)}';
final timeText =
'${date.hour.padLeft(2)}:${date.minute.padLeft(2)}:${date.second.padLeft(2)}';
final timeZoneOffset = date.timeZoneOffset;
final timeZoneSign = timeZoneOffset.isNegative ? '-' : '+';
final timeZoneText = '$timeZoneSign${timeZoneOffset.inHours.padLeft(2)}:00';
return {
'weather_condition': weatherCondition.name,
'max_temperature': maxTemperature,
'min_temperature': minTemperature,
'date': '${dateText}T$timeText$timeZoneText',
};
}
}
enum WeatherCondition {
sunny,
cloudy,
rainy,
}
enum YumemiWeatherError {
invalidParameter,
unknown,
}
extension RandomExt on Random {
int range(int min, int max) => min + nextInt(1 + max - min);
}
extension IntExt on int {
String padLeft(int width, [String padding = '0']) {
return toString().padLeft(width, padding);
}
}
const apiDuration = Duration(seconds: 2);
class YumemiWeather {
Response _makeRandomResponse({
WeatherCondition? weatherCondition,
int? maxTemperature,
int? minTemperature,
DateTime? date,
}) {
final seed = Random().range(0, 100);
final random = Random(seed);
final randomWeather = weatherCondition ??
WeatherCondition.values[random.nextInt(WeatherCondition.values.length)];
final randomMaxTemperature = maxTemperature ?? random.range(10, 40);
final randomMinTemperature =
minTemperature ?? random.range(-40, randomMaxTemperature);
final randomDate = date ?? DateTime.now();
return Response(
weatherCondition: randomWeather,
maxTemperature: randomMaxTemperature,
minTemperature: randomMinTemperature,
date: randomDate.toLocal(),
);
}
/// Pseudo Weather Forecast API simple ver
///
/// Randomly returns `sunny`, `cloudy`, or `rainy`. For example:
///
/// ```dart
/// final weatherCondition = YumemiWeather().fetchSimpleWeather();
/// print(weatherCondition); // "sunny"
/// ```
String fetchSimpleWeather() {
return _makeRandomResponse().weatherCondition.name;
}
/// Pseudo Weather Forecast API throws ver
///
/// Set [area] to the area for which you want to get a weather forecast.
/// Randomly throws `YumemiWeatherError.unknown` or returns `sunny`, `cloudy`,
/// or `rainy`. For example:
///
/// ```dart
/// final weatherCondition = YumemiWeather().fetchThrowsWeather('tokyo');
/// print(weatherCondition); // "sunny"
/// ```
String fetchThrowsWeather(String area) {
final randomInt = Random().range(0, 4);
if (randomInt == 4) {
throw YumemiWeatherError.unknown;
}
return _makeRandomResponse().weatherCondition.name;
}
/// Pseudo Weather Forecast API JSON ver
///
/// Set [jsonString] to the JSON string containing the area and datetime for
/// which you want to get a weather forecast.
/// If the parsing fails, throw `YumemiWeatherError.invalidParameter`.
/// Randomly throws `YumemiWeatherError.unknown` or returns the JSON string
/// with weather forecast information. For example:
///
/// ```dart
/// const jsonString = '''
/// {
/// "area": "tokyo",
/// "date": "2020-04-01T12:00:00+09:00"
/// }''';
/// final weatherJson = YumemiWeather().fetchWeather(jsonString);
/// print(weatherJson); // "{"weather_condition":"cloudy","max_temperature":25,"min_temperature":7,"date":"2020-04-01T12:00:00+09:00"}"
/// ```
String fetchWeather(String jsonString) {
final Request request;
try {
final json = jsonDecode(jsonString);
request = Request.fromJson(json);
} catch (_) {
throw YumemiWeatherError.invalidParameter;
}
final randomInt = Random().range(0, 4);
if (randomInt == 4) {
throw YumemiWeatherError.unknown;
}
final response = _makeRandomResponse(date: request.date);
return jsonEncode(response);
}
String syncFetchWeather(String jsonString) {
sleep(apiDuration);
return fetchWeather(jsonString);
}
}
確認できましたか?最初は??となるかもしれません。
一旦整理しましょう。
・使うことができるメソッド
・メソッドを使うと何が取得できるのか
・何が必要なのか
これらを探してみると理解が深まると思います。
僕なりにメソッドを探してみると4つ見つかりました。
・fetchSimpleWeather()
・fetchThrowsWeather(String area)
・fetchWeather(String jsonString)
・syncFetchWeather(String jsonString)
Issuesには「使用する APIは、fetchSimpleWeather()
」と指定されているので、fetchSimpleWeather()
を使っていこうと思います。
初めにやるにはとてもいいちょうど良い内容ですね。
fetchSimpleWeather()
をより詳しく捉える
/// Randomly returns `sunny`, `cloudy`, or `rainy`.
String fetchSimpleWeather() {
return _makeRandomResponse().weatherCondition.name;
}
return
以降の _makeRandomResponse()
とweatherCondition.name
に分けてみてみましょう。
Response _makeRandomResponse({
WeatherCondition? weatherCondition,
int? maxTemperature,
int? minTemperature,
DateTime? date,
}) {
final seed = Random().range(0, 100);
final random = Random(seed);
final randomWeather = weatherCondition ??
WeatherCondition.values[random.nextInt(WeatherCondition.values.length)];
final randomMaxTemperature = maxTemperature ?? random.range(10, 40);
final randomMinTemperature =
minTemperature ?? random.range(-40, randomMaxTemperature);
final randomDate = date ?? DateTime.now();
return Response(
weatherCondition: randomWeather,
maxTemperature: randomMaxTemperature,
minTemperature: randomMinTemperature,
date: randomDate.toLocal(),
);
}
enum WeatherCondition {
sunny,
cloudy,
rainy,
}
コードと書かれている説明文をみると
WeatherConditionの中身をランダムに取得ができる。ということです。
では実際に使ってみましょう。
使用例
新しく作成したファイルを用意します。(コメントアウトを削除したもの)
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.headline4,
),
//TODO ここに天気の種類を載せたい。
Text(
'天気の情報',
style: Theme.of(context).textTheme.headline4,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}
}
fetchSimpleWeather()
は、3種類の天気をランダムに取得するので、_counter
の下にTextを追加してみようと思います。
この使い方をREADME.md
みてみましょう
import 'package:yumemi_weather/yumemi_weather.dart';
void main() {
final yumemiWeather = YumemiWeather();
final weatherCondition = yumemiWeather.fetchSimpleWeather();
print('Weather Condition: $weatherCondition'); // "sunny"
}
これを作成したファイルに使ってみましょう。
main()
の中に書かれていますが慌てないで落ち着いて入れてみましょう。最初はどこに入れていいかわからないと思うので、下を参考に入れてみてください。
import 'package:flutter/material.dart';
import 'package:yumemi_weather/yumemi_weather.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
final yumemiWeather = YumemiWeather();
String _weather = 'アプリを開いた時に入れる値';
//今回は、カウントアップするメソッドの中に入れてみました。
void _incrementCounter() {
setState(() {
//TODO +ボタンを押すと天気がランダムに取得できる
_weather = yumemiWeather.fetchSimpleWeather();
_counter++;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.headline4,
),
Text(
//TODO '天気の情報', → _weather,に変更
_weather,
style: Theme.of(context).textTheme.headline4,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}
}
では、実際にビルドしてみてみましょう。
どうでしょうか?取得できましたね!!!意外とシンプルに実装できますね。作成者の方がとても丁寧に作ってくれたので簡単に実装ができました。
別の記事では、他の3つを取り上げてみようと思います。
最後に
とてもいい学習になるリポジトリだと思います。UIの学習だけでなく、APIの使い方、状態管理、テストまで幅広い範囲の学習に取り組めます。Flutterマスターの方が確認のために学習したり、Flutterに少し慣れた方がチャレンジに取り組んでみたりするといいと思います。
少し難易度は高めっぽいです。
参考文献
Githubのリポジトリ
yumemi_weatherのREADME.md
yumemi_weatherの使用例