RxJavaに会う:Android用の欠落しているリアクティブプログラミングライブラリ
公開: 2022-03-11Android開発者の場合、RxJavaについて聞いたことがあるかもしれません。 これは、Android開発でリアクティブプログラミングを有効にするための最も議論されているライブラリの1つです。 これは、モバイルプログラミングに固有の並行性/非同期タスクを簡素化するための頼りになるフレームワークとして宣伝されています。
しかし…RxJavaとは何ですか、そしてそれはどのように物事を「単純化」するのでしょうか?
RxJavaとは何かを説明するオンラインですでに利用可能なリソースはたくさんありますが、この記事での私の目標は、RxJavaの基本的な紹介、特にAndroid開発にどのように適合するかを説明することです。 また、新しいプロジェクトまたは既存のプロジェクトに統合する方法について、具体的な例と提案をいくつか示します。
なぜRxJavaを検討するのですか?
RxJavaは、スレッド化に関する抽象化のレベルを上げるため、基本的に開発を簡素化します。 つまり、開発者は、さまざまなスレッドで発生する必要のある操作を実行する方法の詳細についてあまり心配する必要はありません。 スレッド化は正しく実行するのが難しく、正しく実装されていない場合、デバッグと修正が最も難しいバグのいくつかを引き起こす可能性があるため、これは特に魅力的です。
確かに、これはスレッド化に関してRxJavaが防弾であることを意味するものではなく、舞台裏で何が起こっているのかを理解することは依然として重要です。 ただし、RxJavaは間違いなくあなたの生活を楽にすることができます。
例を見てみましょう。
ネットワーク呼び出し-RxJavaとAsyncTask
ネットワークを介してデータを取得し、その結果UIを更新するとします。 これを行う1つの方法は、(1) Activity
/ Fragment
に内部AsyncTask
サブクラスを作成し、(2)バックグラウンドでネットワーク操作を実行し、(3)その操作の結果を取得して、メインスレッドのUIを更新することです。 。
public class NetworkRequestTask extends AsyncTask<Void, Void, User> { private final int userId; public NetworkRequestTask(int userId) { this.userId = userId; } @Override protected User doInBackground(Void... params) { return networkService.getUser(userId); } @Override protected void onPostExecute(User user) { nameTextView.setText(user.getName()); // ...set other views } } private void onButtonClicked(Button button) { new NetworkRequestTask(123).execute() }
これは無害に見えるかもしれませんが、このアプローチにはいくつかの問題と制限があります。 つまり、 NetworkRequestTask
は内部クラスであり、外部クラスへの暗黙の参照を保持しているため、メモリ/コンテキストリークは簡単に作成されます。 また、ネットワーク呼び出しの後に別の長い操作をチェーンしたい場合はどうなりますか? 読みやすさを大幅に低下させる可能性のある2つのAsyncTask
をネストする必要があります。
対照的に、ネットワーク呼び出しを実行するためのRxJavaアプローチは、次のようになります。
private Subscription subscription; private void onButtonClicked(Button button) { subscription = networkService.getObservableUser(123) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Action1<User>() { @Override public void call(User user) { nameTextView.setText(user.getName()); // ... set other views } }); } @Override protected void onDestroy() { if (subscription != null && !subscription.isUnsubscribed()) { subscription.unsubscribe(); } super.onDestroy(); }
このアプローチを使用して、返されたSubscription
オブジェクトへの参照を保持することにより、(外部コンテキストへの参照を保持している実行中のスレッドによって引き起こされる潜在的なメモリリークの)問題を解決します。 次に、このSubscription
オブジェクトは、 Activity
/ Fragment
オブジェクトの#onDestroy()
メソッドに関連付けられ、 Activity
/ Fragment
を破棄する必要があるときにAction1#call
操作が実行されないようにします。
また、 #getObservableUser(...)
の戻りタイプ(つまり、 Observable<User>
)は、それへのさらなる呼び出しと連鎖していることに注意してください。 この流動的なAPIを通じて、 AsyncTask
を使用するという2番目の問題を解決できます。これは、ネットワーク呼び出し/長い操作の連鎖をさらに可能にすることです。 かなりきちんとしていますね
RxJavaの概念について詳しく見ていきましょう。
オブザーバブル、オブザーバー、およびオペレーター-RxJavaコアの3つのO
RxJavaの世界では、すべてをストリームとしてモデル化できます。 ストリームは時間の経過とともにアイテムを放出し、各放出は消費/監視できます。
考えてみれば、ストリームは新しい概念ではありません。クリックイベントはストリーム、位置情報の更新はストリーム、プッシュ通知はストリームなどです。
ストリームの抽象化は、私が「3つのO」と呼んでいる3つのコア構造を介して実装されます。 つまり、 O bservable、 O bserver 、およびOperatorです。 Observableはアイテム(ストリーム)を発行します。 オブザーバーはそれらのアイテムを消費します。 Observableオブジェクトからの放出は、 Operator呼び出しを連鎖させることにより、さらに変更、変換、および操作できます。
観察可能
Observableは、RxJavaのストリーム抽象化です。 これは、シーケンスが与えられると、それらのアイテムを順番に繰り返して生成するという点でイテレータに似ています。 コンシューマーは、基になるシーケンスに関係なく、同じインターフェイスを介してこれらのアイテムを消費できます。
番号1、2、3をこの順序で発行したいとします。 これを行うには、 Observable<T>#create(OnSubscribe<T>)
メソッドを使用できます。
Observable<Integer> observable = Observable.create(new Observable.OnSubscribe<Integer>() { @Override public void call(Subscriber<? super Integer> subscriber) { subscriber.onNext(1); subscriber.onNext(2); subscriber.onNext(3); subscriber.onCompleted(); } });
subscriber.onNext(Integer)
を呼び出すと、ストリーム内のアイテムが発行され、ストリームの発行が終了すると、 subscriber.onCompleted()
が呼び出されます。
Observableを作成するためのこのアプローチは、かなり冗長です。 このため、ほとんどすべての場合に優先される必要があるObservableインスタンスを作成するための便利な方法があります。
Observableを作成する最も簡単な方法は、 Observable#just(...)
を使用することです。 メソッド名が示すように、メソッド引数として渡したアイテムを出力するだけです。
Observable.just(1, 2, 3); // 1, 2, 3 will be emitted, respectively
観察者
Observableストリームの次のコンポーネントは、それにサブスクライブされているObserver(またはObservers)です。 ストリームで「興味深い」何かが発生するたびに、オブザーバーに通知されます。 オブザーバーは、次のイベントを介して通知されます。
-
Observer#onNext(T)
-アイテムがストリームから発行されたときに呼び出されます Observable#onError(Throwable)
-ストリーム内でエラーが発生したときに呼び出されますObservable#onCompleted()
-ストリームがアイテムの送信を終了したときに呼び出されます。
ストリームをサブスクライブするには、 Observable<T>#subscribe(...)
を呼び出して、Observerインスタンスを渡します。
Observable<Integer> observable = Observable.just(1, 2, 3); observable.subscribe(new Observer<Integer>() { @Override public void onCompleted() { Log.d("Test", "In onCompleted()"); } @Override public void onError(Throwable e) { Log.d("Test", "In onError()"); } @Override public void onNext(Integer integer) { Log.d("Test", "In onNext():" + integer); } });
上記のコードは、Logcatで次のように出力します。
In onNext(): 1 In onNext(): 2 In onNext(): 3 In onNext(): 4 In onCompleted()
また、Observableの排出量に関心がなくなった場合もあります。 これは、たとえば、 Activity
/ Fragment
をメモリで再利用する必要がある場合に、Androidで特に関係があります。
アイテムの監視を停止するには、返されたサブスクリプションオブジェクトでSubscription#unsubscribe()
を呼び出す必要があります。
Subscription subscription = someInfiniteObservable.subscribe(new Observer<Integer>() { @Override public void onCompleted() { // ... } @Override public void onError(Throwable e) { // ... } @Override public void onNext(Integer integer) { // ... } }); // Call unsubscribe when appropriate subscription.unsubscribe();
上記のコードスニペットに見られるように、Observableにサブスクライブすると、返されたSubscriptionオブジェクトへの参照を保持し、後で必要に応じsubscription#unsubscribe()
を呼び出します。 Androidでは、これはActivity#onDestroy()
またはFragment#onDestroy()
)内で呼び出すのが最適です。
オペレーター
Observableによって発行されたアイテムは、サブスクライブされたObserverオブジェクトに通知する前に、Operatorを介して変換、変更、およびフィルタリングできます。 関数型プログラミングで見られる最も一般的な操作のいくつか(マップ、フィルター、リデュースなど)は、Observableストリームにも適用できます。 例として地図を見てみましょう。
Observable.just(1, 2, 3, 4, 5).map(new Func1<Integer, Integer>() { @Override public Integer call(Integer integer) { return integer * 3; } }).subscribe(new Observer<Integer>() { @Override public void onCompleted() { // ... } @Override public void onError(Throwable e) { // ... } @Override public void onNext(Integer integer) { // ... } });
上記のコードスニペットは、Observableからの各放出を取得し、それぞれに3を掛けて、それぞれストリーム3、6、9、12、15を生成します。 演算子を適用すると、通常、結果として別のObservableが返されます。これにより、複数の操作を連鎖させて目的の結果を得ることができるので便利です。
上記のストリームを考えると、偶数のみを受信したいとします。 これは、フィルター操作を連鎖させることで実現できます。
Observable.just(1, 2, 3, 4, 5).map(new Func1<Integer, Integer>() { @Override public Integer call(Integer integer) { return integer * 3; } }).filter(new Func1<Integer, Boolean>() { @Override public Boolean call(Integer integer) { return integer % 2 == 0; } }).subscribe(new Observer<Integer>() { @Override public void onCompleted() { // ... } @Override public void onError(Throwable e) { // ... } @Override public void onNext(Integer integer) { // ... } });
Observableストリームを変更するRxJavaツールセットに組み込まれている多くの演算子があります。 ストリームを変更する方法を考えることができれば、そのためのオペレーターがいる可能性があります。 ほとんどの技術文書とは異なり、RxJava/ReactiveXの文書を読むのはかなり簡単で要領を得ています。 ドキュメントの各オペレーターには、オペレーターがストリームにどのように影響するかについての視覚化が付属しています。 これらの視覚化は「マーブルダイアグラム」と呼ばれます。
フリップと呼ばれる架空の演算子を大理石の図でモデル化する方法は次のとおりです。
RxJavaを使用したマルチスレッド
Observableチェーンで操作が発生するスレッドの制御は、オペレーターが発生するスケジューラーを指定することによって行われます。 基本的に、スケジューラーは、指定されたときにオペレーターが使用して実行するスレッドプールと考えることができます。 デフォルトでは、そのようなスケジューラーが提供されていない場合、ObservableチェーンはObservable#subscribe(...)
が呼び出されたのと同じスレッドで動作します。 それ以外の場合、スケジューラーはObservable#subscribeOn(Scheduler)
および/またはObservable#observeOn(Scheduler)
を介して指定でき、スケジュールされた操作はスケジューラーによって選択されたスレッドで発生します。
2つの方法の主な違いは、 Observable#subscribeOn(Scheduler)
がソースObservableにどのスケジューラーを実行するかを指示することです。 チェーンは、Observable#subscribeOn( Observable#observeOn(Scheduler)
が別のスケジューラーで呼び出されるまで、 Observable#subscribeOn(Scheduler)
で指定されたスケジューラーからのスレッドで実行され続けます。 このような呼び出しが行われると、それ以降のすべてのオブザーバー(つまり、チェーンの後続の操作)は、 observeOn
スケジューラーから取得したスレッドで通知を受け取ります。
これらのメソッドが操作の実行場所にどのように影響するかを示す大理石の図を次に示します。
Androidのコンテキストでは、長い操作の結果としてUI操作を実行する必要がある場合は、その操作をUIスレッドで実行する必要があります。 この目的のために、RxAndroidライブラリで提供されるスケジューラーの1つであるAndroidScheduler#mainThread()
を使用できます。
Android上のRxJava
いくつかの基本事項がわかったので、疑問に思われるかもしれません。RxJavaをAndroidアプリケーションに統合するための最良の方法は何でしょうか。 ご想像のとおり、RxJavaには多くのユースケースがありますが、この例では、ネットワークスタックの一部としてObservableオブジェクトを使用するという1つの特定のケースを見てみましょう。
この例では、GitHubのAPIと対話するためのRxJavaとの組み込みバインディングを備えたSquareによってオープンソース化されたHTTPクライアントであるRetrofitを見ていきます。 具体的には、GitHubユーザー名を指定されたユーザーのすべてのスター付きリポジトリを表示するシンプルなアプリを作成します。 先に進みたい場合は、ここからソースコードを入手できます。
新しいAndroidプロジェクトを作成する
- 新しいAndroidプロジェクトを作成し、 GitHubRxJavaという名前を付けることから始めます。
- [ターゲットAndroidデバイス]画面で、[電話とタブレット]を選択したまま、SDKの最小レベルを17に設定します。APIレベルを低く/高く設定してください。ただし、この例では、APIレベル17で十分です。
- 次のプロンプトで[空のアクティビティ]を選択します。

- 最後のステップでは、アクティビティ名をMainActivityのままにして、レイアウトファイルactivity_mainを生成します。
プロジェクトの設定
app/build.gradle
にRxJava、RxAndroid、およびRetrofitライブラリを含めます。 RxAndroidを含めると、暗黙的にRxJavaも含まれることに注意してください。 ただし、RxAndroidには常に最新バージョンのRxJavaが含まれているとは限らないため、これら2つのライブラリを常に明示的に含めることをお勧めします。 RxJavaの最新バージョンを明示的に含めると、最新バージョンの使用が保証されます。
dependencies { compile 'com.squareup.retrofit2:adapter-rxjava:2.1.0' compile 'com.squareup.retrofit2:converter-gson:2.1.0' compile 'com.squareup.retrofit2:retrofit:2.1.0' compile 'io.reactivex:rxandroid:1.2.0' compile 'io.reactivex:rxjava:1.1.8' // ...other dependencies }
データオブジェクトの作成
GitHubRepo
データオブジェクトクラスを作成します。 このクラスは、GitHubのリポジトリをカプセル化します(ネットワーク応答にはより多くのデータが含まれていますが、関心があるのはそのサブセットのみです)。
public class GitHubRepo { public final int id; public final String name; public final String htmlUrl; public final String description; public final String language; public final int stargazersCount; public GitHubRepo(int id, String name, String htmlUrl, String description, String language, int stargazersCount) { this.id = id; this.name = name; this.htmlUrl = htmlUrl; this.description = description; this.language = language; this.stargazersCount = stargazersCount; } }
セットアップの改造
GitHubService
インターフェースを作成します。 このインターフェースをRetrofitに渡し、RetrofitはGitHubService
の実装を作成します。
public interface GitHubService { @GET("users/{user}/starred") Observable<List<GitHubRepo>> getStarredRepositories(@Path("user") String userName); }
GitHubClient
クラスを作成します。 これは、UIレベルからネットワーク呼び出しを行うために対話するオブジェクトになります。Retrofitを介して
GitHubService
の実装を構築する場合、ネットワーク呼び出しがObservableオブジェクトを返すことができるように、呼び出しアダプターとしてRxJavaCallAdapterFactory
を渡す必要があります(Call
以外の結果を返すネットワーク呼び出しには、呼び出しアダプターを渡す必要があります)。また、GsonをJavaオブジェクトにマーシャリングする方法としてGsonを使用できるように、
GsonConverterFactory
を渡す必要があります。
public class GitHubClient { private static final String GITHUB_BASE_URL = "https://api.github.com/"; private static GitHubClient instance; private GitHubService gitHubService; private GitHubClient() { final Gson gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create(); final Retrofit retrofit = new Retrofit.Builder().baseUrl(GITHUB_BASE_URL) .addCallAdapterFactory(RxJavaCallAdapterFactory.create()) .addConverterFactory(GsonConverterFactory.create(gson)) .build(); gitHubService = retrofit.create(GitHubService.class); } public static GitHubClient getInstance() { if (instance == null) { instance = new GitHubClient(); } return instance; } public Observable<List<GitHubRepo>> getStarredRepos(@NonNull String userName) { return gitHubService.getStarredRepositories(userName); } }
セットアップレイアウト
次に、入力されたGitHubユーザー名を指定して、取得したリポジトリを表示するシンプルなUIを作成します。 次のようなものを使用して、activity_home.xml( activity_home.xml
のレイアウト)を作成します。
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:andro android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <ListView android: android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1"/> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal"> <EditText android: android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:hint="@string/username"/> <Button android: android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/search"/> </LinearLayout> </LinearLayout>
次のようなものを使用して、 item_github_repo.xml
(GitHubリポジトリオブジェクトのListView
アイテムレイアウト)を作成します。
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:andro xmlns:tools="http://schemas.android.com/tools" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent" android:padding="6dp"> <TextView android: android:layout_width="match_parent" android:layout_height="wrap_content" android:textSize="24sp" android:text tools:text="Cropper"/> <TextView android: android:layout_width="match_parent" android:layout_height="wrap_content" android:lines="2" android:ellipsize="end" android:textSize="16sp" android:layout_below="@+id/text_repo_name" tools:text="Android widget for cropping and rotating an image."/> <TextView android: android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_below="@+id/text_repo_description" android:layout_alignParentLeft="true" android:textColor="?attr/colorPrimary" android:textSize="14sp" android:text tools:text="Language: Java"/> <TextView android: android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_below="@+id/text_repo_description" android:layout_alignParentRight="true" android:textColor="?attr/colorAccent" android:textSize="14sp" android:text tools:text="Stars: 1953"/> </RelativeLayout>
すべてを接着する
GitHubRepo
オブジェクトのListView
アイテムへのバインドを担当するListAdapter
を作成します。 このプロセスでは、リサイクルされたView
が提供されていない場合、基本的にitem_github_repo.xml
をView
にインフレーションします。 それ以外の場合は、リサイクルされたView
が再利用され、多くのView
オブジェクトが過剰に膨らむのを防ぎます。
public class GitHubRepoAdapter extends BaseAdapter { private List<GitHubRepo> gitHubRepos = new ArrayList<>(); @Override public int getCount() { return gitHubRepos.size(); } @Override public GitHubRepo getItem(int position) { if (position < 0 || position >= gitHubRepos.size()) { return null; } else { return gitHubRepos.get(position); } } @Override public long getItemId(int position) { return position; } @Override public View getView(int position, View convertView, ViewGroup parent) { final View view = (convertView != null ? convertView : createView(parent)); final GitHubRepoViewHolder viewHolder = (GitHubRepoViewHolder) view.getTag(); viewHolder.setGitHubRepo(getItem(position)); return view; } public void setGitHubRepos(@Nullable List<GitHubRepo> repos) { if (repos == null) { return; } gitHubRepos.clear(); gitHubRepos.addAll(repos); notifyDataSetChanged(); } private View createView(ViewGroup parent) { final LayoutInflater inflater = LayoutInflater.from(parent.getContext()); final View view = inflater.inflate(R.layout.item_github_repo, parent, false); final GitHubRepoViewHolder viewHolder = new GitHubRepoViewHolder(view); view.setTag(viewHolder); return view; } private static class GitHubRepoViewHolder { private TextView textRepoName; private TextView textRepoDescription; private TextView textLanguage; private TextView textStars; public GitHubRepoViewHolder(View view) { textRepoName = (TextView) view.findViewById(R.id.text_repo_name); textRepoDescription = (TextView) view.findViewById(R.id.text_repo_description); textLanguage = (TextView) view.findViewById(R.id.text_language); textStars = (TextView) view.findViewById(R.id.text_stars); } public void setGitHubRepo(GitHubRepo gitHubRepo) { textRepoName.setText(gitHubRepo.name); textRepoDescription.setText(gitHubRepo.description); textLanguage.setText("Language: " + gitHubRepo.language); textStars.setText("Stars: " + gitHubRepo.stargazersCount); } } }
MainActivity
ですべてを接着します。 これは基本的に、アプリを最初に起動したときに表示されるActivity
です。 ここでは、ユーザーにGitHubユーザー名を入力してもらい、最後に、そのユーザー名でスター付きのすべてのリポジトリを表示します。
public class MainActivity extends AppCompatActivity { private static final String TAG = MainActivity.class.getSimpleName(); private GitHubRepoAdapter adapter = new GitHubRepoAdapter(); private Subscription subscription; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); final ListView listView = (ListView) findViewById(R.id.list_view_repos); listView.setAdapter(adapter); final EditText editTextUsername = (EditText) findViewById(R.id.edit_text_username); final Button buttonSearch = (Button) findViewById(R.id.button_search); buttonSearch.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { final String username = editTextUsername.getText().toString(); if (!TextUtils.isEmpty(username)) { getStarredRepos(username); } } }); } @Override protected void onDestroy() { if (subscription != null && !subscription.isUnsubscribed()) { subscription.unsubscribe(); } super.onDestroy(); } private void getStarredRepos(String username) { subscription = GitHubClient.getInstance() .getStarredRepos(username) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Observer<List<GitHubRepo>>() { @Override public void onCompleted() { Log.d(TAG, "In onCompleted()"); } @Override public void onError(Throwable e) { e.printStackTrace(); Log.d(TAG, "In onError()"); } @Override public void onNext(List<GitHubRepo> gitHubRepos) { Log.d(TAG, "In onNext()"); adapter.setGitHubRepos(gitHubRepos); } }); } }
アプリを実行する
アプリを実行すると、GitHubユーザー名を入力するための入力ボックスが表示された画面が表示されます。 検索すると、スター付きのすべてのリポジトリのリストが表示されます。
結論
これがRxJavaの有用な紹介とその基本機能の概要として役立つことを願っています。 RxJavaには強力な概念がたくさんあります。十分に文書化されたRxJavawikiをさらに深く掘り下げて、それらを探索することをお勧めします。
下のコメント欄に質問やコメントを残してください。 Twitterの@arriolachrisで私をフォローすることもできます。ここでは、RxJavaやAndroidに関するすべてのことについてたくさんツイートしています。
RxJavaの包括的な学習リソースが必要な場合は、LeanpubでAngusHuangと一緒に書いた電子ブックをチェックしてください。