アプリを公開してコメントもらえて嬉しかった話

Google Playで公開しているカスタムフォトウォッチで初めてコメントをもらいました。

英語でのコメントだったのですが、「12時間表示のオプションが欲しい」っていう内容でした。

おそらく私は、単にそれだけのコメントでも嬉しかったと思います。自分で作ったものに対して反応がもらえるっていうのは、それだけでうれしいのです。

しかし今回のコメントはさらに嬉しい要素がありました。「12時間表示が必要だからアンインストールしたけど、時刻や日付の色やサイズを変えれるのはとてもいい」と褒めてくれていました。

英語でのコメントでしたけど、褒めてくれてるっぽくてテンションが上りました。

つたない英語力とGoogle Translateの力を借りつつ、コメントくれた人とメッセージのやり取りしました。思えば生まれて初めて海外の人とコミュニケーションした気がします。

12時間表示は実は奥が深い

今回の話は「単に嬉しかった」だけに終わりません。実はいい勉強にもなりました。それは12時間表示についてです。

今回もらったコメントに対して、さっそく12時間表示への対応を行いました。スマホの時刻設定で12時間表示にしている場合、wear側での表示を12時間表示にするようにしたのです。12時になったら0時と表示するようにしたわけです。

しかしコメントをくれた方とのやりとりの中で、「うちの国では0時は深夜の0時(24時)を意味するんだ」ということを言われました。

カスタムフォトウォッチでは日付の表示方法についてはいろいろなパターンを提供するようにしていました。海外では月/日ではなく、日/月と表示する場合もあることは知っていたからです。

しかし時刻の表示についても似たような問題があることを、ここにきて初めて知りました。

アプリの海外対応は、文字面だけを英語にするだけではないのだなと、とてもいい勉強になりました。

とりあえず公開してしまえも必要なのかも

アプリを公開するにあたって、「こんな機能も要るかな」とかいろいろ迷ったりもしました。色の選択肢はもっと増やしたほうがいいのかとか、デザインはこれでいいのかとか。

それでも最終的には「ぐだぐだ考えるの面倒くさくなったからもういいや」でアプリを公開しました。いつまでたっても公開できる気がしないし、何のリアクションももらえない状態で開発するのもモチベーションが保てなかったのです。

全てのユーザが今回の方のように丁寧なレビューをくれるとは限りません。それどころか「何やねんこの糞アプリ」みたいなコメントが寄せられるかもしれません。が、それでも公開しなければ何の反応ももらえません。

作るところまで作ったら、後は利用者の反応を見ながら開発するっていうのもいいのかもしれません。

コメント付きレビューをもらえたのが初めてだったので、なんか無性に嬉しかったっていう話でした。

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

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)

Read full post gblog_arrow_right

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

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の更新処理を実装します。

Read full post gblog_arrow_right

Android StudioでJunit4によるテストを走らせる

基本的にはここに書いてあるとおりにやればいいだけの話です。

準備

/app/build.gradleのdependenciesにjunitを追加します。

testCompile 'junit:junit:4.12'
testCompile 'org.mockito:mockito-core:1.9.5'

この際に注意が必要なのは、androidTestCompiletestCompileは別物であるということです。

何が別物かというと、テストコードを配置するディレクトリがそれぞれ違います。

その名の通りandroidTestCompileandroidTestディレクトリに配置したテストコードのコンパイル時にだけ使うライブラリです。同じくtestCompiletestディレクトリに配置したテストコードのみに使われるライブラリになります。

なお、androidTestディレクトリは自動的に作成されていますが、testディレクトリは自分で作らなければなりません。(ディレクトリは/app/src/test/java/パッケージ名にしてやればOK)

androidTestとtestの切り替え

Build Variantsウィンドウを開くと、Test Artifactという欄があります。

Build VariantsのTest Artifact

Android Instrumentation Testsを選択していると、androidTestディレクトリ以下にあるテストコードが有効化されます。有効化されるというのが適切なのかは分かりませんが、Android Studioからコンパイル対象のソースコードであると認識されます。

Unit Testsに切り替えると、testディレクトリが有効化されます。試しに切り替えてみると、androidTestディレクトリの色が変わって、テストコードのアイコンに赤いJアイコンが出るようになると思います。

IDE上からテストを実行しようと思うと、このBuild Variantsをいちいち切り替えないといけないのが面倒くさいかもしれません。

しかし、ターミナルからGradleを使って実行する場合は、このTest Artifactsの切り替えはしなくてもいいみたいです。Gradleからテストを実行する場合、./gradlew connectedAndroidTestがAndroid Instrumentation Testsを、./gradlew testがUnit Testsを選択してテストを実行するのと同じになります。この場合のテスト結果は/app/build/reports/testsの中に出力されます。

テストコードはViewやActivityなどのUIに関するテストをandroidTestディレクトリに、純粋なJavaコードのテストはtestディレクトリに置くように工夫すべきでしょう。そうしてやれば、ユニットテストにかかる時間を削減できて幸せになれると思います。

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

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.というエラーが出てビルドが止まってしまう問題が生じるかもしれません。

Read full post gblog_arrow_right

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

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を使って作られているか調べる方法

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

調べたい画面を表示させた状態で、ターミナルから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の状態を復元する際の注意点

カスタム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);
 }
 }
} “`

Android StudioでLogcatをフィルタリングする

Logcatでアプリのデバッグをする際に、自分のアプリからのログ出力だけ見たいなんてときありますよね。

そんなときにどうやってフィルタリングをするかという話です。

キーワードやログレベルでのフィルタリングはここでできます。

ログレベル・キーワードでフィルタリング

ちなみにログレベルでのフィルタリングは、指定したレベル以下のものを出力するというフィルタ設定になります。

例えばここでInfoを選ぶとInfo以下のものだけが出力されるようになり、VerboseとDebugレベルのログは表示されなくなります。

ログレベルは下図で囲った部分です。ログレベル/タグという形式で出力されています。

ログレベル

特定のアプリからのログ出力だけ見たい場合は、Show only selected applicationのところをクリックして、Edit Filter Configrationを選びます。

Edit Filter Configration

ここでPackage Nameのところで調べたいアプリのパッケージ名を指定してやると、自分のアプリからの出力しか表示されなくなります。

Package Nameでアプリからの出力のみに絞れる

こういったフィルタリングの設定をうまく使えば、アプリ開発も捗ると思います。

一方でLogcatにこれらのフィルタ設定をしているときには注意しなければならないことがあります。それは、アプリが落ちた時のスタックトレースまでフィルタされて表示されないことがあるということです。

「アプリが落ちたのにLogcatに何も表示されない・・・」と混乱しないようにしましょう。

AsyncTaskLoaderの動きを確認中 その2

前回の続きでAsyncTaskLoaderを使ったサンプルを作って、Loaderの動きを確認していたのですが、1つの問題点にぶち当たりました。

initLoaderでLoaderを動かす分にはとてもスッキリしたのですが、restartLoaderを使うと非同期処理がイメージ通りに動きませんでした。

それは以前の非同期処理が終わらないと、restartLoaderで新しく動かす非同期処理が始まらないということです。

私が作ったサンプルでは、指定した数字までカウントアップを行う非同期処理をしています。しかし非同期処理中にrestartLoaderを呼び出すと、今動いている非同期処理が終わらないと新しい非同期処理が動いてくれないのです。

restartLoaderを呼んだら今動いている非同期処理には停止してもらい、すぐに新しい非同期処理が始まって欲しいです。使いもしない非同期処理の終了を待つのは時間の無駄ですし、使いもしない処理にリソースを割くのももったいないです。 
## ソースコードを読んで分かったこと
 現在進行形で格闘しているので、まとまっていないですがこんな感じ。

  • onStopLoadingはActivityがonStopになったときに呼ばれる(画面回転時は除く)
  • キャンセルの処理(restartLoader実行時)は、まずonCancelLoadが呼ばれる
  • AsyncTaskLoader.onCancelLoadでLoaderの状態に合わせてキャンセル処理を行う
  • 実際のキャンセル処理はcancelLoadInBackgroundメソッドで行われる
  • しかしAsyncTaskLoader.cancelLoadInBackgroundでは何もしていない
  • すなわち実際にloadInBackgroundの処理を止めるのは自分で実装しなければならない
  • AsyncTaskLoader.onCancelLoadを経ていれば、loadInBackgroundの処理結果は最終的にonCanceledに通知される
  • LoaderManagerがうまいこと管理してくれているので、restartLoader呼んだ数だけ非同期処理が乱立するわけではない(それでもいくつか並行して走るけれども)

そもそもLoaderManagerは何している?

LoaderManagerはrestartLoaderが呼ばれた時に何をしているのかも、同時進行で読み解いています。

LoaderManagerはLoaderをmLoadersとmInactiveLoadersという2つのリストで管理しています。

mLoadersでは現在実行中のLoaderを、mInactiveLoadersでは以前実行されていたLoaderを管理しています。mInactiveLoadersはLoaderを破棄するためのもののようです。おそらく。

restartLoaderをすると、LoaderManagerはLoaderの状態によってあれやこれやしながら新しいLoaderを作成します。現在実行中のLoaderがあればキャンセル処理を行いますが、新しいタスクはmPendingLoaderに登録します。

mPendingLoaderが何者かというと、その名が表すように次に実行される非同期処理のタスク(Loader)です。このmPendingLoaderがいつ実行されるのかというと、今実行されているタスクのloadInBackgroundが終了した時です。 
そのため実行中のタスクが終わらないと、restartLoaderで作られた新しいタスクが始まらないのです。

AsyncTaskLoader上のキャンセル処理

Loaderの非同期処理が実行されているときにキャンセルがかかると、以下の場合にonCanceledが呼ばれます。

  • Loaderが非同期処理実行中の間に、Activity等でinitLoader().forceLoad()をしたとき
  • Activity等でrestartLoaderを呼んだ時(非同期処理が実行中かは問わない)

AsyncTaskLoader.onCancelLoad()でキャンセル関連の処理が行われているためsuper.onCancelLoad()を呼ぶ必要があります。

ただしやってるのはLoaderの管理情報の更新だけで、実行中のloadInBackgroundを止めるような処理は何もしていません。

具体的には、、現在実行中の非同期処理があるか確認(mTask != null)し、タスクがなければキャンセル対象がないので何もしません。

ある場合には、キャンセル処理中の非同期処理があるかを確認します(mCancellingTask != null)。

mCancellingTaskがある場合、onCancelLoadが呼び出されたLoaderがPendingTaskなら破棄します(mTask.waiting == true)。これは実行待ち状態のLoaderをキャンセルすることを意味しています。実行待ちのタスクはまだ開始されてないから破棄するだけでいいわけです。

mCancellingTaskがない場合は、onCancelLoadが呼び出されたLoaderがPendingTaskか確認します。上と同じことをやっていますが、mCancellingTaskがない場合、このLoaderをキャンセルされたタスクとして退避させる必要があるので条件分岐されてます。

で、PendingTaskであればLoaderをそのまま破棄します。まだ非同期処理が始まっていないのでそのまま破棄するだけでいいからです。

PendingTaskでないのであれば、このLoaderは現在稼働中の非同期処理ということになります。そこでこれをキャンセルし、mCancellingTaskへと退避します。その上でcancelLoadInBackgroundを呼び出します。

そのため、Loaderをキャンセルするための処理は、cancelLoadInBackgroundで実装すればいいことになります。

cancelLoadInBackgroundで何をすればいいか

Loaderをキャンセルするための処理を実装するといっても、具体的にどう実装すればいいかというとよく分かりません。

このメソッドの中からloadInBackgroundの処理を停止させることはできないでしょう。むしろこのメソッドは、メインスレッド(Activityとか)からLoaderを停止させるためのメソッドのような気がします。

しかし直接ActivityからこのcancelLoadInBackgroundを呼ぶと、LoaderManagerの管理下から外れた動きをすることになって、変なことになりそうな気がします。

結局のところ、loadInBackgroundの中でisLoadInBackgroundCancelled(Loaderがキャンセルされたらtrueになる)をチェックして非同期処理を途中で止めるように実装するしかなさそうです。

新たな謎

今気づいたんですが、ActivityからLoaderのforceLoadを呼んだ後でrestartLoaderすると、前の非同期処理完了を待つことなくrestartLoaderした処理が走っていることに気づきました。前の処理は走ったままなので、2つの非同期処理が並列で走ってますけども。

この違いはいったいどこからやってくるのか・・・。

ちなみにサンプルはGitHubで公開中です。