天天看點

RxJava2+Retrofit2實作網絡請求和解析封裝

半年多前寫過一篇用Retrofit2請求網絡和解析的部落格,Retrofit2的簡單應用與封裝,不過當時其實還是遺留了不少細節問題沒有處理,比如如果有公共參數放Header裡面怎麼處理,請求過程中想顯示進度框怎麼處理,退出時要退出網絡請求怎麼處理等等,這兩天看了下RxJava,主要是看了這篇文章RxJava詳解,雖然并不是說的RxJava2,但原理差不多,講得非常清楚,覺得有點意思,就想用RxJava來重新改進一下之前的這個封裝

首先聲明,這篇部落格側重是講如何使用Retrofit2和RxJava2封裝網絡請求和解析,并不會重點介紹Retrofit2和RxJava2的知識,如對基礎知識不了解,請先檢視上面的連結學習,另外,Retrofit和Retrofit2,RxJava和RxJava2還是有一些不同的地方,本篇部落格是基于Retrofit2和RxJava2

1. 添加依賴

項目中用到Retrofit2,RxJava2,還用到Jackson,我們首先要在build.gradle中添加相關的依賴

compile 'com.squareup.retrofit2:retrofit:2.0.0'
compile 'com.squareup.retrofit2:converter-jackson:2.0.0'
compile 'com.jakewharton.retrofit:retrofit2-rxjava2-adapter:1.0.0-RC3'
compile 'io.reactivex.rxjava2:rxjava:2.0.0-RC3'
compile 'io.reactivex.rxjava2:rxandroid:2.0.0-RC1'
           

2. 定義Retrofit通路的接口

public interface RetrofitService {
    @FormUrlEncoded
    @POST("getUser")
    Observable<BaseEntity<User>> getUser(@FieldMap Map<String, String> map);
}
           

指定使用POST方式,通路伺服器方法名為getUser,參數@FieldMap可以用來傳遞多個參數,當然如果參數較少,也可以直接使用@Field

這裡唯一要注意的是方法的傳回值,首先如果我們記得的話,在直接使用Retrofit,不使用RxJava時,我們的傳回值都是Call,而如果我們想跟RxJava結合,這裡的傳回值對象就應該為Observable,Observable的泛型這裡為BaseEntity<User>,這個在上面的部落格中也講過,這裡再說明下

public class BaseEntity<E> implements Serializable {
    private int code;
    private String message;
    private E data;

    public int getCode() {
        return code;
    }

    public void setCode(int code) {
        this.code = code;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    public E getData() {
        return data;
    }

    public void setData(E data) {
        this.data = data;
    }
}
           

BaseEntity是伺服器傳回值的通用格式,它由三個部分組成,code表示成功還是失敗,0為成功,非0為失敗,message是提示内容,而主要的内容都封裝在data裡面,data為泛型,可以指定為任何内容,我們這個例子中就是一個User對象

public class User implements Serializable{
    private String name;
    private int age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}
           

就是一個簡單的實體類,這個根據實際情況大家可以随意定義

3. 對Retrofit2的基本設定

網絡請求部分主要使用Retrofit2來實作,我們先看下基礎的設定,直接上代碼

public class RetroFactory {
    private static String baseUrl = "http://192.168.0.107:8082/MyWeb/";

    private RetroFactory() {
    }

    private static OkHttpClient httpClient = new OkHttpClient.Builder()
            .addInterceptor(new Interceptor() {
                @Override
                public Response intercept(Chain chain) throws IOException {
                    Request.Builder builder = chain.request().newBuilder();
                    builder.addHeader("token", "abc");
                    return chain.proceed(builder.build());
                }
            }).connectTimeout(30, TimeUnit.SECONDS)
            .readTimeout(30, TimeUnit.SECONDS)
            .build();

    private static RetrofitService retrofitService = new Retrofit.Builder()
            .baseUrl(baseUrl)
            .addConverterFactory(JacksonConverterFactory.create())
            .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
            .client(httpClient)
            .build()
            .create(RetrofitService.class);

    public static RetrofitService getInstance() {
        return retrofitService;
    }
}
           

這裡baseUrl就是我們伺服器的項目運作的位址,我是在我本機啟動了一個Tomcat,192.168.0.107是我本機的IP,8082是Tomcat的端口号,MyWeb是我伺服器項目的名稱。

接着看,Retrofit2内部是使用OkHttp3的,我們對網絡通路的一些設定都可以通過OkHttp來進行,這裡首先調用了一個addInterceptor方法,用來增加一個攔截器,而攔截器的内容是給每個網絡請求增加了一個通用的Header字段,名為token,值為abc,這在實際項目中是非常常見的,每個請求都通過token來識别是否是有效的請求,防止惡意請求,當然,實際token應該是動态生成的,我這裡隻是示範如何在Header中添加通用内容,就直接指派為abc了。

下面接着設定了connectTimeout和readTimeout的逾時時間為30秒,實際網絡通路中,存在各種異常情況,掉線,不通,時斷時續等等,那設定逾時時間就非常必要了,肯定不能無限等待,其中connectTimeout是連接配接逾時時間,在指定時間内還沒有連接配接到伺服器就會報SocketTimeout異常,而readTimeout是讀取逾時時間,是連接配接後在指定時間還沒有擷取到資料就逾時。

設定為OkHttp,我們再來看Retrofit本身的設定,這裡baseUrl就是我們上面講的公用的連結,addConverterFactory是指定使用Jackson來解析Json資料,當然,你也可以使用Gson或者FastJson,不過資料量大的時候,Gson的效率不高,推薦使用Jackson和FastJson,而addCallAdapterFactory,通過這個轉換,才能将伺服器的傳回值從Retrofit預設的Call變為Observable

最後,提供一個方法傳回RetrofitService對象,這整個其實就是一個懶漢式單例模式

4. 定義網絡請求Activity的公共基類

public class NetworkBaseActivity extends AppCompatActivity {
    public ProgressDialog pd;
    public Function<Observable, ObservableSource> composeFunction;
    private final long RETRY_TIMES = 1;
    private boolean showLoading = true;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        init();
    }

    private void init() {
        pd = new ProgressDialog(this);

        composeFunction = new Function<Observable, ObservableSource>() {
            @Override
            public ObservableSource apply(Observable observable) throws Exception {
                return observable.retry(RETRY_TIMES)
                        .subscribeOn(Schedulers.io())
                        .doOnSubscribe(new Consumer<Disposable>() {
                            @Override
                            public void accept(Disposable disposable) throws Exception {
                                if (NetworkUtil.isNetworkAvailable(NetworkBaseActivity.this)) {
                                    if (showLoading) {
                                        if(pd != null && !pd.isShowing()){
                                            pd.show();
                                        }
                                    }
                                } else {
                                    Toast.makeText(NetworkBaseActivity.this, "網絡連接配接異常,請檢查網絡", Toast.LENGTH_LONG).show();
                                }
                            }
                        })
                        .observeOn(AndroidSchedulers.mainThread());
            }
        };
    }

    public void setLoadingFlag(boolean show) {
        showLoading = show;
    }

    @Override
    protected void onStop() {
        super.onStop();

        if (pd != null && pd.isShowing()) {
            pd.dismiss();
        }
    }
}
           

這裡的ProgressDialog是一個簡單的進度框,因為有的網絡請求可能耗時較長,如果界面不提供任何互動的話,使用者會誤以為程式卡死,使用者體驗較差,提供一個進度框就可以解決這個問題。Function是對Observable的一些基礎設定,等會再具體看,RETRY_TIMES,顧名思義,就是重試的次數,網絡環境較差或出現其它異常情況的時候,我們希望程式可以自動進行重試,最後一個showLoading用來設定是否顯示進度框,原則上一般的請求都要顯示,是以預設值是true,但也有例外,舉個例子,進應用的時候需要檢測是否有版本更新,這個操作我們肯定是希望在背景進行而使用者感覺不到的,如果這裡出現一個進度框就會莫名其妙,是以我們提供這樣一個設定。

下面我們具體看下這個composeFunction,retry方法就是剛才說到的重試次數,不指定預設為0,subscribeOn用來指定網絡請求所在的線程,這裡用IO線程,doOnSubscribe是在事件發送前進行的操作,是以我們可以做一些初始化的工作,isNetworkAvailable用來檢測網絡是否是連接配接的

public class NetworkUtil {
    public static boolean isNetworkAvailable(Activity activity) {
        Context context = activity.getApplicationContext();
        ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);

        if (connectivityManager == null) {
            return false;
        } else {
            NetworkInfo[] networkInfo = connectivityManager.getAllNetworkInfo();

            if (networkInfo != null && networkInfo.length > 0) {
                for (int i = 0; i < networkInfo.length; i++) {

                    if (networkInfo[i].getState() == NetworkInfo.State.CONNECTED) {
                        return true;
                    }
                }
            }
        }
        return false;
    }
}
           

這都是套路,不多說,如果網絡連接配接正常,而且showLoading也為true,我們就顯示一個進度框,否則提示使用者網絡連接配接異常。

observeOn是用來指定Observer操作的線程,也就是我們得到伺服器傳回的結果後的線程,因為我們需要操作控件,是以隻能在UI線程進行。這裡多說一句,doOnSubscribe的線程是什麼呢?它既不是在subscribeOn指定的線程,更不是在observeOn指定的線程,而是執行subscribe時所在的線程,subscribe我們現在還沒用到,後面會看到,本程式subscribe是在主線程執行,是以doOnSubscribe也就是在主線程了,我們這裡顯示進度框就是要在主線程進行,是以不用特意去指定,如果subscribe不在主線程,那可以在doOnSubscribe後通過subscribeOn指定它所在的線程。

setLoadingFlag方法就是提供一個設定是否顯示進度框的途徑。

最後的onStop,保險起見,我們判斷一下,進度框是否關閉,如果沒關閉要關掉,後面我們還會看到,進度框關閉時我們也會取消訂閱,防止已經退出後還在處理請求。

5. 封裝Observer

上面我們對Observable進行了封裝,那現在我們再來封裝Observer

public abstract class BaseObserver<T> implements Observer<BaseEntity<T>> {
    private Context mContext;
    private ProgressDialog mDialog;
    private Disposable mDisposable;
    private final int SUCCESS_CODE = 0;

    public BaseObserver(Context context, ProgressDialog dialog) {
        mContext = context;
        mDialog = dialog;

        mDialog.setOnCancelListener(new DialogInterface.OnCancelListener() {
            @Override
            public void onCancel(DialogInterface dialog) {
                mDisposable.dispose();
            }
        });
    }

    @Override
    public void onSubscribe(Disposable d) {
        mDisposable = d;
    }

    @Override
    public void onNext(BaseEntity<T> value) {
        if (value.getCode() == SUCCESS_CODE) {
            T t = value.getData();
            onHandleSuccess(t);
        } else {
            onHandleError(value.getCode(), value.getMessage());
        }
    }

    @Override
    public void onError(Throwable e) {
        Log.d("gesanri", "error:" + e.toString());

        if(mDialog != null && mDialog.isShowing()){
            mDialog.dismiss();
        }

        Toast.makeText(mContext, "網絡異常,請稍後再試", Toast.LENGTH_LONG).show();
    }

    @Override
    public void onComplete() {
        Log.d("gesanri", "onComplete");

        if(mDialog != null && mDialog.isShowing()){
            mDialog.dismiss();
        }
    }

    abstract void onHandleSuccess(T t);

    void onHandleError(int code, String message) {
        Toast.makeText(mContext, message, Toast.LENGTH_LONG).show();
    }
}
           

Observer是一個接口,它提供了4個方法,onSubscribe用來随時取消和Observable的連接配接,onNext用來處理Observable的傳回,也就是網絡連接配接的傳回,onComplete在onNext後被調用,表示完成,onError表示發生了錯誤,onComplete和onError兩個方法中隻會并且肯定會有一個方法被調用。

onSubscribe提供了一個Disposable參數,我們調用它的dispose方法就可以終止訂閱,在dialog的setOnCancelListener中,我們調用它來取消訂閱,這樣如果使用者在請求的過程中覺得等待時間過長,點選傳回鍵關閉進度框或者退出應用時,我們就可以取消訂閱而不繼續進行處理了。不過這裡有一點要注意的是,這個dispose方法并不會影響到伺服器端,如果請求已經發送到伺服器端,那就算用戶端調用了dispose方法,伺服器端的代碼依然會繼續執行,在一些服務端接口涉及插入資料庫的操作時,就要特别注意,考慮用戶端在dispose方法後,調用伺服器端的方法去執行一個類似復原的操作,否則用戶端取消了訂閱,伺服器端依然會執行完成,這個要根據項目的實際情況來具體對待。

onNext方法,是我們要處理的主要方法,在這之中,我們通過判斷傳回值中的code,來判斷要做的操作,如果code為0,也就是成功,我們就執行onHandleSuccess方法,如果code不為0,也就是失敗,我們就執行onHandleError方法,注意,一般情況下,成功是要單獨處理,而失敗隻給使用者提示就可以了,是以這裡我将onHandleSuccess聲明為抽象的,也就是子類必須要實作,而onHandleError不是抽象的,子類可以選擇實作或就用預設的實作即可。

最後,不管是進入了onComplete還是onError方法,都要記得關閉進度框

6. 定義調用網絡請求的Activity

public class MainActivity extends NetworkBaseActivity {
    private TextView name;
    private TextView age;
    private Observable observable;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        name = (TextView) findViewById(R.id.name);
        age = (TextView) findViewById(R.id.age);

        getUsers();
    }

    private void getUsers() {
        Map<String, String> map = new HashMap<String, String>();
        map.put("id", "123");
        map.put("name", "gesanri");

        observable = RetroFactory.getInstance().getUser(map);
        observable.compose(composeFunction).subscribe(new BaseObserver<User>(MainActivity.this, pd) {
            @Override
            void onHandleSuccess(User user) {
                name.setText("姓名:" + user.getName());
                age.setText("年齡:" + user.getAge());
            }
        });
    }
}
           

這個類繼承了我們之前第4步中定義的公共基類,布局檔案也很簡單

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    >

    <TextView
        android:id="@+id/name"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

    <TextView
        android:id="@+id/age"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />
</LinearLayout>
           

這裡功能很簡單,就是假設傳了2個參數,id和name,完後接收伺服器傳回的資料,顯示姓名和年齡。可以看到,經過我們前面幾步的封裝,Activity的實作已經非常幹淨了,短短幾行代碼就實作了網絡的請求和傳回資料的解析

7.伺服器端的實作

@WebServlet(name = "getUser", value = "/getUser")
public class GetUserServlet extends HttpServlet {
	private static final long serialVersionUID = 1L;

	@Override
	protected void doPost(HttpServletRequest req, HttpServletResponse resp)
			throws ServletException, IOException {

		System.out.println("token:" + req.getHeader("token"));
		
		System.out.println("id:" + req.getParameter("id"));
		System.out.println("name:" + req.getParameter("name"));
		
		resp.setCharacterEncoding("utf-8");
		
		if (req.getHeader("token").equals("abc")) {
			try {
				Thread.sleep(5000);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			
			resp.getWriter()
			.write("{\"code\":0, \"message\":\"擷取使用者成功!\", \"data\":{\"name\":\"張三\", \"age\":23}}");
		} else {
			resp.getWriter()
			.write("{\"code\":1, \"message\":\"擷取使用者失敗!\", \"data\":\"\"}");
		}
	}
}
           

最後我們簡單看下伺服器端的實作,我們接收了三個參數,一個是Header裡面的Token, 這個是每個請求都有的,另外兩個是這個getUser請求特定的參數,我們判斷如果token不為abc,就傳回錯誤,如果是abc,就傳回成功,實際項目肯定是要從資料庫擷取,這裡主要是示範用戶端,就偷懶直接傳回資料了。這裡為了在用戶端示範進度框,就休眠了5秒。

整個封裝過程就是這樣,還是比較基礎的,當然,不管是Retrofit還是RxJava,能做的工作遠遠不止這些,我個人了解的也比較少和淺,大家可以自己繼續深入學習,根據項目的實際需要來不斷優化自己的架構。

源碼下載下傳

繼續閱讀