Hondaハッカソン体験記

# はじめに
本田技研工業にてデジタルサービスを企画するGrに所属している安原と申します
今回はTechブログ第三弾の投稿になります


# TL;DR
- 部門内でハッカソンが開催されたので参加してきました
- ハッカソンの内容を紹介
- 自チームの企画した内容について紹介


# 目次
[ハッカソン概要](#1-ハッカソン概要)
[1日目(アイデア考案)](#2-アイデア考案)
[2日目/3日目(アプリ開発)](#3-アプリ開発)
[まとめ](#4-まとめ)


# 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. アイデア考案
ハッカソンの会場であるホテルに到着
(のどかでいい所でしたが参加者の一人がのんびりして電車に乗り遅れるハプニングも…)
![P3081591.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3295567/b82ef80e-e8c9-4fb6-d2bb-45dbe0068e5e.jpeg)
そして今回のテーマ発表！！
テーマは...
***ストップウォッチ、タイマー機能を利用するコネクテッド/MaaS関連アプリの考案***
2泊3日(しかも残業禁止！)という厳しい時間制限で
アイデアを出し、Flutterにてプロトタイプを作らなければなりません！

1日目はどんなアプリを作るかみんなで企画を考えることに
皆さん思い思いの方法でアイデアを出し、議論します
![1-3.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3295567/08bf4797-663c-8879-9c19-5efc85bb4216.jpeg)
我々のチームは何のモビリティを対象にするか、
利用シーンはどうするかなど決め、気分転換も兼ねて外へ出て考えることに
駐車場に行ってみると、、おや？これは？
![1-5.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3295567/56191261-cb8f-f513-b6f9-b20926696906.jpeg)
EV用の充電設備しかも普通充電と急速充電の両方を発見！
さらに観察すると急速充電の方にだけ制限時間があることを見つけました
(これは使えそう…)
その後ホテルのフロントにて話を聞いたり、遊歩道を散歩しながら皆で考えを練ったりして会場へ戻ってきました
我々チームの考えた企画は以下のようになりました
![App1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3295567/5477db01-d42c-971f-e535-1af59fb1280c.png)
![App2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3295567/7813ad1e-fde8-12b2-944c-16838ec5cbc5.png)
(ここだけの話アプリ名が中々思いつかなかったので助っ人としてChat gpt君を迎えて助言を頂きました…
アプリ名だけなら使ってもいいよね？？)

その後、チーム間で考えた企画を発表！
皆さんユニークな企画を考えており、これは明日からの実装が楽しみ
![1-6.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3295567/d0ff2822-8ea2-823d-de94-5bc4b30e43ec.jpeg)


# 3. アプリ開発
今日からFlutterを用いたアプリ開発が始まります
制限時間は3日目の朝10時！！
しかも残業禁止なので実質9時間で形にしなくてはいけません

皆さん不慣れなFlutterに悪戦苦闘
![2-1.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3295567/ecf25685-f669-914e-1d55-52aafe9d92c5.jpeg)
我々のチームも場所を変えながら開発に臨みました
![2-2.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3295567/daddd72d-0732-307c-718d-58425b27f3cd.jpeg)
ちなみに我々のチームはQCquestを形にするため以下のような実装を行いました

**画面**
それぞれStartView, QuestView, ResultViewとして定義しました
![Screen.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3295567/c037a86c-d750-6f54-26d9-234866565dc9.png)
本来はEVの充電開始時に自動的にQuestViewに遷移する予定ですが、
今回はプロトタイプなのでStartViewにてボタンを押すことでQuestViewへ遷移します

**実装コード**
主な実装内容は以下となります

- 位置情報
充電開始位置に戻ってこないと回答ボタンが押せないようにするため
[geolocator](https://pub.dev/packages/geolocator)プラグインを用いて「QuestView遷移ボタン」が押されたときに
その場所の位置情報(開始位置)を`getCurrentPosition()`にて保存し、
QuestView遷移後は`getPositionStream()`にて常にユーザーの現在位置を習得し、
現在地点と開始地点の距離によって回答ボタンの活性化を制御しています
```dart
Future<Position> currentPosition() async {
    return await Geolocator.getCurrentPosition();
  }

Stream<Position> streamPosition() {
    return Geolocator.getPositionStream();
  }
```
```dart
locationStream = geoState.streamPosition().listen((event) {
      distance = Geolocator.distanceBetween(
          location.latitude, location.longitude, event.latitude, event.longitude);
    });
```
```dart
ElevatedButton(
  onPressed: distance < 5.0
      ? () => [
    {
      Navigator.push(
          context,
          MaterialPageRoute(
            builder: (context) => ResultView(),
          ))
    },
  ] : null,
  child: Text('回答!!'),
)
```
- カルーセルスライダー/入力欄の保存
QuestViewでは[carousel_slider](https://pub.dev/packages/carousel_slider)を用いることで分かりやすいUIとなるようにしています
また画像の変更を`onPageChanged`にて検知し、画像の`index`を`_current`に格納しておくことで
画像とヒント文章、入力欄などが1対1にて対応するようになっています
また入力欄は`TextEditingController`にて入力値を常に取得するようにしておきます
(こうしないと折角入力したキーワードが画像の切り替えと共に消えてしまいます)
```dart
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へ遷移します
```dart
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
});
}
```
最終的にこのようになりました
![photo.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3295567/40959233-259c-e93d-cced-8aaeae287e4c.gif)

そしてあっという間に時間は過ぎ、最終発表！！
企画説明とドキドキのデモ発表を行います
![2-3.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3295567/54669251-66fc-d335-d549-4bbcfc95f7de.jpeg)
結果は…
見事1位を取りました！やった！


# 4. まとめ
普段の業務ではあまり接点のないメンバーかつ解放的な場所でのハッカソンはいい意味で刺激になり、
短い期間の中、コンセプト設定〜アプリの開発まで行い手と頭をかなり働かせました（笑）

改めてチームで何かを形にする楽しさを感じられたすごく満足度の高いイベントでした！

皆さん、お疲れ様でした！！
![3.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3295567/8141ebb9-d039-c985-7c00-b10975cef880.jpeg)