2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Android 非同期処理について整理する

2
Posted at

はじめに

本記事では、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 はライフサイクルと切り離されているため、画面破棄後もコルーチンが走り続けリークの原因になる。viewModelScopelifecycleScope など、ライフサイクルに紐づくスコープを必ず使う。

💡 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();
    }
}

postValue vs setValueLiveData.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 に永遠に到達しない。

launchInlifecycleScope.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 objectApplication スコープで 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() の呼び忘れは ExecutorServiceshutdown() と同様にスレッドリークの原因になる。

Kotlin プロジェクトでは Handler を直接使う場面はほとんどなく、
Dispatchers.MainDispatchers.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 を使うと ListenableFutureawait() で待てます。

// 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 の非同期処理は選択肢が多く、どれを使えばいいか迷うことがありました。今回シーン別に整理したことで、各手段の特性と使い分けの基準が掴めました。咄嗟に「このシーンならこれ」と判断できるよう、実務の中で意識して使い分けていきたいと思います。


参考

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?