はじめに
非同期処理は、モダンなAndroidアプリケーション開発において欠かせない技術の一つです。アプリがユーザーの操作に対して素早く反応し、スムーズな体験を提供するためには、バックグラウンドでの作業(例: ネットワーク通信やファイル操作など)を効率的に行う必要があります。これを同期的に処理してしまうと、操作が完了するまでアプリ全体が固まったり、ユーザーに「フリーズしている」という印象を与えることになります。
非同期処理を正しく使うことで、こうした問題を回避し、ユーザーが操作中でもアプリがスムーズに動作し続けるように設計できます。特にAndroidアプリでは、メインスレッド(UIスレッド)で重たい処理を行うと、アプリが応答しなくなる恐れがあり、最悪の場合、システムから「アプリが応答していません」(ANR: Application Not Responding)エラーを出されてしまいます。
ここで役立つのが、Javaの標準ライブラリで提供されている CompletableFuture
です。CompletableFuture
は、Java 8 から導入された強力な非同期処理ツールで、手軽に非同期タスクを実行し、結果を処理することができます。Thread
クラスや AsyncTask
、ExecutorService
といった従来の非同期処理と比べて、よりシンプルかつ柔軟に非同期処理を行えるのが特徴です。
本記事では、CompletableFuture
を使ってAndroidアプリで非同期処理を簡単に実装する方法を解説します。初めて非同期処理に触れる方でも、この記事を読み終わる頃には、CompletableFuture
を使ってバックグラウンドで処理を実行し、その結果をメインスレッドに反映させる基本的な操作ができるようになります。
CompletableFuture の基本操作
ここでは、CompletableFuture
を使った非同期処理の基本的な操作を具体的に説明します。非同期タスクの実行から、その完了時の処理をシンプルに書く方法を見ていきましょう。特に、supplyAsync
と whenComplete
の使い方に焦点を当てます。
supplyAsync
で非同期タスクを実行する
CompletableFuture.supplyAsync
メソッドを使って、バックグラウンドで非同期にタスクを実行できます。タスクは別スレッドで処理され、メインスレッド(UIスレッド)に負荷をかけません。
次のコードでは、バックグラウンドで重い処理を実行しています。
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
// バックグラウンドで実行される重い処理
try {
Thread.sleep(2000); // 2秒間の処理
} catch (InterruptedException e) {
throw new RuntimeException("処理が中断されました", e);
}
return "非同期処理完了";
});
CompletableFuture.supplyAsync
はラムダ式を引数に取り、その中に実行したい処理を記述します。ここでは2秒の遅延をシミュレートしています。エラーが発生した場合は例外をスローします。
ここで重要なのは、CompletableFuture.supplyAsync
の呼び出し自体はすぐにリターンするということです。 この呼び出しにより別スレッド上で引数として渡した処理の実行が開始されます。実行するスレッドは、Java実行環境が管理する共有スレッドプールから選択されるため、明示的にスレッドを生成する必要はありません。
whenComplete
でタスク完了時の処理を行う
whenComplete
メソッドを使って、非同期タスクが完了した際の処理を実行できます。このメソッドは、タスクの結果と、エラー発生時の例外を受け取り、それに応じて処理を行います。
次のコードは、非同期タスクの完了後に結果をログに表示する例です。
future.whenComplete((result, exception) -> {
if (exception == null) {
// 正常にタスクが完了した場合の処理
System.out.println("タスク成功: " + result);
} else {
// エラーが発生した場合の処理
System.out.println("エラー発生: " + exception.getMessage());
}
});
この例では、whenComplete
メソッドで成功時には結果を表示し、エラー発生時にはエラーメッセージを表示しています。これにより、タスクの成功・失敗を分岐して処理できるようになります。
ここで重要なのは、whenComplete
の呼び出し自体はすぐにリターンするということです。whenComplete
の引数として渡したタスクは、supplyAsync
で引数に指定した非同期タスクの完了後に実行されます。
supplyAsync
内でのエラー処理
非同期タスクの実行中にエラーが発生するケースも考慮しましょう。例えば、supplyAsync
内で例外がスローされた場合、そのエラーも whenComplete
で処理することができます。以下は、その例です。
CompletableFuture<String> futureWithError = CompletableFuture.supplyAsync(() -> {
// バックグラウンドでの処理中にエラーを発生させる
if (true) {
throw new RuntimeException("意図的なエラー");
}
return "非同期処理完了";
});
futureWithError.whenComplete((result, exception) -> {
if (exception == null) {
// 正常にタスクが完了した場合
System.out.println("タスク成功: " + result);
} else {
// エラーが発生した場合
System.out.println("エラー発生: " + exception.getMessage());
}
});
このコードでは、supplyAsync
の中で意図的に例外をスローし、whenComplete
でその例外をキャッチして処理しています。このようにして、非同期処理の中でエラーが発生した場合でも、それを適切に扱うことができます。
エラーハンドリングを含めて、非同期処理の流れを理解できるようになると、アプリがより堅牢になり、ユーザーにスムーズな体験を提供できるようになります。
CompletableFuture の実装例
DataFetcher クラスの実装
public class DataFetcher {
// 非同期でサーバーからデータを取得するメソッド
public CompletableFuture<String> fetchDataFromServer() {
return CompletableFuture.supplyAsync(() -> {
try {
// サーバーからのデータ取得処理(ダミー)
Thread.sleep(2000); // 2秒間の遅延をシミュレート
} catch (InterruptedException e) {
throw new RuntimeException("データ取得が中断されました", e);
}
return "サーバーからのデータ";
});
}
}
Activity からの呼び出し
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
TextView textView = findViewById(R.id.textView);
Button fetchButton = findViewById(R.id.fetchButton);
DataFetcher dataFetcher = new DataFetcher();
// ボタンを押したときに非同期でデータを取得
fetchButton.setOnClickListener(view -> {
dataFetcher.fetchDataFromServer().whenComplete((result, exception) -> {
// メインスレッドでUIを更新
runOnUiThread(() -> {
if (exception == null) {
textView.setText("取得したデータ: " + result);
} else {
textView.setText("エラー発生: " + exception.getMessage());
}
});
});
});
}
}
解説
-
DataFetcher
クラスの使用
DataFetcher
クラスは、非同期でサーバーからデータを取得するロジックを担当します。これにより、MainActivity
にはビジネスロジックが含まれず、責務が分離されている状態です。 -
onCreate
での呼び出し
onCreate
内でfetchButton
がクリックされた際に、DataFetcher
のfetchDataFromServer
メソッドを直接呼び出して、非同期で処理を実行します。 -
UI の更新
whenComplete
内で結果を取得した後、runOnUiThread
を使ってTextView
を更新します。これにより、バックグラウンド処理の結果を メインスレッドで受け取り、UIに反映させることができます。
このシンプルな実装例では、ボタンを押したときに非同期でサーバーからデータを取得し、その結果を画面に表示します。DataFetcher クラスにビジネスロジックを分けることで、Activity はUIの制御に専念でき、非同期処理も簡潔に管理できます。この方法は、メインスレッドをブロックせず、アプリの応答性を維持しながら非同期タスクを実装する上で有効です。
CompletableFuture を使った非同期処理の利点
従来の非同期処理との比較
Androidアプリ開発では、非同期処理を行うために従来から AsyncTask
や Handler
、HandlerThread
、 ExecutorService
クラスが使用されてきました。これらの方法には次のような問題がありました:
-
AsyncTask
のデメリット
AsyncTask
は、非同期処理とメインスレッドへの結果の受け渡しを比較的簡単に実装できるものの、構造が複雑になりやすく、コードが煩雑になる傾向があります。また、AsyncTask
は Android API 30 で廃止されるため、新しいプロジェクトでは推奨されません。 -
Handler
やHandlerThread
のデメリット
Handler
やHandlerThread
を使用して非同期処理を行う場合、明示的にスレッドを作成し、そのスレッドに対して Handler を用いてメッセージを送信する実装になります。非同期で処理した結果の受け取りやエラーハンドリングを行うために、煩雑なコードが必要になります。処理の流れも追いにくくなります。また、スレッドの終了処理を忘れるとメモリリークに繋がります。 -
ExecutorService
のデメリット
JavaにおけるFutureパターンの実装としてExecutorService.submit
を使う例がありますが、ここで使われるFuture
クラスは、CompletableFuture.whenComplete
に相当するメソッドを持たず、Future.get
で値を受け取る必要があるため、結局メインスレッドをブロックすることになってしまいます。また、Handler
の時と同様に、スレッドの管理を行う必要があり、終了処理を忘れるとメモリリークに繋がります。
CompletableFuture の利点
CompletableFuture
を使用すると、従来の方法に比べて以下の利点があります。
-
スレッド管理の簡便さ
CompletableFuture
を使うと、内部でスレッドプールを管理してくれるため、開発者がスレッドを直接扱う必要がありません。これにより、複雑なスレッド管理が不要になり、コードがシンプルに保たれます。 -
エラーハンドリングの一貫性
whenComplete
やexceptionally
メソッドを使えば、エラーが発生した場合でも簡単に処理を行えます。従来の方法では、エラーハンドリングが煩雑になりがちですが、CompletableFuture
では一貫してシンプルに実装できます。 -
シンプルで読みやすいコード
CompletableFuture
を使えば、非同期処理が終了した後の動作をwhenComplete
などのメソッドで簡潔に記述できます。コードが自然なフローで書かれるため、可読性が向上します。dataFetcher.fetchDataFromServer().whenComplete((result, exception) -> { if (exception == null) { textView.setText(result); } else { textView.setText("エラー発生"); } });
このコード例では、非同期処理の開始から結果の取得、エラーハンドリングまでが一連のフローで書かれており、読みやすくなっています。
-
柔軟な非同期タスクの組み合わせ
CompletableFuture
を使えば、複数の非同期タスクを簡単に組み合わせたり、チェーンさせることが可能です。例えば、データの取得が終わった後に別の非同期処理を行いたい場合でも、thenApply
やthenCompose
といったメソッドを使って自然な流れで処理を記述できます。dataFetcher.fetchDataFromServer() .thenApply(result -> processResult(result)) // 結果を処理 .whenComplete((finalResult, exception) -> { if (exception == null) { textView.setText(finalResult); } else { textView.setText("エラー発生"); } });
実際のプロジェクトでの使用例
CompletableFuture
は、小規模なプロジェクトだけでなく、大規模なプロジェクトでも非常に役立ちます。特に、次のようなシナリオで有効です:
-
ネットワーク通信
サーバーからデータを取得し、それに基づいてUIを更新する処理を非同期で行う場合に最適です。 -
データベースアクセス
ローカルのデータベース(例えば、SQLite)にアクセスし、その結果を元に次のアクションを実行する場合にも、非同期処理の組み合わせが役立ちます。 -
複数の非同期処理の組み合わせ
複数のネットワークリクエストやデータ処理を連続して実行する場合でも、CompletableFuture
のメソッドチェーンを使うことで簡潔に記述できます。
まとめ
この記事では、Android アプリ開発における非同期処理の基本として、CompletableFuture
を使ったシンプルな例を紹介しました。CompletableFuture
は、従来の非同期処理手法よりもシンプルで直感的にコードを記述でき、スレッド管理やエラーハンドリングも簡潔に行える強力なツールです。
-
CompletableFuture
の基本操作として、supplyAsync
を使った非同期処理の実行方法と、whenComplete
を使った結果処理やエラーハンドリングについて説明しました。 -
ビジネスロジックの分離を意識して、
DataFetcher
クラスを用いることで、Activity
からロジックを切り離し、UI の操作と非同期処理をスッキリと実装する手法を紹介しました。 - Android アプリ開発における利点として、スレッド管理を意識せずに済むこと、柔軟な非同期処理の組み合わせが可能であることを挙げました。
CompletableFuture
を活用することで、UI の応答性を損なうことなく、複雑な非同期処理を簡単に管理できるようになります。今回紹介した基礎的な使い方を学ぶことで、実際のプロジェクトでも効果的に非同期処理を扱えるようになるでしょう。ぜひ、積極的に活用してみてください。