言いたいことはそれだけか

JavaとかAndroidとか調べたことをメモします。٩( 'ω' )و

Kotlin 1.1.2-5くらいからSAM変換がちょっと変わってcrashが起きた件

Kotlinのバージョンあげたら既存コードがcrashしたのでちょっと調べてみたメモ。

何が起こったのか

kotlin のversionを1.0.5-3から1.1.2-5にあげたら既存コードがcrashするようになった。具体的には、こんな感じのIllegalStateExceptionが吐かれる。

java.lang.IllegalStateException: invoke(...) must not be null

何が悪かったか

Kotlin側に渡すJavaで定義したFunction0のオブジェクトでinvoke()をoverrideした際にreturn nullしていたのが悪かった。Javaでいうvoidに当たるUnitのインスタンスをreturnすることで落ちなくなる。

        new SamConversionExperimentalKt().invokeFunctionFromKotlin(new Function0<Unit>() {
            @Override
            public Unit invoke() {
                doSomething();
                return Unit.INSTANCE; // return nullするとcrashになるよ
            }
        });

Android Studio(v2.3)だとinvoke()メソッドを補完するときにreturn nullで補完してくれるからいちいち手で書き換えないといけない_:(´ཀ`」 ∠):
Javaでいうvoidはkotlinのnull許容・非許容と離れた概念だからどっち返しても大丈夫でしょ、とか思ってたら思わぬところで痛い目をみる。

で、問題はなんだったの?

実はこのcrashが発生するのにいくつか条件があって、

  1. JavaでFunctionN系のinvoke()メソッドをreturn nullしてoverrideする
  2. 1で定義したインスタンスをkotlinのコードに渡す
  3. 2のkotlinのコードからさらにjavaのコードに渡して、invoke()ではなくSAM変換後のメソッドとして実行する

3がちょっとなんて言ったらわからないのでコードを交えて説明。

適当なAndroidのコードで試したものです。まずはFunction0型のインスタンスをkotlinのコードに渡します。

public class SamConversionExperimental {

    public void invokeFunction() {
        new SamConversionExperimentalKt().invokeFunctionFromKotlin(new Function0<Unit>() {
            @Override
            public Unit invoke() {
                doSomething();
                return Unit.INSTANCE; // return nullするとcrashになるよ
            }
        });
    }

    private void doSomething() {

    }
}

その後、受け取ったkotlinコード側でSAM変換がかかるような感じで実行してやればcrash. 手元だとRunnable#run() が走るようにしてcrashさせた。
(Runnableは単一メソッドrun()だけを持つインターフェイス

    fun invokeFunctionFromKotlin(function: () -> Unit) =
        Handler(Looper.getMainLooper()).post(function)

SAM変換変わったね?

エラー文から察するに、この辺りのチェックコードがversion上がって入ってきたっぽい。 github.com

    public static void checkExpressionValueIsNotNull(Object value, String expression) {
        if (value == null) {
            throw sanitizeStackTrace(new IllegalStateException(expression + " must not be null"));
        }
    }

で、このメソッドがどこから呼ばれているかというと、この辺り。

kotlin/RedundantNullCheckMethodTransformer.kt at a5620454fa2fef926b4ca35b95fdb46a44506211 · JetBrains/kotlin · GitHub

        private fun analyzeTypesAndRemoveDeadCode(): Map<AbstractInsnNode, Type> {
            val insns = methodNode.instructions.toArray()
            val frames = analyze(internalClassName, methodNode, OptimizationBasicInterpreter())

            val checkedReferenceTypes = HashMap<AbstractInsnNode, Type>()
            for (i in insns.indices) {
                val insn = insns[i]
                val frame = frames[i]
                if (insn.isInstanceOfOrNullCheck()) {
                    checkedReferenceTypes[insn] = frame?.top()?.type ?: continue
                }
                else if (insn.isCheckParameterIsNotNull() || insn.isCheckExpressionValueIsNotNull()) { // ここにチェックが入っている
                    checkedReferenceTypes[insn] = frame?.peek(1)?.type ?: continue
                }
            }

            val dceResult = DeadCodeEliminationMethodTransformer().removeDeadCodeByFrames(methodNode, frames)
            if (dceResult.hasRemovedAnything()) {
                changes = true
            }

            return checkedReferenceTypes
        }

なんかこの辺りのcommitでcheck増えてそうな気配を感じるのでこれかなぁ

github.com

Intentを使って複数枚画像を取得するときのメモ

Intentを使ってPhotoとかいい感じの(=ユーザが選択した)アプリから画像を選択したいときに、1枚なのか複数枚なのかで色々違うのでメモ。

複数枚画像を選択する場合

選択させるアプリを起動するとき

ポイントは Intent.EXTRA_ALLOW_MULTIPLE のextraをtrueに設定すること。
色々ググったときにactionが Intent.ACTION_GET_CONTENT でいけると書いていたけど、これだと複数画像選択できず一枚選択した時点で元のアプリに戻ってしまう挙動になった。(OS 7.1.2/Nexus 6P実機環境)
下記コードのように Intent.ACTION_PICK だと動く。

val intent = Intent()
intent.type = "image/*"
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
intent.action = Intent.ACTION_PICK
this.startActivityForResult(Intent.createChooser(intent, "Choose Photo"),
                                        CHOOSE_PHOTO_REQUEST_CODE)

選択した画像の情報を取り出すとき

Activity#onActivityResult() で受け取るintentの clipDatauriの情報が入っているのでこれを使う。 下記のサンプルコードは選択した画像のuriの情報を取り出してlistViewに表示するコードの一部。 getItemAt() で各要素にアクセスできる。

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        if (requestCode == CHOOSE_PHOTO_REQUEST_CODE && resultCode == Activity.RESULT_OK) {
            val itemCount = data?.clipData?.itemCount ?: 0
            val uriList = mutableListOf<Uri>()
            for (i in 0..itemCount - 1) {
                val uri = data?.clipData?.getItemAt(i)?.uri
                uri?.let { uriList.add(it) }
            }
            this.adapter.data = uriList
            this.adapter.notifyDataSetChanged()
        }
    }

余談だけど、 clipData が保持しているuriの情報を持っているArrayListがpublicではない、かつ直接アクセスするためのAPIが公開されていないのでこんな汚い感じでmutableListだったりfor文だったりを使わないといけない感じになった(´;ω;`)

↓ClipDataのコード
Cross Reference: /frameworks/base/core/java/android/content/ClipData.java

153 public class ClipData implements Parcelable {

167     final ArrayList<Item> mItems; // publicじゃない

819     /**
820      * Return a single item inside of the clip data.  The index can range
821      * from 0 to {@link #getItemCount()}-1.
822      */
823     public Item getItemAt(int index) {
824         return mItems.get(index); // 指定したindexの要素しか取れない
825     }

もしimmutableなmItemsがgetできたり別のiterableな何かが取得できるAPIが生えてたら、こんな感じでもっと綺麗にかけるのに…

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        if (requestCode == CHOOSE_PHOTO_REQUEST_CODE && resultCode == Activity.RESULT_OK) {
            // もしmItemsに外部からアクセスできたらこんな感じで1行で済ませられそう
            this.adapter.data = data?.clipData?.items.map { it.uri } ?: emptyList()
            this.adapter.notifyDataSetChanged()
        }
    }

拡張関数とか自分で書いてもいいけど、そんなに利用頻度高くなさそう、ということで諦めてしまいそう。

1枚だけ画像を選択する場合

選択させるアプリを起動するとき

この場合のactionは Intent.ACTION_GET_CONTENT でいけた。

            val intent = Intent()
            intent.type = "image/*"
            intent.action = Intent.ACTION_GET_CONTENT
            this.startActivityForResult(Intent.createChooser(intent, "Choose Photo"),
                                        CHOOSE_PHOTO_REQUEST_CODE)

選択した画像の情報を取り出すとき

取り出すときは複数枚の場合と同じく、Activity#onActivityResult() で受け取るintentの clipDatauriの情報が入っているので同様に扱うことができる。

MediaStoreのThumbnailsの罠

先日参加したCA.apk #2 で前川さんが発表されていた「やさしい画像ギャラリー改善tips」がいい感じだったので、試してみた結果と気になった部分のメモ書きです。

発表の概要

画像ギャラリーを作る時に、MediaStore.Images.Media.EXTERNAL_CONTENT_URI だと画像の元サイズで読み込んでメモリが逼迫されるので、MediaStore.Images.Thumbnails.EXTERNAL_CONTENT_URIを使って小さいサイズで読み込むと動作がサクサクになるよ!という話だと理解しました。

気になったこと

基本的にいい感じなのですが、thumbnailのテーブルだとコンテンツが登録されない契機があるように見えます。具体的にいうと、ギャラリーを表示した直後にカメラアプリを起動し、ギャラリーに戻って来た際にthumbnailのテーブルには先ほど撮った写真はまだ登録されていません。(MediaStore.Images.Media.EXTERNAL_CONTENT_URIには登録されていました。)手元のNexus 6Pでしか試せていないのですが、/storage/emulated/0/DCIM/.thumbnails/配下のキャッシュされたファイルしか探しに行けてないような?thunbmail生成のタイミングに気をつけたほうがよさそうです。

で、どうする?

確実な方法としては、MediaStore.Images.Media.EXTERNAL_CONTENT_URIでちゃんと最新のデータが登録されているので、このテーブルからID一覧を取得するのが楽そうです。こっちのテーブルにはthumbnailのテーブルのようにKIND列が用意されていないので、サイズを指定してデータを参照することはできません。というわけで、Thumbnails.getThumbnail() を使って元データのIDからthumbnail画像のbitmapを取得するのが正攻法なのかなぁという気がしています。パフォーマンスはちょっと下がる気がしているので、他にいい方法があれば教えてください。

おまけ:thumbnail テーブルが更新されるタイミングを知りたくてframeworkを読んで見た

MediaProviderupdate()が呼ばれたタイミングでthumbnailが生成されているけど、insert()のタイミングではthumbnailは生成されていないような気がする…1
ということはMediaScannerでscanすればいいのでは?と思ってMediaScanner#scanDirectories() とか使いたかったけどそもそもクラスが @hide だし、publicなMediaScannerConnection#scanFile()ディレクト単位でscanできるのかわからないので諦めました :p

BottomNavigationViewのコード読んでみた

このエントリは先日参加した「まったりAndroid Framework Code Reading #5」の成果です٩( ‘ω’ )و
Support LibraryのBottomNavigationViewのコード読んできたのでまとめるよ!

mandroidfcr.connpass.com

BottomNavigationViewとは

Support Library 25.0.0から追加された画面下部に配置するviewです。
API Referenceはこちら

なぜBottomNavigationVIewを読もうと思ったか?

iOSではおなじみの下タブデザインをAndroidでも取り入れたいという話はよく出てくるかと思いますが、このBottomNavigationViewはボタンの数が多くなってくるほどめちゃめちゃアニメーションが入ってきます。

アニメーションいらないし、inactiveなボタンだとtext表示されないしで、割とプロダクトに取り入れるのはハードル高かったりします。 そういった場合、自分でcustom viewを作ることになると思うのですが、次に問題になるのはmenuをxmlで設定できるようにするか?という部分になります。 menuで設定できた方が綺麗だけど、そこまで汎用性高める必要もないしもはやコード量によるよなぁと思って、では本家ではどれくらいのコード量なのか?を知るために読んでみました。

で、どうだった?

アニメーションを含めるとBottomNavigationViewを構成する関連クラスは7つほどでした。というわけでmenuで設定できる必要はないかなーという結論。Library作っていろんなプロダクトで使い回すなら別ですけどね。

わかったこと

関連コードとそれぞれ読んだ時のメモを残しておきます。

BottomNavigationView

  • コードはここ
  • FrameLayoutをextendsしたクラス
  • prensenter, menu, menuViewをそれぞれお互いにバインド
  • custom attributeを取得してmenuViewにセット
  • custom attributeからmenuを取ってきてinflateするメソッドがpublicなので、コードから任意のタイミングでinflateできるっぽい
        181     /**
        182      * Inflate a menu resource into this navigation view.
        183      *
        184      * <p>Existing items in the menu will not be modified or removed.</p>
        185      *
        186      * @param resId ID of a menu resource to inflate
        187      */
        188     public void inflateMenu(int resId) {
        189         mPresenter.setUpdateSuspended(true);
        190         getMenuInflater().inflate(resId, mMenu);
        191         mPresenter.initForMenu(getContext(), mMenu);
        192         mPresenter.setUpdateSuspended(false);
        193         mPresenter.updateMenuView(true);
        194     }
  • このView自体はただのFrameLayoutでコンテナ状態
    • menu viewをaddView()することによりViewの描画を行う
      • menu viewがさらにitem viewを再帰的にaddViewしている
  • itemIconTintがattributeで指定されていなかったら、disable/ emptyにはandroid.R.attr.textColorSecondary がが設定される。checkedはprimary
  • BottomNavigationMenuにcall backを設定
    • 提供するinterfaceはonMenuItemSelected()とonMenuModeChange()

BottomNavigationMenu

  • コードはここ
  • 50行程度の小さなクラス
  • MenuBuilderクラスの拡張
  • @hideかつpublic
  • このクラスのMAX_ITEM_COUNTは5
  • sub menuは非サポート

BottomNavigationMenuView

  • コードはここ
  • 300行程度のクラス
  • ViewGroupの拡張、MenuViewをimplements
  • @hideかつpublic
  • BottomNavigationViewから強制的にGravity.centerをセットされる
        111         mMenuView = new BottomNavigationMenuView(context);
        112         FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(
        113                 ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
        114         params.gravity = Gravity.CENTER;
        115         mMenuView.setLayoutParams(params);
  • initialize()でメンバで持ってるMenuが更新されてもcheckedの位置はリセットされず引き継ぐ
         97     @Override
         98     public void initialize(MenuBuilder menu) {
         99         mMenu = menu;
        100         if (mMenu == null) return;
        101         if (mMenu.size() > mActiveButton) {
        102             mMenu.getItem(mActiveButton).setChecked(true);
        103         }
        104     }
  • BottomNavigationItemViewの配列を持っている
  • Pools.SynchronizedPoolというhideなクラスを使ってButtonのオンジェクトをプールしている
  • Buttonが3つ以上ある場合はアニメーション(shifting mode)
    • アニメーション機能ははBottomNavigationAnimationHelper* クラスに移譲
    • ここでいうアニメーションとは、ボタン自体の移動をさす
      • ボタンのコンテンツのサイズが変わるtransitionはBottomNavigationItemViewの責務

BottomNavigationItemView

  • コードはここ
  • 実際のボタンを担当するクラス
  • FrameLayoutをextends, MenuView.ItemViewをimplements
  • active/inactiveが切り替わったタイミングでボタン内のコンテンツ(lable, icon)の拡大・縮小、表示・非表示の制御を行う

BottomNavigationPresenter

  • コードはここ
  • 100行程度の小さなクラス
  • MenuPresenterをimplements
    • MenuPresenterはhideなinterface
  • @hideかつpublic
  • updateViewのロックは意外とフラグ制御

BottomNavigationAnimationHelperIcs

  • コードはここ
  • ボタンが3つ以上だった時のanimationのhelper class
  • AutoTransitionとTransitionManagerを使ってanimation
    • material motionの土台となってるのはこのクラスっぽい
    • 意外とめっちゃシンプルなコードでかけるっぽい
         30     BottomNavigationAnimationHelperIcs() {
         31         mSet = new AutoTransition();
         32         mSet.setOrdering(TransitionSet.ORDERING_TOGETHER);
         33         mSet.setDuration(ACTIVE_ANIMATION_DURATION_MS);
         34         mSet.setInterpolator(new FastOutSlowInInterpolator());
         35         TextScale textScale = new TextScale();
         36         mSet.addTransition(textScale);
         37     }
         38 
         39     void beginDelayedTransition(ViewGroup view) {
         40         TransitionManager.beginDelayedTransition(view, mSet);
         41     }

BottomNavigationAnimationHelperBase

  • コードはここ
  • ICSより前のOSだとこっちが使われる
  • 中身はこれだけ(つまりanimationしない)
         21 class BottomNavigationAnimationHelperBase {
         22     void beginDelayedTransition(ViewGroup view) {
         23         // Do nothing.
         24     }
         25 }

SearchViewの詰まったところ備忘録

久しぶりにSearchViewをさわると忘れているポイントが結構あったので、今更感がありますがメモ書きを残します。サンプルコードはKotlinです。

SearchViewをactionbarいっぱいに表示したい

SearchViewをAction Barに配置するとデフォルトで左側に謎の余白ができてしまう問題によく出会います。 SearchViewの android:layout_widthmatch_parent にしても効きません。 ポイントはmaxWidthを変えること。

    override fun onCreateOptionsMenu(menu: Menu?): Boolean {
        this.menuInflater.inflate(R.menu.search_menu, menu)
        val menuItem = menu?.findItem(R.id.search_view)
        val searchView = menuItem?.actionView as SearchView
        searchView.maxWidth = Int.MAX_VALUE
        return super.onCreateOptionsMenu(menu)
    }

hint textの色を変えたい

SearchViewの中で定義されているidを使ってSeachView内のEditText(実際はEditTextをextendsした@hideなクラスであるSearchAutoComplete)にアクセスし、hint text colorを書き換えます。

        (searchView.findViewById(R.id.search_src_text) as EditText)
                .setHintTextColor(ContextCompat.getColor(this, android.R.color.white))

他にもなんか思い出したら適宜追記したい。

DroidKaigi 2017で登壇しました

3月9日、10日と二日間に渡って開催されたDroid Kaigi 2017にスピーカーとして参加してきました。

 

私のトークテーマはパフォーマンス改善について。オーディエンスの対象を初心者から中級者としていたため、わかりやすく丁寧に伝えるように心掛けたのですが、その結果たぶん全セッションの中で一番ゆるふわでほっこりセッションになったんじゃないかなぁと思います。(実際に聞きに来て頂いた人達に手を上げてもらったところ、「Android開発を初めて1年以内」、「2, 3年の経験がある」「それ以上」が同じくらいいらっしゃいました。あれ?笑)

 

 

発表練習に何度か付き合ってくれて沢山フィードバックくれたAndroidチームのみんなとか、社内のSlackに投げてたスライドにツッコミをいれてくれたエンジニア勢とか、セッション見に来てくれた皆様やオフィスアワーや懇親会で話しかけて下さった方々、本当にありがとうございますという圧倒的感謝でいっぱいです。

 

何が言いたかったかというと😺は可愛くて正義ってことです。

Android + Kotlin + Mockito のメモ書き

Kotlin大好きで、Korlin最高だよって言いまくってるんですけど、Kotlinのつらみみたいな部分も残しておかないとフェアじゃないかなと思ったので残しておきます。

Instrument Testがつらい

実機だったりエミュレータ上で実行するAndroid Instrument Testがつらいです。なぜかというとKotlinは基本的にクラスがデフォルトでfinalになるからです。
Auto testはみなさん大抵mockitoを使うと思います。mockito 2.0 からfinal classもmockできるようになりましたが、こちらの機能はopt in機能で素の(Androidではない) Java では src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker のパスに以下の1行を書いたファイルをおけば動くようになります。

mock-maker-inline

ところがAndroidJVMだとこちらのopt inがうまく機能しません。

mockito-androidに期待したでしょ?

mockito-androidがmockito 2.6.1 から出て来ました。こちらでdexmakerとの依存から解放されると喜んでいる皆さん、残念ながらinline mock makerは対象外です\(^o^)/

Mockito (Mockito 2.7.13 API)

Be aware that you cannot use the inline mock maker on Android due to limitations in the Android VM.

最終手段All Open

Kotlin 1.0.6からcompile pluginでall openが追加されました。
こちらを利用するとclassに open 修飾子をつけなくても特定のアノテーションをつけたクラスがopenになります。
blog.jetbrains.com

ただし、Test compile時だけこの機能を有効にする方法が見つからず、結局productionに導入するのは諦めました。
テスト時だけ動くようなGradle Taskを使ってうまくやろうと思ったけれど断念。誰かうまく言ったという人がいればぜひ教えてください(´;ω;`)

結論

powermockとか導入するのもtoo muchな感じでつらみがあるので、
Android JVMを利用しなくても良いunit testに一旦絞ってauto testを描く方針になりそう。