へなちょこSEの考察

0x22歳のへなちょこSEが、日々思うことを考察します。自社内、金融系を経て現在法人系PKG開発に従事。

【Expo / React Native】mobx-persistを使ってクラスを永続化した場合に、setter/getterが動かない

React Nativeで永続化したくて、Mobxの永続化が可能なmobx-persistを使いました。

mobx-persistはプリミティブ型なら特に問題なく永続化/読み出しが可能ですが、オブジェクトなどは注意が必要です。

基本的にはプリミティブ型に変換してstoreに保存し、取り出す際に変換して戻す、といったことをした方が安全ですね。

で、今回はクラスを定義してそいつをstoreに保存、永続化しようとしました。

諸悪の根源:Date型

Date型なのでnumberで保存してgetで変換して戻す形にしたのですが、今思えばこれが諸悪の根源ですね。

こんなクラスを書いていました

export default class HogeClass {
    /** ID */
    id: string;

    /** 作成日時 */
    private _createdAt: number;

    /** 更新日時 */
    private _updatedAt: number;

    public get createdAt(){
        return new Date(this._createdAt);
    }

    public get updatedAt(){
        return new Date(this._updatedAt);
    }
}

で、これを動かします。

すると、なぜかgetを呼んでも実行されず、undefinedが返ってくる・・・。

どうも、mobx-persistで永続化するときにメソッドはちゃんと保存できないようです。

まぁ、全体的にstringになっちゃうっぽいので仕方ないですね。

JSON.stringfyとか使えばいい感じになったりするのかしら?


追記

どうやら、JSON.stringifyの仕様によるものらしい??

JSON.stringifyはオブジェクトをStringに変換する際、関数は消してしまう仕様の様子。

残すことも可能っぽいけど、ちょっと面倒そうなのでもうあきらめることにした。

【Expo (React Native)】Expo SDK 36が出てたので更新してみた

ふと見たらExpoSDKのv36がリリースされてました。

これまでは35を使ってたので更新してみました。

blog.expo.io

こちらの記事を参考に実施。

まずはブランチ切り替え

うまくいかなかったらやなので、ブランチ作りました。

名前はSDK36。まんまです。

ブランチ作ったら、そちらに切り替えます。

expo-cliの更新

まず、expo-cliを最新にする必要があるようなので、こちらを更新します。

npm i -g expo-cli

ちなみにWindowsだとexpoコマンドを一回でも叩いてると上記は失敗しますね。

その場合は再起動してから実行しましょう。

アップグレードコマンドの実行

記事の通り、アップグレードコマンドを実行します。

expo upgrade

で、それだけやって、とりあえず実行。

念のため、

expo start -c

そしたら、React Nativeのバージョンが違うと怒られました。

React Native version mismatch への対応

メッセージを見るとReact Nativeのバージョンが合ってないよと。

よくわからんけどキャッシュの問題っぽい。エラーメッセージに対応方法出てるけど、watchmanがないため実行できない。

watchman watch-del-all ←実行できない

前回似たようなキャッシュ絡みの事象が起きた際は、node_modulesを消したらうまくいったので、それをやってみる。

del node_modules npm i expo start -c

治らない・・・。

react-native-navigationをアップデートする

いろいろググると、react-native-navigationのバージョン違いで起きることがあるらしい。 とりあえず試してみる。

npm i --save react-navigation react-navigation-stack react-navigation-tabs

・・・違うエラー出た。

フォント(アイコン)がロードできないエラー解消

package-lock.jsonを消したらうまくいったよという書き込みを見つけたのでやってみる。

(ただし、その人のはSDK34→35のアップデート)

package-lock.jsonの削除と再npmを実施。

起動しました!!


そこそこ時間かかりましたが、とりあえず起動成功。

ただ、起動したらたくさん黄色いwarningが出ました。

一個ずつ見ていかないとなぁ。

【Chrome拡張】Twitter Timerをリリースしました。

Google Chrome拡張機能、「Twitter Timer」をリリースしました。

chrome.google.com

Twitterをついつい使いすぎてしまうツイ廃の方に、現実との接点の時間を増やしてもらうことを目的とした拡張機能です(?)。

何ができるのか

この拡張機能をインストールしてTwitterのサイトを開くと、設定されている時間の経過後、タブが勝手に閉じます。

ただ、それだけの機能です。

一応、機能のON/OFFの切り替えと、タブを閉じるまでの時間は設定可能です。

何で作ったの

Twitterでとある方が「5分でTwitterやめられない」的な発言(90%端折った)をされていたので、なるほど5分で見れなくすればいいんだな、と思って作ることにしました。

というのが5%の理由で、残り95%はこれくらいの機能ならすぐ作れそうだし、Chrome拡張って気になってたからお試しにちょうどいいや、という理由です。

実際、雰囲気はつかむことができたので良かったです。

ただ、Chrome拡張で何か作るアイデアが浮かびそうかというと、こういうネタ的なのしかないなぁというところですが・・・。

どんな風に作ったか

以下にソースは置いてあるので、Chrome拡張を触ってみたいという方は参考にしてくださってもよいかと思います。

ほんと、たったこれだけ書けば動かせるので、とっかかりには良いです。

github.com

【セワシタ?】カレンダー機能とグラフ機能を追加しました。

個人開発中のセワシタ?アプリにカレンダー機能とグラフ機能を追加しました。

どちらも全然きれいに作ってないのですが、もうちょっと修正したらGitHubで公開してみたいところです。

グラフ機能

f:id:hareruya_maro:20191125162423p:plain
グラフ機能

シンプルな折れ線グラフが書けるReact Native用コンポーネントを作っています(react-native-svg使用)。

日付と数値のデータを渡すと、最大日と最小日の間で日付ごとにプロットし、ない日付はうまく飛ばして表示してくれます。

既存のグラフコンポーネントは渡したポイントを等間隔にプロットしてしまうので、毎日とか毎週同じ曜日とか、等間隔のデータでないとグラフとして意味をなさないものが多かったのですが、今回はデータの取得日の間隔が等間隔でなくてもうまく表示してくれるので、データの傾きが正しく見れます。

一応、グラフエリアをスクロールすることも可能です。

カレンダー機能

f:id:hareruya_maro:20191125162428p:plain
カレンダー機能

カレンダー機能ははじめ、「react-native-calenders」を使ってました。

GitHub - wix/react-native-calendars: React Native Calendar Components 📆

とてもきれいなコンポーネントで、いろいろ便利な機能もあるのですが、紙のカレンダーの視認性を目指したかったので、自前で作りました。

指定した日付にアイコンの設定を渡すと、アイコンを表示してくれるカレンダーです。

各日付をタップした際にイベントを実行することも可能なので、一覧で見て日付タップしたら詳細、みたいな使い方も可能です。

どの程度作りこんだら公開するか悩みどころですが、まぁ完璧を目指してもあれなので、適当なところで公開します。

セワシタ?

セワシタ?

  • Haruya Nakamura
  • ライフスタイル
  • 無料
apps.apple.com

play.google.com

【React Native/firebase】firestoreのページネーションを使ってみた

作っている「セワシタ?」アプリでチャットルームの機能を提供しています。

ユーザー(家族)同士で会話したり、誰かがお世話を実施したことを登録すると、ペットが「お世話してくれてありがとう!」と言ってくれる機能です。

(将来的にはLINE連携にしたいのですが、今はまだできていません)

ページネーション(pagenation)とは

複数件のデータが取れる場面で、

  1. まずは最初の100件を取得し、
  2. ユーザーが希望したら(次へボタンを押す、ページの最後までくる、など)次の100件を表示する、
  3. それを繰り返すことで最初の取得件数を節約したり、ロード時間を短縮したり、

といったことをするための方法ですね。

これをチャットルームで、最新のメッセージ25件だけ表示し、過去分はボタンをクリックしたら表示する、という機能を追加しました。

firestoreでのページネーション

firestoreにはページネーションを実現するための機能が提供されています。

まずデータを取得する際に「limit」で件数を指定することができます。

今回のチャットアプリではメッセージの登録日時を持っているため、登録日時の新しい順に25件取得する、ということをします。

chatCollection.orderBy('createdAt', 'desc').limit(25).onSnapshot(async snapshot => {
            this.setState({
                lastMessage: snapshot.docs[snapshot.docs.length - 1] ,
            });

※最後の一件をstateに退避しておきます

その後、次の25件を取得したいとユーザーが指示した場合、先ほど取得した25件の最後のものを指定して、「startAfter」を使用します。

let next = chatCollection.orderBy('createdAt', 'desc')
            .startAfter(this.state.lastMessage!.data().createdAt)
            .limit(25);

stateに退避しておいたlastMessageのcreatedAtを指定して「startAfter」することで、指定された「createdAd」より後のデータが取得できます。

ちなみに今回は並べ替えのキーがcreatedAt(つまり日時)のみでしたが、複数項目で並べ替えをする場合は、startAfterにはorderByした項目をその順番で指定してやります。

複数指定については公式ドキュメントも参照してください。

クエリカーソルを使用したデータのページ設定  |  Firebase

snapshotを使った時の問題点

firestoreのsnapshotを使うと、データに変更があった際に通知してくれ、画面の更新ができます。

ただ、snapshotとlimitを同時に使うと問題も発生します。

はじめ、25件取得した後で、新しいメッセージが投稿されたとしましょう。

すると、新しく1件追加されると同時に、25件目だったデータは26件目になってしまうため取得されるデータから消えます。

これをそのまま画面に表示していると、一件新しいメッセージが来たら、一件古いメッセージが消えた、みたいなことになります。

また、追加の25件を取得していた場合も、うまく取得していないとあいだの一件が消えちゃったとか、そういうことになりかねません。

今回の「セワシタ?」アプリでは過去のメッセージの削除や更新は考慮せず、常に新しいメッセージの追加しかないという前提のため、以下のような仕組みとしました。

  1. snapshotについては最初の25件のみ設定する
  2. 追加のもの以降は常に通常のgetで取得する
  3. 取得したデータはstateにコピーを作成しそこに追記していく形とする
  4. snapshotで取得された新規メッセージはunshiftでstateのメッセージの1番前に追加する

最終的にはデータ取得する箇所はこんな感じになりました。

   /** チャットのメッセージ取得 */
    componentDidMount = async () => {
        // コレクションの取得
        const chatCollection = Fire.shared.chatCollection(store.familyId);

        // 最初の25件のみ取得、onSnapshotを設定し変更を受信する
        const chatUnsubscribe = chatCollection.orderBy('createdAt', 'desc').limit(25).onSnapshot(async snapshot => {

            // stateのメッセージをコピー
            const newMessages = this.state.messages.slice();

            // snapshotのうち変更のあった行のみ取得(docChanges)
            // 25件取得したものを逆順(reverse)に取得して一番前に追加(unshift)することで
            // 複数件更新を受信した場合にも対応する
            // (通信状態が悪く、一度に2件以上のメッセージを受信した場合を想定)
            snapshot.docChanges().reverse().forEach(async chatMessage => {

                // 追加されたメッセージのみ対象
                if (chatMessage.type === 'added') {
                    const message = new ChatMessage(chatMessage.doc);

                    // unshiftで一番前に追加
                    newMessages.unshift(message);
                }
            });

            this.setState({
                // 初回以降はonSnapshotの中では最後の一件を設定しない
                // pagenation対応のため
                lastMessage: this.state.firstFlg ? snapshot.docs[snapshot.docs.length - 1] : this.state.lastMessage,
                // チャットのメッセージ配列を設定
                messages: newMessages,
                // 最初の一回の時のみtrue、以降はコンポーネントが再ロードされるときまでずっとfalse
                firstFlg: false,
            });
        });
        Fire.shared.unsubscribeFunctions.push(chatUnsubscribe);
    }

    /** 過去のメッセージを読み込む */
    getOlderMessage = () => {

        // react-native-gifted-chat用の設定
        this.setState({ isLoadingEarlier: true });

        const chatCollection = Fire.shared.chatCollection(store.familyId);

        let next = chatCollection.orderBy('createdAt', 'desc')
            .startAfter(this.state.lastMessage!.data().createdAt) // 前回の最後のデータの日時
            .limit(25);

        // 過去のデータの更新はないため、通常のgetで取得する(onSnapshotしない)
        next.get().then((snapshot) => {
            const newMessages = this.state.messages.slice();
            snapshot.forEach(async chatMessage => {
                const message = new ChatMessage(chatMessage);
                newMessages.push(message);
            });
            this.setState({
                lastMessage: snapshot.docs[snapshot.docs.length - 1], // 新しい最後の一件を設定
                messages: newMessages,
                isLoadingEarlier: false, // react-native-gifted-chat用の設定
            });

        })
    }

ちなみに、チャット部分にはreact-native-gifted-chatを使ってます。

すごく簡単にチャット機能作れちゃうのすごいんですが、ちょっと癖がある・・・。

github.com

チャット機能だとたぶんこんな感じで最初の部分だけonSnapshot設定して追加されたデータは自分でマージって感じにするのが、データのロード量とかも少なく抑えられていいのかなぁという感じ。

もっといい方法思いつくかもしれませんが。

チャットみたいに常に最新のみ追加されるわけじゃないときのページネーションはとりあえず今は考えたくない。

本当にキレイに表示するなら、データのマージとかをゴリゴリ自分で頑張るしかないんだろうなぁ。。。

【セワシタ?】グラフ機能を追加しました。

セワシタ?アプリのアップデートを行いました。

タイトルの通り、グラフ機能を追加しています。

グラフ機能を追加するにあたり、グラフの描画コンポーネントをreact-native-svgを使って自作しました。

まだ自分に必要な機能しか付加してませんが、とりあえず日付と値を渡すと、抜けてる日付を補完して折れ線グラフを作ってくれます。

もうちょっと奇麗にしたらgithubに上げる予定です。

f:id:hareruya_maro:20191122000945p:plain
折れ線グラフ

こんな感じで、値のない日付もグラフは作って、値のあるところだけ折れ線でつないでくれます。

一応グラフエリアはスクロールにも対応。

縦横にスクロールします。

こんなの、需要ありますかね?

React Nativeで開発するならExpoなんだけど、OTAアップデートが逆に困る件

React Nativeで開発をするなら、Expoはとっても便利です。

最高なのは実機での確認がすごく簡単なこと。

特にiPhoneでの開発には通常はMacが必須なわけですが、普段使うのはWindowsなんだよなぁなんて自分みたいなエンジニアには、Windowsで開発しながら即時でiPhone実機で確認ができるのはすげー便利です。

ただ、実際に運用するフェーズに入ったら、Expoのメリットの一つであるOTAアップデートが困る面もあります。

OTAアップデートとは

ここで言うOTAアップデートとは、Expoで「Publish」なるコマンドを使うと、アプリストアのバイナリをアップしなおさなくても、ユーザーのアプリが更新される機能のことです。

更新されるタイミングは再起動が必要だったり、制御するにはそれようのプログラミングが必要ですが、いちいち審査に出して公開を待つ、なんてことをするよりは全然早くて便利です。

ただ、一点だけ困るのは、アプリストアのアプリを更新するために「build:ios or android」を実行しても、Publishされてしまうこと。

一応、release-channelなんて機能もありますが、公開用のチャンネルに対してビルドしたら、やはり勝手にアップデートされてしまいます。

個人開発だったり気にしないレベルの更新とかならいいんですが、大規模アップデートだったりとか、会社で作ってるアプリだったりすると、更新されるべきタイミングを制御したい場合もあったりします。

そうすると、リリースチャンネルは使い捨てのように毎回変えながらビルドしていく形にしないといけない・・・??

たぶん、ブランチを切って開発したらそのブランチ名でリリースチャンネルを設定してリリース、次のブランチはまたリリース時にブランチ名でリリースチャンネルを指定してリリース、って感じで行くのが良いのかなぁ。

そうすると、通常の開発だとブランチで開発して終わったらマスターにマージ、って感じになりますが、ブランチで開発してリリースチャンネルを作ったら以後そのブランチをマスターとして、また別にブランチを切って、バグは元のブランチ=新しいマスター側に反映してリリース・・・みたいな運用になるんでしょうか。

そういうルールで、リリースチャンネルをブランチ名から自動指定できるようにしたら、まぁできないこともない運用かなぁ。

リリースチャンネル運用、計画的にやりましょう。