はじめに
ほぼ皆勤賞で NRI Hackathon というイベントに10年前から参加し続けてきました。毎年楽しく参加させてもらい、そして毎回新しい学びがあります。
NRIハッカソンとは?
みんなで楽しむアプリ開発イベントです!
ハッカソンとは、決められた期間内にアプリ開発をするイベントです。NRIハッカソンは、NRIの有志メンバー「Arumon」によって企画しており、 毎年「楽しく課題解決する」ことを目標にかかげています。
出典)https://bitconnect.nri.co.jp/2023/
今年のテーマは「旅 -Trip-」日本の玄関口・羽田空港のすぐ近くにある羽田イノベーションシティでハッカソンをやってきました。飛行機が離陸するところを見ながら足湯に入れるおもしろい施設でした。当日の様子はちょまどさんが投稿していました。
10年も参加していると運営メンバーとも仲良くなります。日本のハッカソンでは Yahoo! HACKDAY に次いで歴史が長くなってきたと話していて運営もしていないのに感慨深くなります。第1回の記事を見つけたのでせっかくだから貼っておきます。
ハッカソンに参加する時には「新しいことに挑戦する」ことを意識しています。新しい技術に触れることは楽しく、学びがあります。これからも続けていきたいです。
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 でプログラミング言語の歴史という動画を見ました。技術は常に進化し続けていくので、来年、再来年と使っていく技術は変わっていくのだろうなと思います。
さて、本題に戻り本記事では NRI Hackathon 2023 Hack for Trip で挑戦した Flutter / Dart を使って得た学びを整理していきたいと思います。
今年のハッカソンで作ったものは「せんべろタクシー」というアプリです。出張の最終日は早めに空港についても時間が余りがちですよね。チェックインと手荷物を預けたら羽田空港から大田区蒲田の居酒屋にお連れして、飛行機の時間に間に合うように戻ってこれるタクシー配車アプリです。ChatGPT の OpenAI を使ってフライトチケットの情報を読み込ませると、フライトまでに帰ってこれる蒲田のせんべろプランを提案してくれます。余談ですが ChatGPT を使うと多言語対応がここまで簡単なのかと驚きました。
このアプリを Flutter で作ったので解説します。せっかくなので Flutter Advent Calendar 2023 にも参加させて頂きます。
Flutter
Flutter は Google が提供しているオープンソースのマルチプラットフォームの開発フレームワークです。2022年以降の Flutter3 では Android, iOS のモバイルアプリ、Web サイト、Windows, Mac, Linux のデスクトップアプリを開発することが出来ます。Dart というプログラミング言語を用います。
Flutter を一から学ぶ上で参考にさせて頂いたのは以下の本です。大変勉強になりました。
開発環境構築
Windows で環境構築をして Android アプリを作りました。詳細な手順はこちらのページの通り進めてください。
諸々インストールしていき、Android アプリの開発を進めていくと 10GB を超える容量が必要になりました。ネイティブアプリを作るのは久しぶりなので PC のスペックが大事なことを忘れていました。もう少し工夫の余地があったのかもしれませんがハッカソンの3日間では一旦諦めて PC に頑張ってもらいました。
Dart
Dart は JavaScript に似たプログラミング言語です。メジャーな言語に慣れている人であれば違和感なく書けると思います。
Dart の公式解説ページやFlutter 実践入門 > Dart の概要ページをご一読頂ければと思います。
void main() {
print('Hello, World!');
}
アプリの雛形作成
基本的にはこちらのページのチュートリアル通りに進めていくことでアプリの雛形が出来上がります。
Flutter プロジェクトを作成 → Android Studio シミュレーター起動 → Flutter アプリを起動という流れです。
$ flutter create myapp
$ flutter run -d [デバイスID]
以下の Flutter 公式の YouTube の解説動画を見ていただくのがイメージ付きやすいかと思います。
ディレクトリ構成
lib ディレクトリにアプリケーションのソースコードを記述していきます。main.dart
が入り口になります。app.dart
はアプリのベースとなるヘッダーやボディ、テーマなどを定義します。その下に個別のページを実装します。
main.dart
└ app.dart
└ pages/chat.dart
└ pages/qrpay.dart
ハッカソンで開発した Flutter コードから一部抜粋します。
import 'package:flutter/material.dart';
import 'app.dart';
void main() {
runApp(const MyApp());
}
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 と会話するアプリにしました。
出典)https://pub.dev/packages/flutter_chat_ui
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 に対してチャット形式で問いかけて返事を受けとるサンプルです。
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 でお会いしましょう。
それでは良いお年を。