ワイの備忘録

IT専門学生が何やかんやしてる記録

AndroidStudio/Kotlin - 卒制のAndroidアプリでつまづいた事まとめ(1)

目次


記事の構成

  1. アプリの概要 ←イマココ
  2. アプリ制作でつまづいた事
  3. DBまわりでつまづいた事(データ構造)


卒業制作を終えて

先週に卒業制作の発表を終えて一段落したので、今回作ってた中で個人的につまづいた事をまとめていこうと思います。
問題の解決には公式ドキュメントの他に技術系ブログやstackoverflow、Qiitaもかなり参考にさせて頂いたのですが、その都度URLをメモしていたわけでは無いのでメモしてあるURLのみリンクとして各記事の最後に貼っておきます。
参考にさせていただた各サイト様へ、この場を借りてお礼申し上げます。ありがとうございます。


卒制で何作ったのか

私(厳密に言えばグループ製作だったので、主にコーディングしていた私ともう一人)が作ったのは、TwitterライクなSNSアプリです。
ただ、卒制なんでTwitterをパクるだけではOKもらえないし、私もパクっただけのやつを卒制にしたくないです。
そこで基本的なSNS機能+αって感じでターゲットユーザーを絞った独自のSNSを作りました。
「筋トレ」x「SNS」です。詳しい内容は割愛します。


卒制アプリの出来る事

  • 記事の投稿(いわゆるツイート)
  • 記事のブクマ
  • 記事の応援(いわゆる拍手機能)
  • タイムラインの表示
  • ユーザのフォロー/アンフォロー
  • ユーザの筋トレメニューの作成
  • ユーザの一週間のトレーニングの設定

あとは各項目の閲覧とかです。


卒制アプリのシステム的な概要

  • フロント:AndroidStudio/Kotlin
  • バックエンド:Firebase(mBaaS)>Authentication,Storage,Cloud Firestore(noSQL)


次回の記事

パート2 (準備中)

パート3 treee333.hatenablog.com

Firebase Cloud Firestore/noSQL - 卒制のAndroidアプリでつまづいた事まとめ(3)

目次


記事の構成

  1. アプリの概要
  2. アプリ制作でつまづいた事
  3. DBまわりでつまづいた事(データ構造) ←イマココ


卒制アプリのDB構造

ググってもnoSQLの概念的な説明記事しか見つけられず手探り状態だったので、もし自分みたいに「初めてnoSQL触るけど、どういう風にデータを置いたらいいか分からない」って人の参考になれば嬉しいかなって思いこのパートを書きました。

これがnoSQLとして正解もしくは不正解な形なのか分かりませんが、
プレゼン時に動かしてたアプリのDB構造です。

// データ群
<collection> timeline
    <document> 記事番号
        <field> date : 投稿日
        <field> time : 投稿時間
        <field> article : 本文
        <field> figure : ユーザグループ
        <field> id : 記事番号
        <field> image1 : 画像URL
        <field> support : 応援数(拍手数)
        <field> userid : ユーザ番号
        <field> username : ユーザ名
    <document> sum
        <field> num : 総ドキュメント数

<collection> musclemenu
    <document> 部位名
    <field> documentName : 部位名
    <sub-collection> menu
        <document> 部位名+筋トレメニュー番号
            <field> 筋トレ内容 : String型
            ...
        <document> sum
            <field> num : Number型

// ブクマ&フォロー関係用
<collection> bookmark
    <document> ユーザ番号
        <field> 記事番号 : ""(何も無し)
        ...

<collection> follows
    <document> ユーザ番号
        <field> ユーザ番号 : ""(何も無し)
        ...

<collection> followers
    <document> ユーザ番号
        <field> ユーザ番号 : ""(何も無し)
        ...

// ユーザ用
<collection> user
    <document> ユーザ番号
        <field> userE : Eメールアドレス
        <field> userName : 名前
        <field> introduce : 自己紹介
        <field> startDate : アプリ利用開始日
        <field> sumSupport : 総応援数(総拍手数)
        <field> figure : グループ
        <field> sex : 性別
        <field> height : 身長
        <field> weight : 体重
        <field> fat : 体脂肪率
    <document> findUserID
        <field> メールアドレス : ユーザ番号
        ...
    <document> findUserName
        <field> ユーザ番号 : ユーザ名
        ...
    <document> sum
        <field> num : Number型

// 索引用
<collection> yourArticle
    <document> ユーザ番号
        <field> 記事番号 : ""(何も無し)
        ...

<collection> yourMuscle
    <document> ユーザ番号
        <field> 筋トレメニュー番号 : ""(何も無し)
        ...

<collection> yourWeekryTore
    <document> ユーザ番号
        <field> 曜日番号(0~6) : 筋トレメニュー番号
        ...


DBまわりでつまづいた事

使ったDBはFirebase Cloud Firestoreで、言語はKotlinです。


1.ユーザが投稿した記事や筋トレメニューをユーザ毎に抽出する

RDBなら記事レコードに作成者の情報(ユーザ番号とか)をもたせればSELECTですぐ抽出出来るのにって思いながら悩んだ結果が『目次的なcollectionを作ればOKでは』って案です。
なのでcollection名は統一して『your○○(あなたの○○)』にしました。

db.collection("your○○").document("ユーザ番号").get()

で持ってきた値を一時保存用のmutableListかmutableMapとかに入れておいて、timelineコレクションやmusclemenuコレクションを.get()した時に一時保存した値と一致すれば画面表示用のmutableMapに一致した物を保存して、最後にAdpterでRecycleViewに表示させていました。

難点としては総当たりなんでtimelineコレクションやmusclemenuコレクションの中身が増えた場合に処理が重くなっちゃうんですよね。

後で思いつた解決策としては、一時保存した値のListやMapを展開して保存した値のdocumentだけ持ってくればよかったのかな?
まだ実際に試してないので下の方法で期待通りの動きをしてくれるのか不明です。
気力があれば検証して追記しておきます。

yourArticleList.forEach { value ->
    db.collection("timeline OR musclemenu").document(value).get().addOnCompleteListener {
        // したい処理
    }
}


2.記事をタイムラインに投稿順に表示

timelineコレクションに記事の総数を保存するsumドキュメントを作って、新規記事ドキュメント名には『総ドキュメント数+1(4桁の先頭0埋め)』のドキュメント名を付けるようにしました。
(例:『0001』『0002』...)

これだと投稿が古い物から新しい物にかけて、番号が昇順になるので並べ替えが楽かなと思いました。
※cloud firestoreには総ドキュメント数を用意してくれるAPIが無いので、記事を投稿した後は別でsumドキュメントの中身をインクリメントする処理を記述しました。

db.collection("timeline").document("sum").update("num", 総ドキュメント数 + 1)


3.フォロー/フォロワー関係

はじめてSNSを作ったので、そもそもRDBでもフォロー/フォロワーってどうやって表現してるんじゃあーーーって所から悩みました。
それでこれも索引みたいに『○○がフォローしているユーザ一覧』『○○をフォローしているユーザ一覧』を抽出できれば、フォロー一覧とフォロワー一覧が実装出来るんじゃないかなと思いました。

<collection> follows
    <document> ユーザ番号                          // 当事者
        <field> ユーザ番号 : ""(何も無し)        // 当事者がフォローしているユーザ
        ...

<collection> followers
    <document> ユーザ番号                          // 当事者
        <field> ユーザ番号 : ""(何も無し)        // 当事者をフォローしているユーザ
        ...

当初はvalueにBoolean型でtrue/falseでも入れようかなと思ったんですけど、値の使いみちがわからなくて結局『""』で放置してます。
相互フォローとかをひと目でわかりやすくする場合にtrueでも入れておけば、follows/followersを行ったり来たりして確認する手間がはぶけるのかなって思います。


4.ユーザ情報の取得/検索

ログイン等の認証処理はFirebase Authenticationを使用したので、DBにはパスワード以外の情報(名前、自己紹介...)を持たせました。
今回プロフィールや名前の表示(ProfileActivity.ktやNavigationDrawer上)には全てユーザ番号を元に抽出するようにしています。

ただ最初のログイン時≒LoginActivity.ktから画面遷移した段階では、FirebaseAuthの関係でユーザ情報はEメールとユーザUIDしか分からないんですよね。
そこでuserコレクションに『Eメールに該当するユーザ番号一覧』と『ユーザ番号に該当するユーザ名一覧』があれば解決すると思いドキュメントを作りました。
『findUserID』と『findUserName』ですね。

『findUserName』に関してはこれを作ったおかげでSNSアプリの検索機能でユーザ名で検索する事が可能になりました。
名前検索はユーザ番号ドキュメントだけでも事足りるんですけど名前以外のいらない情報(自己紹介、メールアドレス...)が邪魔だし、DBから持ってくる必要ないなと思ってたので。一石二鳥でした。


5.ユーザ番号を取得して他のユーザ情報を抽出する

4番目の項目で『ユーザ番号を元に他の情報を抽出する』って書いたんですけど、

var userNumber = ""
db.collection("user").document("findUserID").get().addOnSuccessListener {  items ->
    //  Eメールからユーザ番号(ドキュメント番号)を探す
    var doc = items.data
    doc!!.forEach {
        if (it.key.equals(user!!.email.toString())) {
            userNumber = it.value.toString()
        }
    }
}

// ユーザ番号で何か探したい処理
db.collection("yourArticle").document(userNumber).get().addOnSuccessListener {
    // したい処理
}

最初は↑これで書いてて、上のdb文ではuserNumberがちゃんと取得出来ているのに下のdb文に移るとuserNumberの中身が""のままでエラーになってたんです。
Log.dとかで色々見てたらどうにもdb文の処理は最後にまとめて実行(comiit?)されるらしく、下のdb文が実行される段階ではuserNumberは更新されていなかったんです。

db.collection("user").document("findUserID").get().addOnSuccessListener {  items ->
    //  Eメールからユーザ番号(ドキュメント番号)を探す
    var doc = items.data
    doc!!.forEach { it ->
        if (it.key.equals(user!!.email.toString())) {
            db.collection("yourArticle").document(it.key).get().addOnSuccessListener {
                // したい処理
            }
        }
    }
}

みたいな感じでdb文のネストして解決させました。物凄く可読性悪いです。
もっと良い方法があるような気がするんですけどねー。


考察

困った時にググって出てきたnoSQLの概念記事と似たような事を言う結果になるんですが、
個人的には『欲しい情報の目次的なコレクションorドキュメントをその都度作る(例:findUserID, yourArticle...)』『情報の冗長は気にしない』がデータ構造を考えるうえで大事だったかなあって思います。

今回のアプリでは記事に対するコメント機能やシェア機能(リツイート)を時間の関係で実装出来なかったのですが、これらの機能も実装するとなったら果たして今のデータ構造で間に合うのかって不安はあります。
アプリ自体はまだまだ未実装の機能や処理があるまま卒制を終えてしまったので、データ構造自体に改善の余地はかなりあると思います(˘ω˘)


前回の記事

パート2 (準備中)

パート1 treee333.hatenablog.com

Lesson4 - FrogRiverOne

作成日:2015/5/16
Task Score:100%
FrogRiverOne coding task - Learn to Code - Codility

// you can also use imports, for example:
// import java.util.*;

// you can write to stdout for debugging purposes, e.g.
// System.out.println("this is a debug message");
import java.util.HashMap;

class Solution {

    public static  int solution(int X, int[] A) {

      int goal = 0;
      int comp_goal = 0;
      HashMap<Integer,Boolean> hash = new HashMap<Integer,Boolean>();

      do {
        goal += X;
      } while(X-- > 0);

      for(int i = 0; i < A.length; i++) {
        if (!hash.containsKey(A[i])) {
          hash.put(A[i], true);
          comp_goal += A[i];
        }
        if (comp_goal != 0 && comp_goal == goal) {
          return i;
        }
      }

      return -1;
    }
}

天気予報アラートを作りたい(0)

はじめ

PythonとLineDeveloperで天気予報アラートを作りたい。
目標は4月末までに完成させること。

「天気予報アラートを作りたい」記事が何個になるのかは分からないが、完成したら全部まとめて書き直そうと思います。

天気予報アラートなのか

ツーリングの日が近づくにつれて、毎日毎時間気づけばYahoo!天気を開いているのに面倒臭みを感じたため。

Pythonで作る理由

1.Pythonに興味があったから。
2.APIやモジュール等の通信に必要な機能が豊富だったから。

LINEDeveloperを使う理由

1.通知が分かりやすい。
2.マルチプラットフォーム(ドヤ)を目指したかった。

機能

1.天気予報が知りたい”日付”と"場所"をクライアントからサーバに投げる。
 例:「4/25の大阪の天気予報が欲しい。」
2.サーバ内で指定の”日付”が取得出来るまでプログラムをまわす。
3.”日付”が取得出来たらクライアントに予報を通知する。
4.“日付”当日になるまで、通知した予報に変更があれば変更後の予報をクライアントに通知する。

目標

1.APIを使用した天気予報の取得。
2.コンソールから値を指定して機能通りの反応を返す。
3.LINEbotから値を投げて機能通りの反応を返す。

現時点での課題、疑問

1.自分以外のユーザが値を投げる場合、誰がどの値を欲しているのかを記録する必要がある。

programming challenge Nickel 2018

作成日:2018/4/4

Strontium 2019 challenge - Codility

初アワード頂きました~嬉しい~(˘ω˘)
しかし相変わらずperformanceが0%なのは進歩0!
素直に何処が無駄なんだろう。

【Codility Silver Award for the Nickel 2018 Challenge】

// you can also use imports, for example:
// import java.util.*;

// you can write to stdout for debugging purposes, e.g.
// System.out.println("this is a debug message");

class Solution {

    int ans = 0;

    public void make(boolean[] p) {
        if (p.length > 0) {
            boolean[] node = new boolean[p.length -1];
            for(int i = 0; i < p.length; i++) {
                if(p[i] == true) {
                    ans++;

                    if(i < node.length) {
                        node[i] = true;    
                    }
                    if(i > 0) {
                        node[i - 1] = true;
                    }
                }
            }

            if (p.length - 1 > 0) {
                make(node);   
            }
        }

    }

    public int solution(boolean[] P) {
        // write your code in Java SE 8

        make(P);
        return ans;
    }
}

Lesson4 - PermCheck

作成日:2018/4/4
Task Score: 58%
Correctness: 100%
Performance: 16%
PermCheck coding task - Learn to Code - Codility

// you can also use imports, for example:
// import java.util.*;

// you can write to stdout for debugging purposes, e.g.
// System.out.println("this is a debug message");

import java.util.*;

class Solution {
    public int solution(int[] A) {
        // write your code in Java SE 8

        ArrayList<Integer> list = new ArrayList<Integer>();

        for(int num : A){
            if (!list.contains(num)) {
                list.add(num);
            } else {
                return 0;
            }
            if (num > A.length) {
                return 0;
            }
        }

        return 1;
    }
}