FirebaseとHerokuでエセAndroidフルスタック開発をした話

# まえがき

このたび、北海道札幌琴似工業高等学校 全日制 情報技術科を卒業することになりました。
今は学年末テストが終わって成績確定を待っている状況で、今後なにもなければ3/1日をもって卒業、進学することとなりました。

弊校では三年時にすべての生徒が「課題研究」という名で、なにかしらものを作ります。
私の所属する班はAndroidアプリを作ることになりました。

つまるところこの記事は、FirebaseとHerokuをバックエンドに持ったAndroidアプリを開発した話です。
主に成果物の概要と、振り返り、苦労をただただ書いていきます。技術的な内容は少ないかもしれませんが、Androidアプリ初学者の戯言と思っていただけると幸いです。

# Contents

以下にこの記事内の記述内容について簡単にまとめます。
目次はハイパーリンクになっています。スマートフォンでご覧のかたや、Webブラウザのウィンドウサイズ等で右の目次が表示されていない方はご利用ください。

- [作ったもの](#作ったもの)
 - 実際の完成品について記述しています。
- [使った技術](#使った技術)
 - バックエンド、フロントエンド、ライブラリなどについてまとめています
- [APIの話](#apiの話)
 - 軽いAPIリファレンスのようなものを書いています。内部実装についても一部言及しています。
- [アプリプログラミングの話](#アプリプログラミングの話)
 - Androidアプリ内での実装について言及しています。
- [時間割の更新/問題の追加](#時間割の更新問題の追加)
 - APIにデータを送信する管理者用アプリケーションについて言及しています。
- [今後の話](#今後の話)
 - 作成したアプリケーションの今後について話します。先にネタバレをするとソースコードを公開せず廃棄する予定です。
- [学び](#学び)
 - レポート的ですが、開発について学んだことを記しています。コーディングの教訓ではなく以下に自分のご機嫌を取るかということについて多く述べています。
- [あとがき](#あとがき)
 - 謝辞です

# 作ったもの
K-Scheduler(班員命名)と名前をつけた、時間割/学習支援アプリを作成しました
(以下に示すヘッダーなどは班員の名前が表示されているものがあったため一部修正を加えています。)

![kscheduler.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/983752/a0270792-f98e-7f01-846f-e45fbb258ebc.png)

主な機能としては時間割の表示と、5教科(国語、数学、理科、社会、英語)の問題演習を行う機能の2つがあります。

# 使った技術
私はWeb系の人間ではないので、使った技術を列挙しろと言われてもイマイチどれをあげればいいのかわからないので思いつくものを挙げてみます。

## バックエンド
- Google Firebase
  - 最近ちょっと流行ってる(?)NoSQLのデータベースです。DB機能のすべてがGUI及びゲッター/セッターで完結し、Webサイトのホスティングなども可能なかなりリッチなBaaS(Backend as a Service)です。この開発ではDB機能のみを用いています。

- Heroku
  - 有名なフリーホスティングサービスです。FirebaseとフロントエンドをつなぐAPIをデプロイしています。APIは主にDB更新や取得などのクエリに答えます。

- Flask
  - 言わずとしれたPython Webフレームワークです。DjangoやFast APIなど代替のフレームワークはいくつか存在しますが、以前に使用したことがあったという理由で選んでいます。深い理由はありません。

## フロントエンド?
- Android App
 - いわずもがな、Androidのアプリです。最近はアプリ内からWebViewなどを用いてWebアプリをAndroidアプリ内で表示するような二重アプリもあるそうですが、このアプリはWebViewを一切使用せず、完全にスタンドアロンで動作します。

## DB管理
- firebase_admin (Python)
 - K-SchedulerにはフロントエンドのAndroidアプリ以外にも2つのアプリがあります。それはHerokuにデプロイされているAPIとそのAPIに変更を投げるための管理者用ソフトがあります。firebase_adminはPython向けのFirebase ゲッター/セッターでAPIからFirebaseにアクセスするために用いています。

![Untitled.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/983752/c22e82de-1291-0264-a88a-a0e393490e31.png)

サービス全体の構成は上の様になっており、世の中の一般的なサービスからするとかなりシンプルなものだと思います。
(最近はQiitaなどで見かける個人開発でも非常にレベルが高いものが多く、社会人の体力を見せつけられて恐れおののいています...)

# APIの話
もともとAPIはRustなどを用いて開発を行おうと思っていました。
が、Herokuにデプロイする都合やライブラリなどの充実度を考えてJavaScriptかPythonのどちらかと考え使い慣れたPythonを使うことにしました。
このプロジェクトではそこまでAPIの速度が求められる状況ではないので、これで十分です。

## Route

Herokuは基本的に「https://アプリ名.herokuapp.com」というURLになります。

```
https://k-schedular.herokuapp.com
```

**"k-scheduler.herokuapp.com"じゃないの?**と思う人がいるかもしれませんが、これはスペルミスではなく、「k-scheduler」のアプリ名はHeroku上で取られています。(え?)
そのためtypoにも見えるこのURLが正式URLになっています...

ところで、QiitaにこういうURLを載せるとアクセスする人が出てくるのですが、**すでにこのAPIはメンテナンスモードに入っているためアクセスできません**。(理由は後述します)

以下にAPIのルートの表を示します

|route|method|header|query|機能|
|-----|------|------|-----|----|
|/tt|GET|必要なし|なし|一週間の時間割とお知らせメッセージを受け取る|
|/tedit|POST|{content-type:application/json, APIKEY:XXXXXXXX}|なし|JSONをPOSTして時間割データを書き換える|
|/getp|GET|必要なし|class=|classの教科の問題を$\min(総問題数, 10)$問返す|
|/addp|POST|{content-type:application/json, APIKEY:XXXXXXXX}|class=|classの問題を追加する。追加データはJSONから生成される|
|404|Any|任意|任意|存在しないURLにアクセスされたとき、エラーメッセージを含むJSONを返す|
|500|Any|任意|任意|APIの処理でエラーが発生したときに、エラーメッセージを含むJSONを返す|

APIに投げるデータはすべてJSONで、返ってくるデータもすべてJSONです。
また、DBアクセス用の秘密鍵がHerokuの中にあるというゲキヤバ構造なのでDBを書き換えるURLにアクセスするにはAPIKEYを必要としています。
APIKEYはいわゆる共通鍵のようなもので、これがバレたらヤバいのでしっかり秘匿しましょう。

## キャッシュ機能

APIは時間割機能だけですが、数分間だけ情報をキャッシュするような設計をしています。
これをする理由は簡単で、一番アクセス回数が多いルートなのに、いちいちFirebaseにデータを取りに行っていたら面倒だしレスポンスが悪い。という理由です。
確かに時間割データをクライアントが受信するまでにHerokuとFirebaseを介する必要があるのでレスポンスタイムは自ずと伸びるのですが、Herokuまでの時間がボトルネックになることが多いです。
ということで、キャッシュ機能は持っていますが、レスポンスタイムの向上という目的からすると別にあってもなくてもそこまで変わらない機構であるような気がしています。(正確に測定したわけではないのですが...)

しかし別の意味もあって、Firebaseへのリクエストが多発すると私のポケットマネーがFirebaseに吸われてしまうことになります。
これはよほどの多人数がDDOSレベルで時間割データを更新しないと起こり得ないかもしれませんが、問題出題のルートでリクエストはたくさん使う予定だったので、ここで少しでも削減しようとおもってこのような実装をしています。

具体的には以下のような機構になっています。

![cacheflow.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/983752/387c47ff-9f80-dae7-bb3b-8e67fe9d858b.png)

最後のリクエストから何分立ったかだけを記録しているのでとてもかんたんです。
時間割の変更は一般にAPIを通してしか行わないので変更されたかの検知も非常にかんたんで、特に難しいこともありません。

## JSONについて

![scienceproblem.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/983752/574c8da5-ec3d-8c72-f600-3259a6817eb7.jpeg)

問題出題機能については上のようなJSONが帰ってきます。"nop"というキーに紐付けられた整数がJSONに格納されている問題の数で、問題はproblemsというキーに紐付けられたリストに一問ずつ格納されています。
JSONでよくみるリストのリストという形になっています。

![timetablejson.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/983752/7f805d31-8cab-4121-8610-d3b424b8a3c7.jpeg)

こちらは時間割で帰ってくるJSONです。
「まあ、そうだよね」みたいなJSONが帰ってきます。

## 10問選ぶ

```python:getp
@app.route('/getp', methods=['GET'])
def getp():
    gotid = list()

    dummy = ["Dummy", "NULL", "NULL", "NULL", "NULL", -1]

    cls = request.args.get('class')
    l = 0

    if cls == "japanese":
        pset = (FBDB.collection('Problems').document('Japanese').get()).to_dict()
    elif cls == 'math':
        pset = (FBDB.collection('Problems').document("Math").get()).to_dict()
    elif cls == 'socialstudy':
        pset = (FBDB.collection('Problems').document("SocialStudy").get()).to_dict()
    elif cls == 'science':
        pset = (FBDB.collection('Problems').document("Science").get()).to_dict()
    elif cls == 'english':
        pset = (FBDB.collection('Problems').document("English").get()).to_dict()
    else:
        return jsonify({'message':'invalid argument'}), 500

    l = len(pset)

    while len(gotid) < min(10, l):
        rnd = random.randint(1, l)
        if rnd in gotid: continue
        gotid.append(rnd)

    problems = {
        'nop':min(10, l), 
        'problems':[]
    }
    for i in gotid:
        problems['problems'].append(pset[str(i)])

    while len(problems['problems']) < 10:
        problems['problems'].append(dummy)

    return jsonify(problems), 200
```

リクエストされた教科の問題セットをFirebaseから全部持ってきて、10問抽出します。
仮に10問未満しか問題が登録されていなかった場合はダミーの意味のない問題が入ります。
わざわざnopというキーで問題数を入れているのは、Androidアプリ側でこのダミーを読まないようにするためです。

ところで、わざわざダミーを入れる意味はないのでは?と思うかもしれませんが、これは受け取り側で楽をするためにこのような実装をしているだけです。
受け取り側でちょっと工夫をするだけで本当はこのような実装をする必要はなくなります...

あと、この関数ではif, elif, elseを用いて条件分岐をしていますが、最近のPythonはパターンマッチが追加されたのでそっちを使っても良かったかもしれません。

## 時間割

```python:tt
dic_collecter = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Notification']

@app.route('/tt', methods=['GET'])
def tt():
    '''Get Timetable data'''
    global CacheTime
    global TCache
    global edited
    if edited == True:
        CacheTime = int(time.time())
        edited = False
        table = FBDB.collection('Timetable').get()
        print("Firebase requested")
        TCache = table
    elif int(time.time())-CacheTime > 300:
        CacheTime = int(time.time())
        table = FBDB.collection('Timetable').get()
        print("Firebase requested")
        TCache = table

    rdic = collections.OrderedDict()
    
    try:
        tempdic = {}
        for i in TCache:
            extract = i.to_dict()
            for key, item in extract.items():
                tempdic[key] = item

        # Sort dic as day order
        for i in dic_collecter:
            rdic[i] = tempdic[i]

        return jsonify(rdic), 200 #200-OK
    except KeyError:
        return jsonify({'message':'API ERROR'}), 500 #500-Internal Server Error
```

Firebase内ではデータが以下のようにしてキーの辞書順に並び替えがされています。

![fbtable.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/983752/66b1f0cd-0078-51cc-2b1b-35436f16474f.jpeg)

Firebaseにリクエストして返ってくるデータもこのような順にソートされているため、月火水木金というイレギュラーな順番に並び替えるには自分で並び替える必要があります。
ということで、一回無駄に見えるソートを挟んでいます。

一応KeyErrorをexceptionでcatchしていますが、よくよく考えるとAPIそのもののエラーハンドラに先にキャッチされるので意味がありませんでした...

# アプリプログラミングの話

Androidアプリ側の話です。
もともとこのアプリはKotlinを使って開発する予定でした。が、結局Javaを選択することとしました。
理由は簡単でKotlinのお気持ちがわからなかったというだけです。
JavaもKotlinもオブジェクト指向の言語ですが、今まで関数指向のみを扱ってきたためイマイチオブジェクト指向の言語ルールに従えず、かなり苦労しました。
KotlinはJavaの後継として考えられる言語なのですが、後継ということは明らかにJavaより複雑です。

もともとAndroidでHello Worldとそのちょっとあと位まではやったことありましたが、複数Activityやインターネット接続などを使用するようなアプリは作成したことがなかったため、「わからないならレガシーに立ち返るしかない」という考えのもとJavaに落ち着きました。

で、JavaもKotlinもほとんど使ったことがなかったので基本文法からやる必要があったわけですが、結局最後まで勘で書きました...(すいません...)
Kotlinはともかく、Javaは文法がほぼほぼC/C++のそれで馴染みやすかったというのもあります。

## APIとのCommunication

### 時間割

```java:KAPI
@Override
protected Void doInBackground(Uri.Builder... builders) {
    try {
        final String Url = "https://k-schedular.herokuapp.com/tt";

        HttpURLConnection con = null;
        java.net.URL u = new java.net.URL(Url);
        con = (HttpURLConnection) u.openConnection();
        con.setRequestMethod("GET");
        con.connect();

        String result = "";
        BufferedReader in = new BufferedReader(new InputStreamReader(con.getInputStream()));
        String temp;
        while ((temp = in.readLine()) != null) {
            result += temp;
        }

        Gson gson = new Gson();
        Type type = new TypeToken<Timetable>(){}.getType();

        //Use list datatype for nested json
        /*eg
        [
            {blah...},
            {blah...},
            {blah...]
        ]
         */
        TableResult = gson.fromJson(result, type);
        n = TableResult.getNotification();
    }
    catch (Exception e){
        e.printStackTrace();
    }

    return null;
}
```

これは時間割データをAPIから取ってくるコードです。
JSONパーサーにはGsonを使っているので以下のようなクラスを定義してAPIからのデータをパースしています。

```java:Timetable
public class Timetable {

    @SerializedName("Monday")
    @Expose
    private List<String> monday = null;
    @SerializedName("Tuesday")
    @Expose
    private List<String> tuesday = null;
    @SerializedName("Wednesday")
    @Expose
    private List<String> wednesday = null;
    @SerializedName("Thursday")
    @Expose
    private List<String> thursday = null;
    @SerializedName("Friday")
    @Expose
    private List<String> friday = null;
    @SerializedName("Notification")
    @Expose
    private String notification;

    public List<String> getMonday() {
        return monday;
    }

    public void setMonday(List<String> monday) {
        this.monday = monday;
    }

    public List<String> getTuesday() {
        return tuesday;
    }

    public void setTuesday(List<String> tuesday) {
        this.tuesday = tuesday;
    }

    public List<String> getWednesday() {
        return wednesday;
    }

    public void setWednesday(List<String> wednesday) {
        this.wednesday = wednesday;
    }

    public List<String> getThursday() {
        return thursday;
    }

    public void setThursday(List<String> thursday) {
        this.thursday = thursday;
    }

    public List<String> getFriday() {
        return friday;
    }

    public void setFriday(List<String> friday) {
        this.friday = friday;
    }

    public String getNotification() {
        return notification;
    }

    public void setNotification(String notification) {
        this.notification = notification;
    }
}
```

毎回自分でクラスを定義するのは面倒なので[jsonschema2pojo](https://www.jsonschema2pojo.org)を用いてAPIから実際に帰ってきたデータを用いてJSONクラスを生成しています。
普段JSONを扱うアプリケーションはPythonだったりを用いて開発しているので、面倒だなぁというのが正直なところ。
自動生成ツールにはかなりお世話になりました。

JSONを受け取るという以外は普通にHttpURLConnectionを形成しているだけなので特筆するところはとくにないです。

```java:changeButtonText
private void changeButtonText(Timetable tt){
    for (int d = 0; d < 5; d++){
        List<String> day;
        if (d == 0) day = tt.getMonday();
        else if (d == 1) day = tt.getTuesday();
        else if (d == 2) day = tt.getWednesday();
        else if (d == 3) day = tt.getThursday();
        else day = tt.getFriday();

        for (int i = 0; i < 6; i++){
            int sid = ffmain.getIdentifier(day.get(i), "string", ac.getPackageName());
            String ClassTemp = ffmain.getString(sid);
            CB[d][i].setText(ClassTemp);
        }
    }
}
```

時間割の表示は以下の画像のようになっていて、1時間がボタンオブジェクトになっています。

![Screenshot (6).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/983752/8457b815-c2a7-16cd-f700-5ee36a3770e0.png)

そのため、時間割を書き換えるのはボタンのテキストをそれに書き換えるのと同義であって、アプリ自体が時間割のデータを保持しておくということはありません。
また、先程APIから返ってきたJSONの画像を載せましたが、そこでは英語で記述された教科名が日本語名に戻っています。
これは以下の画像のような感じでStringを定義しているからです。

![classstring.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/983752/5bab63d5-4ddc-e009-6e9c-a9dd4b464186.jpeg)

あとから考えると、自由度を上げるという意味でAPIからのテキストをそのままボタンに書き込むシステムにしても良かったかもしれませんが、本番環境に変なものを持ち込まないという意味でこの実装も悪くないと思っています。(参考 : [株式会社インフィニットループ : 新卒向け研修資料「テスト文字列に”うんこ”と入れるな」を公開しました](https://www.infiniteloop.co.jp/blog/2021/09/unko/), ただの参考ですがこれは普通にタメになる話だと思っています。あとスライドの2枚目の「どうも!社長です」が結構面白くてツボです)

本番環境に変なものを持ち込まないという話から転じてですが、APIは「ローカルで動くプログラムが本当にデプロイ後も動くプログラムであるか?」を確認するようにしましょう。( $n$ 敗しました ($n \ge 3$))

### 問題演習

```java:problemgetter
public class ProblemGetter extends AsyncTask<Uri.Builder, Void, Void> {
    private final String target;
    private ProblemMain PMain;

    public ProblemGetter(String t, ProblemMain PTemp){
        target = t;
        PMain = PTemp;
    }

    @Override
    protected Void doInBackground(Uri.Builder... builders) {
        final String URL = "https://k-schedular.herokuapp.com/getp?class="+target;

        //HTTP URL connection
        try{
            java.net.URL url = new java.net.URL(URL);
            HttpURLConnection con = (HttpURLConnection) url.openConnection();
            con.setRequestMethod("GET");
            con.connect();

            String result = "";
            BufferedReader in = new BufferedReader(new InputStreamReader(con.getInputStream()));
            String temp;
            while((temp = in.readLine()) != null){
                result += temp;
            }

            PMain.SetProblem(result);
        }
        catch (Exception e){
            e.printStackTrace();
        }

        return null;
    }

    @Override
    protected void onPostExecute(Void unused) {
        PMain.setRequest_complete();
    }
}
```

おおよそ先ほどとやっていることはかわらなくて、普通に取ってきています。
先ほどとの違いは、APIから取ってきたデータを呼び出し元のActivityに保存しているところで、doInBackground関数ではnull以外を返せないので親のActivityにセッターを用意して保存しています。

![Screenshot (9).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/983752/63f16377-0290-5008-e859-5f98989d03de.png)

取ってきた以降はまあいい感じにやっています。

```java:CheckAnswer
private void CheckAnswer(String ans){
    if (ans.equals(PSet.getProblems().get(NowState).get(5))){
        Correct += 1;
        NowState += 1;

        new AlertDialog.Builder(getActivity())
                .setTitle("正解")
                .setMessage("正解です！")
                .setPositiveButton("次へ", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        NextProblem(NowState);
                    }
                })
                .show();
    }
    else{
        Wrong += 1;
        NowState += 1;

        new AlertDialog.Builder(getActivity())
                .setTitle("不正解")
                .setMessage("不正解です。"+"\n"+"正解は「"+PSet
                        .getProblems()
                        .get(NowState-1).get(
                                Integer.parseInt(PSet
                                .getProblems()
                                .get(NowState-1)
                                .get(5))
                        )
                        +"」です。"
                )
                .setPositiveButton("次へ", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        NextProblem(NowState);
                    }
                })
                .show();
    }
}
```

こんな感じの判定用の関数を用意しておいて、

```java:Binding
    Choice1.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            CheckAnswer("1");
        }
    });

    Choice2.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            CheckAnswer("2");
        }
     });

    Choice3.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            CheckAnswer("3");
        }
    });

    Choice4.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            CheckAnswer("4");
        }
    });
```

4択を表示するボタンにリスナーとして先程の関数を紐付けてあります。

1問解いたら次の問題に画面表示を更新します(それ以上問題が存在しないなら終了ダイアログを表示して終了)

```java:NextProblem
private void NextProblem(Integer index){
    if (index < End) {
        List<String> Problem = PSet.getProblems().get(index);
        Statement.setText(Problem.get(0));

        Choice1.setText(Problem.get(1));
        Choice2.setText(Problem.get(2));
        Choice3.setText(Problem.get(3));
        Choice4.setText(Problem.get(4));
    }
    else{
        new AlertDialog.Builder(getActivity())
                .setTitle("最終結果")
                .setMessage(End.toString()+"問中 : "+Correct.toString()+"問正解, "+Wrong.toString()+"問不正解でした")
                .setPositiveButton("終了", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        parentActivity.Return_to_table();
                    }
                })
                .show();
    }
}
```

![finishproblem.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/983752/948d2279-db08-f53d-a67f-7a9b6beae317.jpeg)

こんな感じのが表示されます。

![problemerror.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/983752/47c05bc2-b0aa-cb1d-dfca-e8e55c6be415.jpeg)

ちなみにDBに問題数が足りないと(1問もないと)怒られます。

# 時間割の更新、問題の追加

今まではすでに存在するデータに対しての操作のみを行っていましたが、運用するにあたってデータの変更や追加を行うことは別に珍しくありません。
ということで変更/追加するためのソフトも存在します。

K-Requesterと呼んでいて、外部からcsvファイルを読み込むことによって時間割を更新したり問題を追加できたりします。
やってることはかんたんで、ローカルのcsvを読んでAPIの変更や追加を行うルートを呼び出しているだけです。
ここでもAPIとの通信はJSONを使います。

## csvのフォーマット

```csv:時間割
月1,月2,月3,月4,月5,月6
火1,火2,火3,火4,火5,火6
水1,水2,水3,水4,水5,水6
木1,木2,木3,木4,木5,木6
金1,金2,金3,金4,金5,金6
お知らせ
```

```csv:問題
教科
問題文
選択肢1,選択肢2,選択肢3,選択肢4
正解の選択肢番号
```

本当はコマンドラインから呼び出せるような実装にしようかと思ったのですが、これを作ってもそこまで使う機会がないことに気づき、普通に.pyファイルをダブルクリックして開くスタイルに変えました。
ファイル指定はtkinter GUIで行うようにしているのでそこだけはかなり便利でした。

![requester.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/983752/8655b87c-6bae-fa10-efdf-826f71b6d911.jpeg)

何が面倒かというとcsvファイルを作るのが面倒です。
ファイルを開くだけとかじゃなくて本当にGUIで全部できたら結構楽だったかもしれないですね。(**これはあくまでもプログラムを組む労力を無視した時の話です。数ヶ月運用するわけでもなく、プログラムを組む労力を考えるとこれは愚策です。**)

# 今後の話

一応卒業研究のようなものなので、発表があります。
多分大学側が入学希望者を得たいという理由があるのでしょうが、近所(いうほど近所ではないかもしれない...)の大学の教授がレビュアーとして発表を聞きに来ます。

その教授からもそうですし、担当教員からも「このアプリの今後は?」という質問を受けました。
はっきり言ってしまうと、「廃棄」となります。

理由は簡単でこれを引き継ぐ人間と土壌がないからです。
また、アプリとしては完成していますがこのアプリで閲覧可能な時間割データというのは(この実装では)たかだか1クラスのみです。
更新も面倒ですし。
さらにいうと、このアプリはAndroidのみしか対応していません。日本の大半の高校生はAndroidスマートフォンではなくiPhoneを持っていますよね?

ということで、ソフトウェアプロダクトとしては異常に短い寿命でしたが、廃棄という選択肢を取りました。

現在APIはherokuのメンテナンスモードでお休み中です。(近日中にheroku app自体を削除します。)

# 学び

## スケジュール管理をする

「いや、当たり前じゃん?」と思う人は多いはずです。が、Qiitaにいるような「**ちょっと意識が高くて、よくわからない記事も読むような怠惰で模範的なプログラマ**」([参考](https://www.capa.co.jp/archives/18424))のみなさんなら身に染みていると思われますが意外とスケジュールはつめつめになりやすいものです。

それに本来1年間ある課題研究の期間、すべてを課題研究に使っていたわけではありません。
わたしは札幌の人間ですが、今年はいろいろなところに旅行に行きました。

- 旭川旅行に行ったり(参考:[北海道札幌琴似工業高等学校全日制 - 第21回高校生ものづくりコンテスト電子回路組立部門全国大会出場](http://www.sapporokotonikougyou.hokkaido-c.ed.jp/index.php?action=pages_view_main&active_action=journal_view_main_detail&post_id=1120&comment_flag=1&block_id=293#_293))
- 神奈川旅行に行ったり(参考:[北海道札幌琴似工業高等学校全日制 - 高校生ものづくりコンテスト全国大会に出場しました](http://www.sapporokotonikougyou.hokkaido-c.ed.jp/index.php?action=pages_view_main&active_action=journal_view_main_detail&post_id=1219&comment_flag=1&block_id=293#_293))
- 函館旅行に行ったり(参考:[Youreinのdesktop.ini - 公立はこだて未来大学 受験記(2022)](https://youreingaming.blog.fc2.com/blog-entry-84.html))

しました。(まあホテルに引きこもってただけなので旅行と形容するのはちょっとダメかもしれません。)

結果として研究発表の2週間前からいろいろ本番環境にデプロイしたり、アプリとAPIの連携部分を作ったりなどなど...
結局1年間あった作業時間のうち、真面目にキーボードを叩いて調べ物をしたりプログラムを打っていた時間は(作業した日数で数えると)1ヶ月にも満たないんですよね...

結局最後の二週間で学内であればまともに活用できるようなアプリを作り上げるところまでは行きましたが、これがあと1週間前に倒れていたらもっと良いものが作れたでしょうし、1ヶ月前に倒れていたらと考えたら...
まあ実際に前倒しにできるような人間ではないのですが、とにかくやるということは大事です。

多くのプログラマは優先度付きキューでtodoを管理していると思われますが、まず自分の優先度付きキューの実装がバグっていないか確かめるところから始めたほうがいいと思います。

## していること/やるべきことを明確にする

これは先程のスケジュール管理ともつながってくる話ですが、自分が今何をしようとしているのか。自分がなにをするべきなのかを言語化して保持しておくことが重要です。
あなたが社会人のプログラマであるならガントチャートを作るのに業務の細分化をしたことがあるかもしれませんが、この開発ではそれをしていませんでした。

もちろん各人がどのような仕事をするべきかは明確でしたが、それ以上の分割はしていなかったです。(例えば各人がどのような分野の仕事を行うのかは決めていましたが、その分野の中でさらにどのような仕事を行うのかを細分化していなかったという話です。)
班員4人中自分以外はプログラムを一切していないので、自分がそれを明確にするべきだったというのが反省です。
逆に仕事分担的にプログラム以外をやらなくて良かったというのがとても精神的に良かったのでこれは感謝です。

## モチベーション/環境管理

さっきも書いたのですが、明らかに課題研究の全期間中、仕事をしていた割合のほうが少ないことがわかります。
結局家で音楽聴きながら開発したら$k$日で終わったみたいな事になっているので、これを学校にいる間に進められたら非常にスムーズに課題研究を進められたのではと思います。

原因として大きくはモチベーションもあると思うのですが環境などもあると思っていて、そもそも「集中状態」に入れるか否かが生産性の分かれ目であると思います。
自宅にいる間は誰かに話しかけられることもなく、時間の区切りというのも存在せず、自分の定めた進捗まで進められればOKということができますが、学校にいる以上50分毎にチャイムが鳴るわけで、周囲の人間の話し声というのも存在します。

もちろん、多少の話し声ならいいですし、軽い雑談なら雰囲気と気分も大きく向上するような気がするのですが、集中力を妨げるコミュニケーションが発生している場所にいるのは明らかに生産性にダメージを与えます。

具体的な話をすると同じ部屋でスマホゲームを1日中やってる人の集団とかそこらへんです。

これについては環境を変えればいい話で、実際に別の部屋に移れば集中できる環境も存在したのを研究の最後半で発見したので、早めに行動を起こせばよかったなと思っています。

え、「どんな環境でも成果を上げるのが本当の社会人」ですか?
少なくとも、コードを書いているときに集中を妨げられて腹が立たないプログラマはいないと思いますが(ただし、それが仕事関係の重要なものではない場合に限ります。また、この重要なものには仕事の質問なども含むとします)。

## 動くコードを書く

当たり前では?と思うかもしれませんが、ここでの動くとは「確定で動く」ということです。
ある言語を始めたてのときは「エラーすら出ていないが、実際に動くかわからない」というコードを多く書くことになります。
そのようなコードを書いたときは当たり前ですがバグが自然発生的に出てきます。

そのため、「多分動くコード」じゃなくて「動くコード」を書くことが必要です。
そのために、「動くであろうコード」を書いてテストをすることも大切です。

余計時間がかかっているのでは?と思うかもしれませんが、適当に「多分動くコード」を書いてデバッグの時間が長くなるよりは数倍マシです。

# あとがき

約一年間の課題研究でしたが、多くの有効な学びを得ることができたと思います。
願わくば今後Androidアプリ開発に関わりたいとは思わないのですが、Webフロントエンドくらいはやってみてもいいかもなと思いました。おそらくWebフロントエンドのほうがアプリ開発より地獄だとは思うのですが、まあ自分のサイトくらいなら大丈夫でしょう。(本当か?)

言いたいことはだいたい記事に書いたつもりなので、以下は謝辞です。

まず第1に共に活動を行った班員の方々。資料作成や問題セットの作成、アプリ内の配色決定などについては完全に私以外の班員の実績ですので、ここでその実績を明記し、謝辞を述べさせていただきます。ありがとうございました。

次に担当教員にはすこし違う形かもしれませんが、「介入が少なかった」という点について感謝しています。
他の班はそれぞれの担当教員と協力してものを作り上げるという形でしたが、私達の班は良くも悪くも介入がありませんでした。
これがいい状態とは言えないかもしれないですが、介入されてもどうしようもないプロジェクトですので、最後までほぼほぼ内政不干渉状態であったことがとてもありがたかったです。

最後になりますが、開発中にさまざまなサイトを参考にシステムを組み上げました。
それらサイトをすべてここに列挙することはできませんが、とてもお世話になりました。
この場を借りて謝辞を述べさせていただきます。ありがとうございました。

以上をもって本当に私の卒業研究はCloseとなります。
情報系学科の卒業生としてふさわしいものは作れたのではないのかと思っています(クオリティは低いですが)。
あと2ヶ月後には大学生として新生活を始めるわけですが、高校3年間での技術の学びを生かしていければよいと考えています。

ここまでお読みいただき、本当にありがとうございました。