読者です 読者をやめる 読者になる 読者になる

画竜点睛を衝く@mapyo

日々やった事をつらつらと書くブログです

RxJavaのdoAfterNextを使う

こういう事がしたかった。

http://reactivex.io/RxJava/2.x/javadoc/io/reactivex/Observable.html#doAfterNext(io.reactivex.functions.Consumer)

doAfterNextを使うと解決出来そうだった。サンプルコードはこちら publishSubject.onNextのとこらへんで送信開始処理みたいなのを行うイメージ

        val publishSubject = PublishSubject.create<Int>()

        Observable.just(1)
                .doAfterNext {
                    println("doAfterNext")
                    publishSubject.onNext(it * 2)
                }
                .flatMap {
                    println("receive start")
                    publishSubject
                }
                .subscribe({ println(it) })

出力結果はこちら

receive start
doAfterNext
2
  1. flatMapでpublishSubjectが設定される
  2. その後doAfterNextが呼ばれて、publishSubjectで1 * 2が流される
  3. subscribeしてるところで2がとれる

という事で無事に出来ました。もうちょっと早く気がついてたらもっといい川を書けてたかもしれない。

株式会社スマートドライブに入社して4ヶ月くらいがたった

気がついたら3ヶ月が過ぎて試用期間が終わって、4ヶ月過ぎようとしているのでもろもろ書いてみる

何やってる会社なの?

ざっくり言うと車に専用のハードウェアを取り付けて走行データを収集して、スマホ経由でサーバに送ってサーバで解析して、見やすい形にしてブラウザで表示するというサービスをやってる。

車から走行データを収集するためのハードウェア開発、 ハードウェアで吸い上げた情報をBluetoothスマホに送り、 スマホからサーバに情報を送る部分や、情報を表示するためアプリ開発。 サーバ側で各種情報を受け取って解析する部分や各種APIを開発しているサーバサイド開発、 解析した結果を見やすい形で表示するフロントエンド周り。

ハードからアプリ、サーバ、フロントエンドといろんな事をやってる会社です。

ハード周りはあまりちゃんとわかってないけど、ファームウェアC言語で書かれてるっぽい。サーバサイドはRailsとGo。フロントエンドはSPAでReactとか使ってるっぽい。

かなりざっくり書いたけど、もうちょっと詳しく知りたい方は以下のページをさらっと見てもらうのがいいと思います。

blog.smartdrive.co.jp

www.smartdrive.co.jp

www.wantedly.com

jp.techcrunch.com

スマートドライブで僕がやってること

Androidエンジニアとして働いています。

一般的なWebサービスのアプリエンジニアと大きく違うところは、ハードウェアとの通信があるところです。 ハードウェアとスマホBluetoothで接続して情報をやりとりする。WebのAPIとは違ってjsonというわけではないので、どうやって情報をやりとりするのかを話して決めて、実装して。みたいな感じ。

ハードウェアはこのページの下の方にあるやつです。

www.smartdrive.co.jp

サーバとの通信について考えるだけでなく、ハードウェアとの通信も考える必要があって、 その辺が大変なところでもあり、やりがいがあるところでもあります。何かうまく動かないなーと思った時にうまくアプリ側が原因か、ハードウェアが原因か切り分けていろいろやります。

入社してしばらくは裏側のServiceで動く処理を主に書いていてAndroidのViewとは完全に無縁の世界でした。その後View側もそれなりに触るようになって全体感がある程度見えてきたかなという感じです。

 Bluetooth Low Energy

今の会社に入るまではBLE周りに触れ合う事はなかったんですが、AndroidでBLEといえばつらいという感じです。

まだドヤ顔で具体的に何がツラミなのか言えるレベルではありませんが、ざっくり以下のような感じです。

qiita.com

↑のQiitaの記事にも書いてありますが、BLEのツラミに対応するために別プロセスで動くようにしており、プロセス間のやりとりがまた大変だったりもします。

プロセス間のやりとりはdex.fmの以下の回で多少言及されてた気がします。たぶん

http://dex.fm/post/151298631133/10-hacks-in-drivemode
dex.fm

SDK

ハードウェアと通信したりする部分をSDKとしてアプリとは別に切り出していて、別アプリで使う時もそれを使っています。 特に外部に公開しているわけではなく、今のところは内部で使っているものです。

この機能はSDKが持つべきだよなとか、ここからはアプリ側が持たないとなとかその辺を考えたり、SDKとアプリは別リポジトリで管理してるので、普段の開発の進め方とか、ブランチの切り方とか、リリースする時にどうするかとか、その辺いろいろ考えさせられます。

言語とかライブラリとか

ほぼすべてKotlinで書かれています。Javaは2〜3ファイルあるくらいです。 RxJavaもバリバリ使ってます。入社するまでKotlinもRxJavaもあんまり使った事なかったけど、だいぶ使えるようになってきました。

Kotlinスタートブック

Kotlinスタートブック

RxJavaリアクティブプログラミング (CodeZine BOOKS)

RxJavaリアクティブプログラミング (CodeZine BOOKS)

この2つの書籍をAndroid開発してるエンジニアはほぼみんな持っているという意識の高さです。

という事で

4ヶ月くらいたった感想でした。

Realmで暗号化して、中身を確認するまで。

暗号化

Realmで暗号化するには、公式のサンプル通り、以下のような感じでやればよい

realm.io

このサンプルの通りだと、毎回ランダムな文字が割り振られてしまうので、一旦文字列をキーにして、byte列に変換して入れる事にする。 実際に使う時はコードに直接書いちゃうと意味ないのでいい感じに見れないところに置く必要がある。

val key = "fadfasdfdsfdassfasdfa…" // 64文字

RealmConfiguration config = new RealmConfiguration.Builder()
  .encryptionKey(key.toByteArray())
  .build();

Realm realm = Realm.getInstance(config);

中身を確認する

さて、実際にどんなデータが入ってるのか中身を確認したい。 中身を確認するには以下の3つの方法がある。

  1. stetho-realmを使う
  2. Realm Browserを使う
  3. Realm Object Serverを使う

順に書いていく

1. stetho-realmを使う

github.com

これを使う。

https://github.com/uPhyca/stetho-realm#integration

この辺のサンプル通りにやれば、普通に使える。(雑

。。。はずなんだけど、僕が作ってるアプリだと見れなかった。。。

https://github.com/realm/realm-java/issues/3700

この辺が関係しているのかも?ゼロから自分で作って試してみたら、普通に見ることが出来たので何かしらの何かが影響して見れなくなっているんだと思う。

2. Realm Browserを使う

Realm Browser

Realm Browser

  • Realm
  • 開発ツール
  • 無料

ざっくりした使い方は以下を参照

blog.techium.jp

ただし、realmファイルを取得するためには、一工夫必要だった。↑の方法では、端末のインストールしたパッケージのデータ置き場に保存しているファイルを、一度どこからでもアクセス可能な領域にコピーする。と書いてあったけど、これがうまくいかなかった。

なので、以下のコマンドでrealmファイルの保存場所から直接自分のMacに落としてきて、開いてる。 Windowsだとどうなるかわからない。。

adb -d shell run-as com.mapyo.sample cat /data/data/com.mapyo.sample/files/default.realm | perl -pe 's/\r\n/\n/' > default.realm

com.mapyo.sampledefault.realmは自分の環境に合わせてよしなに修正してください。

そして、落としてきたrealmファイルをいざ開こうとすると、

The encryption key must be entered as a 128-character string of hexadecimal values.

こういうメッセージが出た。まあ、暗号化した時のやつを入れればいいんだな。という事はわかったのだけど、128文字??16進数??どういう事??という感じでよくわからなかった。

いろいろググると答えを見つけた。

https://teratail.com/questions/52401

暗号化キーのバイト列を16進数で表せばいいだけだった。 Kotlinだとこんな感じ

val key = "3fdafad...." // 64文字

val encryptionKey = key.toByteArray().map {
    String.format("%x", it)
}.joinToString(separator = "")

println(encryptionKey)

ということで、ここで確認出来たキーを入れれば見れた!

3. Realm Object Serverを使う

僕は試してないけど、

exception-think.hatenablog.com

こんな感じでやれば出来るっぽい(雑 Twitterでstetho-realmで見れないー。。。とつぶやいてたら紹介してもらった!! ありがたや!!

所感

stetho-realmが使えると一番便利そうなんだけど、見れなくて切ない。。。

KotlinでGsonでパースした時の挙動の調査

Kotlin書いてて、基本的にはvalで宣言してかつ書けるのであればnon-nullな形で書いていくのが良い書き方だなぁと考えている。

data classでGsonを使ってパースした時に、non-nullな形で書きたい。 しかし、Jsonはnullとか、そもそもキーが欠落していたりとか、Kotlinのnon-nullでは表せない状態を取る事が出来る。 そうなった時に一体どのような挙動するのか、どのように書くとそのような挙動を吸収できるのか?疑問に思ったので試してみた。

例えば、以下のような感じで書いた時にGsonでパースした結果がどうなるかについて調べた。

NonNullで定義しているケース

data class UserNonNull(
        val id: Int,
        val name: String,
        @SerializedName("has_family")
        val hasFamily: Boolean
)

Nullableで定義しているケース

data class UserNullable(
        val id: Int?,
        val name: String?,
        @SerializedName("has_family")
        val hasFamily: Boolean?
)

パースするJsonは正常系、すべてnullが入った場合、そもそもキーがない場合の3パターン。 合計6パターンでどうなるか調べた。

検証したテストケースはこの辺にアップしているのでご覧ください。

https://github.com/mapyo/GsonKotlinSample/blob/master/app/src/test/java/com/mapyo/gsonkotlinsample/GsonSampleUnitTest.kt

結果

調べてる途中にそりゃそうだわー。と思ってきたので詳細は↑に書いてたテストケースのURLを御覧ください。 KotlinのByteCodeを表示してJavaデコンパイルするとよくわかる。

val id: Int
↓
int id;
val id: Int?
↓
Integer id;

という具合のjavaコードが生成されるからだ。Gsonでパースした結果は、Javaのそれと同じ。 intだとjsonがnullでも0になるし、Integerだとjsonがnullだとnullになる。

Stringの場合は違う。

val name: String
↓
@NotNull
String name;
val name: String?
↓
@Nullable
String name;

NotNullがついているとはいえ、jsonがnullの場合やキーがない場合は容赦なくnullになる。なので、val name: Stringで宣言してても、nullが入ってきて、それを使おうとするとぬるぽになる。

余談だが、AndroidのNullじゃないアノテーション@NonNullで表されてるけど、 KotlinをJavaに変換した時は、@NotNullという名前がつけられてるんだなぁ。

僕の意見

Gsonでパースするときは、生成されるJavaコードを意識しつつ、 Jsonでnullが入ってくるのかどうか。キーが必ずあるのかどうか。 を意識して、NonNullにするか、Nullableにするか選択しましょう。

val name: Stringとしていても、Jsonの内容によってはNullが返ってくるので注意。 これは、別のクラスや配列などをパースするようにしていても同じ事だと思う。 未検証だけど、生成されるJavaのコードの事を考えるとそうなるはず。

この辺をある程度わかった上でコードを書いてると厚みが出るかなぁと思った。そんな今日この頃。

アプリでどこまで難読化されてるか確認する

自分が作ったアプリを難読化した時に、どんな感じで難読化されるのか?というのを確認しないとちゃんと難読化出来てるのかわからないのでやってみる。

ぽちぽちコマンドもろもろ打って確認して。。というのをやるのめんどそうだなぁと思ったら、 コマンド一つでいい感じにやってるqiitaを見つけた。

qiita.com

brew周りとか違ってたり、コマンド周りとか改善ポイントがあったのでちょっと手順をまとめてみる

brewでインストール

dex2jar(名前の通りdexファイルからjarファイルを生成する)とjad(Javaにでコンパイルする)が必要なのでbrewで入れる

brew install dex2jar
brew cask install Caskroom/cask/jad

※ちょっとガチャガチャやりながら入れたので、他にもbrew周りで必要かもしれない

スクリプト作る

.zshrcとかに以下のように追加する。bashは普段使ってないのでわかりません。。。

# Decompile Java classes recursively keeping hierarchy
jadr() {
  jad -8 -s java -d $2 -r $1/**/*.class
}

# Decompile Android application
deapk() {
  local dst=${${1##*/}%%.*}
  dst+=".depackaged.`date +"%Y%m%d%H%M"`"
  unzip $1 -d $dst
  d2j-dex2jar -o ${dst}/classes-dex2jar.jar ${dst}/classes.dex
  mkdir -p ${dst}/classes
  unzip ${dst}/classes-dex2jar.jar -d ${dst}/classes
  jadr ${dst} ${dst}/src
}

以下のコマンドで実行できるようになる

deapk app-release.apk

これを実行すると

app-release.depackaged.201704022234

このフォルダが出来て、srcフォルダ配下にjavaデコンパイルされたコードが入ってる。 普段kotlinで書いてるのでkotlinバージョンとかあるのかなぁ。

参考元のqiitaとの変更点は以下の2つ

  • コマンド実行する度にフォルダの最後に日付が入るようにした
  • brewから入れた場合にコマンドがdex2jard2j-dex2jarになってたので修正

Gson周りで難読化するとどうなるか調べてみたかったのでこれでやりやすい感じになった。 そんな今日この頃。

BaseActivityは作らないようにしよう

割りとあってもいいかなぁー。という派だったけど、

konifar.hatenablog.com

メンバー個々の判断に委ねるといつか瓦解します。これはメンバーのスキル不足ということではなく、締切とのトレードオフを鑑みた結果、とりあえずBaseActivityに実装して回収されない、ということがあり得るからです。

確かにその通りだなーと思ってやっぱりやめようと思った。一番のモチベーションはCalligraphyをどうにかしたいなぁというものだったので、

Calligraphyを使ってフォントを変更したい場合は、Applicationクラスで初期化することも可能です。

という一文をみて、なるほどそれで解決出来るのかー。そんな事を思った今日この頃。

Betaでアプリを配信する

Crashlyticsはちょこちょこ使った事あるけど、Betaは使い慣れてないのでやってみた。 昔作ったサンプルアプリに導入してみた。

コマンドラインからBetaにアップしたい

Android Studioプラグインを使えばアップ出来るのだが、apkファイルを手作業で選択してアップする必要があったので異様にめんどくさい。以下のコマンドでアップする。既にCrashlyticsが導入済みであれば普通に使えるはず。

./gradlew assembleDebug crashlyticsUploadDistributionDebug

crashlyticsUploadDistributionDebugを実行すればよしなにビルドしてアップしてくれるのかと思ったけど、単純にapkファイルをアップするだけでビルドしてくれないので注意。

Release noteを自動的に入れたい

何も考えずにアップすると空欄になるので、どのビルド分がアップされたのか全然わからない。

gradleに以下のような感じで追加する

# defaultConfig配下に追加する
ext.betaDistributionReleaseNotes = getBranch() + getCommitLog()

static def getCommitLog() {
    return  'git log -n 1'.execute().text
}

static def getBranch() {
    return  'git rev-parse --abbrev-ref HEAD'.execute().text
}

これを入れると以下のようにRelese noteに入るようになってた。

f:id:mapyo:20170326134648p:plain

このプルリクで実際に動いてた https://github.com/mapyo/FindTravel/pull/2/files

Betaにアップした時に自動的に配信する

Betaは何もしないとWeb画面上でポチポチしないと配信されない??っぽいので、build.gradleに以下を追加する

ext.betaDistributionGroupAliases = "testers"

testersというグループにアップした時に配信してくれる。 ユーザを指定する時は以下のように書けばよさそう

ext.betaDistributionEmails="BetaUser@yourcompany.com, BetaUser2@yourcompany.com"

参考

Distribution with Gradle — Fabric for Android documentation

qiita.com

qiita.com