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的寫法:首先分析介面中有哪些功能,把這些功能寫成介面,在介面的方法中傳入所需要的引數,然後寫一個該介面的實現類,實現這個介面,在重寫的方法中寫這個功能的具體邏輯,然後再為了簡化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,到此結束,工程結構圖如下:
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基礎)