ticktakclockの日記

技術ポエムを綴ったりします。GitHub idも同じです (@ticktakclock)

Android Billing Library 2.0を使って課金フローを確認してみる

こんにちは、tkyです。

PlayConsoleに登録しないでBilling Library2.0を使ってみた所感などを書いていきます。

目的は『課金フローを理解する』ことにあります。

基本ドキュメント見たら書いてあるため公式のドキュメントを信頼するのが一番良いです。本記事ではドキュメントを読みすすめるにあたって理解の助けになれたらと思います。

Google Play Billing Library を使用する  |  Android デベロッパー

確認したときのリポジトリです。

github.com

どこまで試すことができるのか?

下記、『アプリを Google Play のクローズド テスト版トラックまたはオープンテスト版トラックに公開します。』

と書かれている通り任意の購入アイテムを準備して課金のテストを行うためには「おためし」でやってみるには少しハードルが高いです。。。

developer.android.com

後述しますが、Googleが特定のテスト用アイテムを用意してくれているためこちらを使用して課金のフローまでは確認することが可能です。

課金実装のおおよその流れ

実際の運用を想定するとクラウド上のどこかにサーバを設置しているかと思いますが、おおよそこのような流れになるかと思います。

よくある課金としては100円=100ポイントのような形でアプリ内通貨(石だったり色々な単位)に換金するようなものをイメージしてみます。

f:id:ticktakclock:20200604164340p:plain
シーケンス図

実際は購入するアイテムの一覧はサーバから取得するケースが多いのかなと思います。

このシーケンスにならい実際に組んでみたのがGitHubリポジトリとなります。 一般的にリリースされているアプリはしっかりと設計されたものだと思うのでそういったアプリに課金実装することを想定して、きっちりとパッケージ分けしてサンプルアプリに見合わない構成になっています。 ロジックはすべてUseCase層にまとめています。

github.com

BillingClient生成時にハマったこと

PurchasesUpdatedListenerのリスナーと enablePendingPurchases() をコールしておく必要があるようでした。

これらがないとIllegalStateExceptionが発生してしまうようで、どこかのクラスで PurchasesUpdatedListener を実装する必要があります。

val billingClient = billingBuilder.setListener(this).enablePendingPurchases().build()

coroutines の拡張と使い所

Billing Library 2.1から com.android.billingclient:billing-ktx という Billing Clientで使えるCoroutines拡張があります。

これを使うことでコールバックでしか受け取れなかった(またはRxを使ってObservableやSingleの形にしていた)ものがsuspend funで記述できるようになります。

BillingClient.startConnection() については拡張はありません。これは非同期通信というよりはイベントの形状のためだとは思いますが

GooglePlayストアがバックグラウンドで更新されているときは接続が切れる可能性があり onBillingServiceDisconnected() が発火されるので再接続して処理を維持する必要があります。

なにかするたびに毎回接続してエラーになったら処理を止める、というようにするとわかりやすいのかなと思いました。

Googleが用意しているテスト用のアイテム ID

BillingLibraryの中では sku: String で表現されています。購入対象のアイテム自体をSkuDetailsというクラスで表現しています。

SKUというのはStock Keeping Unit(ストック・キーピング・ユニット)の略で、受発注・在庫管理を行うときの、最小の管理単位をいいます。商品IDみたいな位置づけで購入対象のアイテムを一意に特定できるものとなります。

幸いにもGoogleがテスト用にアイテム ID を用意してくれており、これを使うと「アイテムが購入された」と見なして処理してくれる為、課金フローを試すことができます。

買い切りタイプのものに対して有効で、月額タイプの定期購入は『静的レスポンスは定期購入のテストには使用できません』となっておりできないようです。

  • android.test.purchased

    このアイテム ID を使用して Google Play 請求サービスをリクエストすると、Google Play は「アイテムが購入された」と見なしてレスポンスを返します。レスポンスには JSON 文字列が含まれており、疑似購入情報(例: 疑似オーダー ID)が格納されます。

  • android.test.canceled

    このアイテム ID を使用して Google Play 請求サービスをリクエストすると、Google Play は「購入がキャンセルされた」と見なしてレスポンスを返します。クレジット カードが無効の場合や、課金前にデベロッパーがユーザーの注文をキャンセルした場合など、注文プロセス内でエラーが発生したケースに相当します。

  • android.test.item_unavailable

    このアイテム ID を使用して Google Play 請求サービスをリクエストすると、Google Play は「購入対象のアイテムがアプリのアイテムリストに含まれていない」と見なしてレスポンスを返します。

考えるべきエラーケース

シーケンスからもわかりますが、全てにレスポンスがありますので、あらゆるところでエラー考慮する必要が出てきます。

  • BillingClientの接続
    • 接続が切れていたケース
  • アイテムIDの問い合わせ
    • 存在しなかったケース
  • 購入フローを開始
    • 無効なアイテムIDだったケース
    • すでに購入済みだったケース
  • 自サーバに反映
    • サーバ反映に失敗したケース
  • 購入商品の消費
    • 消費に失敗したケース

しかも面倒なのがBillingClientのメソッドのレスポンスコードが BillingResponseCode に集約されているのもあり網羅的にするにはコスト高だなと思いました。。。

課題になるのが自サーバの反映に成功しても購入商品の消費に失敗した場合、課金履歴とサーバの状態に齟齬が生じるのでどちらかが失敗した場合ロールバックする必要があるのかなと思います。

こちらについては、サーバ側で検証ロジックを持つことである程度解消するのかもしれません。

developer.android.com

まとめ

  • サンプル作るだけでもかなり理解度が増した
  • PurchasesUpdatedListenerが使いにくくてどこで処理していいかわからなかった・・・
  • ついでにFlowとかCoroutinesの処理も書いてそっちの理解度も増した
  • 定期購入についてはα版までリリースを進める必要があるのでテスト方法が悩ましい
  • BillingLibrary3.0の情報がそろそろ出てほしい

gradleのライブラリ依存関係を出す

こんにちは、tkyです。

Android小ネタ集です。色々なブログでも書かれている内容で、ほぼ自分用として書いておきます。

Dependency treeが見たい時、ありますよね?

(ライブラリを特定のバージョンに上げると何故か別の機能がバグるみたいなことがたまにありますそれを解析するためにやったりします。)

先に結論から記載すると以下のようになります。(ファイル出力するかどうかはお好みで)

./gradlew :app:dependencies --configuration developDebugRuntimeClasspath > dependency.txt

configurationについて

./gradlew :app:dependencies だけでコマンドは動きますが、これだと全ビルドバリアント(buildType x productFlavor)分調べるのでめちゃめちゃ時間かかります。

そのためvariantが developDebug(buildType=debug, productFlavor=develop)の時だけ調べるようにしています

調べたいvariantによってここは変えると良いでしょう。

また、RuntimeClasspathのほかにCompileClasspashもあります。

RuntimeClasspath のほうがgradleの dependencies{} ブロックに記述したライブラリの依存ツリーとして見れるので関係性を整理する意味でこちらのほうが可視性が高いかなと思いこちらを使っています。

もうすこしRuntimeとCompileの指定方法による違いを調べたかったのですが今日はここまで。。。

以上、コピペで使えるDependency treeでした。


2020/10/13 追記

こんな感じで .bash_profile とかにalias作っておくといちいちタイプしなくて良いので便利です。

alias dependency='./gradlew :app:dependencies --configuration developDebugRuntimeClasspath > dependency_`date "+%Y%m%d_%H%M%S"`.txt'

EpoxyとDoNotHashとequals()

こんにちは、tkyです。

Androidネタです。epoxyの話です。 ※epoxy is 何の話はしません

epoxyはデータバインディングをサポートしていて、リソースファイルからデータバインディング用のモデルクラスを自動生成してくれたりします。

github.com

上記のドキュメントを読んだり、差分更新について調べているとたまに見かける 「 DoNotHash 」これが何者なのかを少し深堀りして調べてみました。

public @interface EpoxyDataBindingPattern {
  Class<?> rClass();
  String layoutPrefix();
  /**
   * If true, any variable whose type does not implement equals and hashcode will have the
   * {@link EpoxyAttribute.Option#DoNotHash} behavior applied to them automatically.
   * <p>
   * This is generally helpful for listeners - other variables should almost always implement
   * equals and hashcode.
   * <p>
   * For details on the nuances of this, see https://github.com/airbnb/epoxy/wiki/DoNotHash
   */
  boolean enableDoNotHash() default true;     // ★★★DoNotHashこれは何なの?という話です★★★
}
@EpoxyModelClass(layout = R.layout.model_button)
public abstract class ButtonModel extends DataBindingEpoxyModel {
  @EpoxyAttribute @StringRes int textRes;
  @EpoxyAttribute(DoNotHash) OnClickListener clickListener;         // ★★★ DoNotHashこれは何なの?という話です★★★
}

DoNotHash

直訳すると「ハッシュしない/ハッシュなし」と受け取れます。

enableDoNotHash() のコメントも見てみましょう。

Google翻訳より

「trueの場合、型がequalsおよびhashcodeを実装していない変数には、動作が自動的に適用されます。
これは一般的にリスナーに役立ちます-他の変数はほとんど常に等しいとハッシュコードを実装するべきです。」

ちょくで訳すと流石に意味が伝わりづらいですね。equalsやhashcodeという単語はできてました。

equals()とhashCode()

Kotlinにおいては Any型がすべての基底クラスとなりますがここに実装されているメソッドです。

またKotlinにおける等価性は Structural equality と表現され、 ==a?.equals(b) ?: (b === null) が同じであると明記されていることから == 比較を使うと内部的に equals() を使っている、と理解可能です。

kotlinlang.org

話がそれましたが、この2つのメソッドを実装していない変数にはenableDoNotHash()をtrueにする(trueになる)と読めます。

また、「リスナーに役立つ」とありますが、OnClickListener などのInterfaceはActivityとViewModelなど全く別のクラスで実装(implement)できますし

この2つの等価性(==)を見ようとしても確かに異なるよね、となりそうですね。

DoNotHashとしているので hashCode()の値が等しいかどうかを見ているのかもしれません。

以上のコメントの深堀りequals()本来の意味を鑑みると enableDoNotHash()がtrueである ということは 等価性の比較をしない と仮定すると辻褄が合ってきます。

なるほどそうなると差分更新の節で等価性について言及しているということはEpoxy内で描画更新するかどうかは ==もしくはequals() の結果で判断しているのかもしれません。

EpoxyModelクラスに聞いてみた

推測を並べても仕方がないのでカスタムepoxyモデルを作って実際に自動生成されたファイルを実際に見て確認してみようと思います。

ちなみに @EpoxyAttribute にはレイアウトファイルでバインド( )している variableのnameが入ります。

DoNotHashをつける/つけないでequals()の実装に少し変化が見られます。

DoNotHashなし

インスタンスのequalで等価性を確認している。インスタンスが異なればreturn false => EpoxyModelとして異なるもの => 更新の必要がある

    if ((clickListener != null ? !clickListener.equals(that.clickListener) : that.clickListener != null)) {
      return false;
    }

DoNotHashあり

インスタンスの存在を確認している。インスタンスが異なっても”有->無", "無->有" に変化して初めてreturn false => 更新の必要がある

    if (((clickListener == null) != (that.clickListener == null))) {
      return false;
    }

実際のコード

ドキュメントにも記載があるサンプルカスタムモデルに少しだけアレンジ

@EpoxyModelClass(layout = R.layout.model_button)
abstract class ButtonModel : DataBindingEpoxyModel() {
    @EpoxyAttribute
    var textRes: Int = 0

    @EpoxyAttribute
    lateinit var clickListener: View.OnClickListener                           // DoNotHash無し版

    @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash)
    lateinit var clickListenerDoNotHash: View.OnClickListener      // DoNotHash有り版
}

自動生成ファイルButtonModel_.javaのequals()とhashCode()抜粋

  @Override
  public boolean equals(Object o) {
    if (o == this) {
      return true;
    }
    if (!(o instanceof ButtonModel_)) {
      return false;
    }
    if (!super.equals(o)) {
      return false;
    }
    ButtonModel_ that = (ButtonModel_) o;
    if (((onModelBoundListener_epoxyGeneratedModel == null) != (that.onModelBoundListener_epoxyGeneratedModel == null))) {
      return false;
    }
    if (((onModelUnboundListener_epoxyGeneratedModel == null) != (that.onModelUnboundListener_epoxyGeneratedModel == null))) {
      return false;
    }
    if (((onModelVisibilityStateChangedListener_epoxyGeneratedModel == null) != (that.onModelVisibilityStateChangedListener_epoxyGeneratedModel == null))) {
      return false;
    }
    if (((onModelVisibilityChangedListener_epoxyGeneratedModel == null) != (that.onModelVisibilityChangedListener_epoxyGeneratedModel == null))) {
      return false;
    }
    // testRes: Int
    if ((getTextRes() != that.getTextRes())) {
      return false;
    }
    // DoNotHash無し版  ★★★ equals()が増えてる!!! ★★★
    if ((clickListener != null ? !clickListener.equals(that.clickListener) : that.clickListener != null)) {
      return false;
    }
    // DoNotHash有り版
    if (((clickListener == null) != (that.clickListener == null))) {
      return false;
    }
    return true;
  }
  @Override
  public int hashCode() {
    int result = super.hashCode();
    result = 31 * result + (onModelBoundListener_epoxyGeneratedModel != null ? 1 : 0);
    result = 31 * result + (onModelUnboundListener_epoxyGeneratedModel != null ? 1 : 0);
    result = 31 * result + (onModelVisibilityStateChangedListener_epoxyGeneratedModel != null ? 1 : 0);
    result = 31 * result + (onModelVisibilityChangedListener_epoxyGeneratedModel != null ? 1 : 0);
    result = 31 * result + getTextRes();
    result = 31 * result + (clickListener != null ? clickListener.hashCode() : 0);   // ★★★ hashCodeが増えてる!!! ★★★
    result = 31 * result + (clickListenerDoNotHash != null ? 1 : 0);
    return result;
  }

わかったこと

  • DoNotHashが付与されている(=フラグがtrueになっている)プロパティはインスタンスの存在が変化しない限り、

    • EpoxyModelとしては等価の扱い(equals()がtrue)となる
  • DoNotHashが付与されていない(=フラグがfalseになっている)プロパティはインスタンスの等価性が変化しない限り、

    • EpoxyModelとしては等価の扱い(equals()がtrue)となる
  • 見方を変えると、DoNotHashが付与されていない場合において、以下の考え方もできる

    • equals()をoverrideして比較ロジックを独自で組んでいる場合、 インスタンスが異なってもEpoxyModelとしては等価の扱い(equals()がtrue)となる
  • 安易にequals()をoverrideしたクラスをEpoxyModelとDataBindingに与えると意図しない動作(描画更新されない)を引き起こす可能性がある

まとめ

  • EpoxyModelの等価性について調べた
  • DoNotHashの有無でEpoxyModelの等価条件が変わることがわかった
  • 最終的には『DoNotHash=差分更新しない』と読み替えることができる
  • よくわかっていないまま差分更新してたんだな・・・と理解度の浅さを思い知った

プルリクのレビュー件数をグラフにしてみた

こんにちは、tkyです。

社内でプルリクをいろいろなメンバーにレビューしてもらっているのですが、

「最近特定の人ばかりにレビュー依頼してる気がする・・・負荷溜まってないかな・・・」

という一抹の不安をいだき誰にレビュー依頼するか判断する材料にしようと考え(完全に個人用で)作った感じです。

何作ったの

社内のメンバーのプルリク溜まってる件数をグラフにしてみました。

token, organization, teamを入力することでこんな感じのグラフが出るようなものです。

こうすることでレビュー溜まってない人にレビューアサインしたほうが早いかな?など検討の余地が生まれて良いですね。

f:id:ticktakclock:20200414221150p:plain:w500
screenshot

github.com

技術的なところ

せっかくだしTypescriptとVueも一緒に触ってみながら成果物残してみるかー!というモチベーションです。 私はAndroidエンジニアですが、Reactとかのフロントの経験はあるのでそんなに抵抗はないです。

chart.jsのおかげでグラフ化はかんたんでした。感謝・・・

使ったAPI

大まかな流れは /orgs/:org_id/team/:team_id/members で特定のチームに属するメンバーを抽出して

そのメンバーが持っているPRレビューの数を /search/issues で取得します。

Search | GitHub Developer Guide

Team Members | GitHub Developer Guide

/search/issues の課題

search/issues?q=is:open+is:pr+review-requested:ticktakclock こんな感じで私がレビューすべきプルリクIssueが取得できるのですが、

review-requestedが1人しか指定できないのか、例えば

Aさん・・・2件、 Bさん・・・1件、 Cさん・・・4件

あったとして

  • search/issues?q=is:open+is:pr+review-requested:a-san+review-requested:b-san ・・・3件取得
  • search/issues?q=is:open+is:pr+review-requested:a-san+review-requested:c-san ・・・0件取得

みたいなことがあってなぜか複数指定して検索できるパターンとできないパターンがあり

指定したメンバー全員を一気に検索することができなかったことです。

そのため本リポジトリでは1人1人API叩いて検索してます。

配列を一気にasync - await  するのにはPromise.all()でくくってあげる必要がある、というのは本件に関係ありませんがそこそこ重要なポイントかなと思います。

      const res = await api(
        "/orgs/" + this.organization + "/teams/" + this.team + "/members"
      );
      const results: Array<RequestedReviewer> = await Promise.all(
        res.data.flatMap(async (user: User) => {
          const res = await api(
            "search/issues?q=is:open+is:pr+org:" +
              this.organization +
              "+archived:false" +
              "+review-requested:" +
              user.login +
              "&sort=created&order=asc&page=1&per_page=10"
          );
          return { user: user, issues: res.data.items };
        })
      );

これで何が問題かというとGitHubAPIのレート制限にすぐに引っかかるということです。

普通にtoken使ってやる場合は1時間あたり最大60件なので、比較的すぐに制限に引っかかります。

GitHub API v3 | GitHub Developer Guide

まとめ

  • レビュー件数がわかって少し幸せ
  • search apiもう少し拡張してほしい
  • typescriptとVueの勉強しようと思ったのにGitHubAPIの勉強になってしまった・・・

こうすれば複数人のIssue検索できるよ、のような知見があるかたはぜひ教えていただきたいです!!

WebView android.view.InflateException on Lollipop

こんにちは、tkyです。

久々にWebViewで落ちた案件です。Lollipop(5.0)です。悲しみです。

手元にLollipopがないので実機で発生するかわからないのですが、Emulatorで発生するものです。

もしかしたらPlay Storeからandroid system webveiwのバージョンを上げるだけでも回避できる可能性は十分にあります。

E/AndroidRuntime: FATAL EXCEPTION: main

java.lang.RuntimeException: Unable to start activity ComponentInfo{com.your.packagename.UsingWebViewActivity}: android.view.InflateException: Binary XML file line #8: Error inflating class android.webkit.WebView
        at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2298)
        at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2360)
        at android.app.ActivityThread.access$800(ActivityThread.java:144)
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1278)

ぐぐると大体以下のような対策的なのが出てくるのですが、こんな事やっていなくても動いておりまして、はい。

override fun applyOverrideConfiguration(overrideConfiguration: Configuration?) {
    if (Build.VERSION.SDK_INT in 21..25) {
        return
    }
    super.applyOverrideConfiguration(overrideConfiguration)
}

この辺のstack overflowとissue見ると 1.0.2 では動いてるが、 1.1.0DayNight 系の機能実装のときにデグレったみたいでした。

stackoverflow.com

issuetracker.google.com

一応この問題は 1.2.0-alpha02 で対応されているようで、Gradleを更新したらたしかにLollipop WebViewで落ちなくなりました。

implementation 'androidx.appcompat:appcompat:1.2.0-alpha02'

mvnrepository.com

※この時Android Studioのリリースノートには 1.2.0-alpha01 までしか載っていなくてちょっとハマりました 🐥

そのAppCompat、バージョン上がってないですか?

動いていた時、appcompat1.0.2使っていたのですが(古)、問題が起こってたときもGradleは変えていませんでした。

+--- androidx.appcompat:appcompat:1.0.2

とある時期からなぜか1.1.0に依存更新されていたのです。

+--- androidx.appcompat:appcompat:1.0.2 -> 1.1.0

materialの更新で依存バージョンが上がっていた

+--- com.google.android.material:material:1.2.0-alpha04

| +--- androidx.appcompat:appcompat:1.1.0 (*)

僕の場合はMaterialの更新でたまたまappcompatのバージョンが上がって、デグレっていました。

どう回避するかはアプリ次第かと思われます。1.2.0-alpha02 のバージョンアップも多少リスクはあると思うので、プログラムで回避するのも一つの正解かと思われます。

implementation 'androidx.appcompat:appcompat:1.2.0-alpha02'

WebView使っているアプリを開発されている皆様は今一度AppCompatのバージョンとLollipopでの動作確認してみるとよいかもしれません!

以上です!

(追記)プログラムで回避する場合

ActivityにapplyOverrideConfigurationを実装してUI_MODE_NIGHT_MASKを除外するか

override fun applyOverrideConfiguration(overrideConfiguration: Configuration) {
    if (Build.VERSION.SDK_INT in Build.VERSION_CODES.LOLLIPOP..Build.VERSION_CODES.N_MR1) {
        overrideConfiguration.uiMode = overrideConfiguration.uiMode and Configuration.UI_MODE_NIGHT_MASK.inv()
    }
    super.applyOverrideConfiguration(overrideConfiguration)
}

AndroidManufest.xmluiMode のconfigurationChangedを付与するか

        <activity
            android:name=".UsingWebViewActivity"
            android:configChanges="uiMode" />

どちらかかなと思いました。バージョン上げるのにリスクある場合や、影響範囲を限りなく少なくしたい場合はプログラムで回避することも視野に入れて良いと思っています。

Kotlinのif elseとletとalso

こんにちは、tkyです。

Kotlinにおいてnullチェックしてそのインスタンスにアクセスする場合、?.let{} を使うことがわりと多いです。

しかしnullだったときの処理も記述する場合?.let{} だとどうしてもわかりづらくなる場合があり、素直にif elseを使ったほうが読みやすいのではないかな〜という思いから記事書いてみました。

いろいろなNullチェック手法

いくつか書いてみました。わかりやすくするために関数で書いています。

このケースならパターン2の早期リターンが一番効率的だと思いますが、 Androidでいうところの context? などフレームワークで定義されている変数はnullableなことがままあり、 そういったものであればパターン3( context?.let{} )で書くことが多いです。

private fun printMessageLength1(message: String?) {
    // パターン1 if文
    if (message != null) {
        println(message.length) // smart castにより安全にmessageにアクセス
    }
}

private fun printMessageLength2(message: String?) {
    // パターン2 エルビス演算子による早期リターン
    message ?: return
    println(message.length) // smart castにより安全にmessageにアクセス
}

private fun printMessageLength3(message: String?) {
    // パターン3 ?.letを使う
    message?.let {
         println(it.length)
    }
}

private fun printMessageLength4(message: String?) {
    // パターン4 ?.alsoを使う
    message?.also {
        println(it.length)
    }
}

nullだったときに別の処理をする場合

nullだったときに"message is null" と表示してみましょう。

private fun printMessageLength1(message: String?) {
    // パターン1 if文
    if (message != null) {
        println(message.length) // smart castにより安全にmessageにアクセス
    } else {
        println("message is null")
    }
}

private fun printMessageLength2(message: String?) {
    // パターン2 エルビス演算子による早期リターン 複数行あるのでrun{}で書く
    message ?: run {
        println("message is null")
        return
    }
    println(message.length) // smart castにより安全にmessageにアクセス
}

private fun printMessageLength3(message: String?) {
    // パターン3 ?.letを使う
    message?.let {
        println(it.length)
    } ?: println("message is null")
}

private fun printMessageLength4(message: String?) {
    // パターン4 ?.alsoを使う
    message?.also {
        println(it.length)
    } ?: println("message is null")
}

なお、エルビス演算子で複数行扱いたいとき run{} を使うことが多いかなと思います

    // 複数行処理したい場合はrun{}
    message?.let {
        println(it.length)
    } ?: run {
        println("message is null")
    }

let と also の違い

一見、パターン3とパターン4に違いが殆どないように感じます。が、違いは戻り値にあります。

  • public inline fun <T, R> T.let(block: (T) -> R): R
  • public inline fun <T> T.also(block: (T) -> Unit): T

引数として渡る値はどちらもTオブジェクト(今回の場合String?です,nullチェックされているのでStringです)

戻り値はletの場合任意に指定でき、alsoの場合Tオブジェクト(今回の場合String?です)

この前提を踏まえるとletの場合ちょっと問題が起こります。

printMessageLength3("hoge")

private fun printMessageLength3(message: String?) {
    message?.let {
        println(it.length)
        null
    } ?: run {
        println("message is null")
    }
}

出力

I/System.out: 4
I/System.out: message is null

messageがnon-nullなのに、let{}句内の記述内容によって動作が変化してしまうことです。

実際こんなコードを書くことはほぼないのですが、 fun doSomething(): String? のような関数がもし存在してlet内に記述されてしまったら 意図せぬところで不具合が起こることになります。

letとエルビス演算子の組み合わせ(?.let{} ?: run{} )はパット見問題なさそうに見えるが故にすぐに不具合に気づけない可能性も秘めているので 使うならalsoとエルビス演算子の組み合わせ(?.also{} ?: run{})が良いかなと思います。

まとめ

also 「また」は中学で習う英単語です。「AのほかにBもまた○○である」という表現で使われます。

message?.also{ // A}: run{ // B }

「メッセージがnullでなけばAもまた行う。」

ある物と別の物に対して言及するときに使うイメージがあるので若干違和感がありますね。。僕だけかな。。。

なので純粋に if(message != null) { //A } else { //B } と書いたほうが他の実装者に意図も伝えやすく不具合が起こりにくくなるのではないかな、ということを考えながらプログラミングしています。

logcatが出力されなくなった時の対処

こんにちは、tkyです。

既出小ネタ集です。

AndroidStudioで開発中たまにAS上のLogcatが出てこないときがあります。

    read: unexpected EOF!

    --------- beginning of crash

昔から対策自体はありますが、全体的に記載記事が古くなっている方に感じるので今でも使えるよっていう意味も込めて書いてます。

adbサーバを再起動する

$adb kill-server

$adb start-server

これでOKです。adbコマンドが使えない場合パスを通してください。(パスの通し方は割愛で!)

何が起こっているのか?

adbのプロセスから吐かれているエラーらしい。デバイスからよきせぬEOF(End Of File)が飛んできたためのクラッシュと読めます。

ワークアラウンド感が否めませんがadbサーバを再起動することで対処する感じです。

バイスのバッファサイズが小さいと起こりやすい?バッファサイズを多めに設定しておくと発生頻度は少なくなるかもです。

f:id:ticktakclock:20200222153829p:plain:w200