AsyncTaskLoaderの実装方法で統一されたものがあまりないようです。
AsyncTaskの使用は推奨されなくなり、今後はAsyncTaskLoaderを使う必要があるらしい。さらにAsyncTaskLoaderはandroid.app.LoadManagerとandroid.support.v4.app.LoaderManagerがあるが、Android Pではv4ではないほうが使えなくなるので、v4のほうを使う必要がある。
AsyncTaskLoaderはバックグラウンドで動作し、処理が完了したらコールバックしてくれる仕組み。Activityが裏にまわって一次停止しても、再びActivityが再開されたときに自動的に再通知をしてくれたりする。
AsyncTaskLoaderを利用するには、AsyncTaskLoaderを継承したbaseクラスを作成し、さらにそれを継承するクラスを作って実際の処理(今回は画像のダウンロード)をするようなことをGoogle本家のサンプルで記述されているので、ネットで公開されているサンプルもほとんどそれに則ったようなものが多い。だけど場合によっては、そのような実装をしなくてもいいこともありそう。
今回Android Developerの流儀に従わず最低限の動作をまとめてみた。
AsyncTaskLoaderを利用するには前述の通り、AsyncTaskLoaderを継承したクラスを作成し、そこに処理を記述する。それをLoadManagerに登録することでバックグラウンドで実行させたり、アクティビティのインスタンスが削除されたり、裏画面になったときに様々な処理をしたりすることを自動的にやってくれる。
具体的には以下のように継承したクラスを作る。今回は使い回しができるように<T>で作成している。
loadInBackground()のメソッドの中で処理を記述し、画像を返してあげればいい。
基本的な流れは上記で作成したAsyncTaskLoaderをLoaderManagerに登録し、AsyncTaskLoaderを実行すると、必要なデータがコールバックされてくるので、そのデータをImageViewで表示してあげればいい。
Activityが開始されinitLoaderを実行すると、onCreateLoaderが実行されAsyncTaskLoaderのインスタンスが生成される。その次にonStartLoadingが実行される。そして、onStartLoadingメソッドの中でforceLoad()の記述がある場合にはloadInBackgroundが実行され非同期処理が行われる。そこで得たデータはdeliverResultを通じてアクティビティのonLoadFinishedがコールバックされる。
同じIDでもういちどinitLoaderを実行した場合には、onCreateLoaderは呼ばれずに、すでに作成されているLoaderを利用する。すでにデータを受信している場合にはLoader内部にデータを保持しているためloadInBackGroundは行われずonLoadFinishedがコールバックされ内部で保持しているデータを直接返すような処理をしている。ただし、forceLoad()かonContentChanged()で強制的にloadInBackgroundが実行される。
また、同じIDで再度initLoaderを実行してもargsは受け付けてくれないので、異なるアドレスからダウンロードをする場合には異なるIDで別のLoaderを作成して実行してあげる必要がある。
スマホの縦横切り替え時でアクティビティが再生成されるときもLoaderは内部にデータを保持し続けるので、縦横切り替え後にinitLoaderを実行すると、やはりhttpのアクセスは行わず内部で保存しているデータを返すような動作をする。縦横切り替え時にはonStopLoadingが呼ばれるので、アクティビティが非表示状態のときに動作して欲しくない処理は停止するようにしておく。
アクティビティが完全に削除されてしまったときにはonResetが呼ばれてLoaderは完全に処理が終了してしまう。内部で保存しているデータも破棄されるので、再びアクティビティが生成されたときはLoaderが再生成されloadInBackgroundが実行されるのでhttp経由で画像がダウンロードされる。
それでは、ダウンロード中に縦横切り替えやアクティビティが削除されたらどうなるか。
Loaderが生成されonStartLoadingが呼ばれloadInBackgroundが実行され、その最中で縦横が切り替えされると、LoaderはonStopLoadingが呼ばれloadInBackgroundも停止する。そのため再度、forceLoadなどを利用してloadInBackgroundを開始する処理を行う必要がある。
縦横ではなくホームボタンを押すなどして一時的にActivityが一次停止し、再びフォアグラウンドに戻ってきたときは自動的にforceLoad()が実行され、ダウンロードの続きをやってくれる。
アクティビティが削除されたときには、すべてなかったことになるのでやはりLoaderも再びLoaderManagerに登録してあげる必要がある。
AsyncTaskLoaderはバックグラウンドで動作し、処理が完了したらコールバックしてくれる仕組み。Activityが裏にまわって一次停止しても、再びActivityが再開されたときに自動的に再通知をしてくれたりする。
AsyncTaskLoaderを利用するには、AsyncTaskLoaderを継承したbaseクラスを作成し、さらにそれを継承するクラスを作って実際の処理(今回は画像のダウンロード)をするようなことをGoogle本家のサンプルで記述されているので、ネットで公開されているサンプルもほとんどそれに則ったようなものが多い。だけど場合によっては、そのような実装をしなくてもいいこともありそう。
今回Android Developerの流儀に従わず最低限の動作をまとめてみた。
AsyncTaskLoaderを利用するには前述の通り、AsyncTaskLoaderを継承したクラスを作成し、そこに処理を記述する。それをLoadManagerに登録することでバックグラウンドで実行させたり、アクティビティのインスタンスが削除されたり、裏画面になったときに様々な処理をしたりすることを自動的にやってくれる。
具体的には以下のように継承したクラスを作る。今回は使い回しができるように<T>で作成している。
open class GetImageBase<T>(context: Context) : AsyncTaskLoader<T>(context) {
//LoaderをLoaderManagerに登録する時に呼ばれる。開始時の初期処理を行う
//縦横切り替え時、アクティビティが生成されたときに呼ばれる
override fun onStartLoading() {
super.onStartLoading()
Log.v("nullpo", "onStartLoading")
//よくあるサンプルだと、受信したデータを保持しておきデータがあればdeliverResult(mData)を呼び出す処理をするが、
//これは実行する必要がなくなったようだ
//情報が更新された場合はLoaderを開始する
//監視する外部プログラムなどからGetImageBase.OnContentChanged()を実行するとtrueになる
//AsyncTaskLoaderをLoaderManagerに登録すると同時に実行する場合に記述する
if (takeContentChanged()) {
//loaderを実行するメソッド
forceLoad()
}
}
//別スレッドで処理を行う本体
override fun loadInBackground(): T? {
Log.v("nullpo", "loadInBackground")
//ここに時間のかかる処理や通信の処理を記述するが、今回はこのクラスを継承するクラスのほうに記述する
return super.onLoadInBackground()
}
//クライアントへ処理結果を返す
override fun deliverResult(data: T?) {
super.deliverResult(data)
}
//Loaderを停止したときに呼ばれる。更新通知も行わない
//画面の縦横切り替え時などActivityのonStopで呼ばれるが、必ずしもすべてで呼ばれるわけではなく、
//AsyncTaskLoaderが必要と思ったときか明示的に停止したときに呼ばれる
override fun onStopLoading() {
super.onStopLoading()
//loaderの停止
//loadInBackgroundでループなど長い処理をしているときなどに停止するなどする
cancelLoad()
}
//Loaderが不要になったときに呼ばれる。リソースの解放などを行う
//主にアクティビティのインスタンスが削除したときに呼ばれる
override fun onReset() {
super.onReset()
//Loaderで利用しているリソースなどを解放する
//明示的にLoaderを停止してデータをnullにする
onStopLoading()
}
}
上記のクラスを継承して実際に画像をダウンロードするクラスを作る。loadInBackground()のメソッドの中で処理を記述し、画像を返してあげればいい。
class GetImage(context: Context, var url: String) : GetImageBase<Bitmap>(context) {
override fun loadInBackground(): Bitmap? {
Log.v("nullpo", "loadInBackGround")
lateinit var urlConnection: HttpURLConnection
val url: URL = URL(url)
try {
urlConnection = (url.openConnection() as HttpURLConnection).also {
it.requestMethod = "GET"
it.connectTimeout = 1000
it.readTimeout = 10000
it.connect()
}
when (urlConnection.responseCode) {
HttpsURLConnection.HTTP_OK -> {
return BitmapFactory.decodeStream(urlConnection.inputStream)
}
else -> {
return null
}
}
} catch (e: Exception) {
return null
} finally {
urlConnection.disconnect()
}
}
}
アクティビティでは、LoaderManager.LoaderCallbacks<T>インターフェースを継承し、メソッドをoverrideする。こうするとさきほど作成したデータを受信すると同時にコールバックされる。基本的な流れは上記で作成したAsyncTaskLoaderをLoaderManagerに登録し、AsyncTaskLoaderを実行すると、必要なデータがコールバックされてくるので、そのデータをImageViewで表示してあげればいい。
//LoaderCallbacks<T>インターフェースを継承する
class MainActivity : AppCompatActivity(), LoaderManager.LoaderCallbacks<Bitmap> {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//ボタン二つのImageViewを設置して画像を受信してみる
button1.setOnClickListener {
Log.v("nullpo", "1a")
//Bundleを利用してデータを受け渡す ここで様々なデータを渡すことで、
//異なるLoaderを作成したり、画像のURLを渡したりする。今回は簡単にURLだけを渡す
var args: Bundle = Bundle().also { it.putString("url", "https://s.yimg.jp/images/top/sp2/cmn/logo-170307.png") }
//第一引数は登録するLoaderを見分けるユニークな数字で、処理によって数値を異なるものにする必要がある
//同じ数値で呼び出すと初期化は行われず内部で保持しているデータを返すだけになる
//第二引数はLoaderを生成するのに必要なデータを格納する
//第三引数はコールバックするLoaderCallbacksを継承しているクラスを指定する
//onStartLoadingにforceLoad()を記述していれば、初期化すると同時に実行される
supportLoaderManager.initLoader(1, args, this)
}
button2.setOnClickListener {
Log.v("nullpo", "1b")
var args: Bundle = Bundle().also { it.putString("url", "https://s.yimg.jp/images/bookstore/common/special/2018/0420_prefri/bn/150x50.png") }
supportLoaderManager.initLoader(2, args, this)
}
}
//LoaderがLoaderManagerに登録されるときに(initLoaderを実行したときに)呼び出される
//実際にAsyncTaskLoaderのインスタンスを生成して返す
override fun onCreateLoader(id: Int, args: Bundle?): Loader<Bitmap> {
Log.v("nullpo", "CreateLoader")
var getImage: GetImage = GetImage(this, args?.getString("url") ?: "")
//ここでonContentChangedすることで、GetImageBase.onStartLoadingでforceload()が実行される
getImage.onContentChanged()
return getImage
}
//データを受信すると呼ばれるメソッド
//ここでBitMapデータが取得できるのでImageViewに割り当てればいい
override fun onLoadFinished(loader: Loader<Bitmap>, data: Bitmap?) {
Log.v("nullpo", "LoadFinished")
if (data != null) imageView.setImageBitmap(data)
}
override fun onLoaderReset(loader: Loader<Bitmap>) {
}
}
AsyncTaskLoaderの基本的な動作はこのようになっている。Activityが開始されinitLoaderを実行すると、onCreateLoaderが実行されAsyncTaskLoaderのインスタンスが生成される。その次にonStartLoadingが実行される。そして、onStartLoadingメソッドの中でforceLoad()の記述がある場合にはloadInBackgroundが実行され非同期処理が行われる。そこで得たデータはdeliverResultを通じてアクティビティのonLoadFinishedがコールバックされる。
同じIDでもういちどinitLoaderを実行した場合には、onCreateLoaderは呼ばれずに、すでに作成されているLoaderを利用する。すでにデータを受信している場合にはLoader内部にデータを保持しているためloadInBackGroundは行われずonLoadFinishedがコールバックされ内部で保持しているデータを直接返すような処理をしている。ただし、forceLoad()かonContentChanged()で強制的にloadInBackgroundが実行される。
また、同じIDで再度initLoaderを実行してもargsは受け付けてくれないので、異なるアドレスからダウンロードをする場合には異なるIDで別のLoaderを作成して実行してあげる必要がある。
スマホの縦横切り替え時でアクティビティが再生成されるときもLoaderは内部にデータを保持し続けるので、縦横切り替え後にinitLoaderを実行すると、やはりhttpのアクセスは行わず内部で保存しているデータを返すような動作をする。縦横切り替え時にはonStopLoadingが呼ばれるので、アクティビティが非表示状態のときに動作して欲しくない処理は停止するようにしておく。
アクティビティが完全に削除されてしまったときにはonResetが呼ばれてLoaderは完全に処理が終了してしまう。内部で保存しているデータも破棄されるので、再びアクティビティが生成されたときはLoaderが再生成されloadInBackgroundが実行されるのでhttp経由で画像がダウンロードされる。
それでは、ダウンロード中に縦横切り替えやアクティビティが削除されたらどうなるか。
Loaderが生成されonStartLoadingが呼ばれloadInBackgroundが実行され、その最中で縦横が切り替えされると、LoaderはonStopLoadingが呼ばれloadInBackgroundも停止する。そのため再度、forceLoadなどを利用してloadInBackgroundを開始する処理を行う必要がある。
縦横ではなくホームボタンを押すなどして一時的にActivityが一次停止し、再びフォアグラウンドに戻ってきたときは自動的にforceLoad()が実行され、ダウンロードの続きをやってくれる。
アクティビティが削除されたときには、すべてなかったことになるのでやはりLoaderも再びLoaderManagerに登録してあげる必要がある。
まとめ
・ (同じアクティビティ内=同じLoadManagerでは)同じIDのAsyncTaskLoaderは使い回しができないと考えたほうがいい
・ (同じアクティビティ内=同じLoadManagerでは)新しい通信を行う場合は別のIDを使う
・ (同じアクティビティ内=同じLoadManagerでは)同じIDのAsyncTaskLoaderは使い回しができないと考えたほうがいい
・ (同じアクティビティ内=同じLoadManagerでは)新しい通信を行う場合は別のIDを使う
・ AsyncTaskLoaderは最初の1回だけ非同期処理を行い内部にデータを保持するので、2回目からはその内部に保持しているデータを返す
・ 非同期処理を行うきっかけはforceLoad()の実行
・ ダウンロード後に縦横切り替えしてもアクティビティが一次停止してもLoaderがデータを保持してくれている
・ ダウンロード中にアクティビティを一次停止し、再び開始したときにはLoaderが非同期処理を自動で再開してくれる(縦横切り替え時には自動で再開しない)
・ アクティビティが削除されたときはLoaderは再生成する必要がある。内部のデータの保持もしていない
・ 非同期処理を行うきっかけはforceLoad()の実行
・ ダウンロード後に縦横切り替えしてもアクティビティが一次停止してもLoaderがデータを保持してくれている
・ ダウンロード中にアクティビティを一次停止し、再び開始したときにはLoaderが非同期処理を自動で再開してくれる(縦横切り替え時には自動で再開しない)
・ アクティビティが削除されたときはLoaderは再生成する必要がある。内部のデータの保持もしていない