iT邦幫忙

2023 iThome 鐵人賽

DAY 15
0

前言

RxJava 是一個在 Java 平台上實現反應式編程(Reactive Programming)的庫。它允許您以更簡單和優雅的方式處理異步事件和數據流。RxJava 的核心概念是可觀察對象(Observables)和觀察者(Observers),它們之間建立了一個異步的數據流。

同樣的因為RxJava是第三方套件,因此需要添加Dependecies,本次要取資料的網站選擇使用https://jsonplaceholder.typicode.com/comments這個網址。

成果展示

Dependencies

    implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
    implementation 'com.squareup.retrofit2:retrofit:2.9.0'
    //Retrofit
    
    implementation 'com.squareup.retrofit2:adapter-rxjava3:2.9.0'
    implementation 'io.reactivex.rxjava3:rxjava:3.1.6'
    implementation 'io.reactivex.rxjava3:rxandroid:3.0.0'
    //RxJava

網路請求

    <uses-permission android:name="android.permission.INTERNET" />

布局設定

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/result"
        android:layout_width="339dp"
        android:layout_height="368dp"
        android:text="TextView"
        android:gravity="center"
        android:textSize="20sp"
        android:textStyle="bold"
        android:textColor="@color/black"
        android:background="@drawable/border"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toStartOf="@+id/guideline"
        app:layout_constraintHorizontal_bias="0.428"
        app:layout_constraintStart_toStartOf="@+id/guideline2"
        app:layout_constraintTop_toTopOf="@+id/guideline3"
        app:layout_constraintVertical_bias="0.069" />

    <Button
        android:id="@+id/search"
        android:layout_width="183dp"
        android:layout_height="126dp"
        android:text="搜尋"
        android:textSize="30sp"
        android:backgroundTint="@color/nae"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toStartOf="@+id/guideline"
        app:layout_constraintHorizontal_bias="0.502"
        app:layout_constraintStart_toStartOf="@+id/guideline2"
        app:layout_constraintTop_toBottomOf="@+id/number"
        app:layout_constraintVertical_bias="0.73" />

    <Spinner
        android:id="@+id/number"
        android:layout_width="344dp"
        android:layout_height="38dp"
        android:background="@drawable/border"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toStartOf="@+id/guideline"
        app:layout_constraintStart_toStartOf="@+id/guideline2"
        app:layout_constraintTop_toBottomOf="@+id/parameter"
        app:layout_constraintVertical_bias="0.12" />

    <Spinner
        android:id="@+id/parameter"
        android:layout_width="344dp"
        android:layout_height="38dp"
        android:background="@drawable/border"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toStartOf="@+id/guideline"
        app:layout_constraintStart_toStartOf="@+id/guideline2"
        app:layout_constraintTop_toBottomOf="@+id/result"
        app:layout_constraintVertical_bias="0.099" />

    <androidx.constraintlayout.widget.Guideline
        android:id="@+id/guideline"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        app:layout_constraintGuide_percent="0.96107054" />

    <androidx.constraintlayout.widget.Guideline
        android:id="@+id/guideline2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        app:layout_constraintGuide_percent="0.05109489" />

    <androidx.constraintlayout.widget.Guideline
        android:id="@+id/guideline3"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        app:layout_constraintGuide_begin="20dp" />
</androidx.constraintlayout.widget.ConstraintLayout>

這次我的設計為使用兩個Spinner選取參數,點選按鈕後就會把Spinner所選取的選項給傳入抓取API資料的方法,讓它依照這個參數去取得我想要的資料,且這次我也有使用到drawble資源,外框使用的是胡桃色,並且也將按鈕更改為更好看的顏色。
這邊附上我使用的顏色及色碼

    <color name="kurumi">#947A6D</color>
    <color name="nae">#86C166</color>

Retrofit環境建立

Retrofit的環境建立基本架構是一樣的,不過多加了這一串

.addCallAdapterFactory(retrofit2.adapter.rxjava3.RxJava3CallAdapterFactory.create())

這個是在讓Retrofit可以跟RxJava連結,所以整體就會長這樣

import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;

public class ApiClient {
    public Retrofit getCommentApi(){
        return new Retrofit.Builder()
                .baseUrl("https://jsonplaceholder.typicode.com/")
                .addConverterFactory(GsonConverterFactory.create())
                .addCallAdapterFactory(retrofit2.adapter.rxjava3.RxJava3CallAdapterFactory.create())
                //使用RxJava需要多添加這個
                .build();
    }
}

interface

import java.util.List;

import io.reactivex.rxjava3.core.Observable;
import retrofit2.http.GET;
import retrofit2.http.Query;

public interface GetApi {
    @GET("comments")//一樣使用Get要取Api資料
    Observable<List<DataResponse>> getCommentsData(
      //使用Observable告知有這個數據流可以觀測,後續才可以透過subscribe去訂閱這個數據流
      @Query("id") int id
      //這邊設定這個數據流有一個參數(id),屆時就要輸入id才可以搜尋
    );
}

RxJava的寫法會用到Observable,它會指定哪個東西是可以被觀測的數據流,到後面要調用的時候就可以subscribe,接著看到@Query,這個是在設定這個Api的參數,像這裡就設定了這個Api的參數是id,要注意()內的參數一定要是該Api資料需要的參數,且不能夠有任何差錯,否則等待你的只會有資料抓取失敗的結果而已!

建立接收資料的class

整體架構還是一樣,這邊就不細說了

public class DataResponse {
    private int postId;
    private int id;
    private String name;
    private String email;
    private String body;

    @Override
    public String toString() {
        return "DataResponse{" +
                "\npostId=" + postId +
                ",\n id=" + id +
                ",\n name='" + name + '\'' +
                ",\n email='" + email + '\'' +
                ",\n body='" + body + '\'' +
                '}';
    }

    public int getPostId() {
        return postId;
    }

    public int getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public String getEmail() {
        return email;
    }

    public String getBody() {
        return body;
    }
}

MainActivty

這次的實作寫的東西比較多放上來有點影響觀感,這邊附上這次的GitHub

首先創立Retrofit的方法還是一樣先宣告

    private ApiClient apiClient;
    private GetApi getApi;

然後就是初始化跟綁定

        apiClient = new ApiClient();
        getApi = apiClient.getCommentApi().create(GetApi.class);

因為所有程式都放在onCreate裡面會有點太攏長,所以這次我選擇將它們包出去,這樣就可以讓程式比較清楚,且好整理

        setSpinner();
        selectedData();
        search.setOnClickListener(view -> getComment(selected_number,selected_parameter));

可以看到我的按鈕的點擊事件,裡面寫了抓取本次Api資料的方法,並且傳入參數

setSpinner()

    private void setSpinner() {

        ArrayAdapter parameter = new ArrayAdapter(
                this,
                android.R.layout.simple_spinner_dropdown_item,
                parameter_data
        );

        ArrayList<Integer> numbers = new ArrayList<>();
        for (int i = 0; i < 100; i++) {
            numbers.add(i+1);
        }
        ArrayAdapter number = new ArrayAdapter(
                this,
                android.R.layout.simple_spinner_dropdown_item,
                numbers
        );

        parameter_spinner.setAdapter(parameter);
        number_spinner.setAdapter(number);
    }//Spinner的基本設定

設定Spinner的方法就跟前面教的一樣。

selectedData()

        parameter_spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
            @Override
            public void onItemSelected(AdapterView<?> adapterView, View view, int i, long l) {
                selected_parameter = parameter_spinner.getSelectedItem().toString();
            }
            @Override
            public void onNothingSelected(AdapterView<?> adapterView) {}
        });

spinner的點擊事件,可以看到我在有選取選項的方法內寫入,將選取的資料丟給selected_parameter紀錄,number的部分寫法也一樣,只是記得要加上轉換成整數的函式Integer.parseInt()

getComment()

    private void getComment(Integer selectedNumber, String selectedParameter) {
        getApi.getCommentsData(selectedNumber)
                .observeOn(Schedulers.io())
                .subscribeOn(AndroidSchedulers.mainThread())
                .subscribe(new DisposableObserver<List<DataResponse>>() {
                    @Override
                    public void onNext(@NonNull List<DataResponse> dataResponses) {

                        switch (selectedParameter){
                            case "postId":
                                result.setText("postId : "+dataResponses.get(0).getPostId());
                                break;
                            case "name":
                                result.setText("name : "+dataResponses.get(0).getName());
                                break;
                            case "email":
                                result.setText("email : "+dataResponses.get(0).getEmail());
                                break;
                            case "body":
                                result.setText("body : "+dataResponses.get(0).getBody());
                                break;
                            case "all":
                                result.setText("all : "+dataResponses.get(0).toString());
                                break;
                        }
                    }

                    @Override
                    public void onError(@NonNull Throwable e) {
                        result.setText("抓取資料失敗");
                    }

                    @Override
                    public void onComplete() {;
                    }
                });
    }//結合RxJava的Retrofit

首先看到這個部分

getApi.getCommentsData(selectedNumber)
                .observeOn(Schedulers.io())
                .subscribeOn(AndroidSchedulers.mainThread())
                .subscribe(new DisposableObserver<List<DataResponse>>() 

第一行就跟上一篇教的概念一樣,使用getApi裡面的getCommentData去抓資料,並且因為在GetApi有用到Observable觀察這個數據流,所以這裡就要設定在哪個線程(theard)觀測及訂閱,這個會影響的是有時候可能資料太龐大或者網路不好,種種原因導致資料取得太慢,就會跟著影響整個程式的流程,這個時候切換線程將複雜且可能出錯的地方放到副線程,就能讓它不會影響主要程式在做的事情,也就不會耽誤到整個程式的流程。

接著到第二行,.observeOn(Schedulers.io())這裡做的就是告知要在哪個線程進行觀測,而這裡指定了要在io線程進行。

第三行,.subscribeOn(AndroidSchedulers.mainThread()),這裡在設定要在哪個線程進行訂閱,也會連帶影響下面的onNextonErroronComplete會在哪個線程工作,現在這個就是指定在主線程進行訂閱的工作。

  • 補充 : 介面的更新會在主線程進行,意味著TextView的文字或著背景顏色這些其實都是在主線程進行更新,因此如果因為抓取資料導致主線程發生Delay,那更新的動作也會跟著Delay,所以才要指定複雜的工作在副線程執行,避免主線程阻塞。

第四行,.subscribe(new DisposableObserver<List<DataResponse>>() ,這裡是在設定訂閱的型態,後面的new有不同種可以選擇使用,現在這個跟其他的相比多了取消訂閱的功能,用意是當這筆資料暫時不用被觀測時,就把它給取消訂閱,這樣可以減少資源的消耗。

最後看到onNextonErroronComplete

  • onNext,當觀測到數據流有更新時會執行的動作,以這次的實作來說,就是當按下按鈕時會將參數傳入並且開始查詢資料,這邊我寫了switch去判斷parameters是什麼,再作出對應的行動,比如是email的時候,就去調用DataResponse裡面的getEmail(),將email的資料印到TextView上。
  • onError,當資料抓取失敗就會到這邊來,因此這裡通常都會寫log或用其他方式,讓錯誤資訊可以顯示出來方便之後修理。
  • onComplete,在執行完onNext之後就會跳到這裡,通常會在這裡加上log告知整個要取資料的動作有沒有失敗。

這次的Retrofit+RxJava就先介紹的這邊,下一篇會教怎麼去查詢氣象局的Api,將今天、明天的各種跟天氣相關的資訊抓下來,作一個專屬於自己的天氣App,模板會使用今天教的作一點改動,所以有興趣的不妨也試著自己做做看~


上一篇
【DAY 14】 怎麼使用Retrofit抓取API資料
下一篇
【DAY 16】 抓取天氣API,做一個自己的天氣APP!(上)
系列文
Android Studio開發過程和介紹30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言