【爆速】Flutterで日経平均株価のチャートを表示するアプリケーションを作る

Flutterの技術検証用に簡単なアプリケーションを実装。
個人的には
- HTTP通信
- 非同期

当たりを理解することができれば、ある程度の開発が行えると判断し、「日経平均株価のチャートを表示する」アプリを開発。
※ React NativeでS&P500, Xamarinでダウ平均のチャートを表示するアプリケーションを開発する予定

# 成果物

https://github.com/yamachita0109/flutter_nikkei_heikin

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/300430/0f6f7f53-bc7c-cc00-3ca7-37959a282390.png" width="200">

# 初めに

## Flutterとは

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/300430/c4ceb1be-ef75-d0ee-b473-d72f0dcad501.png)
Flutterとは、GoogleによってDartで開発されたフリーかつオープンソースのUIのSDK。
単一のコードから、Android、iOS、Linux、macOS、Windows、Google Fuchsia向けのクロスプラットフォームアプリケーションを開発することができる。

## Dartとは

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/300430/40202a46-ebab-8456-8236-1f6088b674f1.png)
Dartとは、Webアプリやモバイルアプリクライアント開発向けに設計されたプログラミング言語で、Googleによって開発された。
オブジェクト指向、クラスベース、ガベージコレクション備えたCベースの言語で、ネイティブコードまたは、JavaScriptにコンパイルすることができる。
構文はJavaに近い。

# 環境構築

## Flutter

```shell
$ mkdir ~/local
$ cd ~/local
# @see: https://docs.flutter.dev/get-started/install/macos
$ curl -OL https://storage.googleapis.com/flutter_infra_release/releases/stable/macos/flutter_macos_2.8.1-stable.zip
$ unzip flutter_macos_2.8.1-stable.zip
$ echo ‘export PATH=“$PATH:$HOME/local/flutter/bin”’ >> ~/.bash_profile
$ source  ~/.bash_profile
$ flutter --version
```

## Android Studio

### Install

公式サイトからInstall.
https://developer.android.com/studio/index.html

### Plugin Install

Android Studio上で
`preference > Plugins > Flutter Install > Android Studio Restart`
と実行。

### Create Flutter App

1. Start a new Flutter projectを選択
2. Flutter Applicationを選択してNextをクリック
3. Project nameは任意の名前で入力しNextをクリック
4. Company domainは任意の名前で入力しFinishをクリック

### Device

適当にInstall.

### Android License

Android Studio上で
`Android SDK > SDK Tools > Android SDK Command-line Tools(latest) にチェック > Apply`
と実行し、
```bash
$ flutter doctor --android-licenses
```

## Xcode

### Install
Mac App StoreからInstall.
https://apps.apple.com/jp/app/xcode/id497799835?mt=12

### CocoaPods

```bash
$ ruby --version
$ sudo gem install -v1.8.4 cocoapods -n /usr/local/bin
```

# Coding

## Tree

```bash
$ tree -L 1
.
├── README.md
├── analysis_options.yaml
├── android // Android用のソース
├── build
├── ios // iOS用のソース
├── lib // クロスプラットフォーム（Dart）←今回はここ
├── nikkei_heikin.iml
├── pubspec.lock
├── pubspec.yaml
├── sample.png
└── test
```

## pubspec.yaml

pubspec.yamlの依存関係にライブラリを追記。
※ pubspec.yamlは環境変数、依存関係等を記載するファイル。
<a href="https://dart.dev/tools/pub/pubspec">公式ドキュメント</a>

```diff_yaml:pubspec.yaml
dependencies:
  flutter:
    sdk: flutter
+ charts_flutter: ^0.12.0 # チャートを描画するのに必要なライブラリ
+ charts_common: ^0.12.0 # チャートを描画するのに必要なライブラリ
+ flutter_sparkline: ^0.1.0 # チャートを描画するのに必要なライブラリ
+ http: ^0.13.4 # HTTPクライアント
```

## main.dart

```main.dart
import 'package:flutter/material.dart';

import 'chart.dart';

// アプリ起動時に、実行
void main() {
  runApp(const MainApp());
}

class MainApp extends StatelessWidget {
  const MainApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
        debugShowCheckedModeBanner: false,
        home: Chart());
  }
}
```

https://github.com/yamachita0109/flutter_nikkei_heikin/blob/Ph1/lib/main.dart

## chart.dart

https://github.com/yamachita0109/flutter_nikkei_heikin/blob/Ph1/lib/chart.dart

各Class単位に分解。

### Chart.class

```dart
class Chart extends StatefulWidget {
  const Chart({Key? key}) : super(key: key);

  @override
  _ChartState createState() => _ChartState();
}
```
StatefulWidget継承。
Flutterには`StatelessWidget`と`StatefulWidget`の二種類、Widget Classが存在しており、それぞれ下記のような特徴がある。

|Widget|特徴|
|:-----------|:------------|
|StatelessWidget|Viewの状態を保持しないWidget。<br>インスタンス化時にWidgetが決定するため変数等定義しても値が更新されることがない。<br>所謂、**静的なWidget**|
|StatefulWidget|状態を保持しているWidget。<br>所謂、**動的なWidget**<br>StatelessWidgetとは異なり、状態を管理するState Classを実装する必要がある。<br>ライフサイクルについては<a href="https://flutterbyexample.com/lesson/stateful-widget-lifecycle">こちら</a>。|

### _ChartState.class

```dart
class _ChartState extends State<Chart> {
  late List<dynamic>? response = null;

  getData() async {
    final parameters = {
      'api_key': 'xxxxxxxxxx',
      'start_date': '2021-01-01',
      'end_date': '2021-03-31',
    };
    final url = Uri.https('www.quandl.com', '/api/v3/datasets/CHRIS/CME_NK2/data.json', parameters);
    final result = await http.get(url);
    setState(() {
      response = json.decode(result.body)['dataset_data']['data'];
    });
  }

  @override
  void initState() {
    super.initState();
    getData();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('日経平均'),
        backgroundColor: Colors.orange
      ),
      body: Container(
        padding: const EdgeInsets.all(20),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text("2021-01-01 ~ 2021-03-31 日足"),
            Expanded(flex: 1,
                child: Card(
                    child: Container(
                        padding: const EdgeInsets.all(10),
                        child: response == null ? null : createChart(response!), // const CircularProgressIndicator()
                    )
                )
            ),
          ],
        ),
      ),
    );
  }

  Widget createChart(List<dynamic> response) {
    List<TimeSeriesSales> data = [];
    for (var element in response) {
      List date = element[0].split('-');
      final settle = element[6];
      data.add(TimeSeriesSales(DateTime(int.parse(date[0]), int.parse(date[1]), int.parse(date[2])), settle));
    }

    List<Series<TimeSeriesSales, DateTime>> seriesList = [
      charts.Series<TimeSeriesSales, DateTime>(
        id: 'Sales',
        colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault,
        domainFn: (TimeSeriesSales sales, _) => sales.time,
        measureFn: (TimeSeriesSales sales, _) => sales.sales,
        data: data,
      )
    ];

    return charts.TimeSeriesChart(
      seriesList,
      animate: false,
      dateTimeFactory: const charts.LocalDateTimeFactory(),
    );
  }
}
```

ステップ数が多いので、関数単位に分解。


#### build

```dart
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('日経平均'),
        backgroundColor: Colors.orange
      ),
      body: Container(
        padding: const EdgeInsets.all(20),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text("2021-01-01 ~ 2021-03-31 日足"),
            Expanded(flex: 1,
                child: Card(
                    child: Container(
                        padding: const EdgeInsets.all(10),
                        child: response == null ? null : createChart(response!), // const CircularProgressIndicator()
                    )
                )
            ),
          ],
        ),
      ),
    );
  }
```
描画するWidgetを実装。
レイアウトなどはここに実装。

#### initState

```dart
  @override
  void initState() {
    super.initState();
    getData();
  }
```

StateClassのイニシャライズ時に実行。
ここでAPIを実行。

#### getData

```dart
  getData() async {
    final parameters = {
      'api_key': 'xxxxxxxxxx',
      'start_date': '2021-01-01',
      'end_date': '2021-03-31',
    };
    final url = Uri.https('www.quandl.com', '/api/v3/datasets/CHRIS/CME_NK2/data.json', parameters);
    final result = await http.get(url);
    setState(() {
      response = json.decode(result.body)['dataset_data']['data'];
    });
  }
```

APIを実行して、日経平均株価の情報を取得。
APIは<a hred="https://data.nasdaq.com/">Naddaq Data Link</a>のAPIを使用。
登録を行い、API Keyを発行する必要がある。
API実行後、必要なデータを取得し、setStateを実行。
setStateを実行することで、再度Widgetがbuildされる。

#### createChart

```dart
  Widget createChart(List<dynamic> response) {
    List<TimeSeriesSales> data = [];
    for (var element in response) {
      List date = element[0].split('-');
      final settle = element[6];
      data.add(TimeSeriesSales(DateTime(int.parse(date[0]), int.parse(date[1]), int.parse(date[2])), settle));
    }

    List<Series<TimeSeriesSales, DateTime>> seriesList = [
      charts.Series<TimeSeriesSales, DateTime>(
        id: 'Sales',
        colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault,
        domainFn: (TimeSeriesSales sales, _) => sales.time,
        measureFn: (TimeSeriesSales sales, _) => sales.sales,
        data: data,
      )
    ];

    return charts.TimeSeriesChart(
      seriesList,
      animate: false,
      dateTimeFactory: const charts.LocalDateTimeFactory(),
    );
  }
```

APIのレスポンスをChart描画ように整形。
本当にJavaっぽい。

# まとめ

今回、簡易アプリケーションを実装してみたが、**思ったより悪くない**
その心は、Android Studioを使用すればそこまでストレスなく開発することもでき、AndroidJavaの経験がある人であればほぼキャッチアップ不要で開発が行える。
しかし、`lib`配下の階層が自由度が高いことが若干マイナス。（デフォルトだと空。）
PJや人に依存してしまうため、しっかりと設計方針を決めないと負債となりかねない。
https://blog.dalt.me/2941
もし、私がスクラッチで実装するのであればMVVM方式を使用するかな・・
```bash
./lib
├── use_cases
├── utils
├── models
├── views
└── view_models
```
次は、ディレクトリ構成やCI/CD、OSのハンドリングの検証を行なっていきたい。