カテゴリー: ライブラリ

Androidプログラミングに便利なライブラリについての情報を取り扱っています。

KotlinでUnitテストでassertionに何を使うか

KotlinでUnitテストを書く際に、assertionに何を使うかという話。

私の場合特にこだわりがあるわけではないのだが、基本的には多数派に従いたいという気持ちが強い。しかしながら、Kotlinのテストのassertionに使うならこれ一択、みたいなものがない(と思っている)ので、いつも困る。

Javaなら特に何もせずともhamcrestのassertThat(sut.getHoge()).is("fuga")みたいなassertionを使っていればいいのだが、Kotlinの場合はisが予約語であるという問題がある。いちいちisをバッククォートでくくらなければならず美しくない。

knit

https://github.com/ntaro/knit

日本ではいちばん有名なやつだと思われる(DroidKaigiのアプリでも使われていたはずなので)。

sut.getHoge().should be "fuga"みたいな感じで使う。

ただrepositoryを追加しないと組み込めないので私はあまり使っていない。build.gradleに一行追加するだけの話なんだけどね・・・。

expekt

https://github.com/winterbe/expekt

knitに似たsut.getHoge().should.equals("fuga")みたいな感じのAPI。リポジトリの追加が必要ないので、私はこっちをよく使っている。

ただメンテされてないのが玉に瑕。

kotlintest

https://github.com/kotlintest/kotlintest

assertionライブラリではなくて、Spec風のテストを書くためのライブラリ。

ただsut.getHoge() shouldBe "fuga"みたいなassertionが含まれている。

SpekというSpec風のテストを書くためのライブラリがあるが、こちらはJava8でなければ使えないという制約があって、Androidのプロジェクトに適用するのはなんだか難しそうで手を出していない。

kotlintestはネストしたテストを書きやすくて、これいいかもとか思ったこともあったのだけれど、androidTestに組み込むとメソッドカウントがオーバーしてしまうので使えない。

assertk

https://github.com/willowtreeapps/assertk

Android Weeklyで流れてきて知った。AssertJチックなassertionライブラリで、Kotlinで使うならコレのが便利なのだろうか。

assert(sut.getHoge()).isEqualTo("fuga")みたいな感じで使う。

しかし個々最近はshould系ばかり使ってきているので、最初にassertを書かないといけない形式はめんどくさいと感じてしまっている。

AssertJ

http://joel-costigliola.github.io/assertj/

Javaのコードも考慮に入れるならJavaで使えるライブラリを使うのがいいのだろう。

私はJavaだとhamcrest使っとけばいいやな人だったので、Javaでassersionライブラリを何使うかなんて特に気にしたことはなかった。

みんなが使っているものが知りたい

みんなはどれを使っているのだろうか。他にこれが使いやすいみたいなのがあったら教えて欲しい。

私はexpektを使う傾向が強いが、揺らいでいる。

正直なところassertionさえできればなんでもいいのだろうからして、自分が使いやすいものを使えばいいってだけの話なのだろうけれども。

https://discuss.kotlinlang.org/t/what-assertions-library-do-you-use/1809

ここをみると、kotlintestが多いっぽい。

Architecture Componentを触ってみた

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

とりあえず軽く触ってみた。

codelab

Google IOの動画はとりあえず2つ見てみた。

最初のやつは「こんなん作ったでー」という話で、2つ目がその中身を説明っていう感じなので、時間がないなら2つ目だけ見ればいいんじゃないかなと思う。全部英語なので、私の英語力では雰囲気しかわからず時間対効果はあまり良くなかった気がしている。

3つ目もあるのだけど、これはまだ見れていない。タイトルから見るとRoomについての説明なのだろうか。

個人的にArchitecture Componentで気になっていたのは、いかにActivityのライフサイクルに振り回されなくてすむようにできるかという部分だ。つまり、LifecycleOwnerLiveDataViewModelについてである。とりあえずその観点で言うと、最初にあげたcodelabを触れば、雰囲気はわかった。2つ目の動画でだいたいスタンスがわかったような気がしている。

たぶん2つ目の動画で話していたと思うのだけど、このArchitecture Componentはすでに個々の開発者がそれぞれの工夫でActivityなどのライフサイクルによる呪縛を回避している手段を置き換えるためのものではないという話が、個人的にはしっくり来た。すでにライフサイクルとの付き合い方がうまくできている人は、別にそれでいいと。

ただ、Androidをこれから学ぼうとする人にとっては、Android特有のライフサイクルにまつわるあれこれは、学習していく上でつまづきやすいポイントで、さらにそれを回避するためのライブラリの使い方を学ぼうとすると余計にややこしくなってしまう。そこで、これからAndroidを学んでいく人にとって、とっつきやすいシンプルな仕組みを用意したよ、というのがArchitecture Componentということらしい。

私は最近ではDaggerやRxJavaを使って、Activityにはデータを持たせない、単に表示するだけのものとして扱うようにしてアプリを作るようにしている。そういった方法ですでにうまいこと回せているなら、無理して移行する必要はないのだろう。すでにうまいことやっている人にとっては、ちょっと触れば雰囲気がつかめるだろうから、とりあえずcodelabだけ触って雰囲気を掴んでおいて、1.0が出るのを待つくらいのスタンスでいいんじゃないかなと思う。

以下は触ってみた感想。

LifecycleOwnerとは

ざっくりした理解で言うと、ActivityとかFragmentとかServiceとか、Android特有のライフサイクルをもってるオブジェクトのことという認識でいる。

LiveDataの購読を行う際に引数に指定してやったり、ViewModelの生成・取得を行う際に引数に渡したりするのに出てくる。

LiveDataの購読に関しては、LifecycleOwner(Activityとか)のライフサイクルにあわせて自動的に購読解除してくれるらしい。つまり、いちいち自分でunsubscribe/disposeとかしたりしなくていいっていうこと。

またActivity等のライフサイクルにあわせた処理を行うのに利用したりできるっぽい。(codelabではLocationManagerから位置情報を受け取るクラスを作って、Activityのライフサイクルにあわせてセンサーの登録と解除を行うのに使っていた)

LiveData

UIに表示したりする実際のデータ。DataBindingでいうObservable<Hoge>みたいなものだし、RxJavaでいうObservable<Hoge>みたいなもの。

その実態は最新の値を保持してよしなに通知してくれるデータホルダー。RxJavaみたいなストリームではないよということだ。またスレッドの概念も持ってないので、バックグラウンドで処理してメインスレッドで通知みたいなことはしない。

LiveDataの更新はメインスレッドでないとできないみたいで、別スレッドからLiveData.setValue()したら落ちた。別スレッドから値を更新したい場合は、postValue()を使うらしい。

ActivityなどのViewは、このLiveDataを購読して、変更を受け取ったらUIを更新することだけ考えるような作りにするのが良いのだろう。

RxJavaでいうBehaviorSubjectみたいな動きをするなぁという印象を持った。

ViewModel

画面回転してもライフサイクルが継続してくれるもの。これが最初からあればAndroidアプリ開発はもっと楽になっていただろうと思う。

今までActivityにもたせていた状態やロジックを、全部こっちに持ってくればうまいことできると思う。

Activity.finish()を呼び出したらViewModelのライフサイクルも終了する(ViewModel::onCleard()が呼び出される)。

画面を回転させた場合はそのまま以前の状態を引き継いだものが、再生成されたActivityに渡ってくる。ただ、あくまでonConfigurationChangeで破棄されないだけなので、ホーム画面に一度移動した後にOSによって終了されたりした場合(Activityを保持しないが有効になってたりした場合)はViewModelのライフサイクルも終了してしまう。

データの永続化は別途考えないといけない。

軽く触ってみて

RxJavaなどの知識を持っているからか、割りとすんなり使えそうな気がしている。

それを抜きにしてもそんなにややこしくないと思うので、Androidをこれから学ぼうという人はとりあえずArchitecture Componentの使い方を学ぶと、シュッと入門できていいんじゃないだろうか。

触る前は「なんかいろいろあってややこしそうだな」とか思っていたのだが、触ってみると思いの外それぞれ独立していて、使いたいコンポーネントだけ利用すればいいという意味がよくわかった。

私個人としては、ViewModelはすぐにでも使いたい。Daggerを使って似たようなことをやっていたけれども、ViewModelを使うほうが楽だ。LiveDataはもうちょっと調べて、DataBindingとの組み合わせ方を掴んだら置き換えるかもしれない。

ActivityScopeを使ってActivityクラスごとにシングルトンになるようにした話

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

まずはじめに。Dagger2の話をしていますが、きちんと理解しているわけではないので間違った内容があるかもしれません。鵜呑みにしないでください。

この記事で言ってることのサンプルコードはGutHubで公開しています。

この記事の要旨は「MainActivityの中でシングルトンを実現したい(した)」ということです。

Dagger2を知ったキッカケ

私がDagger2を知ったきっかけはdroidkaigi2016です。その時からずっと腑に落ちなかったのがActivityScopeの存在です。ActivityScopeがわからないというか、子コンポーネントをわざわざ作る意義がわからなかったのです。

droidkaigi2016のコードを見よう見まねでDagger2を使ったアプリを作ったのですが、そのアプリではほぼすべての依存性をAppModuleに定義してありました。そのためAppModuleだけがやたらと肥大化し、ActivityModuleには何も定義されていないような状態で、ActivityScopeを作っている意味がまったくありませんでした。

結果そのアプリでは、Dagger2を依存性の充足のために用いるのではなく、シングルトンパターンを使うことなくアプリ内でインスタンスが1つになるようにするための道具として使っている状態でした。

ActivityScopeでやりたかったこと

私は、例えば端末の画面が回転しても、同じMainActivityであれば常に同じコントローラなりPresenterなりがセットされるようにしたいと思っていました。そうすれば非同期処理を引き継ぐためにアレコレする煩雑さから解放されます。

それを実現するために子コンポーネントを区切ってActivityScopeを作ってるんだろうと考えていたのですが、実際の挙動はそうはなりません。ActivityModuleで@ActivityScopeなんて指定したところで、画面回転したら注入されるのは異なるインスタンスです。

(これはActivityComponentをActivityのonCreateで初期化して、Activityがそのインスタンスを保持していることに原因がありましたが、詳細は後述)

そもそもActivityのライフサイクルは非常に短命で、初心者がまず躓くポイントとして挙げられるほどに感覚値とずれたものです。画面回転しただけでインスタンスが変わる。同じMainActivityなのに。同じMainActivityが表示されてるのに、実は内部では異なるインスタンスのものなんですというのがややこしいポイントです。

私はずっとActivityScopeを使えば、同じMainActivityなら常に同じインスタンスを注入できるようになるんじゃないかなと思っていました。でもそれができない。それが私の「Dagger2よく分からん」の原因の1つでした。

Dagger2がインスタンスをシングルトンのように扱うことが出来る仕組み

そもそもDagger2でインスタンスを使いまわせるのは、スコープをアノテーションで指定しているからではありません。ApplicationModuleで@Singletonを指定したインスタンスが常に同一であるのは、アプリ内で同じApplicationComponentを参照しているからできていることです。

ApplicationComponentはApplicationクラスを拡張した独自のクラスに保持して、そこにアクセスしていると思います。例えばこれを、(普通そんなことはしませんが)ActivityのonCreateでApplicationComponentを生成するようにしたらどうなるでしょう。画面回転によるActivityの再生成が起こる度に@Singletonとしたインスタンスであろうが毎回異なるインスタンスが注入されることになります。つまり@Singletonをつけてるからシングルトンになるわけではないということです。

ActivityのonCreateでApplicationComponentを生成した場合、同じActivityのインスタンス内ではシングルトンにできます。例えばそのActivityでViewPagerを使っていて、Fragmentを複数内包しているとしましょう。そのFragmentたちはActivityのもつApplicationComponent(ややこしい)にアクセスすることで、@Singletonで指定したインスタンスを使いまわすことが出来ます。

ApplicationクラスでActivityComponentのインスタンスを保持

同じActivityクラスでActivityComponentを使いまわせるようにするためには、Activityより長いライフサイクルを持つものにComponentのインスタンスを保持してもらう他ありません。

私はとりあえずApplicationクラスにActivityクラス名をキーとしたHashMapを持たせて管理させるようにしました。CustomApplication.classのソースコード

こうすることで、例えばFilterEditActivity.classでは、画面回転でActivityのインスタンスが変わろうと、常に同じActivityComponentを取得でき、FilterEditAcitivy内で常に同じPresenterが使えるようになります。

デメリット

ほんとうの意味でのActivityのライフサイクルと異なるわけなので、逆にわかりづらくなっている気がしないでもありません。ActivityScopeといいながらその生存期間はApplicationと同じになってしまっています。

Fragmentを使う場合に更に混乱します。実際にサンプルのプロジェクトでは、Activity+ViewPager+Fragmentを使う部分でややこしいことになっています。

Activityの場合はActivityクラスで識別すればいいのですが、ViewPager内のFragmentはクラス名で識別することが出来ません(同じクラスでも中身が異なるため)。

またActivityContextを注入したい場合に困ります。まず間違っても@ActivityScopeで定義してはいけません。それをやると一番最初に生成されたActivityのインスタンスが使いまわされることになってしまいます。ただスコープをつけなくても、Componentが参照しているActivityContextは最初に作成されたActivityのインスタンスとなってしまうので、スコープをつけないだけでも足りません。

このサンプルプロジェクトでは、苦肉の策としてApplicationクラスにActivityModuleのインスタンスも一緒に管理させるようにしています。ActivityModuleがもつContext(Activity)を更新するためです。

しかしそうやったところで、ActivityScopeで使いまわしたい何らかのインスタンスに、ActivityContextを持たせなければならない場合はどうしようもありません。サンプルプロジェクトでは幸いActivityContextに依存するものがないのでなんとかなっていますが、将来的には不明です。

変な依存を産んでしまっている気がしないでもない

依存性をなくすためのDagger2で、逆に変な依存を産んでしまっているような気がしないでもありません。

ただ、個人的にActivityScopeに持っていたモヤモヤが晴れたことと、Activityクラスごとにシングルトンというのが実現できてよかったと思っています。

ここまで書いておきながら言うのもなんですが、Activityクラスごとにシングルトンにするということは、Application内でシングルトンということと考えて、素直にApplicationScopeで定義したほうがいいのかもしれません。実際このサンプルプロジェクトでも、やっぱりActivityComponentの存在意義があまりないように思います(ApplicationComponentだけあれば事足りるような状態)。

Moduleの肥大化に対しても、役割ごとにモジュールを分けるという方法で対策しているので、ActivityComponent自体をなくしてしまったほうがスッキリするような気もしています。

やっぱりActivityComponentを分ける意義が分かってないです。何かいいことがあるから分けてるんですよね・・・?

英語の記事ですがこちらの記事も参考になるかもしれません。たぶん同じようなことができて、かつスマートな実装なんだと思います。私にはややこしくてよく理解できないので、もうちょっとDagger2の経験値積んでから挑戦しようと思っております。

http://frogermcs.github.io/activities-multibinding-in-dagger-2/

Realmのテストのやり方を知りたい

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

Realmを使ってみました。ちなみに私は、今まではGreenDAOとAndroid Ormaしか使ったことがありません。

とりあえずCRUD操作のやり方をつかもうとテストを書いてみました。テストの書き方が根本的に間違っている可能性が無きにしもあらずですが、こんな感じで作りました。

public class FilterDataSourceRealmTest {
    private static RealmConfiguration    config;
    private static FilterDataSourceRealm sut;

    @BeforeClass
    public static void initializeTest() {
        config = new RealmConfiguration.Builder()
                .name("test_realm")
                .deleteRealmIfMigrationNeeded()
                .build();
        sut = new FilterDataSourceRealm(config);
    }

    @Before
    public void setUp() {
        Realm.deleteRealm(config);
    }

    @After
    public void tearDown() {
        Realm.deleteRealm(config);
    }

    @Test
    public void insertFilter() throws Exception {
        final CountDownLatch latch = new CountDownLatch(1);
        sut.insertFilter("test.com/");
        sut.getFilter("test.com/")
           .subscribe(new Action1<UriFilter>() {
               @Override
               public void call(UriFilter uriFilter) {
                   assertThat(uriFilter.getFilter(), is("test.com/"));
                   latch.countDown();
               }
           });
        latch.await(2, TimeUnit.SECONDS);
    }
}

テスト対象のコード(一部抜粋)はこんな感じです。

public class FilterDataSourceRealm implements FilterDataSource {
    private RealmConfiguration config;

    public FilterDataSourceRealm(RealmConfiguration config) {
        this.config = config;
    }

    @Override
    public void insertFilter(String insert) {
        Realm realm = Realm.getInstance(config);
        realm.beginTransaction();
        realm.copyToRealmOrUpdate(new UriFilter(insert));
        realm.commitTransaction();
        realm.close();
    }

このテストコードはandroidTestに配置して実機で実行します(Instrumentation Test)。

テストのたびにデータをまっさらにするため、@Before@AfterRealm.deleteRealm()を呼び出しています。

FilterDataSourceRealmはDaggerを使ってシングルトンで運用します。初期化時にRealmConfigrationを与えて、Realmのインスタンスは各メソッドの中でインスタンス生成&closeを行うようになっています。

で、このテストコードでテストを行うと、テストメソッド内でRealm関連の操作が失敗した場合にjava.lang.IllegalStateException: It's not allowed to delete the file associated with an open Realm. Remember to close() all the instances of the Realm before deleting its file: /data/data/jp.gcreate.sample.daggersandbox/files/test_realmというエラーが出ます。Realmへの操作で何らかのエラーが生じたんでしょうが、出て来るエラーは「close忘れてるぞ」になります。

例外の意味は分かります。Realm.deleteRealm()を実行するときにcloseしてないRealmのインスタンスが存在してはいけないということです。ですが、この場合のエラーの本質はClose忘れではなく、Realmの操作が失敗していることです。私はその原因が何なのか知りたいわけです。insertFilterが失敗したからcloseが行われず、その結果@Afterで実行しようとしたRealm.deleteRealm()が失敗しているわけですから。

実装中はこの「close忘れ」エラーが出るたびに、@Before@Afterの処理をコメントアウトし、本当のエラーの原因を確認していました。すると実際にどこで失敗しているのかエラーメッセージが教えてくれました。

テストメソッドの度にRealmのファイルを消すというのが愚策なんですかねぇ・・・。でも消さないとテストの実行順で登録データが異なることになってテストにならないし。

公式サンプルにPowerMockを使ったテストがありましたが、PowerMockよくわからないのと、Mockじゃなくて実際に書き込みとかしたかったのでこんな形のテストを書いてみたのですが、テスト失敗のログがまったく役に立たなくて苦労しました。

Realmもテストも詳しいわけではないので、いろいろ勘違いしている部分があるのかもしれません。

Daggerを使ってSingletonにする仕組み

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

ものすごいあほうなことを書いているかもしれませんが、そのときはご指摘ください。

Daggerを使って依存性を注入する際に、アプリ内でSingletonになるようにすることあるじゃないですか。

@Singleton
@Component(modules = AppModule.class)
public interface AppComponent {
    void inject(MainActivity activity);
}

@Module
public class AppModule {
    private Context context;
    public AppModule(Context context) {
        this.context = context;
    }

    @Provides
    @Singleton
    public SomeClass provideSomeClass() {
        return new SomeClass().initializeWithDefault();
    }
}

みたいに、SomeClassがアプリ内でシングルトンになるようにすると。

今までずっと、@Singletonって指定してるから実現できてるんだと思っておりました。実際には違います。これはそもそもAppComponent自体がアプリ内でシングルトンになっていなければ実現されません。

このAppComponentはApplicationクラスを拡張して、そこで初期化してるから@Singletonという指定が効くのです。このAppComponentを、ActivityのonCreateで初期化していたらシングルトンにはなりません。AppComponentインスタンスの中ではSomeClassのインスタンスは一度生成されたら使いまわされますが、AppComponentのインスタンスが複数生まれてしまえば生成されるSomeClassもAppComponentのインスタンスの数と同じだけ増えていくことになります。

そして@Singletonは別に@Singletonでなくてもいいのです。自分でスコープを作って、例えば

@AppScope
@Component(modules = AppModule.class)
public interface AppComponent {
}

@Module
public class AppModule {
    @Provides
    @AppScope
    public SomeClass provideSomeClass() {
    }
}

としても結果は同じです。Componentにつけたスコープ名の中でインスタンスを使いまわすっていう感じになるわけです。

だから@Singletonつけてるからシングルトンになるわけではないのです。AppComponentのインスタンスがアプリ内で1つだからこそ、シングルトンにできているわけです。

ここがあやふやなままだったので、Daggerよく分からん状態だったのですが、これで一歩前進できます。

Crashlyticsのクラッシュレポートをdebugビルドでは行わないようにする

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

新しいアプリを作るときにしかやらないので、いつもやり方を忘れてしまうCrashlyticsのレポート設定のメモ。

デバッグビルドでまでクラッシュレポートが報告されてしまうと、通知がうっとうしいだけですし、手元でスタックトレースが読めるわけですから完全に無駄です。必要なのは基本的にはリリースビルドだけでしょうから、開発中は無効にしてしまうのが便利です。

やることは下記のページのとおりです。

https://docs.fabric.io/android/crashlytics/build-tools.html?gradle#build-tools

まずはレポートを無効にしたいbuildTypeにext.enableCrashlytics = falseを追加します。

この設定はGradleのビルド時に、Crashlyticsで使うIDなんかを生成する処理を行わないようにするもののようです。レポート送信を止めるわけではないので、これを追加しただけでは下記のようなエラーが出てしまいます。

This app relies on Crashlytics. Please sign up for access at https://fabric.io/sign_up,
                                                   install an Android build tool and ask a team member to invite you to this app's organization.

開発時にレポート送信自体を止めるためには、アプリ実行時にCrashlytics自体が動かないようにする必要があります。そのためには、Crashlyticsの初期化を行う部分で、下記のように初期化を行います。

        Crashlytics crashlyticsKit = new Crashlytics.Builder()
                .core(new CrashlyticsCore.Builder()
                        .disabled(BuildConfig.DEBUG).build())
                .build();
        Fabric.with(this, crashlyticsKit);

ちなみにdebuggableがfalseなんだけどCrashlyticsのレポートを無効化したい、なんていう場合には、.disabled(BuildConfig.DEBUG || BuildConfig.BUILD_TYPE.equals("test"))のようにしてやればOK。

レポートが送信されないことの確認

アプリの任意の場所でthrow new RuntimeException("Crashlytics report test");と例外を投げてみればOK。

ただし例外を投げる場所に注意が必要です。Crashlyticsの初期化が終わっていないと、そもそもレポートが送信できません。Applicationクラスの外か、ActivityのonCreateの外で例外を投げるようにしないとテストにならないので注意しましょう。

https://docs.fabric.io/android/crashlytics/introduction.html

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

この記事は最終更新から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に持ち込む

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の具体的な使用例としていい勉強にもなって、個人的には一石二鳥な感じです。ありがたや〜。

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

UndoBarを使った削除処理を考える

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

UndoBar – GitHub

削除処理を行って、それを取り消し可能なようにする実装を考えます。例えばGmailのアプリでメッセージをアーカイブしたときなどに表示されるあれです。

削除する際に「本当に消しますか?」みたいな確認ダイアログを表示するのはナンセンスっぽいです。

なぜならその確認ダイアログは、データが消えてアクセスできなくなるという責任をユーザーに転嫁してるだけだということです。そもそも削除したいだけなのにいちいち確認されるのもうっとおしいだけではないかという理由もあります。

それならば、もし消したくないデータを誤って削除してしまったとしても、それを復元できるようにするのがユーザー目線でいいだろって話です。

この辺りの話はSmashing Android UI レスポンシブUIとデザインパターンという本で知りました。

では実際に取消可能な削除機能を実装するためにはどうすればいいかというと、UndoBarを利用するのが簡単そうな気がします。Crouton使おうかと思ったけども、いざ使おうと思ったらレイアウトを実装したりするのが~~面倒~~大変だったので、シンプルなUndoBarを使いました。ソースコードはCroutonの方が読みやすかったけど。

UndoBarの表示

今回私がやりたかったのは、データベースに保存してあるデータを消すという例です。データベースに保存されているデータをListViewで表示している。そのListViewのうちの1つのアイテムを選んで削除をするというパターン。

実装方法としては削除の操作を行った時(UndoBarを表示させる時点)でどうするかによって、2パターンに分岐するかと思います。

  1. この時点で実際にDB上のデータを消してしまう
  2. この時点ではDBは触らず、見た目上のデータを消す

どちらのパターンにせよ、この時点でListViewに表示されてるデータを消すのは共通です。データベースのデータを消すタイミングがここかどうかです。

1.のパターンは見た目と実装がそのままリンクしています。Listから消えたらデータベースからも消える、シンプルです。

このパターンの場合は操作の取消を選んだら、消したデータを復元する必要があります。そのためどうやってデータをロールバックするのかに頭を悩ませることになります。データベースの構造が複雑だと元に戻すのが大変かもしれません。

2.の場合はUndoといいつつ、実際には削除処理を猶予しているだけです。このパターンの場合、取消操作はデータベースからデータを削除するのを取り消す処理(ややこしい)になります。

こちらは復元処理を考える必要がありません。一方でUndoBarが消える前にアプリを終了したり、画面が回転したときどうすんのっていう新たな問題が生じます。

UndoBarからのコールバック

UndoBarController.AdvancedUndoListenerを使うことで3つのタイミングによるコールバックを受け取ることができます。

この3つのコールバックをうまいこと利用して削除機能を実装していくことになります。

onUndo()

取消ボタンを押した際に呼ばれます。Undo時の処理をここで行うと良いです。

パターン1のときならここで復元処理を行うことになります。

onHide()

取消ボタンが押されず、UndoBarが消える際に呼ばれます。UndoBarに設定した表示時間が経過した後でコールされるわけです。UndoBar表示中に画面回転した場合には呼ばれません。

ちなみに画面回転に対応するには、自分でハンドリングしてやらないといけないそうです。今回私はそこまで踏み込みませんでした。

onClear()

UndoBar.clear()を明示的にコールした際に呼ばれます。

単に画面回転させたら呼ばれるのかというとそうではありません。画面回転時にコールバックを受け取りたいなら、Activity#onDestroy()(もしくはonStop(),onPause())でclear()してやる必要があります。

そのためにはUndoBarのインスタンスをフィールド変数として保持しておかなりといけません。サンプルのように、単にnew UndoBar()して表示させていると、表示したUndoBarを識別できないので、clear()することができません。

ただし単にUndoBarを消したいときとごっちゃになってややこしくなるので、画面回転時にclearするのはよくないかもしれません。回転時は自分でハンドリングして再表示してやるのが一番いいと思います。

削除機能を実装する

1.実際に削除するパターン

onUndo()で削除した対象のデータを元に戻します。このパターンだとここがややこしいです。

消すデータをメモリ上に確保しておいて、Undoされたら消したデータを再インサートする。データベースに削除フラグ持たせてそれを立てる。退避用テーブルにデータを移しておく。・・・どういう方法を取るのがベストなんでしょうかね?

ちなみに私はメモリ上にデータを持たせて、Undoされた際に再登録する方法を選びました。

onHide()は何もしないでいいです。UndoBar表示時点でデータは消えてますので。

onClear()はonHide()と同じ処理でいいでしょう。

2.Undoされなかったら消すパターン

onUndo()はListViewを元に戻すだけでいいです。データベースは変更されていないので、DBから読みなおすなり、消したデータだけAdapterに追加するでもいいと思います。

onHide()で実際にデータベースからデータを消します。

onClear()はonHide()と同様にデータベースからデータを消します。でないとUndoBar表示中に追加で削除処理が実行された際に困ります。

UndoBar表示中に追加の削除処理を行ったとき

UndoBar表示中に更に削除処理を行い新しいUndoBarを表示させたらどうなるか。表示されているUndoBarは以前のもののままです。新しいUndoBarは古いUndoBarの表示時間が過ぎた後で表示されます。

これでは削除対象と表示されてるUndoBarがリンクしないのでよろしくありません。今削除したものを取り消したつもりが、以前処理した奴が取り消されてしまったということになります。

そのため削除処理を行ったら、現在表示中のUndoBarはclear()で消すべきでしょう。

簡単そうでいざ実装してみると大変だった

UndoBarを使って取消可能な削除機能を実装してみましたが、やってみると考えることが多くて意外と大変でした。

それでも自力で実装しようと思うともっと大変でしょうから、ライブラリ作者様には頭が上がりません。

UndoBarはbuild.gradleに1行追加するだけで使えるようになるので、削除処理を実装するときには仕様を検討してみてください。

UndoBar – GitHub