はじめに
本記事では、Android における非同期処理を 実務での使い分け にフォーカスして整理します。
自身の整理のためにアウトプットしました。
1. シーン別早見表
| シーン | 起点 | 推奨手段 |
|---|---|---|
| UI 操作の結果を非同期で処理する | ViewModel | viewModelScope.launch |
| 画面表示中だけ収集する | Fragment / Activity |
lifecycleScope + repeatOnLifecycle
|
| BroadcastReceiver 内で重い処理をする | onReceive() |
goAsync() + Coroutines / Executor |
| アプリ終了・再起動をまたぐ処理 | システム | WorkManager |
| Worker Thread から Main Thread に結果を返す | Java / フレームワーク連携 | Handler(Looper.getMainLooper()) |
| 継続的なセンサー・カメラ処理 | ハードウェア連携 | HandlerThread |
| Java プロジェクトで単発の非同期処理をする | Java コード |
ExecutorService + ListenableFuture
|
2. UI 操作の結果を非同期で処理する
起点:ViewModel
使う手段:viewModelScope.launch / ExecutorService
ボタン押下や画面初期化など、UI 起点の非同期処理はここが基本です。
ViewModel のライフサイクルと連動しているため、画面破棄時に自動でキャンセルされます。
Kotlin:viewModelScope
class MyViewModel : ViewModel() {
private val _uiState = MutableStateFlow<UiState>(UiState.Loading)
val uiState: StateFlow<UiState> = _uiState.asStateFlow()
fun onButtonClicked() {
viewModelScope.launch {
_uiState.value = UiState.Loading
try {
val data = withContext(Dispatchers.IO) { repository.fetchData() }
_uiState.value = UiState.Success(data)
} catch (e: IOException) {
_uiState.value = UiState.Error(e.message)
}
}
}
// 2つの API を並列で叩く場合
fun loadParallel(id: String) {
viewModelScope.launch {
val userDeferred = async(Dispatchers.IO) { repository.getUser(id) }
val postsDeferred = async(Dispatchers.IO) { repository.getPosts(id) }
val user = userDeferred.await()
val posts = postsDeferred.await()
_uiState.value = UiState.Success(user, posts)
}
}
}
⚠️
GlobalScopeはライフサイクルと切り離されているため、画面破棄後もコルーチンが走り続けリークの原因になる。viewModelScopeやlifecycleScopeなど、ライフサイクルに紐づくスコープを必ず使う。
💡
viewModelScopeの実体はCloseableCoroutineScopeで、ViewModel.onCleared()が呼ばれると自動でcancel()が走る。画面回転では ViewModel は生き続けるためonCleared()は呼ばれず、コルーチンもキャンセルされない。
💡
withContext(Dispatchers.IO)は Main Thread を止めるのではなく、コルーチンを中断するだけ。Main Thread はwithContextブロックの実行中も UI 描画やタッチイベントを処理し続ける。これが ANR を起こさない理由。
💡
async/awaitは戻り値が必要な処理・並列実行に使い、launchは結果を受け取らない処理に使う。asyncブロック内の例外はawait()を呼ぶまで投げられないため、await()は必ずtry-catchで囲む。
⚠️
asyncを使ってawait()を呼ばないと例外が握りつぶされ、結果も受け取れない。asyncを使う場合は必ずセットでawait()を呼ぶ。
Java:ExecutorService + LiveData
public class MyViewModel extends ViewModel {
private final MutableLiveData<UiState> uiState = new MutableLiveData<>(UiState.loading());
private final ExecutorService executor = Executors.newFixedThreadPool(4);
public void onButtonClicked() {
executor.execute(() -> {
try {
Data data = repository.fetchData();
uiState.postValue(UiState.success(data)); // postValue で Main Thread に通知
} catch (IOException e) {
uiState.postValue(UiState.error(e.getMessage()));
}
});
}
@Override
protected void onCleared() {
executor.shutdown();
}
}
postValuevssetValue:LiveData.setValue()は Main Thread のみ呼び出し可。
Worker Thread から更新するときはpostValue()を使います。
Dispatcher の使い分け
| Dispatcher | 使う場面 |
|---|---|
Dispatchers.Main |
UI 更新(launch のデフォルト) |
Dispatchers.IO |
ネットワーク・DB・ファイル |
Dispatchers.Default |
JSON パース・ソートなど CPU 処理 |
⚠️ runBlocking はコルーチンを起動しつつ、呼び出したスレッドをブロックして完了まで待つ関数。Main Thread で呼ぶと UI がフリーズし ANR の原因になる。
3. 画面表示中だけ収集する
起点:Fragment / Activity
使う手段:lifecycleScope + repeatOnLifecycle
ViewModel の StateFlow / SharedFlow を Fragment で収集するパターンです。
repeatOnLifecycle を使うことで、バックグラウンド時は収集を自動停止してリソースを節約します。
// Fragment
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewLifecycleOwner.lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
// UI State の収集
launch {
viewModel.uiState.collect { state -> render(state) }
}
// 一度きりのイベント(SnackBar・ナビゲーション)
launch {
viewModel.event.collect { event -> handleEvent(event) }
}
}
}
}
StateFlow vs SharedFlow
| StateFlow | SharedFlow | |
|---|---|---|
| 初期値 | 必要 | 不要 |
| 最新値の保持 | 常に 1 件 |
replay で設定可能 |
| 同値の再通知 | しない | する(デフォルト) |
| 主な用途 | UI State | 一度きりのイベント |
// ViewModel 側
private val _uiState = MutableStateFlow<UiState>(UiState.Loading)
val uiState: StateFlow<UiState> = _uiState.asStateFlow()
private val _event = MutableSharedFlow<UiEvent>()
val event: SharedFlow<UiEvent> = _event.asSharedFlow()
⚠️
MutableStateFlow/MutableSharedFlowをそのまま外部に公開すると、ViewModel 以外から値を書き換えられてしまう。privateにしてasStateFlow()/asSharedFlow()で読み取り専用として公開するのがベストプラクティス。
💡 判断に迷ったときは「それは状態か、イベントか」と自問する。UI State は「今の画面の状態」なので同じ値なら再描画不要 →
StateFlow。イベントは「起きた出来事」なので同じ内容でも発生した回数だけ通知すべき →SharedFlow。例えばボタンを2回押して同じ文言の Snackbar を2回出したい場合、StateFlowでは2回目が無視されてしまう。
💡
collectは値が来るまでそこで待ち続ける性質があるため、複数の Flow を収集する場合は必ず別々のlaunchに分ける。1つのlaunchにまとめると、最初のcollectが終わらず後続のcollectに永遠に到達しない。
launchInやlifecycleScope.launch { collect {} }だけでは不十分。
repeatOnLifecycleがないとバックグラウンド中も収集が続き、不要な処理が走ります。
Flow のオペレーター早見表
| オペレーター | 説明 |
|---|---|
map |
値を変換する |
filter |
条件に一致する値だけ流す |
flatMapLatest |
新しい値が来たら前の Flow をキャンセルして切り替え |
combine |
複数の Flow を合成する |
debounce |
最後の値から指定時間後に流す(検索入力など) |
distinctUntilChanged |
直前と同じ値を流さない |
catch |
上流の例外をキャッチする |
flowOn |
上流の実行コンテキストを変える |
4. BroadcastReceiver 内で重い処理をする
起点:onReceive()
使う手段:goAsync() + Coroutines / Executor
BroadcastReceiver.onReceive() は Main Thread で実行され、メソッドを抜けた瞬間に処理完了とみなされます。
そのままネットワーク通信や DB アクセスを行うと ANR の原因になります。
goAsync() を呼ぶと完了判定を延長でき、別スレッドや Coroutines で処理を続けることができます。
処理が終わったら必ず PendingResult.finish() を呼ぶことが必須です。
Kotlin:goAsync() + Coroutines
class MyReceiver : BroadcastReceiver() {
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
override fun onReceive(context: Context, intent: Intent) {
val pendingResult = goAsync() // 完了判定を延長
scope.launch {
try {
withContext(Dispatchers.IO) {
repository.syncData() // 重い処理
}
} finally {
pendingResult.finish() // 必ず呼ぶ
}
}
}
}
Java:goAsync() + ExecutorService
public class MyReceiver extends BroadcastReceiver {
private static final ExecutorService executor = Executors.newSingleThreadExecutor();
@Override
public void onReceive(Context context, Intent intent) {
PendingResult pendingResult = goAsync();
executor.execute(() -> {
try {
repository.syncData();
} finally {
pendingResult.finish(); // 必ず呼ぶ
}
});
}
}
注意点
| 項目 | 内容 |
|---|---|
| タイムアウト |
goAsync() にも上限(約 10 秒)があります。長時間処理は WorkManager に委ねる |
finish() の呼び忘れ |
必ず finally ブロックで呼ぶ |
| Scope の管理 | Receiver は複数回インスタンス化されうるため、companion object や Application スコープで Scope を保持する設計が安全 |
処理が 10 秒を超える可能性があるなら
WorkManagerに移譲するのが正しい設計です。
goAsync()はあくまで「少し時間がかかる処理を onReceive の外に逃がす」手段です。
5. アプリ終了・再起動をまたぐ処理
起点:システム / スケジューラ
使う手段:WorkManager
Coroutines や ExecutorService はアプリプロセスが生きている間しか処理を保証しません。
WorkManager は処理をシステムに登録するため、アプリ終了・端末再起動後も実行を保証します。
いつ WorkManager を使うか
| 条件 | WorkManager | Coroutines |
|---|---|---|
| アプリ終了後も継続 | ✅ | ❌ |
| 端末再起動後も継続 | ✅ | ❌ |
| ネットワーク接続を条件にする | ✅ | ❌ |
| 充電中のみ実行 | ✅ | ❌ |
| UI と連動した即時処理 | ❌ | ✅ |
向いているユースケース:ログのアップロード、画像の圧縮・同期、定期的なデータ更新。
💡 WorkManager がアプリ終了・再起動後も処理を保証できる理由は、タスクを内部の SQLite DB に永続化しているから。Coroutines はメモリ上で動くためアプリ終了とともに消えるが、WorkManager は再起動後に DB を読み直してタスクを再開する。
Worker の定義
// Java
public class UploadWorker extends Worker {
public UploadWorker(@NonNull Context context, @NonNull WorkerParameters params) {
super(context, params);
}
@NonNull
@Override
public Result doWork() {
try {
repository.uploadLogs();
return Result.success();
} catch (Exception e) {
return Result.failure();
}
}
}
// Kotlin:CoroutineWorker で suspend 関数をそのまま呼べる
class UploadWorker(context: Context, params: WorkerParameters) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
return try {
repository.uploadLogs()
Result.success()
} catch (e: IOException) {
Result.failure()
}
}
}
リクエストの組み立てと実行
// Java:制約付きで一回実行
Constraints constraints = new Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build();
OneTimeWorkRequest request = new OneTimeWorkRequest.Builder(UploadWorker.class)
.setConstraints(constraints)
.build();
WorkManager.getInstance(context).enqueue(request);
// Kotlin
val request = OneTimeWorkRequestBuilder<UploadWorker>()
.setConstraints(
Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
)
.build()
WorkManager.getInstance(context).enqueue(request)
定期実行
val periodicRequest = PeriodicWorkRequestBuilder<SyncWorker>(15, TimeUnit.MINUTES)
.setConstraints(
Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.setRequiresBatteryNotLow(true)
.build()
)
.build()
WorkManager.getInstance(context).enqueueUniquePeriodicWork(
"sync",
ExistingPeriodicWorkPolicy.KEEP,
periodicRequest
)
6. Worker Thread から Main Thread に結果を返す
起点:Java コード / Android フレームワーク連携
使う手段:Handler / HandlerThread
Kotlin + Coroutines であれば withContext(Dispatchers.Main) で自然に Main Thread に戻れますが、
Java コードやフレームワーク内部では Handler が今も使われます。
Handler:Main Thread に Runnable を送る
// Java
Handler mainHandler = new Handler(Looper.getMainLooper());
executor.execute(() -> {
String result = repository.fetchData(); // Worker Thread
mainHandler.post(() -> textView.setText(result)); // Main Thread
});
// Kotlin(Coroutines が使えない場面での代替)
val mainHandler = Handler(Looper.getMainLooper())
executor.execute {
val result = repository.fetchData()
mainHandler.post { textView.text = result }
}
💡
Looperは MessageQueue を監視し続けるループで、Main Thread は常に Looper を持っている。Handlerはその MessageQueue にRunnableを積む窓口。Handler(Looper.getMainLooper())は「Main Thread の MessageQueue に積む Handler」という意味になる。
💡 Kotlin の
withContext(Dispatchers.Main)は内部で同じことをしている。Main Thread の Looper に コルーチンの再開をポストしているだけで、表記が違うだけで仕組みは同じ。
HandlerThread:継続的な処理に専用スレッドを割り当てる
カメラやセンサーなど、継続的にメッセージを処理し続ける専用スレッド が必要なとき。
// Java
HandlerThread handlerThread = new HandlerThread("CameraWorker");
handlerThread.start();
Handler cameraHandler = new Handler(handlerThread.getLooper());
cameraHandler.post(() -> {
// カメラ処理
});
// 終了時
handlerThread.quitSafely();
// Kotlin
val handlerThread = HandlerThread("CameraWorker").also { it.start() }
val cameraHandler = Handler(handlerThread.looper)
cameraHandler.post {
// カメラ処理
}
handlerThread.quitSafely()
💡 通常の
Threadは Looper を持たないため、割り当てられた処理を1回実行したら終了する。HandlerThreadは自前の Looper を持つため、quitSafely()を呼ぶまで生き続け、メッセージが来るたびに処理できる。カメラやセンサーのような継続的なイベント処理に向いている理由はここにある。またquitSafely()の呼び忘れはExecutorServiceのshutdown()と同様にスレッドリークの原因になる。
Kotlin プロジェクトでは
Handlerを直接使う場面はほとんどなく、
Dispatchers.MainやDispatchers.IOで代替できます。
ただしフレームワーク API や Java コードとの連携では今も登場します。
7. Java プロジェクトで単発の非同期処理をする
起点:Java コード
使う手段:ExecutorService + ListenableFuture
ExecutorService:スレッドプール管理
// Java:ViewModel での管理パターン
public class MyViewModel extends ViewModel {
private final ExecutorService executor = Executors.newFixedThreadPool(4);
public void loadData() {
executor.execute(() -> {
// Worker Thread で実行
});
}
@Override
protected void onCleared() {
executor.shutdown(); // ViewModel 破棄時にシャットダウン
}
}
スレッドプールの選び方
| Factory メソッド | 特徴 | 向いている用途 |
|---|---|---|
newSingleThreadExecutor() |
スレッド 1 本・直列実行 | 順序保証が必要な DB 書き込み |
newFixedThreadPool(n) |
固定数スレッド | 並列ネットワーク通信 |
newCachedThreadPool() |
動的にスレッド生成 | 短時間タスクが大量発生(上限なしに注意) |
ListenableFuture:非同期結果にコールバックを登録する
標準の Future.get() はブロッキング待機しかできません。
ListenableFuture(Jetpack concurrent ライブラリ)を使うと完了コールバックを登録できます。
💡
Future.get()を Main Thread で呼ぶとその間 Main Thread が止まり ANR の原因になる。Worker Thread で呼べば回避できるが、その結果を Main Thread に返す処理が別途必要になり煩雑になる。
// Java
// build.gradle: implementation "androidx.concurrent:concurrent-futures:1.x.x"
ListeningExecutorService service =
MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(4));
ListenableFuture<String> future = service.submit(() -> repository.fetchData());
Futures.addCallback(future, new FutureCallback<String>() {
@Override
public void onSuccess(String result) {
runOnUiThread(() -> textView.setText(result));
}
@Override
public void onFailure(@NonNull Throwable t) {
Log.e("TAG", "Error", t);
}
}, MoreExecutors.directExecutor());
💡
Future.get()は「完了まで待つ(ブロッキング)」のに対し、ListenableFutureは「完了したら通知してもらう(ノンブロッキング)」設計。Futures.addCallback()を登録すると即座に返るため Main Thread をブロックしない。Kotlin のsuspend関数と発想が近い。
Kotlin との相互運用
concurrent-futures-ktx を使うと ListenableFuture を await() で待てます。
// Kotlin から Java の ListenableFuture を呼ぶ
// build.gradle: implementation "androidx.concurrent:concurrent-futures-ktx:1.x.x"
viewModelScope.launch {
val result = javaRepository.fetchDataFuture().await()
_uiState.value = UiState.Success(result)
}
💡
await()はListenableFutureの完了コールバックを内部で登録し、完了したらコルーチンを再開する仕組みに変換する。Main Thread はブロックされず、完了後は元のDispatchers.Mainに戻る。Java の非同期の世界(ListenableFuture)と Kotlin の非同期の世界(Coroutines)をつなぐ橋渡し役。
8. 補足:Service クラスと非同期処理
Service はバックグラウンドで動くコンポーネントですが、デフォルトでは Main Thread で動きます。そのまま重い処理を書くと ANR になるため、Service 内でも非同期処理は必要です。
Service
└─ デフォルトは Main Thread で動く
└─ onStartCommand() や onBind() も Main Thread
└─ そのまま重い処理を書くと ANR になる
Service 内での非同期処理の選択肢
| ユースケース | 推奨手段 |
|---|---|
| 継続的な処理(音楽再生・位置情報取得) |
ForegroundService + Coroutines |
| 単発の重い処理・永続性が必要 | WorkManager |
| Java プロジェクト | ExecutorService |
// LifecycleService を使うと lifecycleScope が使える
// build.gradle: implementation "androidx.lifecycle:lifecycle-service:x.x.x"
class MyService : LifecycleService() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId)
lifecycleScope.launch {
withContext(Dispatchers.IO) {
// 重い処理
}
}
return START_STICKY
}
}
💡 以前は
IntentService(自動でバックグラウンドスレッドを用意してくれる Service)がよく使われていたが、現在は非推奨。単発の重い処理はWorkManagerが後継にあたる。Service はあくまでコンポーネントの種類であり、スレッド管理とは別の概念。
9. おわりに
Android の非同期処理は選択肢が多く、どれを使えばいいか迷うことがありました。今回シーン別に整理したことで、各手段の特性と使い分けの基準が掴めました。咄嗟に「このシーンならこれ」と判断できるよう、実務の中で意識して使い分けていきたいと思います。
参考
- Kotlin 公式ドキュメント – Coroutines
- Android Developers – Kotlin Flow
- Android Developers – BroadcastReceiver
- Android Developers – WorkManager
- Android Developers – Java スレッドとバックグラウンド処理
- Jetpack concurrent-futures
- Android Developers – Service
- Android Developers – LifecycleService
- Android Developers – Processes and threads overview