NRI Hackathon に10年前から参加し続けて学んだこと。2023年は Flutter でした。

# はじめに

ほぼ皆勤賞で [NRI Hackathon というイベント](https://bitconnect.nri.co.jp/)に10年前から参加し続けてきました。毎年楽しく参加させてもらい、そして毎回新しい学びがあります。

> NRIハッカソンとは？
みんなで楽しむアプリ開発イベントです！
ハッカソンとは、決められた期間内にアプリ開発をするイベントです。NRIハッカソンは、NRIの有志メンバー「Arumon」によって企画しており、 毎年「楽しく課題解決する」ことを目標にかかげています。
出典）https://bitconnect.nri.co.jp/2023/

今年のテーマは「旅 -Trip-」日本の玄関口・羽田空港のすぐ近くにある羽田イノベーションシティでハッカソンをやってきました。飛行機が離陸するところを見ながら足湯に入れるおもしろい施設でした。[当日の様子はちょまどさんが投稿](https://twitter.com/chomado/status/1721111261126238633)していました。


10年も参加していると運営メンバーとも仲良くなります。日本のハッカソンでは [Yahoo! HACKDAY](https://hackday.yahoo.co.jp/) に次いで歴史が長くなってきたと話していて運営もしていないのに感慨深くなります。第1回の記事を見つけたのでせっかくだから貼っておきます。

https://xtech.nikkei.com/it/article/NEWS/20140313/543442/

ハッカソンに参加する時には「新しいことに挑戦する」ことを意識しています。新しい技術に触れることは楽しく、学びがあります。これからも続けていきたいです。

2014年 : CakePHP MySQL Apache Linux
2015年 : Node.js Express MongoDB
2016年 : Swift MongoDB
2017年 : Angular Firebase
2019年 : React Amplify 
2021年 : ノーコード
2022年 : React CDK
2023年 : Flutter Dart ChatGPT


話は完全に逸れますが、先日 YouTube でプログラミング言語の歴史という動画を見ました。技術は常に進化し続けていくので、来年、再来年と使っていく技術は変わっていくのだろうなと思います。

<iframe width="560" height="315" src="https://www.youtube.com/embed/jBksc8SdUF8?si=BfJLawJiCu432y5J" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>


さて、本題に戻り本記事では NRI Hackathon 2023 Hack for Trip で挑戦した Flutter / Dart を使って得た学びを整理していきたいと思います。
今年のハッカソンで作ったものは「せんべろタクシー」というアプリです。出張の最終日は早めに空港についても時間が余りがちですよね。チェックインと手荷物を預けたら羽田空港から大田区蒲田の居酒屋にお連れして、飛行機の時間に間に合うように戻ってこれるタクシー配車アプリです。ChatGPT の OpenAI を使ってフライトチケットの情報を読み込ませると、フライトまでに帰ってこれる蒲田のせんべろプランを提案してくれます。余談ですが ChatGPT を使うと多言語対応がここまで簡単なのかと驚きました。

フライトチケットを読み込むと
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/127260/5c4ae305-c873-e461-c43c-d9bedc2ae18f.png)

ChatGPT がフライト情報を理解し
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/127260/1c419345-a0ff-bb81-baaa-54583dc14220.png)

せんべろプランを提案してくれる
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/127260/df0578e8-b6e2-158e-d20c-a4d70eba56ce.png)


このアプリを Flutter で作ったので解説します。せっかくなので Flutter Advent Calendar 2023 にも参加させて頂きます。

https://qiita.com/advent-calendar/2023/flutter



# Flutter

[Flutter は Google が提供しているオープンソースのマルチプラットフォームの開発フレームワーク](https://flutter.dev/)です。2022年以降の Flutter3 では Android, iOS のモバイルアプリ、Web サイト、Windows, Mac, Linux のデスクトップアプリを開発することが出来ます。Dart というプログラミング言語を用います。

Flutter を一から学ぶ上で参考にさせて頂いたのは以下の本です。大変勉強になりました。

https://zenn.dev/heyhey1028/books/flutter-basics


## 開発環境構築

Windows で環境構築をして Android アプリを作りました。詳細な手順は[こちらのページ](https://zenn.dev/heyhey1028/books/flutter-basics/viewer/getting_started_windows)の通り進めてください。

諸々インストールしていき、Android アプリの開発を進めていくと 10GB を超える容量が必要になりました。ネイティブアプリを作るのは久しぶりなので PC のスペックが大事なことを忘れていました。もう少し工夫の余地があったのかもしれませんがハッカソンの３日間では一旦諦めて PC に頑張ってもらいました。


## Dart

Dart は JavaScript に似たプログラミング言語です。メジャーな言語に慣れている人であれば違和感なく書けると思います。

[Dart の公式解説ページ](https://dart.dev/language)や[Flutter 実践入門 > Dart の概要ページ](https://zenn.dev/kazutxt/books/flutter_practice_introduction/viewer/05_chapter1_dart)をご一読頂ければと思います。


https://dart.dev/

```hello.dart
void main() {
  print('Hello, World!');
}
```


## アプリの雛形作成

基本的には[こちらのページ](https://zenn.dev/heyhey1028/books/flutter-basics/viewer/flutter_intro)のチュートリアル通りに進めていくことでアプリの雛形が出来上がります。

Flutter プロジェクトを作成　→　Android Studio シミュレーター起動　→　Flutter アプリを起動という流れです。

```
$ flutter create myapp
$ flutter run -d [デバイスID]
```

以下の Flutter 公式の YouTube の解説動画を見ていただくのがイメージ付きやすいかと思います。

https://youtu.be/xWV71C2kp38?si=0xB8PqheoCLreupe&t=397



## ディレクトリ構成

lib ディレクトリにアプリケーションのソースコードを記述していきます。`main.dart` が入り口になります。`app.dart` はアプリのベースとなるヘッダーやボディ、テーマなどを定義します。その下に個別のページを実装します。

main.dart
　└ app.dart
　　　└ pages/chat.dart
　　　└ pages/qrpay.dart


ハッカソンで開発した Flutter コードから一部抜粋します。

https://github.com/t-kurasawa/senbero



![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/127260/c32fb21d-8526-3929-e54d-6b839ba4f39f.png)


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

import 'app.dart';

void main() {
  runApp(const MyApp());
}

```


```app.dart
import 'package:flutter/material.dart';
import 'pages/chat.dart';
import 'pages/qrpay.dart';

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

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Senbero',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const MyStatefulWidget(),
    );
  }
}

class MyStatefulWidget extends StatefulWidget {
  const MyStatefulWidget({Key? key}) : super(key: key);

  @override
  State<MyStatefulWidget> createState() => _MyStatefulWidgetState();
}

class _MyStatefulWidgetState extends State<MyStatefulWidget> {

  static const _pages = [
    QRPayPage(),
    ChatPage(),
  ];

  int _selectedIndex = 0;

  void _onItemTapped(int index) {
    setState(() {
      _selectedIndex = index;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: _pages[_selectedIndex],
      bottomNavigationBar: BottomNavigationBar(
        currentIndex: _selectedIndex,
        onTap: _onItemTapped,
        items: const <BottomNavigationBarItem>[
          BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Home'),
          BottomNavigationBarItem(icon: Icon(Icons.chat), label: 'Chat'),
        ],
        type: BottomNavigationBarType.fixed,
      )
    );
  }
}
```


## 画面遷移

画面遷移はスマホアプリの開発と同じくページを積み重ねていく作り方になります。詳しい説明は以下のページが大変わかりやすいので割愛します。

> Flutter では Navigatorというクラスに対し、Routeクラスでラップしたページを渡す事でページの遷移を行います。Navigatorクラスは全てのページを入れる容器のようなものとイメージしてください。ページを遷移する行為は、その容器にページをどんどん積み上げていく(スタックする)イメージです。
出典）https://zenn.dev/heyhey1028/books/flutter-basics/viewer/navigation



## 画面作成

画面はコンポーネントを組み合わせて作っていきます。ヘッダーのコンポーネント、画像やテキストを表示するコンポーネント、ボタンのコンポーネントなどを配置していきます。
今回は `Flutter Chat UI` という UI コンポーネントライブラリを用いて ChatGPT の OpenAI と会話するアプリにしました。

![flutter_chat_ui.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/127260/a1c145f4-37cb-4f11-1b0c-b93df19e036d.jpeg)
出典）https://pub.dev/packages/flutter_chat_ui


```chat.dart
import 'package:flutter_chat_types/flutter_chat_types.dart' as types;
import 'package:flutter_chat_ui/flutter_chat_ui.dart';

class ChatPage extends StatefulWidget {
  const ChatPage({Key? key}) : super(key: key);

  @override
  State<ChatPage> createState() => _ChatPageState();
}

class _ChatPageState extends State<ChatPage> {
  List<types.Message> _messages = [];
  final _user = const types.User(id: '82091008-a484-4a89-ae75-a22bf8d6f3ac');
  final _chatgptuser = const types.User(id: 'chatgpt4');

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

  void _addMessage(types.Message message) {
    setState(() {
      _messages.insert(0, message);
    });
  }

  (略) 

  void _loadMessages() async {
    const response = [
      {
        "author": {
          "id": "chatgpt4"
        },
        "createdAt": 1677649421032,
        "id": "message_id_2",
        "text": "Ask ChatGPT a question"
      },
    ];
    final messages = (response as List)
        .map((e) => types.TextMessage.fromJson(e as Map<String, dynamic>))
        .toList();

    setState(() {
      _messages = messages;
    });
  }  

  (略)

  @override
  Widget build(BuildContext context) => Scaffold(
    body: Chat(
      user: _user,
      messages: _messages,
      onMessageTap: _handleMessageTap,
      onAttachmentPressed: _handleAttachmentPressed,
      onPreviewDataFetched: _handlePreviewDataFetched,
      onSendPressed: _handleSendPressed,
    ),
  ); 
```


## API 通信

アプリから API を呼び出す際には http パッケージを用いました。
以下は ChatGPT の OpenAI API に対してチャット形式で問いかけて返事を受けとるサンプルです。


```chat.dart
Future fetchChat(question) async {
  final response = await http.post(
    Uri.parse('https://xxxxxx.execute-api.ap-northeast-1.amazonaws.com/prod/chat'),
    headers: <String, String>{
      'Content-Type': 'application/json; charset=UTF-8',
    },
    body: jsonEncode(<String, String>{
      "prompt": question,
      "model": "gpt-4"
    }),
  );

  if (response.statusCode == 200) {
    // If the server did return a 200 OK response,
    // then parse the JSON.
    String jsonString = response.body; // ここに提供されたJSONデータを入力してください

    // JSONデータをデコード
    Map<String, dynamic> decodedData = jsonDecode(jsonString);

    // 必要なデータを取得
    Map<String, dynamic> messageData = decodedData['message'];
    Map<String, dynamic> innerMessageData = messageData['message'];
    String content = innerMessageData['content'];
    final decodedContent = utf8.decode(content.runes.toList());

    return decodedContent;
  } else {
    // If the server did not return a 200 OK response,
    // then throw an exception.
    throw Exception('Failed to API');
  }
}
```


# おわりに
以上で 2023 年の NRI Hackathon で挑戦した Flutter の振り返りを終わります。

ハッカソンに参加することは機会を創り出すことだなと思います。
新しい技術に触れることは楽しく、学びがあります。これからも続けていきたいです。

ハッカソンに興味がある方は、ぜひ来年の NRI Hackathon でお会いしましょう。
それでは良いお年を。