画竜点睛を衝く@mapyo

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

fastlaneを使ってGoogle Playのベータ版にアップする

http://mapyo.hatenablog.com/entry/2017/07/23/032630

この辺で日々のビルドをするためにfastlaneを使ったので、次はGoogle Playにアップするところで使ってみる。

いきなり本番は流石に辛いのでベータ版にアップする部分を自動化して、手元で確認してから、Developer Console上で操作して本番に公開という流れをとりたい。

fastlane的なbetaとFabricのBetaとデベロッパーコンソールのBetaといろいろ混同しそうなので注意。

基本的な導入周りはこの辺でいい感じに出来てる

http://qiita.com/gupuru/items/9ddeff0906b02d779ed9

公式ドキュメントも参照

https://docs.fastlane.tools/getting-started/android/setup/

認証情報の設定

https://docs.fastlane.tools/getting-started/android/setup/#collect-your-google-credentials

この辺を見ればOK。内容的には同じだけど、僕のメモを書いとく

  1. Google Play Consoleにアクセス https://play.google.com/apps/publish/
  2. 左上のハンバーガーメニューをクリック
  3. 設定→APIアクセス
  4. サービスアカウントを作成をクリック
  5. ダイアログが表示されて、説明に書いてある通りにGoogle Api コンソールに移動
  6. サービスアカウントを作成
  7. サービスアカウント名を入れる。役割はプロジェクト→サービスアカウントアクターを選択
  8. 新しい秘密鍵の提供にチェックを入れる。キーのタイプはJSON(デフォルトでそうなってた気がする)
  9. 作成をクリック
  10. 秘密鍵が作成されてダウンロードされるので、大事に保存しときましょう
  11. Google Play Consoleに戻って、出てたダイアログの完了をクリック
  12. 新しいサービスアカウントが出来てる事を確認
  13. 新しく出来たやつの、アクセスを許可をタップ
  14. 権限で、「アルファ版とベータ版の APK の管理」以外の権限すべてをOFFにする。これで安心。
  15. ↑と思ったら、「表示設定」の権限は最低限必要だった。アプリ個別に設定するか、全体に設定するかが必要そう。
  16. 終わったらユーザを追加をクリック(しかし、何で権限の変更をしたつもりがユーザの追加になるんだ?権限の設定が終わって初めてユーザを追加するという意味なのかな。)

という感じでもろもろ設定が終わり。権限を絞っているので、何か起きたとしてもアルファ版かベータ版がどうにかなるだけなので、ある意味安心出来る(笑)

ダウンロードしたjsonを好きな場所に置いて、

fastlane/Appfile

ここに追記すればOK

fastlane側の設定

https://docs.fastlane.tools/getting-started/android/beta-deployment/#supported-beta-testing-services

この辺を見て設定する

今回追加したlaneはこちら

  lane :googleplay_beta do
    gradle(task: "clean assembleRelease")
    supply(track: 'beta')
  end

ハマったところは、ここの設定じゃないんだけど、サンプル用にGoogle Play上にアプリを作ったんだけど、公開するために必要なものを一通り設定して、1回でも公開(ベータ版でも可)しないとapiでアップする事が出来なかった。。。

mapping.txtとかどうするの?

BITRISEを使っていると、 deploy-to-bitrise-ioというstepをとりあえず実行しとけば apkとかも含めてそのビルドに保存してくれるので楽。

所感

これでgoogle playのベータにアップするところが自動化出来て最高。という感じ

アプリで日々のビルドを配布する作業を自動化する時に考えた事

現在開発中のdevelopブランチの最新を日々自動的に配布したい。 日々配布してQA担当の方にテストしていただいたり、今のアプリの動きどうなってるんだろ?と思った時にさくっと確認出来て便利。

developブランチにMergeしたタイミングでapk作るのはちょっと細かすぎるかな?という気持ちがあって、1日に1回。 でも、何もdevelopにMergeされてない時にもapkが作られるのでそれはそれで微妙かもしれない。

やりたい事

ざっくり書くと以下のような感じ。

  • 日々の最新のdevelopを使ってFabricのBetaで配布したい
  • バージョン番号もしくは、ビルド番号をカウントアップしてどのビルドなのかをわかるようにしたい

スケジューリング

bitriseを使う。workflowを簡単に設定できて非常に便利。しかしお金はかかりますよと。 毎日指定した時間に発動させる。

バージョン番号もしくは、ビルド番号のカウントアップ

versionCodeとversionNameのどちらかを更新する

日々ビルドしてアップするので、一体どのビルドなんだろう。。。?? というのをわかるようにしないと混乱してしまう。

versionCodeとversionNameのどちらをカウントアップするのがいいのだろうか?

versionCode 1
versionName "1.0.0-alpha.1” // こちらをカウントアップするのがいいと考えた

軽く考えたところで、versionNameの方を変更する事にした。 気分的な問題だけど、versionCodeを日々カウントアップすると実際にアプリをリリースする時に無駄に増えた状態でリリースされてしまいそうと考えたからだ。

日々のビルドは、versionNameの最後に-alpha.1とつけて配布する事にしてみる。

どうやってversionNameの更新するか?

やろうと思えば、いろいろと方法があると思う。

gradleでやるには

https://stackoverflow.com/a/21951328/2172277

この辺を参考にすれば書けるかもしれない。

また、ちょっと話はそれるのだが、将来的にGoogle Playへのデプロイも自動化したいので、その辺も考慮に入れると、

Gradleのgradle-play-publisher vs fastlane

という感じになってくる

どっちがいいんだろうか。。。?と調べてみたところ、1年以上前だけど、いい比較記事を見つけた!

http://qiita.com/shiraji/items/518cb1a20671a6388dd7

fastlaneの方が確かに開発が活発そうな感じなのと、

https://docs.fastlane.tools/getting-started/android/setup/

この辺のドキュメントみたりして比較記事から1年以上たって、いろいろと機能が追加されてそうなので、fastlaneを使ってみる。GroovyよりもRubyの方が僕にとってはコードが読めそう。

fastlaneを使ってversionNameを更新する

fastlaneの準備とかもろもろはちょっとめんどくさくなったので省略する。 versionNameを更新するactionを以下のような感じで作ってみた。

https://gist.github.com/mapyo/e0387f94bf94f6bcb119be7b40ea23ac

fastlane使ったの初めてだし、action作ったのも初めてなのでそんな感じ。 後は、versionNameなどはこの表示形式にする。という決め打ち前提なので、あまり使い勝手は良くないかもしれない。

ビルドと配布

fastlaneのlaneの中で以下のような感じでやる

gradle(task: "clean assembleDebug crashlyticsUploadDistributionDebug")

crashlytics用のactionも用意されているっぽい感じだったけど、既にgradleで配布出来てたし、別に使わなくてもええやん。という気持ちで雑にgradleを使うようにした。

所感

fastlane、今まで全然使った事なかったけど、Android開発者にとっても、便利そうだ!! そして、bitriseが結構便利です!!

今度はアプリをgoogle playに配布するとこら辺を自動化してみたい気持ち。

KotlinでSealed Classを継承したクラスをGsonでシリアライズする時の話

こんな感じのseald classを作る

sealed class SampleEvent(open val name: String)

class SampleEventA(
        override val name: String
): SampleEvent(name)

こんな感じでJson作る

Gson().toJson(SampleEventA("aaaaa"))

気持ちとしては以下のようなjsonが出て欲しい

{"name":"aaaa"}

でもエラーが出る

SampleEventA declares multiple JSON fields named name

ほほう

Android Studioの、

Tools > Kotlin > Show Kotlin Bytecode

でBytecodeからのJavaにDecompileしてどんな感じか見る。

public final class SampleEventA extends SampleEvent {
   @NotNull
   private final String name;

   @NotNull
   public String getName() {
      return this.name;
   }

   public SampleEventA(@NotNull String name) {
      Intrinsics.checkParameterIsNotNull(name, "name");
      super(name, (DefaultConstructorMarker)null);
      this.name = name;
   }
}

public abstract class SampleEvent {
   @NotNull
   private final String name;

   @NotNull
   public String getName() {
      return this.name;
   }

   private SampleEvent(String name) {
      this.name = name;
   }

   // $FF: synthetic method
   public SampleEvent(@NotNull String name, DefaultConstructorMarker $constructor_marker) {
      this(name);
   }
}

abstract classと継承したclassの両方でnameが作られていてそれが原因でエラーが出てるようだ。

javaでabstractを使った時にちゃんと動かすならこんな感じで書けばよさそうだった

abstract class JavaSampleEvent {
    protected final String name;

    public JavaSampleEvent(String name) {
        this.name = name;
    }
}

class JavaSampleA extends JavaSampleEvent {

    public JavaSampleA(String name) {
        super(name);
    }
}
String json = new Gson().toJson(new JavaSampleA("aaaa"));

最後に

結局、Seald Classを使わずにInterfaceを使って対応したのだけど、こういう場合はどうやってやるのがよかったのだろうか。。。??

追記

Twitterでいい感じに出来る方法を教えて頂きました!!

sealed class SampleEvent {
    abstract val name: String
}

class SampleEventA(
        override val name: String
) : SampleEvent()

abstractでプロパティを宣言すれば大丈夫だった。 sealed class使う時にいつもopen使ってやってたけど、こっちの方がよさそうだなー

How to convert Completable to Observable? Rxjava2

Observableな川の流れの中で、Completableを使った時に、どうやってまたObservableとして流せばいいのか?と考えてた。

http://reactivex.io/RxJava/javadoc/io/reactivex/Completable.html#andThen(io.reactivex.ObservableSource)

andThenというオペレータを使えばよさそう。

具体的なコードはこんな感じ

        Observable.just(1, 2, 3)
                .flatMap { number ->
                    Completable.fromAction {
                        showMessage("completableTest: " + number)
                        Thread.sleep(300)
                    }.andThen(Observable.just(number))
                }
                .doOnNext {
                    showMessage("doOnNext")
                }
                .doOnComplete {
                    showMessage("doOnComplete")
                }
                .subscribe({
                    showMessage("onNext: " + it.toString())
                }, {
                    it.printStackTrace()
                    showMessage("hoge")
                }, {
                    showMessage("onComplete")
                })

実行結果はこんな感じ

main:completableTest: 1
main:doOnNext
main:onNext: 1
main:completableTest: 2
main:doOnNext
main:onNext: 2
main:completableTest: 3
main:doOnNext
main:onNext: 3
main:doOnComplete
main:onComplete

※mainはスレッドの名前を出しているだけ

どう書いたらいいかわからなかったので、いろいろと試行錯誤してた

ダメなパターン1

.toObservable<Int>()でObservableに変換するだけのコード

        Observable.just(1, 2, 3)
                .flatMap { number ->
                    Completable.fromAction {
                        showMessage("completableTest: " + number)
                        Thread.sleep(300)
                    }.toObservable<Int>() // ここが変わっただけ
                }
                .doOnNext {
                    showMessage("doOnNext")
                }
                .doOnComplete {
                    showMessage("doOnComplete")
                }
                .subscribe({
                    showMessage("onNext: " + it.toString())
                }, {
                    it.printStackTrace()
                    showMessage("hoge")
                }, {
                    showMessage("onComplete")
                })

実行結果

main:completableTest: 1
main:completableTest: 2
main:completableTest: 3
main:doOnComplete
main:onComplete

Completableの中身はちゃんと実行されているけど、onNextには何も値が流れてこない。

ダメなパターン2

toObservableからの、startWith

        Observable.just(1, 2, 3)
                .flatMap { number ->
                    Completable.fromAction {
                        showMessage("completableTest: " + number)
                        Thread.sleep(300)
                    }.toObservable<Int>()
                            .startWith(number)
                }
                .doOnNext {
                    showMessage("doOnNext")
                }
                .doOnComplete {
                    showMessage("doOnComplete")
                }
                .subscribe({
                    showMessage("onNext: " + it.toString())
                }, {
                    it.printStackTrace()
                    showMessage("hoge")
                }, {
                    showMessage("onComplete")
                })

実行結果

main:doOnNext
main:onNext: 1
main:completableTest: 1
main:doOnNext
main:onNext: 2
main:completableTest: 2
main:doOnNext
main:onNext: 3
main:completableTest: 3
main:doOnComplete
main:onComplete

一見、onNextも呼ばれてそうだし、大丈夫そう!と思うのだが、順番がおかしい。 川の流れ的に、completableTestが呼ばれてからonNextが呼ばれてほしいが、逆になってしまっている。

よさげなパターン

一度singleに変換してobservableに変換したら上手く言った。 上手く言ったのでCompletableのところだけ貼る。

                    Completable.fromAction {
                        showMessage("completableTest: " + number)
                        Thread.sleep(300)
                    }
                            .toSingleDefault(number)
                            .toObservable()

Conclusion

川を使いこなすにはまだまだ修行が必要だー

雑なサンプル置き場を作ってる https://github.com/mapyo/RxJavaSamples/blob/master/app/src/test/java/com/mapyo/rxjavasamples/RxExampleUnitTest.kt

Pythonでバイナリを保存する時

最初は

        f = open('hogedata','w')
        for packet in packets:
            f.write(packet)
        f.close()

こう書いてた。Macだと上手くいくのだけど、Windowsで試して貰った時にダメだった。ダメだったというのは、Macで作った時と何故か違うファイルが出来ているっぽかった。ファイルサイズもそもそも違うし、出来たファイルを後工程で使うものに渡してもエラーで動かない。

あれなんでだろ。。?と思っていろいろ調べていくと、

https://docs.python.jp/2.7/library/functions.html?highlight=open#open

 バイナリとテキストファイルを区別するシステムでは、ファイルをバイナリモードで開くためには 'b' を追加してください; 区別しないシステムでは 'b' は無視されます。

あと、毎回新しくファイルを作り直したかったので、w+にした

        f = open('hogedata','w+b')
        for packet in packets:
            f.write(packet)
        f.close()

バイナリをなんかする時はちゃんとバイナリモードでファイルを開きましょう。

それにしても、Mac上でもうまくいかないでほしい。。けど、Wndowsとなにかしら違うからそういうもんなのかな?(雑)

別プロセスで動いている生き死にするServiceで非同期処理をする事を考える

AndroidのServiceのお話。以下のようなServiceがある。

  • Serviceを別プロセスで動かしている
  • destroyが呼ばれた時は、Process.killProcess(Process.myPid())を呼んで自分自身のプロセスをkillする。
  • 定期的に生き死にする

こういったSerivceで非同期処理をすると、ちゃんと非同期処理の終了を待つ処理をいれないと、 非同期処理してる間にサービスが死しに、プロセス自体が死ぬので非同期処理が完了する前に終わってしまう。。。

みたいな事があります。

なので、別プロセスのSerivceのライフサイクルに依存しない形で非同期処理をしたい。

IntentServiceを使ってアプリのプロセスで動かすようにすると、 管理しやすいのでないか?と思ったので実際にコードを書いて試してみます。また、Serviceの中で非同期処理して、見事に途中で終わってるよね。という事も確認したかったので、それも試してみています。

実験した内容は以下の2つ

  1. 何も考えずにServiceの中で非同期処理をする
  2. アプリのプロセスでIntentServiceを呼んで非同期処理する

実験するServiceの仕様

  1. Serviceが起動する
  2. 5秒たったらstopServiceを呼んで停止する
  3. onDestroyが呼ばれた時にProcess.killProcess(Process.myPid())を呼んで自分自身をkillする

以下のようなコードをSerivceのonCreateに入れて、5秒たったら停止するようにする

        Handler(Looper.getMainLooper()).postDelayed({
            showMessage("stop service")
            this.stopService(Intent(this, SampleService::class.java))
        }, 5_000)

1. 何も考えずにServiceの中で非同期処理する

以下のような感じで1秒毎にメッセージを表示して、10秒たったら終了。みたいな処理をServiceのonCreateの中に書く。

        var counter = 0
        timer.schedule(object : TimerTask() {
            override fun run() {
                counter++
                showMessage("counter: " + counter)
                if (counter == 10) {
                    Handler(Looper.getMainLooper()).post {
                        showMessage("counter finished")
                        timer.cancel()
                    }
                }
            }

        }, 1_000, 1_000)

結果

counter: 1
counter: 2
counter: 3
counter: 4
stop service
destroy service

Serviceは5秒で停止する。onDestoryが呼ばれた時に自分自身をkillしてるので、counterは10秒までは回らない。 まぁ、プロセスが死ぬんだから、その通りだ。

2. アプリのプロセスでIntentServiceを呼んで非同期処理する

こんな感じでAndroidManifest上で以下のように書きます。Serviceを別プロセスで動かして、IntentServiceをアプリのプロセスで動かす。という書き方。

        <service android:name=".SampleService"
            android:process=":SampleService" />

        <service android:name=".SampleIntentService" />

https://developer.android.com/guide/topics/manifest/service-element.html

android:processの仕様はこの辺に書いてある。

processを書かない場合はアプリのプロセスで動くと書いてあって、ちゃんとアプリのプロセスで動くよね?呼び出し元のプロセスで動かないよね?という事も確認したかった。

実験したIntentServiceの仕様

SampleIntentServiceというIntentServiceのクラスを作って、onHandleIntentの中で以下のように書きます。

        (1..15).map {
            Thread.sleep(1000)
            showMessage("SampleIntentService counter: " + it)
        }

こんな感じで、1秒毎にカウンターを表示している。

結果

  • IntentServiceの方は最後までカウンターが表示されていた。
  • プロセスもちゃんとアプリのプロセスで動いてた。
  • 別プロセスで動いているServiceの方は普通に起動して5秒後に死んでいた。

最後に

実験に使ったコードはこちらにアップしています。 https://github.com/mapyo/ProcessSample

IntentServiceを使うと、処理が終わったら勝手に終了してくれるし、連続で呼んでしまったとしても、逐次実行してくれるようになってるし、いろいろと便利です。

RxJavaでretryWhenを使ってエラーをキャッチして別のエラーを流す

https://speakerdeck.com/yuyakaido/droidkaigi-2017?slide=86

この辺の話に近い。

Observable.create<Int> { emitter ->
    emitter.onNext(1)
    emitter.onNext(2)
    emitter.onError(HogeException())
    emitter.onComplete()
}.retryWhen { observable ->
    observable.flatMap { e ->
        val exception = if (e is HogeException) FooException() else e
        Observable.error<Int>(exception)
    }
}.subscribe({
    println(it)
}, { e ->
    println(e.javaClass.simpleName)
}, {})

class HogeException : RuntimeException()
class FooException : RuntimeException()

こんな感じでHogeExceptionが流れてきたら、FooExceptionにExceptionを変更して流す。

そんな1日だった。いや違うけど。