1. 程式人生 > >RxJava+Retrofit+OkHttp3+Dagger2+MVP構建Android專案簡單例子

RxJava+Retrofit+OkHttp3+Dagger2+MVP構建Android專案簡單例子

以前的專案都是用的很老的MVC來做的,當然我覺得MVC在維護方面是比MVP強的,網路請求框架是用的嚴大神的NoHttp,個人這個網路請求框架是非常nb的,有機會看到這部落格的可以去GitHub上搜一下,非常好用。最近有點時間就開始去接觸最近非常流行的Android開發組合RxJava+Retrofit+OkHttp3+Dagger2+MVP,因為剛上手,所以不是很熟,都是在學習別人的東西,基本上從別人的專案中剝離出來的,在這對那些高手錶示感謝。

一、用RxJava+Retrofit+OkHttp3封裝成網路請求框架:
1、OkHttp3工具類封裝,在okhttp中設定快取目錄、請求攔截器,響應攔截器,程式碼如下:

public class OkHttpUtil {

    private static OkHttpClient mOkHttpClient;

    //設定快取目錄
    private static final File cacheDirectory = new File(MyApplication.getMyApplication().getCacheDir().getAbsolutePath(), "httpCache");

    private static Cache cache = new Cache(cacheDirectory, 10 * 1024 * 1024);

    //請求攔截
private static RequestInterceptor requestInterceptor = new RequestInterceptor(); //響應攔截 private static ResponseInterceptor responseInterceptor = new ResponseInterceptor(); public static OkHttpClient getOkHttpClient() { if (null == mOkHttpClient) { mOkHttpClient = new
OkHttpClient.Builder() .cookieJar(CookieJar.NO_COOKIES) .connectTimeout(10, TimeUnit.SECONDS) .readTimeout(30, TimeUnit.SECONDS) .writeTimeout(30, TimeUnit.SECONDS) .addInterceptor(responseInterceptor) .addInterceptor(new HttpLoggingInterceptor(new HttpLoggingInterceptor.Logger() { @Override public void log(String message) { Log.i("http", message); } }).setLevel(HttpLoggingInterceptor.Level.BODY)) .cache(cache) .build(); } return mOkHttpClient; } }

在程式碼中有請求攔截器RequestInterceptor和響應攔截器ResponseInterceptor,一般的請求都需要帶上請求頭和一些公共的請求引數以及Cookie的管理,這些配置都是在請求攔截器中配置,而響應攔截器一般對返回的引數進行一定格式化,便於處理資料,具體見原始碼。這裡還用okhttp中的日誌攔截,這個非常厲害HttpLoggingInterceptor,可以打印出所有http請求的資訊,再也不用跟你的後臺小哥爭論了,有詳細日誌有真相。

2、Retrofit工具類封裝,Retrofit之所以牛逼是他可以跟RxJava完美的結合,無縫!!!具體程式碼如下

public abstract class RetrofitUtil {
    //服務路徑
    private static final String Url = "http://hzqb.sftsdg.com/YMF_Webs/";
    private static Retrofit mRetrofit;
    private static OkHttpClient mOkHttpClient;

    //獲取Retrofit物件

    protected static Retrofit getRetrofit(){
        if (null == mRetrofit){
            if (null == mOkHttpClient){
                mOkHttpClient = OkHttpUtil.getOkHttpClient();
            }
            mRetrofit = new Retrofit.Builder()
                    .baseUrl(Url)
                    .addConverterFactory(GsonConverterFactory.create())
                    .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
                    .client(mOkHttpClient)
                    .build();
        }
        return mRetrofit;
    }
}

這裡是將Retrofit與OkHttp完美結合,因為Retrofit需要傳入一個Request Client,此時用OkHttp再合適不過了,同時,還可以用GsonConverterFactory來自動解析資料。需要注意的是,此處的url必須要以“/”結尾,不然會拋異常

3、獲取將OkHttpClient和Retrofit結合好Retrofit物件,此處,Retrofit.create()返回一個泛型,完美結合RxJava,程式碼如下:

public class RequestEngine {

    private static Retrofit mRetrofit;
    //private static RequestEngine instance;

    public RequestEngine() {
        mRetrofit = RetrofitUtil.getRetrofit();
    }

    /*public static RequestEngine getInstance() {
        if (instance == null) {
            synchronized (RequestEngine.class) {
                if (null == instance) {
                    instance = new RequestEngine();
                }
            }
        }
        return instance;
    }*/

    //返回一個泛型
    public <T> T getServer(Class<T> server) {
        return mRetrofit.create(server);
    }

}

程式碼片中註釋的是用一般的方法引入的封裝好的Retrofit,此處把程式碼註釋掉是因為用到了註解Dagger2,

4、用Retrofit寫請求方法,返回的是Observable泛型,程式碼如下:

public interface RequestApi {

    @FormUrlEncoded
    @POST("login/u.php")
    Observable<BaseBean<LoginInfo>> login(@Field("username") String username, @Field("password") String password, @Field("app") String type);

}

至於Retrofit一般用法還是挺簡單的,簡單介紹下,在retrofit中通過一個Java介面作為http請求的api介面
get請求:在方法上使用@Get註解來標誌該方法為get請求,在方法中的引數需要用@Query來註釋,如:

@GET("search/repositories")
Call<RetrofitBean> queryRetrofitByGetCall(@Query("q")String owner,
                                      @Query("since")String time,
                                      @Query("page")int page,
                                      @Query("per_page")int per_Page);

Post請求:使用@FormUrlEncoded和@POST註解來發送表單資料。使用 @Field註解和引數來指定每個表單項的Key,value為引數的值。需要注意的是必須要使用@FormUrlEncoded來註解,因為post是以表單方式來請求的,如:

@FormUrlEncoded
@POST("user/edit")
Call<User> updateUser(@Field("first_name") String first, @Field("last_name") String last);

5、封裝請求方法,程式碼如下:

public class RequestMethod {

    private RequestApi RequestApi;
    @Inject
    RequestEngine requestEngine;

    public RequestMethod() {
        //此處也可以用註解來做,
       // this.RequestApi = RequestEngine.getInstance().getServer(RequestApi.class);
        //用註解的方式
        DaggerLoginComponent.builder().loginModel(new LoginModule()).build().inject(this);
        RequestApi = requestEngine.getServer(RequestApi.class);
    }

    //該方法不能獲取去到baseBean中的result和msg的值
    public void loginRequest(String userName, String password, String type, HttpSubscriber<LoginInfo> subscriber){
        RequestApi.login(userName,password,type)
                .compose(RxHelper.<LoginInfo>handleResult())
                .subscribe(subscriber);
    }

    //取到所有的解析後的json資料
    public void loginRequestWithBaseBean(String userName, String password, String type, HttpSubscriber<BaseBean<LoginInfo>> subscriber){
        RequestApi.login(userName,password,type)
                .compose(RxHelper.<BaseBean<LoginInfo>>schedulersThread())
                .subscribe(subscriber);
    }

}

在該類中獲取RequestEngine使用的註解的方式來做的,程式碼裡面有註釋,在請求方法中,封裝了HttpSubscriber,這個是繼承Subscriber類,用來處理RxJava接受資料,以及網路請求載入框,網路請求異常處理類,程式碼如下:

public abstract class HttpSubscriber<T> extends Subscriber<T> {

    private Context context;
    private boolean isShowDialog;
    private ProgressDialog progressDialog;

    public HttpSubscriber(Context context, boolean isShowDialog) {
        this.context = context;
        this.isShowDialog = isShowDialog;
    }

    @Override
    public void onStart() {
        super.onStart();
        if (!isNetWorking(context)) {
            onError("網路不可用");
            onFinish();
            if (!isUnsubscribed()) {
                unsubscribe();
            }
        } else {
            if (progressDialog == null && isShowDialog) {
                progressDialog = new ProgressDialog(context);
                progressDialog.setMessage("正在載入...");
                progressDialog.show();
            }
        }
    }

    @Override
    public void onCompleted() {
        onFinish();
        if (!isUnsubscribed()) {
            unsubscribe();
        }
        if (progressDialog != null && isShowDialog) {
            progressDialog.dismiss();
            progressDialog = null;
        }
    }

    /**
     * onCompleted和onError是互斥的,佇列中呼叫了其中一個,就不應該再呼叫另一個。也是事件序列中的最後一個
     *
     */

    @Override
    public void onError(Throwable e) {
        onFinish();
        if (!isNetWorking(context)) {
            onError("網路不可用");
        } else if (e instanceof SocketTimeoutException) {
            onError("伺服器響應超時");
        } else if (e instanceof ConnectException) {
            onError("伺服器請求超時");
        } else if (e instanceof HttpException) {
            onError("伺服器異常");
        } else {
            onError("未知異常:"+e.getMessage());
        }
        if (progressDialog != null && isShowDialog) {
            progressDialog.dismiss();
            progressDialog = null;
        }
    }

    @Override
    public void onNext(T t) {
        onSuccess(t);
    }

    public abstract void onSuccess(T t);

    public abstract void onError(String msg);

    public abstract void onFinish();

    /**
     * 網路監測
     *
     * @param context
     * @return
     */
    public static boolean isNetWorking(Context context) {
        boolean flag = checkNet(context);
        if (!flag) {

            Toast.makeText(context, "當前裝置網路異常,請檢查後再重試!", Toast.LENGTH_SHORT).show();
        }
        return flag;
    }

    private static boolean checkNet(Context context) {

        if (context != null) {
            ConnectivityManager mConnectivityManager = (ConnectivityManager) context
                    .getSystemService(Context.CONNECTIVITY_SERVICE);
            NetworkInfo mNetworkInfo = mConnectivityManager
                    .getActiveNetworkInfo();
            if (mNetworkInfo != null) {
                return mNetworkInfo.isAvailable();
            }
        }
        return false;
    }
}

在這個demo中使用的Rxjava1.0的,所以重寫了onStart() 方法,在該方法中處理一些請求前工作,需要注意的是,改方法是在子執行緒執行的,但是在這裡彈出載入框肯定是有問題的,別慌,RxJava可以指定執行緒來處理訂閱結果的,這裡我們指定在AndroidSchedulers.mainThread()這個執行緒中。可以看到,在請求方法中(5)我們用到了compose操作符,這個操作符強大是因為,他能解決一般Rxjava普通寫法打斷鏈式結構,這個操作符需要傳入一個Transformers,Transformer實際上就是一個Func1<Observable<T>, Observable<R>>,換言之就是:可以通過它將一種型別的Observable轉換成另一種型別的Observable,在程式碼中,封裝了一個RxHelper類,程式碼如下:

public class RxHelper {

    /**
     * 處理http請求返回的結果,result_code,當返回成功的時候將data剝離出來,返回給subscriber
     *
     * @param <T>
     * @return
     */
    public static <T> Observable.Transformer<BaseBean<T>, T> handleResult() {
        return new Observable.Transformer<BaseBean<T>, T>() {
            @Override
            public Observable<T> call(Observable<BaseBean<T>> baseBeanObservable) {
                return baseBeanObservable.flatMap(new Func1<BaseBean<T>, Observable<T>>() {
                    @Override
                    public Observable<T> call(BaseBean<T> tBaseBean) {
                        if ("1".equals(tBaseBean.getResult())) {
                            //返回成功
                            return addData(tBaseBean.getData());
                        } else {
                            //返回失敗
                            return Observable.error(new Exception(tBaseBean.getMsg()));
                        }
                    }
                }).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
            }
        };
    }

    /**
     * 將服務端返回的資料加入subscriber
     *
     * @param data
     * @param <T>
     * @return
     */

    private static <T> Observable<T> addData(final T data) {
        return Observable.create(new Observable.OnSubscribe<T>() {
            @Override
            public void call(Subscriber<? super T> subscriber) {
                try {
                    subscriber.onNext(data);
                    subscriber.onCompleted();
                } catch (Exception e) {
                    subscriber.onError(e);
                }
            }
        });
    }

    /**
     * rxJava執行緒轉換,在io執行緒中發起請求,回撥給主執行緒
     *
     * @param <T>
     * @return
     */

    public static <T> Observable.Transformer<T, T> schedulersThread() {
        return new Observable.Transformer<T, T>() {
            @Override
            public Observable<T> call(Observable<T> tObservable) {
                return tObservable
                        .subscribeOn(Schedulers.io())
                        .unsubscribeOn(Schedulers.io())
                        .observeOn(AndroidSchedulers.mainThread());
            }
        };
    }
}

與RxJava結合使用我們一般會封裝一個BaseBean來處理返回資料,這裡我們使用登入方法類做例子:

public class BaseBean<T> implements Serializable {
    private String result;
    private String msg;
    private T data;

    public String getResult() {
        return result;
    }

    public void setResult(String result) {
        this.result = result;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }

    public T getData() {
        return data;
    }

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

    @Override
    public String toString() {
        return "BaseBean{" +
                "result='" + result + '\'' +
                ", msg='" + msg + '\'' +
                ", data=" + data +
                '}';
    }

result,msg分別是你後臺返回的請求狀態碼,我們是result=1是代表請求成功,然後msg就會返回成功的Message,而泛型T就是返回的你需要的具體資料,一般是一個Object,或者裡面還嵌套了陣列。
登入返回的資料的bean如下:

public class LoginInfo extends BaseBean{
   private String token;
    private String type;
    private String is_allow_create;

    public String getIs_allow_create() {
        return is_allow_create;
    }

    public void setIs_allow_create(String is_allow_create) {
        this.is_allow_create = is_allow_create;
    }

    public String getToken() {
        return token;
    }

    public void setToken(String token) {
        this.token = token;
    }

    public String getType() {
        return type;
    }

    public void setType(String type) {
        this.type = type;
    }

    @Override
    public String toString() {
        return "LoginInfo{" +
                "token='" + token + '\'' +
                ", type='" + type + '\'' +
                ", is_allow_create='" + is_allow_create + '\'' +
                '}';
    }

到此為止,網路層封裝完畢,但是此處有一個帶解決的問題就是RxJava的生命週期問題,如果不與Activity和Fragment生命中期繫結來判定是否要取消訂閱,會出現兩個問題,第一,記憶體洩漏;第二,有可能會導致View拋異常而崩潰App,但是這個情況出現的機率較小,現在網路上的這些框架很少人加入了生命週期管理,在這裡,我也不知道怎麼加入到這個工程裡面。如果有大神,可以告訴我怎麼加入生命週期管理,當然是要嵌入到這個工程裡面後的程式碼,不是要告訴方法,哈哈哈,彩筆一般都這樣!附上網路層的結構圖:
網路層工程結構圖

接下來,以登入的例子來寫MVP模式以及Dagger2,介面如圖:
登入介面
1、分析,這個介面有兩個功能,一個是登入,一個是清除,所以我們寫一個介面,裡面兩個方法,分別是commit()和clear(),程式碼如下:

/**
 * @author: wangbo
 * @description:    提交,清除邏輯介面
 * @date: 2017-08-08   11:02
 */
public interface IMainActivityPresenter {
    void commit(Context context,boolean isShowProgress,List<EditText> editTexts, TextView msg, RequestMethod requestMethod);
    void clear(List<EditText> editTexts);
}

然後在寫這個介面的實現類,過載這兩個方法,在方法裡面處理具體的邏輯,程式碼如下:

/**
 * @author: wangbo
 * @description: 處理介面邏輯
 * @date: 2017-08-08   11:04
 */
public class MainActivityImpl implements IMainActivityPresenter {
    @Override
    public void commit(Context context, boolean isShowProgress, List<EditText> editTexts, final TextView msg, RequestMethod requestMethod) {
        String tel = editTexts.get(0).getText().toString();
        String psw = editTexts.get(1).getText().toString();
        String type = editTexts.get(2).getText().toString();
        requestMethod.loginRequestWithBaseBean(tel, psw, type, new HttpSubscriber<BaseBean<LoginInfo>>(context, isShowProgress) {
            @Override
            public void onSuccess(BaseBean<LoginInfo> loginInfoBaseBean) {
                msg.setText(loginInfoBaseBean.toString());
            }

            @Override
            public void onError(String msg) {

            }

            @Override
            public void onFinish() {

            }
        });
    }

    @Override
    public void clear(List<EditText> editTexts) {
        for (EditText editText : editTexts) {
            editText.setText("");
        }
    }
}

2、寫個在MainActiviy中處理View的邏輯介面,有,初始化,View的初始化,清除,提交,這些行為,程式碼如下:

/**
 * @author: wangbo
 * @description:    view動作介面
 * @date: 2017-08-08   11:13
 */
public interface IMainActivityView {
    void init();

    void intView();

    void commit();

    void clear();

}

3、在MainActivity中實現IMainActivityView 這個介面,看到過載方法:

public class MainActivity extends AppCompatActivity implements IMainActivityView, View.OnClickListener {
    /**
     * 通過@Inject來宣告依賴物件,注意,被註解的欄位不能用private和protected修飾
     */
    @Inject
    IMainActivityPresenter mainActivityPresenter;
    @Inject
    RequestMethod requestMethod;
    private List<EditText> editTexts;
    private TextView textView;
    private Button commit, clear;

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

    @Override
    public void init() {
        /**
         * 編寫完Component和Module和Dagger2並不會自動建立對應的類,此時需要我們手動點選開發工具的Rebuild後,
         * 自動生成DaggerLoginComponent,通過生成的DaggerLoginComponent類來建立LoginModule例項
         * Component所需要的Module類是通過系統自動生成的Module類類名首字母小寫對應的方法來例項化的
         */
        DaggerLoginComponent.builder().loginModel(new LoginModule()).build().inject(this);
        editTexts = new ArrayList<>();
    }

    @Override
    public void intView() {
        editTexts.add((EditText) findViewById(R.id.tel));
        editTexts.add((EditText) findViewById(R.id.psw));
        editTexts.add((EditText) findViewById(R.id.type));
        textView = (TextView) findViewById(R.id.tv_msg);
        commit = (Button) findViewById(R.id.commit);
        commit.setOnClickListener(this);
        clear = (Button) findViewById(R.id.clear);
        clear.setOnClickListener(this);

    }

    @Override
    public void commit() {
        mainActivityPresenter.commit(MainActivity.this, true, editTexts, textView, requestMethod);
    }

    @Override
    public void clear() {
        mainActivityPresenter.clear(editTexts);
    }

    @Override
    public void onClick(View view) {
        switch (view.getId()) {
            case R.id.commit:
                commit();
                break;
            case R.id.clear:
                clear();
                break;
        }
    }
}

專案結構圖如下:
MVP模式工程結構圖

在這裡總結下我的MVP的寫法:首先分析介面中有哪些功能,把這些功能寫成介面,在介面的方法中傳入所需要的引數,然後寫一個該介面的實現類,實現這個介面,在重寫的方法中寫這個功能的具體邏輯,然後再為了簡化Activity中的程式碼,美化結構,在一個處理Activity中所有總邏輯的介面,比如,初始化類,初始化view,Activity中涉及的view的動作的介面,讓Activity去實現這個介面,最後,在Activity中傳入邏輯處理的介面,即IMainActivityPresenter,例項化該介面是通過new它的實現類來完成的。說到例項化類,這裡就引入了Dagger2,依賴注入的方式來傳遞物件,避免在使用物件的過程中去new物件,具體如下:

1、寫一個在該工程中索要使用的Module類,這個類中是提供你工程中要用的物件,具體程式碼如下,用法說明見程式碼註釋:

/**
 * @author: wangbo
 * @description: 宣告Module
 * @date: 2017-08-08   14:05
 */

/**
 * @ Module申明該類是Module類
 * @ Provides宣告Module類中哪些方法是用來提供依賴物件,當Component類需要依賴物件時,他就會根據返回值的型別來在有@Provides註解的方法中選擇呼叫哪個方法
 * @ Singleton的作用就是宣告單例模式,^-^以後再也不用寫單例模式了
 * @ Singleton的單利模式還以通過自定義註解來實現,這樣做的目的就是方法檢視該類的作用域
 * 如: @ Scope
 *      @ Retention(RetentionPolicy.RUNTIME)
 *      public @interface PerActivity {}
 *   此處的@Singleton可以用 @PerActivity來代替,可以清楚的看到這是作用字Activity,則能與Fragment區別
 */

@Module
public class LoginModule {

    @Singleton
    @Provides
    IMainActivityPresenter mainActivity(){
        return new MainActivityImpl();
    }

    @Singleton
    @Provides
    RequestMethod requestMethod(){
        return new RequestMethod();
    }

    @Singleton
    @Provides
    RequestEngine requestEngine(){
        return new RequestEngine();
    }
}

2、寫一個Component,這個類是一箇中間橋接類,她的作用是將你要使用的物件,通過Component來注入到你的物件需求方,具體程式碼如下,詳細說明見程式碼註釋:

/**
 * @author: wangbo
 * @description: 宣告Component
 * @date: 2017-08-08   14:08
 */

/**
 * 當@module中聲明瞭單例模式Singleton的時候在Component中也需要宣告
 * @ Component註解有兩個屬性,modules和dependencies這兩個屬性的型別都是Class陣列,modules的作用就是宣告該Component含有哪幾個Module,當Component需要某個依賴物件時,就會通過這些Module類中對應的方法獲取依賴物件
 * inject方法就是將module中對應方法取出的物件通過Component來把依賴需求方(MainActivity、RequestMethod)所需要的物件注入到依賴需求方
 */
@Singleton
@Component(modules = LoginModule.class)
public interface LoginComponent {
    void inject(MainActivity mainActivity);
    void inject(RequestMethod requestMethod);
}

Component介面中有兩個inject方法,這個方法就是將LoginModule.class注入到MainActivity 和RequestMethod 中。

3、在你所需要用到這兩個物件的地方用@Inject註解來標識該物件,注意,使用註解的欄位不能用private和protected修飾,然後rebuild工程,生成DaggerLoginComponent物件,呼叫

 DaggerLoginComponent.builder().loginModel(new LoginModule()).build().inject(this);

方法例項化Module。然後註解的類不需要new就可以直接用了。ok,到此結束,工程結構圖如下:
Dagger2註解工程結構

4、Dagger2的引入方法:

a、在工程跟目錄下的build.gradle下面的dependencies下新增:
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'

b、在app目錄下的build.gradle下新增外掛:
apply plugin: 'com.neenbedankt.android-apt'

c、引入依賴:

  //引入dagger2
    compile 'com.google.dagger:dagger:2.6'
    apt 'com.google.dagger:dagger-compiler:2.6'
    //java註解
    provided 'org.glassfish:javax.annotation:10.0-b28'

最後,這個工程所用的其他專案依賴:

    compile 'io.reactivex:rxandroid:1.1.0'
    compile 'io.reactivex:rxjava:1.1.0'
    compile 'com.squareup.retrofit2:retrofit:2.3.0'
    compile 'com.squareup.retrofit2:converter-gson:2.3.0'
    compile 'com.squareup.retrofit2:adapter-rxjava:2.3.0'
    compile 'com.squareup.okhttp3:okhttp:3.8.1'
    compile 'com.squareup.okhttp3:logging-interceptor:3.8.1'

附上工程的專案原始碼:
專案原始碼

最後,完了。本人也是剛學這些東西,並不能透徹的理解,如果有什麼問題,歡迎指正。一起學習。
最後感謝這些大神:
謝謝大神1(網路框架封裝)
謝謝大神2(Dagge2基礎)