[Flutter] APIの使い方に慣れよう（ゆめみの課題編）

![yumemi_logo_thum.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/672609/4f40c2f5-2128-a4b3-8d50-36e6364f11f9.png)

こんにちわ　いせきです。　今日も元気にコードを書いています。

<b>今回は、APIを使ってアプリを使ってみたいけど「複雑そうだな。。。」や「難しくて自分にはできないよ」と思う駆け出しエンジニアがいるのではないでしょうか。僕もそうでした！！</b>

 上記の方や復習した方に簡単にできると思えるような方法を共有したいと思います。

# はじめに
今回使用するAPIです。こちらは、ゆめみの研修課題で使用するAPIとなっています。
この課題の中のissuesの中にある「Session２」をやってみようと思います。

https://github.com/yumemi-inc/flutter-training-template/tree/main/packages/yumemi_weather

https://github.com/iseruuuuu/flutter_yumemi_training_app/issues/3

## やること
・Reload ボタンをタップして天気予報を取得する
・天気予報を画面に表示する


### 導入
`README.md`に書いてある通りに導入していこうと思います。

#### １、pubspec.yamlに記載する。

<details>
<summary>Flutterファイル構造を見てみる</summary>

```
 - .dart.tool
 - .idea
 - android
 - build
 - ios
 - lib
 - test
 - gitignore
 - .metadata
 - analysis_option.yaml
 - pubspec.lock
 - pubspec.yaml //このファイルです〜
 - README.md
 ...(ここまでとしておきます。)
```
</details>


`pubspec.yaml`のファイルを開いて、整理しつつ、`yumemi_weather`を導入してみましょう。

:::note warn
`pubspec.yaml`の内容をコピペする際にはインデントに注意してください
場合によっては、`Pub get`できない場合があります。
:::

```yaml
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`は何ができるかを確認します。

<details>
<summary>ファイルを確認する</summary>

```dart
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);
  }
}

```

</details>

確認できましたか？最初は？？となるかもしれません。

 一旦整理しましょう。

<b>・使うことができるメソッド</b>
<b>・メソッドを使うと何が取得できるのか</b>
<b>・何が必要なのか</b>

これらを探してみると理解が深まると思います。

僕なりにメソッドを探してみると４つ見つかりました。
・`fetchSimpleWeather()`
・`fetchThrowsWeather(String area)`
・`fetchWeather(String jsonString)`
・`syncFetchWeather(String jsonString)`


Issuesには<b>「使用する APIは、`fetchSimpleWeather()`」</b>と指定されているので、`fetchSimpleWeather()`を使っていこうと思います。
初めにやるにはとてもいいちょうど良い内容ですね。



## `fetchSimpleWeather()`をより詳しく捉える

```dart
  /// Randomly returns `sunny`, `cloudy`, or `rainy`.
  String fetchSimpleWeather() {
    return _makeRandomResponse().weatherCondition.name;
  }
```

`return`以降の `_makeRandomResponse()`と`weatherCondition.name`に分けてみてみましょう。

```dart 
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(),
    );
  }

```

```dart
enum WeatherCondition {
  sunny,
  cloudy,
  rainy,
}
```

コードと書かれている説明文をみると
<b>WeatherConditionの中身をランダムに取得ができる。</b>ということです。

では実際に使ってみましょう。



## 使用例

新しく作成したファイルを用意します。（コメントアウトを削除したもの）

```dart:main.dart
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()`は、３種類の天気をランダムに取得するので、`_counter`の下にTextを追加してみようと思います。

この使い方を`README.md`みてみましょう


```dart
import 'package:yumemi_weather/yumemi_weather.dart';

void main() {
  final yumemiWeather = YumemiWeather();
  final weatherCondition = yumemiWeather.fetchSimpleWeather();
  print('Weather Condition: $weatherCondition'); // "sunny"
}
```

これを作成したファイルに使ってみましょう。

:::note warn
`main()`の中に書かれていますが慌てないで落ち着いて入れてみましょう。最初はどこに入れていいかわからないと思うので、下を参考に入れてみてください。
:::



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


```

では、実際にビルドしてみてみましょう。



<img src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/672609/44f51aea-2704-9253-b428-66224b0c865d.gif  width=320>


どうでしょうか？取得できましたね！！！意外とシンプルに実装できますね。作成者の方がとても丁寧に作ってくれたので簡単に実装ができました。


別の記事では、他の３つを取り上げてみようと思います。


# 最後に
とてもいい学習になるリポジトリだと思います。UIの学習だけでなく、APIの使い方、状態管理、テストまで幅広い範囲の学習に取り組めます。Flutterマスターの方が確認のために学習したり、Flutterに少し慣れた方がチャレンジに取り組んでみたりするといいと思います。
少し難易度は高めっぽいです。


# 参考文献
<b>Githubのリポジトリ</b>

https://github.com/yumemi-inc/flutter-training-template

<b>yumemi_weatherのREADME.md</b>

https://github.com/yumemi-inc/flutter-training-template/tree/main/packages/yumemi_weather

<b>yumemi_weatherの使用例</b>

https://github.com/yumemi-inc/flutter-training-template/blob/main/packages/yumemi_weather/example/yumemi_weather_example.dart