カテゴリー: Android

Androidのプログラミングについての情報を取り扱っています。

UdacityでAndroid開発の勉強(ついでに英語も?)

この記事は最終更新から3ヶ月以上が経過しています。情報が古い可能性があります。

Udacityをご存知でしょうか。動画を見ながらAndroid開発(だけじゃありませんが)の勉強ができるサイトです。

入門的な内容から高度な内容(アプリのパフォーマンスを向上させるとか)まで幅広く学ぶことができます。しかも無料で。

私は今この講座を勉強中です。動画に日本語字幕がついているありがたいコースです。Udacity – Developing Android Apps

私がUdacityの動画で勉強している理由はいくつかあります。

  • 無料で見れる(有料の講座もあるので注意)
  • Googleが提供しているので品質が保証されている
  • ちゃんとまとまった1つのコンテンツとして勉強したい
  • ついでに英語の勉強にもなりそう

すでにAndroid Development for BeginnerやMaterial Desgin for Android Developersなどを受講しましたが、「そういうふうにやるんだ」という新たな発見もあって面白いです。(そんなん知っとるわ、ということもまた多いですけど)

動画で「こんなふうにやるんだよ」と教えるだけでなく、GitHubにあるサンプルコードを元に自分でAndroid Studio使いながら実践する内容もあるので、無料といえどかなり本格的です。

Udacityのコンテンツは全て英語です。ですが扱っている内容はプログラミングなので、雰囲気でなんとか進めていくことはできると思います。言ってることはよく分からなくても、コードを見ればなんとなく理解できると思います。

英語の勉強になりそうというのも意外とバカにできない重要な要素だと思います。少なくとも私は、Udacityで勉強するようになってからというものの、英語に対する苦手意識が薄らいできたように思います。おかげでAndroid Developersの情報を原文で読むのが自分の中で普通になってきたりしてます。

ただし抵抗感が薄らいだだけであって、英語ができるようになっているわけではありませんけどね。しかしそれだけのことでも、こと英語に関しては充分な進歩だと思います。英語も勉強したいなぁなんていう人には、Udacityを利用するのがついでにAndroidの勉強もできるおすすめな方法じゃないかなと思います。

ラムダ式とは一体何なのか

この記事は最終更新から3ヶ月以上が経過しています。情報が古い可能性があります。

Java8から使えるようになったラムダ式はAndroidではそのままでは使えません。Android Studioが「ラムダ式で書いたらこうなる」と見た目だけ表示してくれたりしますが、実際にラムダ式でコードが記述されているわけではありません。

例えば、このようなボタンにクリックリスナーを設定するコードがあったとして、

mButton.setOnClickListener(new View.OnClickListener(){
    @Override
    public void onClick(View v){
        Log.d(TAG, "Click!!");
    }
});

Android Studioがラムダ式スタイルで見た目をすっきりさせてくれるわけです。

mButton.setOnClickListener((v) -> { Log.d(TAG, "Click!!"); });

これはエディタ上で折りたたまれて表示されているだけで、実際に記述されているコードは上のものです(マウスカーソルを上に持って行くと表示されます)。

で、これを本当にラムダ式で記述できるようにするライブラリとしてretrolambdaというものがあります。あるんですが、その前に、そもそも私はラムダ式自体がよく分かっていません。そこでまずは、ラムダ式とはなんぞやというところから調べることにしました。

ざっくりした書き方で、私の中のイメージを書き連ねたのでわかりにくいところがあると思います。間違ってるところもあると思いますが、その際はご指摘いただけるとうれしいです。(長い前置き終わり)

ラムダ式はいろいろ省略することのできる記法

ラムダ式はアロー演算子を利用して(引数) -> {処理}という書き方ができるものです。この書き方ができるのは一定の条件下においてですが、引数にOnClickListenerのような抽象メソッドが1つだけのinterfaceを取る場合と考えておけばいいと思います。

OnClickListenerは以下のように、onClickという抽象メソッドが1つだけ定義されたインターフェースです。

    public interface OnClickListener {
        /**
         * Called when a view has been clicked.
         *
         * @param v The view that was clicked.
         */
        void onClick(View v);
    }

これをsetOnClickListener()する際に、無名クラスとして定義して使っているわけですが、その無名クラスの定義をすっ飛ばして、直接抽象メソッドへの引数と処理だけを書くことができるのです。

なぜ省略できるか

なぜ省略して書けるかというと、まずsetOnClickListener()というメソッドは、引数にOnClickListenerというインターフェースをとります。そしてJavaのinterfaceという仕組みによって、onClick()というメソッドが必ず実装されていることが保障されます。すなわち、この中では少なくともonClick()というメソッドが呼ばれることがわかっているわけです。だから省略できるのです。

ラムダ式の左辺については、引数が1つであれば()を省略できたり、型を省略することができます。型を省略できる理由は、インターフェースの定義で型が決められているため、省略されても分かるからです。

右辺の{}はメソッドの中身が1文ですむ場合に省略可能です。また、return文だけですむ場合も同様で、さらにreturn句も省略できます。なぜならソッドの戻り値がインターフェースの定義で決められており、右辺の処理が戻り値を表していると自明だからです。

あくまでこれらは「省略できる」であって、別に省略せずに書いても問題ありません。(もっともわざわざラムダ式を使う目的を考えれば、省略できるところは省略すべきでしょうが)

それでも分からない人に、もしかしたら引っかかるかもしれない情報

省略して書けることは分かったけど、やっぱりラムダ式よく分からない。そんな人は、「そもそもなぜメソッド1つだけが定義されたinterfaceを用意して使っているのか」が分かっていないことが原因かもしれません(私はそうでした)。

ラムダ式が適用できるパターンが全てそうかは知りませんが、少なくともOnClickListenerについてはObserverパターンによる実装です。なぜメソッド1つだけのinterfaceを定義して使うかというと、「クリックされた」というイベントを監視するのに便利だからです。

イベントの発生を監視するためには、イベントの発生を通知する人が必要になります。そのときに監視側が「俺はnowClickメソッドで通知してくれ」、「僕にはclickメソッドで教えて」、「私はonClickメソッドで」とバラバラな実装になっていたらどうでしょう。こうなるとイベントを通知するクラスは、イベントを監視するクラスが増える度に通知処理を書き換える必要性に追われます。そんなのはナンセンスです。

そこで「うるせー! クリックイベントは今後OnClickListenerというインターフェースのonClickメソッドで通知する。異論は認めん!」と通知する側が決めてしまえば全てがすっきりします。

イベント通知側は監視者が誰であるかを気にする必要がなくなります。どんなクラスが通知を受けたがっているのか、どんなメソッドで通知して欲しいのかを考えなくてもよくなるのです。イベントが発生したときにインターフェースを介して通知すればいいだけなのですから。

受け取る側は、指定されたインターフェースを実装しさえすれば、イベント発生時にそのメソッドを通じてイベントの発生を検知することができるわけです。誰がイベントを通知してくるのかを考えなくても良くなります。実際、AndroidにおいてonClickメソッドを誰が通知してくるのか考えなくても処理できています(Androidがよしなにやってくれてるんでしょう)。

それをスマートに実現する方法が、メソッド1つだけ定義したinterfaceを使うことです。

私自身まだふわっとした理解なので、余計に混乱させてしまったら申し訳ないです・・・。

まとめ

  • ラムダ式は省略記法
  • OnClickListenerのような抽象メソッド1つのインターフェースで使える
  • そういうインターフェースはObserverパターンで使われる
  • なぜならイベントの発生・監視に便利だから

ラムダは単なる省略記法というわけではないのでしょうが、とりあえず今は単純に割りきって考えようと思います。単なる省略記法と思えば、ラムダ式への心理的なハードルがぐっと下がりました。最初の一歩としては「省略できるんだ」でいいんじゃないかと思います。ただし変な覚え方して後々困ることになるのかもしれませんが・・・。

ちなみにそんなラムダ式をAndroid Studioで使えるようにするライブラリが、gradle-retrolambda – GitHubです。(導入の仕方とかはここでは語りません、よく分かってないので)

参考

知っといてムダにならない、Java SE 8の肝となるラムダ式の基本文法 (1/3)

矢沢久雄の早わかりGoFデザインパターン(6) 第6回 Stateパターン/Observerパターン

RxAndroidとRetrolambdaで大体Java8をAndroidに持ち込む

パララックスイメージのAppBarをListViewを使って実装しようとしてハマった話

この記事は最終更新から3ヶ月以上が経過しています。情報が古い可能性があります。

AppBar(Toolbar、ActionBar)の部分が大きめの画像になっていて、コンテンツをスクロールするとそれに合わせて画像が縮んでいき、最終的にToolbarだけが残る(もしくは全部隠れる)みたいなデザインがありますよね。あれを実装しようと思って試行錯誤してみました。

試行錯誤になってしまった原因は、スクロール可能なコンテンツ部分を横着してListViewで作ってしまったからでした。見かけるサンプルはだいたいRecyclerViewを使っていたのですが、使ったことがないため使い慣れているListViewでやろうとしたのが間違いでした。

ListViewで実装すると、ListViewをスクロールしてもAppBarは連動して動いてくれません。AppBarの部分をスクロールすると伸縮してはくれますが、巷にあふれるパララックスAppBarはこんな残念な動きはしていません。

コードで何か手を加えないといけないのだろうかと調べるうちに、なぜListViewではAppBarが連動して動かないのか原因が分かりました。今回はそのお話です。

Patterns– Scrolling techniques

layout.xmlの設定

基本的にパララックスなAppBarを実装するには、レイアウトXMLの記述のみで実装できます。

サンプルコード – GitHub

このMaterial Design(Android desgin support library)による階層構造を初めて見ると、なんだかややこしく感じてしまいますが、1つずつ紐解いていけばそう難しい構造ではありません。

正確にはandroid.support.design.widget.〜とFQCN(パッケージ名を含めたクラス指定)になりますが、ここでは長くなるので省略しています。

CoordinatorLayout
├AppBarLayout
│└CollapsingToolbarLayout
│ ├ImageView
│ └Toolbar
├ListView(などスクロール可能なコンテンツ)
└FABなどお好みで

基本的にXML上でちゃんと必要な指定さえ行えば動きます。コードは不要です。

CoordinatorLayout

今回の例ではListViewのスクロールにあわせてAppBarLayoutを伸縮させるために存在しています(FABをToolbarとListViewの中間に配置する役割も担っていますが)。このCoordinatorLayout自体は内包したView同士を連携させたりする単なる入れ物です。全然「単なる」ではないですけど。

AppBarLayout

AppBar部分のLayoutを管理するコンテナで、AppBarの部分に表示するViewをこの中に入れてやります。Blank Activityを作成すると、この中にはToolbarだけが入っていると思います。

ここではAppBarの高さを指定してやります。android:layout_height="192dp"

CollapsingToolbarLayout

折りたためるToolbarのための入れ物です。スクロールによるAppBarの動き方を指定することができます。ここではapp:layout_scrollFlags="scroll|exitUntilCollapsed"と指定しています。

ImageView

AppBarが全開のときに表示されるイメージ画像です。コンテンツのスクロールに合わせて縮み、最終的にToolbarだけが残ります。ここではapp:layout_collapseMode="parallax"を指定しています。

Toolbar

Toolbarです。ここではapp:layout_collapseMode="pin"を指定しています。この指定でToolbar自体は隠れずに残ります。

ListView

よく見かけるサンプルではRecyclerViewやNestedScrollViewが利用されています。しかし私はRecyclerViewの使い方がよくわからなかったのでListViewで代用しています。

ここでは必ずapp:layout_behavior="@string/appbar_scrolling_view_behavior"の指定が必要です。

この@string/〜はAndroid Support Libraryのstringリソースを参照していて、その中身はandroid.support.design.widget.AppBarLayout$ScrollingViewBehaviorとなっています。つまりこのListViewの振る舞いとして、AppBarLayoutのScrollingViewBehaviorを指定しているわけです。

ListViewを使うと、そのままではListViewがスクロールされるだけでAppBarが伸縮しません。ListViewのスクロールと連動させるためには、ListViewにandroid:nestedScrollingEnabled="true"を指定する必要があります。

なぜか。スクロール可能なコンテンツとAppBarの伸縮を連携させるためには、ListViewがスクロールされたということをCoordinatorLayoutに伝える必要があります。RecyclerViewやNestedScrollViewは標準でこれをやってくれるわけですが、ListViewは何もしません。そこでCoordinatorLayoutにスクロールイベントを通知するための設定を有効にしてやる必要があるのです。

android:nestedScrollingEnabled="true"(NestedScrollに関する処理)はAPI21以上のViewに実装されています。

余談:なぜAppBarが動くのか

仕組みを完全に理解したわけではないので、ざっくりとした説明です。

ListViewの上でスクロールを行うと、ListViewの中身がスクロールされます。これはListViewのonTouchEventで処理されています。これだけではListViewの中でスクロールイベントが処理されるだけで、AppBarの変形にはつながりません。

そこで登場するのがCoordinatorLayoutです。こいつが子Viewのスクロールと、別の子Viewを連携させるわけです。

連携させるためにはCoordinatorLayoutにスクロールイベントを通知する必要があり、その仕組がNestedScrollです。RecyclerViewやNestedScrollViewは初めからCoordinatorLayoutと連携する前提で作られていますし、ListViewなどでもSDK21からNestedScrollに関する処理が追加されています。ただし初期状態では無効化されているので、NestedScrollの処理を有効化してやる必要があり、それがandroid:nestedScrollingEnabled="true"になります。

NestedScrollの処理は子ViewのonTouchEvent(onTouchMove)でCoordinatorLayoutに伝わります。CoodinatorLayoutはonNestedPreScroll内でBehaviorが設定されている子Viewを探し、見つかったBehaviorに対してdispatchOnDependentViewChangedを呼び出します。今回の例ではScrollingViewBehaviorです。

最終的にAppBarのサイズを伸縮させる処理は、このBehaviorのonDependentViewChangedで行われているみたいです。

参考

Handling Scrolls with CoordinatorLayout

support libraryのバージョンの調べ方

この記事は最終更新から3ヶ月以上が経過しています。情報が古い可能性があります。

app/build.gradleのdependenciesにさり気なく書いてあるサポートライブラリ。

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.android.support:appcompat-v7:22.2.1'
}

appcompat-v7:22.2.1のこの22.2.1の部分。自分で指定しようと思うと、バージョンいくつが存在しているのかが分からず、どうやって確認すればいいのかも分かりません。とりあえず最新のものが当たればいいやと、+を使ってごまかしたりしてきましたが、最新のものが当たるとそれはそれでうまくいかないことがあったりします(23が出てるんだけど、とりあえずはtargetSDKを22で作ってる今とか)。

要するに、1つ前のバージョンを指定したいのだけど、その1つ前のバージョンというのはいったいいくつなんだというのが困ります。結論から言うと22.2.1なんですけど、じゃあそれをどうやって調べたらいいのかという話です。

Support Library – Android Developersで確認できます。

また、パソコンにインストールしているAndroid SDKのディレクトリを潜って行くことでも調べることはできます。

(Android SDKのインストールディレクトリ)/extras/android/m2repository/com/android/support/appcompat-v7

このディレクトリに、バージョンごとのフォルダがあるのでそこでも確認可能です。

そもそも指定できるバージョンの候補を出してくれると楽なんですけどね。

EventBusを使ってAsyncTaskLoaderでProgressを通知する

この記事は最終更新から3ヶ月以上が経過しています。情報が古い可能性があります。

greenrobot/EventBus – GitHubを使ってみました。

異なるスレッドからのイベントの通知でもうまくハンドリングしてくれるので、AsyncTaskLoaderでProgressを通知するのにも利用できます。

Broadcastを使って実装するのと比較するとコードがシンプルになって良いです。IntentFilterやIntentにデータを埋め込む際に使うキー文字列を定義したりしなくて済みます。

更に独自のオブジェクトをイベントとして渡すこともできるので、Broadcastでは難しいイベントの通知も簡単に出来ます。

準備

/app/build.gradleのdependenciesにcompile 'de.greenrobot:eventbus:2.4.0'と、1行追加するだけで使えます。

イベントの送信

イベントを送信したいところで、EventBus.getDefault().post("イベント発生");とするだけです。

これは単にStringオブジェクトを渡しているだけですが、独自オブジェクトを定義して渡すことも可能です。

イベントの受信

イベントの購読・解除

Activityでイベントを受信するなら、onResume()EventBus.getDefault().register(this);とすることでイベントの購読を行います。

この際、onPause()EventBus.getDefault().unregister(this);で購読解除を忘れないように。

イベントのハンドリング

AsyncTaskLoaderからのイベントを受信してUIを更新するなら、onEventMainThread()をActivityに実装します。

public void onEventMainThread(String event){
    mTextView.setText(event);
}

送信するイベントのオブジェクトごとにこのメソッドを用意してやる必要があります。例えば他にMyEventという独自オブジェクトがある場合、別途public void onEventMainThread(MyEvent event){}を実装してやります。

メソッドをオーバーライドするわけではないので、コード補完は効きません。タイポするとこんな例外が起きてアプリが落ちます。

java.lang.RuntimeException: Unable to start activity ComponentInfo{jp.gcreate.sample.asynctaskloadersample/jp.gcreate.sample.asynctaskloadersample.MainActivity}: de.greenrobot.event.EventBusException: Illegal onEvent method, check for typos: public void jp.gcreate.sample.asynctaskloadersample.MainActivity.onEventBackgroundThrad(jp.gcreate.sample.asynctaskloadersample.MyEvent)

まとめ

AsyncTaskLoaderのProgressの通知で使ってみましたが、とても便利だなと思いました。

内部的にはHandlerを使ってイベントのハンドリングを行っているみたいでした。Handlerの具体的な使用例としていい勉強にもなって、個人的には一石二鳥な感じです。ありがたや〜。

便利な半面、無計画に使うとイベントが乱立してカオスな事になりそうなので、実際に使うときには気を付けないといけないなと思いました。

LocalBroadcastを使ってAsyncTaskLoaderでProgressの通知を実装する

この記事は最終更新から3ヶ月以上が経過しています。情報が古い可能性があります。

AsyncTaskLoaderにはAsyncTaskのpublishProgress()のような途中経過を通知するメソッドが標準では用意されていません。

そこでブロードキャストを利用してこれを実装します。

Context#sendBroadcast()を使ってもいいのですが、これだと自分のアプリ外にもブロードキャストが送信されてしまうので、LocalBroadcastManagerを利用します。

AsyncTaskLoaderでの処理

AsyncTaskLoader側ではloadInBackground()内で、ブロードキャストの送信を行うだけです。

この際、途中経過のデータはIntentに埋め込んで送信する必要があります。

@Override
public String loadInBackground(){
    //非同期処理
    Intent intent = new Intent(MainActivity.ACTION_PROGRESS)
        .putExtra("key", "hoge");
    LocalBroadcastManager.getInstance(getContext()).sendBroadcast(intent);

}

MainActivity.ACTION_PROGRESSはインテントフィルタを表す文字列です。

Activity側の実装

//インテントフィルタの定義
public static final String ACTION_PROGRESS = "jp.gcreate.sample.asynctaskloadersample.ACTION_PROGRESS";
//ブロードキャストレシーバーの作成
private BroadcastReceiver mReceiver = new BroadcastReceiver() {
    @Override
    public void onReceive(Context context, Intent intent) {
        String progress = intent.getStringExtra("key");
        mTextView.setText(progress);
    }
};

@Override
protected void onStart() {
    super.onStart();
    //ブロードキャストレシーバーの登録
    LocalBroadcastManager manager = LocalBroadcastManager.getInstance(this);
    manager.registerReceiver(mReceiver, new IntentFilter(ACTION_PROGRESS));
}

@Override
protected void onStop() {
    super.onStop();
    //ブロードキャストレシーバーの解除
    LocalBroadcastManager.getInstance(this).unregisterReceiver(mReceiver);
}

ブロードキャストレシーバーを作成して、ここでAsyncTaskLoaderから送られてくるProgressを受け取りとUIの更新処理を実装します。

後はonStart()でブロードキャストレシーバーの登録、onStop()で解除を行ってやればOKです。

細かいサンプルはGitHubにおいてます。

Espressoを使ってUIテストを書いてみた

この記事は最終更新から3ヶ月以上が経過しています。情報が古い可能性があります。

Android Studio 1.2でEspresso2.1を使ったUIテストをやってみました。

テストの実行に実機(エミュレータ)が必要なのが面倒くさいですが、実機無しでテストが実行できるようにする方が面倒くさい(というかやり方がわからない)ので、テストができるだけマシだと考えることにしました。

テストを実行する際に、開発者オプションでアニメーションの無効化をしておかないと、アニメーションのせいで同じテストが失敗することがあるのは注意が必要かもしれません。

しかしながら、Android Support Libraryに入っているおかげでテストを実行するまでのハードルが低いのはうれしいところです。

また、EspressoによるUIテストの書き方も非常にシンプルで分かりやすいと思います。

準備

詳しい手順はここに書いてあるとおりです。Get started – android-test-kit

まずは/app/build.gradleにテストで利用するライブラリを追加します。

dependencies {

    // Testing-only dependencies

    androidTestCompile 'com.android.support.test:runner:0.2'

    androidTestCompile 'com.android.support.test:rules:0.2'

    androidTestCompile 'com.android.support.test.espresso:espresso-core:2.1'

}

/app/build.gradleにtestInstrumentationRunnerを追記します。場所はdefaultConfigの中です。

testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"

更に以下のおまじないも追加します。

packagingOptions {

    exclude 'LICENSE.txt'

}

最終的な/app/build.gradle

適宜読み替えて使ってください。

apply plugin: 'com.android.application'

android {

    compileSdkVersion 22

    buildToolsVersion "22.0.1"


    defaultConfig {

        applicationId "jp.gcreate.product.developersettingsshortcut"

        minSdkVersion 15

        targetSdkVersion 22

        versionCode 1

        versionName "1.0"


        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"

    }

    buildTypes {

        release {

            minifyEnabled false

            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'

        }

    }

    packagingOptions {

        exclude 'LICENSE.txt'

    }

}



dependencies {

    compile fileTree(dir: 'libs', include: ['*.jar'])

    compile 'com.android.support:appcompat-v7:22.1.1'

    compile 'com.jakewharton:butterknife:6.1.0'


    // Testing-only dependencies

    androidTestCompile 'com.android.support.test:runner:0.2'

    androidTestCompile 'com.android.support.test:rules:0.2'

    androidTestCompile 'com.android.support.test.espresso:espresso-core:2.1'

}

一時的なバグ?

Warning:Conflict with dependency 'com.android.support:support-annotations'. Resolved versions for app and test app differ.というエラーが出てビルドが止まってしまう問題が生じるかもしれません。

どうもバグらしくて、プロジェクトルートのbuild.gradleに以下を追記することで対処できるそうです。

// Temporary workaround for bug: https://code.google.com/p/android-test-kit/issues/detail?id=136

configurations.all {

    resolutionStrategy.force 'com.android.support:support-annotations:22.1.0'

}

suppot-annotationsの利用するバージョンがコンフリクトを起こしているせいで出ている警告メッセージなので、利用するsupport-annotationsのバージョンを強制的に指定して問題をやり過ごしています。

Gradle徹底入門のP244あたりから読むと何となく分かるかもしれません。

テストを書く

package jp.gcreate.product.developersettingsshortcut;



import android.support.test.rule.ActivityTestRule;

import android.support.test.runner.AndroidJUnit4;

import android.test.suitebuilder.annotation.LargeTest;



import org.junit.Rule;

import org.junit.Test;

import org.junit.runner.RunWith;



import static android.support.test.espresso.Espresso.onView;

import static android.support.test.espresso.action.ViewActions.click;

import static android.support.test.espresso.assertion.ViewAssertions.matches;

import static android.support.test.espresso.matcher.ViewMatchers.isChecked;

import static android.support.test.espresso.matcher.ViewMatchers.isNotChecked;

import static android.support.test.espresso.matcher.ViewMatchers.withId;



@RunWith(AndroidJUnit4.class)

@LargeTest

public class TextableSwitchViewUiTest {

    @Rule

    public ActivityTestRule<UsingTestActivity> mActivityRule = new ActivityTestRule<>(UsingTestActivity.class);



    @Test

    public void 初期値がfalseになっている(){

        onView(withId(R.id.preference_switch)).check(matches(isNotChecked()));

    }



    @Test

    public void タップすると内部のスイッチが有効に切り替わる(){

        //Preference > Editor > Code Style > Java > importでEspresso関係をスタティックインポートするように

        //設定しておかないとコード補完聞いてくれない

        onView(withId(R.id.test)).perform(click());

        onView(withId(R.id.preference_switch)).check(matches(isChecked()));

    }


}


extends ActivityInstrumentationTestCase2を書かなくてもよくなったようです。前々から「ActivityInstrumentationTestCase2って長いわ、そもそも2ってなんやねん」と思っていたので、この変更はうれしいです。

代わりに@Rule ActivityTestRuleを使うようになり、かなりシンプルになったように思います。

このテストはSwitchの初期値がfalseになっているか、タップしたら有効になるかのテストです。書き方もシンプルで分かりやすい気がします。

static importについて

コメントにも書いていますが、Preference > Editor > Code Style > Java > importのタブでstatic importするクラスとしてEspresso関連のクラスを登録しておきましょう。そうすると、onView()と書いた時にalt + Enterでstatic importが候補に出てきてくれるようになります。

static importの設定

テストの実行

テストを実行するためにはエミュレータか実機をパソコンに接続しておく必要があります。

単純なJUnit4のテストを実機無しで実行したくていろいろ調べたりもしましたが、最近はテストができればいいんじゃないかと妥協することにしました。

IDE上でテストを実行

androidTest/javaのフォルダを右クリック、Run→ドロイド君のアイコンのAll Testsを選択すると、テストの実行結果がAndroid Studioで確認できます。

Android Studioからテストを実行

テストが失敗した時のスタックトレースをすぐに確認できますし、IDE上でテストを実行した方が分かりやすいと思います。

コマンドでテストを実行

ターミナルから./gradlew connectedAndroidTestを実行、もしくはGradleツールウィンドウから> connectedAndroidTestをダブルクリックします。

Gradleツールウィンドウから実行

テストの結果は/app/build/outputs/reports/index.htmlを開くと確認できます。

アプリのパフォーマンスを向上させる GPUオーバードロー

この記事は最終更新から3ヶ月以上が経過しています。情報が古い可能性があります。

LinearLayoutをネストしすぎたりするなど、Viewの階層を深くするとアプリのパフォーマンスに良くないという話はよく聞くと思います。

それと似たような話で、画面を何回描画しているかを確認して、アプリのパフォーマンスに役立てることができきます。今回はそれの紹介です。

確認の仕方

端末の開発者オプションで「GPUオーバードローをデバッグ」を有効にします。

GPUオーバードローをデバッグ

これを有効にすると、目に悪そうな色で画面が表示されるようになります。

各色の意味

この各色が、GPUによって何回上書き描画されているのかを示しています。

  • 青色:1回
  • 緑色:2回
  • 薄赤:3回
  • 濃赤:4回以上

この状態で画面が真っ赤っ赤だと、描画方法を改善した方がいいぞということになります。

対策

例えばFrameLayoutでbackgroundDrawableを持ったViewを何個も重ねていくと、見えているのは一番上のものだけなのに、見えない下の要素まで描画するため上書き回数が増えて赤色になってしまいます。

そのため不要なbackgroundDrawableを描画しないようにすることが、この問題の対策になります。

例えばActivityでgetWindow().setBackgroundDrawable(null)とするだけでも画面の赤色が薄くなると思います。(ただし、これをやるとListViewやGridViewなど、スクロールをともなうViewの描画がおかしくなります)

重ねて描画せざるをえない場合は、canvas.clipRectを使って重なって見えない部分を描画しないようにすることで対応できるようです。

効果

ムダな描画回数を減らすことにつながるので、その分アプリの動きが軽快になるでしょう。

さらにバッテリーにも優しくなると思います。

ただし、アプリのもっさり感解消のための施策としては、優先度は低いのかなと思います。ちまたに出ているアプリでも、割と真っ赤なアプリが多いですし、赤くとも動作がもっさりしているものは少ない印象です。

やらないよりやった方がマシでしょうが、ここを気にするより、メモリの使用量を抑えるといったチューニングの方が、アプリのパフォーマンスにとって効果が高いような気がします。

Android Performance

この話はUDACITYのAndroid Performanceという動画を見て知りました。

英語オンリーかつ字幕すらありませんが、大体雰囲気で分かるんじゃないかなと思います。

Android Performance – UDACITY

その画面がどんなViewを使って作られているか調べる方法

この記事は最終更新から3ヶ月以上が経過しています。情報が古い可能性があります。

「このアプリのデザインを参考にしたいんだけど、どうやって作ってるのか知りたい」というときに便利かもしれないコマンドです。

調べたい画面を表示させた状態で、ターミナルからadb shell dumpsys activity topと入力すると、現在表示中のView階層などが表示されます。

View階層だけを調べたいなら、hierarchyviewerを使った方がグラフィカルに見えて便利なのですが、hierarchyviewerはroot権限がないと起動しないので、実機で調べたい画面を表示して解析することができません。

その点、このadb shell dumpsys activity topはroot権限を必要としないので、実機でちょっと調べたいという時に便利だと思います。

どこからどこまでがActionBarの領域で、どこがコンテンツの領域なのかが非常に分かりづらいのですが、Viewに割り振られているIDも一緒に表示されるのである程度把握できると思います。

このIDが表示されるのを利用して、View階層の中でIDの衝突が起こっていないかなんてことを調べるのにも便利かもしれません。

ちなみにadb shell dumpsys activityと最後のtopを省略すると、Activity Managerの情報がズラズラと表示されます。

Broad castがどうなってるかとか、Content Providerがどんなのが動いているかとか、どんなServiceが動いているかとか、スタックがどうなってるかとかが出力されます。

実機でコマンドを打つだけで調べられるので、手軽で便利だと思います。

BaseSaveStateにを拡張してカスタムViewの状態を復元する際の注意点

この記事は最終更新から3ヶ月以上が経過しています。情報が古い可能性があります。

カスタムViewを作った場合、BaseSaveStateを拡張してViewの状態をカスタムView自身で復元できるようにできます。

この際に注意すべきことが3点あります。

Activityを保持しないを有効にしてチェックする

カスタムViewの復元機能を実装したら、必ず開発者オプションのActivityを保持しないを有効にしてちゃんどう動くかどうか確認しましょう。

自分ではちゃんと実装したつもりでも、これを有効にした状態で画面回転させるとアプリが落ちる場合があります。

フィールド名のタイポに注意

BaseSaveStateを拡張したクラスには、必ずpublic static final Parcelable.Creator<BaseSaveStateを拡張したクラス名> CREATORというフィールドが必要です。

このフィールドの名前はCREATORでなければなりません。

CREATERとタイポすると動きません。動かない上にエラーメッセージはjava.lang.RuntimeException: Unable to start activity ComponentInfo{jp.gcreate.sample.savestatecustomview/jp.gcreate.sample.savestatecustomview.MainActivity2Activity}: java.lang.RuntimeException: Parcel android.os.Parcel@18c09797: Unmarshalling unknown type code 2131296303 at offset 264のように、「フィールド名が違います」と教えてくれません。

writeToParcelで書き出す順番

writeToParcelで書き出す順番とコンストラクタで読み出す順番は同じ順番にしなければなりません。

書き出す順番と読み出す順番が異なるとうまく復元することができません。

順番を同じにすることと一緒に忘れていけないのは、最初にsuperを呼び出すことです。

        public ImageState(Parcel source) {
            super(source);
            savedUri = source.readParcelable(Uri.class.getClassLoader());
        }

        @Override
        public void writeToParcel(@NonNull Parcel dest, int flags) {
            super.writeToParcel(dest, flags);
            dest.writeParcelable(savedUri, flags);
        }
```

コンストラクタで`super(source)`を最初に呼び出す、`writeToParcel`の最初で`super.writeToParcel(dest, flags)`を呼び出すことも忘れてはいけません。

単純なことですが、エラーメッセージからどこが悪いのか把握しづらいので、知らないとドはまりするので注意しましょう。

## サンプル

public class UriImageView extends ImageView{
 private Uri mUri;

 public UriImageView(Context context, AttributeSet attrs) {
 super(context, attrs);
 setImage();
 }

 private void setImage() {
 if(mUri == null){
 setImageDrawable(getContext().getResources().getDrawable(android.R.drawable.btn_star, getContext().getTheme()));
 }else{
 setImageURI(mUri);
 }
 }

 public void setUri(Uri uri) {
 mUri = uri;
 setImage();
 }

 @Override
 protected Parcelable onSaveInstanceState() {
 Parcelable superState = super.onSaveInstanceState();
 ImageState imageState = new ImageState(superState);
 imageState.savedUri = mUri;
 return imageState;
 }

 @Override
 protected void onRestoreInstanceState(Parcelable state) {
 ImageState imageState = (ImageState) state;
 super.onRestoreInstanceState(imageState.getSuperState());
 setUri(imageState.savedUri);
 requestLayout();
 }

 static class ImageState extends BaseSavedState{
 public static final Parcelable.Creator CREATOR = new Parcelable.Creator(){

 @Override
 public ImageState createFromParcel(Parcel source) {
 return new ImageState(source);
 }

 @Override
 public ImageState[] newArray(int size) {
 return new ImageState[size];
 }
 };
 Uri savedUri;

 public ImageState(Parcel source) {
 super(source);
 savedUri = source.readParcelable(Uri.class.getClassLoader());
 }

 public ImageState(final Parcelable superState) {
 super(superState);
 }

 @Override
 public void writeToParcel(@NonNull Parcel dest, int flags) {
 super.writeToParcel(dest, flags);
 dest.writeParcelable(savedUri, flags);
 }
 }
}
“`