はじめに
本田技研工業にてデジタルサービスを企画するGrに所属している安原と申します
今回はTechブログ第三弾の投稿になります
TL;DR
- 部門内でハッカソンが開催されたので参加してきました
- ハッカソンの内容を紹介
- 自チームの企画した内容について紹介
目次
ハッカソン概要
1日目(アイデア考案)
2日目/3日目(アプリ開発)
まとめ
1. ハッカソン概要
この度、部門内にてハッカソンが開催されました
私はサービスを企画する立場ですが、自分で簡単なプロトタイプくらいは
作れるようになりたいなと思っていたので良い機会だと思い参加してきました
ちなみに以下のような内容でした
概要
2泊3日で「アイデアの生み出し」「Flutterを用いたプロトタイピング」を体験・実施する
目的
普段接点の少ない立場の異なる人たちとチームを組み、協力しながら以下の実現を図る
・新しいサービスや機能に関するアイデア、ヒントの発掘
・普段の職場では得られない刺激の体験
・コミュニケーション促進
タイムスケジュール
1日目
13:30~14:00:オリエンテーション、チーム分け、アイスブレイク
14:00~17:30:アイデア考案
17:30~18:15:アイデア共有
2日目
9:00~18:00:プロトタイプ制作
3日目
9:00~11:30:プロトタイプ制作
12:30~14:30:最終発表、表彰
2. アイデア考案
ハッカソンの会場であるホテルに到着
(のどかでいい所でしたが参加者の一人がのんびりして電車に乗り遅れるハプニングも…)
そして今回のテーマ発表!!
テーマは...
ストップウォッチ、タイマー機能を利用するコネクテッド/MaaS関連アプリの考案
2泊3日(しかも残業禁止!)という厳しい時間制限で
アイデアを出し、Flutterにてプロトタイプを作らなければなりません!
1日目はどんなアプリを作るかみんなで企画を考えることに
皆さん思い思いの方法でアイデアを出し、議論します
我々のチームは何のモビリティを対象にするか、
利用シーンはどうするかなど決め、気分転換も兼ねて外へ出て考えることに
駐車場に行ってみると、、おや?これは?
EV用の充電設備しかも普通充電と急速充電の両方を発見!
さらに観察すると急速充電の方にだけ制限時間があることを見つけました
(これは使えそう…)
その後ホテルのフロントにて話を聞いたり、遊歩道を散歩しながら皆で考えを練ったりして会場へ戻ってきました
我々チームの考えた企画は以下のようになりました
(ここだけの話アプリ名が中々思いつかなかったので助っ人としてChat gpt君を迎えて助言を頂きました…
アプリ名だけなら使ってもいいよね??)
その後、チーム間で考えた企画を発表!
皆さんユニークな企画を考えており、これは明日からの実装が楽しみ
3. アプリ開発
今日からFlutterを用いたアプリ開発が始まります
制限時間は3日目の朝10時!!
しかも残業禁止なので実質9時間で形にしなくてはいけません
皆さん不慣れなFlutterに悪戦苦闘
我々のチームも場所を変えながら開発に臨みました
ちなみに我々のチームはQCquestを形にするため以下のような実装を行いました
画面
それぞれStartView, QuestView, ResultViewとして定義しました
本来はEVの充電開始時に自動的にQuestViewに遷移する予定ですが、
今回はプロトタイプなのでStartViewにてボタンを押すことでQuestViewへ遷移します
実装コード
主な実装内容は以下となります
- 位置情報
充電開始位置に戻ってこないと回答ボタンが押せないようにするため
geolocatorプラグインを用いて「QuestView遷移ボタン」が押されたときに
その場所の位置情報(開始位置)をgetCurrentPosition()
にて保存し、
QuestView遷移後はgetPositionStream()
にて常にユーザーの現在位置を習得し、
現在地点と開始地点の距離によって回答ボタンの活性化を制御しています
Future<Position> currentPosition() async {
return await Geolocator.getCurrentPosition();
}
Stream<Position> streamPosition() {
return Geolocator.getPositionStream();
}
locationStream = geoState.streamPosition().listen((event) {
distance = Geolocator.distanceBetween(
location.latitude, location.longitude, event.latitude, event.longitude);
});
ElevatedButton(
onPressed: distance < 5.0
? () => [
{
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ResultView(),
))
},
] : null,
child: Text('回答!!'),
)
- カルーセルスライダー/入力欄の保存
QuestViewではcarousel_sliderを用いることで分かりやすいUIとなるようにしています
また画像の変更をonPageChanged
にて検知し、画像のindex
を_current
に格納しておくことで
画像とヒント文章、入力欄などが1対1にて対応するようになっています
また入力欄はTextEditingController
にて入力値を常に取得するようにしておきます
(こうしないと折角入力したキーワードが画像の切り替えと共に消えてしまいます)
class QuestSection extends StatefulWidget {
@override
State<QuestSection> createState() => _QuestSectionState();
}
class _QuestSectionState extends State<QuestSection> {
int _current = 0;
late final TextEditingController texteditinng;
@override
initState() {
super.initState();
texteditinng = TextEditingController();
}
@override
dispose() {
super.dispose();
texteditinng.dispose();
}
List<String> _hints = [
'散策路を通って水源まで行ってみよう!',
'教会を探してみよう!',
'ホテルの屋上に行ってみよう!',
];
List<Widget> _images = [
Image.asset('images/quest/promenado.JPG'),
Image.asset('images/quest/church.JPG'),
Image.asset('images/quest/observatory.JPG'),
];
@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
Text(_hints[_current]),
CarouselSlider(
items: _images,
options: CarouselOptions(
initialPage: 0,
height: 200.0,
enableInfiniteScroll: false,
onPageChanged: (index, reason) {
setState(() {
texteditinng.text = inputs[index];
_current = index;
});
})),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: _images.map((imageUrl) {
int index = _images.indexOf(imageUrl);
return Container(
width: 8.0,
height: 8.0,
margin: EdgeInsets.symmetric(vertical: 10.0, horizontal: 4.0),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: _current == index ? Colors.black : Colors.grey,
),
);
}).toList(),
),
TextFormField(
controller: texteditinng,
decoration: InputDecoration(
hintText: 'キーワードを入力',
border: OutlineInputBorder(),
),
onChanged: (value) {
setState(() {
inputs[_current] = value;
});
},
),
],
);
}
}
- タイマー機能
QuestView遷移と共にstartClock
を呼びます
また時間切れの場合にはResultViewへ遷移します
class TimerState extends ChangeNotifier {
final Stopwatch _stopWatchTimer = Stopwatch();
final totalTime = const Duration(minutes: 30);
void startClock(BuildContext buildContext) {
_stopWatchTimer.start();
Timer.periodic(const Duration(milliseconds: 30), (timer) {
int seconds = remainingInSeconds();
if (seconds <= 0) {
Navigator.push(buildContext,
MaterialPageRoute(builder: (context) => ResultView()));
stopClock();
timer.cancel();
}
notifyListeners();
});
}
void stopClock() {
_stopWatchTimer.stop();
}
int remainingInSeconds() {
Duration elapsed = _stopWatchTimer.elapsed;
Duration remaining = totalTime - elapsed;
return remaining.inSeconds;
}
DurationForDisplay getRemainingForDisplay() {
Duration elapsed = _stopWatchTimer.elapsed;
Duration remaining = totalTime - elapsed;
int minutes = remaining.inMinutes;
int seconds = remaining.inSeconds - minutes * 60;
return DurationForDisplay(minute: minutes, second: seconds);
}
}
class DurationForDisplay {
final int minute;
final int second;
DurationForDisplay({
required this.minute,
required this.second
});
}
そしてあっという間に時間は過ぎ、最終発表!!
企画説明とドキドキのデモ発表を行います
結果は…
見事1位を取りました!やった!
4. まとめ
普段の業務ではあまり接点のないメンバーかつ解放的な場所でのハッカソンはいい意味で刺激になり、
短い期間の中、コンセプト設定〜アプリの開発まで行い手と頭をかなり働かせました(笑)
改めてチームで何かを形にする楽しさを感じられたすごく満足度の高いイベントでした!