手把手教你搭建【rxjava+retrofit+okhttp】网络框架
废话不多说,直接上代码。
第一步:添加依赖
implementation 'io.reactivex.rxjava2:rxandroid:2.1.1' //线程调度
implementation 'com.squareup.retrofit2:adapter-rxjava2:2.9.0' //配合Rxjava使用
implementation 'com.squareup.retrofit2:retrofit:2.9.0' //retrofit2.0
implementation 'com.squareup.retrofit2:converter-gson:2.9.0' //ConverterFactory的Gson:
implementation 'com.google.code.gson:gson:2.9.0' //gson解析
implementation 'com.squareup.okhttp3:okhttp:4.9.3' //核心库
implementation 'com.squareup.okhttp3:logging-interceptor:4.9.3' //请求日志
implementation 'com.squareup.retrofit2:converter-scalars:2.9.0' //增加返回值为字符串的支持
第二步:AndroidManifest.xml中添加网络权限
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
此外,还需解决network security policy的问题,该问题比较简单,请参考https://www.jianshu.com/writer#/notebooks/49491345/notes/98737436/preview
第三步:代码实现
3.1 新建base目录,同时在该目录下创建几个基础文件:
3.1.1 【BaseListModel】-----返回结果为数组的实体类
注意:字段名需要根据项目自行修改。
public class BaseListModel<T> implements Serializable {
private int code;
private List<T> data;
private String message;
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public List<T> getData() {
return data;
}
public void setData(List<T> data) {
this.data = data;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}
3.1.2 【BaseModel】----返回结果为对象的实体类
注意:字段名需要根据项目自行修改。
public class BaseModel<T> implements Serializable {
public final static int CODE_SUCCESS = 0;
private int code;
private T data;
private String desc;
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
public String getDesc() {
return desc;
}
public void setDesc(String desc) {
this.desc = desc;
}
}
3.1.3 【BaseObserver】----请求状态封装
public abstract class BaseObserver<T> extends DisposableObserver<T> {
protected BaseView view;
/**
* 解析数据失败
*/
public static final int PARSE_ERROR = 1001;
/**
* 网络问题
*/
public static final int BAD_NETWORK = 1002;
/**
* 连接错误
*/
public static final int CONNECT_ERROR = 1003;
/**
* 连接超时
*/
public static final int CONNECT_TIMEOUT = 1004;
public BaseObserver(BaseView view){
this.view = view;
}
@Override
public void onStart(){
if(view != null){
view.hideLoading();
view.showLoading("");
}
}
@Override
public void onNext(@NotNull T o) {
try {
int code;
String message = "";
if (o instanceof BaseModel) {
BaseModel<?> model = (BaseModel<?>) o;
code = model.getCode();
message = model.getDesc();
} else if (o instanceof BaseListModel) {
BaseListModel<?> model = (BaseListModel<?>) o;
code = model.getCode();
message = model.getMessage();
} else {
code = BaseModel.CODE_SUCCESS;
}
if (code == BaseModel.CODE_SUCCESS) {
onSuccess(o);
}else {
view.showError(message);
onError();
}
} catch (Exception e) {
e.printStackTrace();
view.showError(e.toString());
onError();
}
}
@Override
public void onError( Throwable e) {
if (view != null){
view.hideLoading();
}
if (e instanceof HttpException) {
//HTTP错误
onException(BAD_NETWORK);
} else if (e instanceof ConnectException || e instanceof UnknownHostException) {
//连接错误
onException(CONNECT_ERROR);
} else if (e instanceof InterruptedIOException) {
//连接超时
onException(CONNECT_TIMEOUT);
} else if (e instanceof JsonParseException
|| e instanceof JSONException
|| e instanceof ParseException) {
//解析错误
onException(PARSE_ERROR);
} else {
if (e != null) {
if(view != null) view.showError(e.toString());
} else {
if(view != null) view.showError("未知错误");
}
onError();
}
}
/**
* 异常情况
* @param unknownError 异常情况对应的数值
*/
private void onException(int unknownError) {
String message = "";
switch (unknownError) {
case CONNECT_ERROR:
message = "连接错误";
break;
case CONNECT_TIMEOUT:
message = "连接超时";
break;
case BAD_NETWORK:
message = "网络问题";
break;
case PARSE_ERROR:
message = "解析数据失败";
break;
default:
break;
}
view.showError(message);
onError();
}
@Override
public void onComplete() {
//完成后隐藏加载框
if (view != null) view.hideLoading();
}
public abstract void onSuccess(T o);
public abstract void onError();
}
3.1.4 【BaseView】----3种请求状态的接口
public interface BaseView {
/**
* 显示dialog
*/
void showLoading(String message);
/**
* 隐藏 dialog
*/
void hideLoading();
/**
* 显示错误信息
*
* @param msg 消息内容
*/
void showError(String msg);
}
3.1.5 【BasePresenter】
public class BasePresenter<V extends BaseView> {
private CompositeDisposable compositeDisposable;
public V baseView;
protected ApiServer apiServer = ApiRetrofit.getInstance().getApiServer();
public BasePresenter(V baseView) {
this.baseView = baseView;
}
public BasePresenter() {
}
/**
* 解除绑定
*/
public void detachView() {
baseView = null;
removeDisposable();
}
/**
* 返回 view
* @return 视图
*/
public V getBaseView() {
return baseView;
}
public void addDisposable(Observable<?> observable, BaseObserver observer) {
if (compositeDisposable == null) {
compositeDisposable = new CompositeDisposable();
}
compositeDisposable.add(observable.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribeWith(observer));
}
public void addDisposable(Observable<?> observable, DisposableObserver observer) {
if (compositeDisposable == null) {
compositeDisposable = new CompositeDisposable();
}
compositeDisposable.add(observable.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribeWith(observer));
}
public void removeDisposable() {
if (compositeDisposable != null) {
compositeDisposable.dispose();
}
}
}
3.1.6 【BaseActivity】
public abstract class BaseActivity<P extends BasePresenter<? extends BaseView>> extends AppCompatActivity implements BaseView{
private LoadingDialog dialog;
public Context context;
protected P presenter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
context = this;
setContentView(getLayoutId());
presenter = createPresenter();
initView();
initData();
}
/**
* 关闭加载框
*/
private void closeLoadingDialog() {
if (dialog != null && dialog.isShowing()) {
dialog.dismiss();
}
}
/**
* 显示加载框
* @param message 加载框内容
*/
private void showLoadingDialog(String message) {
if (dialog == null) {
dialog = new LoadingDialog(context);
}
dialog.setMessage(message);
dialog.setCancelable(false);
dialog.show();
}
/**
* 吐司弹窗
*/
public void showToast(String s) {
Toast.makeText(context, s, Toast.LENGTH_SHORT).show();
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
View v = getCurrentFocus();
//当isShouldHideInput(v, ev)为true时,表示的是点击输入框区域,则需要显示键盘,
//同时显示光标,反之,需要隐藏键盘、光标
if (isShouldHideInput(v, ev)) {
//处理EditText的光标隐藏、显示逻辑
closeKeyboard((EditText) v, this);
}
return super.dispatchTouchEvent(ev);
}
// 必不可少,否则所有的组件都不会有TouchEvent了
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
/**
* 关闭软键盘的方法(写在 KeyBoardUtils 的工具类里面)
* @param mEditText 输入框
* @param mContext 上下文
*/
public static void closeKeyboard(EditText mEditText, Context mContext) {
if (mEditText != null && mContext != null) {
InputMethodManager imm = (InputMethodManager) mContext
.getSystemService(Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(mEditText.getWindowToken(), 0);
}
}
/**
* 判断点击的区域是否EditText之外
* @param v 视图
* @param event 手势事件
* @return 返回true说明点击的是输入框区域外
*/
public boolean isShouldHideInput(View v, MotionEvent event) {
if ((v instanceof EditText)) {
int[] leftTop = {0, 0};
//获取输入框当前的location位置
v.getLocationInWindow(leftTop);
int left = leftTop[0];
int top = leftTop[1];
int bottom = top + v.getHeight();
int right = left + v.getWidth();
// 点击的是输入框区域,保留点击EditText的事件
return !(event.getX() > left) || !(event.getX() < right)
|| !(event.getY() > top) || !(event.getY() < bottom);
}
return false;
}
@Override
public void showLoading(String message) {
showLoadingDialog(message);
}
@Override
public void hideLoading() {
closeLoadingDialog();
}
@Override
public void showError(String msg) {
showToast(msg);
}
protected abstract int getLayoutId();
public abstract void initData();
public abstract void initView();
protected abstract P createPresenter();
}
3.2 新建api目录,在该目录下创建ApiService,ApiRetrofit文件
3.2.1 【ApiService】----接口url都放在这里
public interface ApiService {
String BASE_HOST = "https://mock.apipost.cn/app/mock/project/74ca9bd8-917d-4615-90f0-98e9e5001f38/"; //[ApiPost]mock测试地址
//用户登录
@POST("demo/network/login")
Observable<BaseModel<LoginBean>> userLogin(@Body Map<String,Object> map);
}
3.2.2 【ApiRetrofit】
public class ApiRetrofit {
private final ApiService apiService;
private static ApiRetrofit apiRetrofit;
private static final String TAG = "ApiRetrofit";
//构造函数
public ApiRetrofit(){
//网络日志
HttpLoggingInterceptor httpLoggingInterceptor = new HttpLoggingInterceptor(s -> {
try {
String text = URLDecoder.decode(s, "utf-8");
Log.e(TAG, text);
}catch (UnsupportedEncodingException e){
e.printStackTrace();
Log.e(TAG, s);
}
});
httpLoggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
//配置OkHttpClient
OkHttpClient client = new OkHttpClient.Builder()
.addInterceptor(httpLoggingInterceptor) //添加日志
.connectTimeout(30, TimeUnit.SECONDS) //请求时限 30s
.readTimeout(30, TimeUnit.SECONDS) //读取时限 30s
.writeTimeout(30, TimeUnit.SECONDS) //写入时限 30s
.build();
//配置Retrofit
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(BASE_HOST) //基础域名
.addConverterFactory(GsonConverterFactory.create()) //增加返回值为Gson的支持(以实体类返回)
.addConverterFactory(ScalarsConverterFactory.create()) //增加返回值为String的支持
.addCallAdapterFactory(RxJava2CallAdapterFactory.create()) //怎加返回值为Observable<T>的支持
.client(client) //绑定OkHttpClient
.build();
apiService = retrofit.create(ApiService.class); //创建api实例
}
//双重检查,实现单例
public static ApiRetrofit getInstance(){
if(apiRetrofit == null){
//使用同步锁,当有线程在执行时,保证其它线程不会进入该方法体.
synchronized (Object.class){
if(apiRetrofit == null){
apiRetrofit = new ApiRetrofit();
}
}
}
return apiRetrofit;
}
public ApiService getApiService(){ return apiService; }
}
3.3 新建model目录,用于存放请求结果实体类。
我们测试一个登录接口。所以在model下生成一个LoginBean的实体类。
public class LoginBean implements Serializable {
private String username;
private String email;
private String address;
private String token;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
}
3.4 新建widgets目录,用于存放自定义的控件。
我们在这里加一个加载组件LoadingDialog。注意:组件中会用到资源文件,请到我的github取,地址在最下面。
public class LoadingDialog extends ProgressDialog {
TextView mLoadingTextView;
View mView;
/**
* 这里的Context 必须用actiivty 不能用applicationContext
*
* @param context 上下文
*/
public LoadingDialog(Context context) {
this(context, "");
}
public LoadingDialog(Context context, CharSequence msg) {
super(context, R.style.LoadingDialogStyle);
mView = View.inflate(context, R.layout.dialog_loading, null);
mLoadingTextView = (TextView) mView.findViewById(R.id.mLoadingTextView);
if (!TextUtils.isEmpty(msg)) {
mLoadingTextView.setText(msg);
mLoadingTextView.setVisibility(View.VISIBLE);
} else {
mLoadingTextView.setVisibility(View.GONE);
}
this.setCanceledOnTouchOutside(false);
this.setCancelable(true);
}
@Override
public void show() {
try {
if (this.isShowing()) {
this.dismiss();
} else {
super.show();
}
//setContentView()一定要在show之后调用
this.setContentView(mView);
} catch (WindowManager.BadTokenException exception) {
}
}
public void setMessage(String message) {
if (!TextUtils.isEmpty(message)) {
if (mLoadingTextView.getVisibility() != View.VISIBLE) {
mLoadingTextView.setVisibility(View.VISIBLE);
}
mLoadingTextView.setText(message);
}else {
if (mLoadingTextView.getVisibility() == View.VISIBLE) {
mLoadingTextView.setVisibility(View.GONE);
}
}
}
public void setMessage(@StringRes int message) {
if (mLoadingTextView.getVisibility() != View.VISIBLE) {
mLoadingTextView.setVisibility(View.VISIBLE);
}
mLoadingTextView.setText(message);
}
}
3.5 新建presenter目录,在该目录下新建LoginPresenter文件。
public class LoginPresenter extends BasePresenter<LoginView> {
LoginView loginView;
//通过构造函数传递LoginView
public LoginPresenter(LoginView loginView){this.loginView = loginView;}
public void login(Map<String,Object> map){
BaseObserver<BaseModel<LoginBean>> baseObserver = new BaseObserver<BaseModel<LoginBean>>(loginView) {
@Override
public void onSuccess(BaseModel<LoginBean> o) {
loginView.onLoginSuccess(o.getData());
}
@Override
public void onError() {
loginView.onLoginFailed();
}
};
addDisposable(apiService.userLogin(map),baseObserver);
}
}
3.6 最后我们来修改MainActivity文件和它的布局文件
3.6.1【MainActivity】
public class MainActivity extends BaseActivity<LoginPresenter> implements LoginView {
EditText account;
EditText password;
TextView confirmedLogin;
LoginPresenter presenter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
@Override
protected int getLayoutId() {
return R.layout.activity_main;
}
@Override
public void initData() {
}
@Override
public void initView() {
account = findViewById(R.id.account);
password = findViewById(R.id.password);
confirmedLogin = findViewById(R.id.confirmed_btn);
confirmedLogin.setOnClickListener(onLoginHandler);
}
/**
* 点击登录按钮事件
*/
private final View.OnClickListener onLoginHandler = new View.OnClickListener() {
@Override
public void onClick(View view) {
if(account.getText().toString().isEmpty()){
Toast.makeText(context, "请输入账号", Toast.LENGTH_SHORT).show();
}else if(password.getText().toString().isEmpty()){
Toast.makeText(context, "请输入密码", Toast.LENGTH_SHORT).show();
}else {
Map<String,Object> map = new HashMap<>();
map.put("username",account.getText().toString());
map.put("password",password.getText().toString());
presenter.login(map);
}
}
};
@Override
protected LoginPresenter createPresenter() {
presenter = new LoginPresenter(this);
return presenter;
}
@Override
public void onLoginSuccess(LoginBean data) {
Toast.makeText(context, "登录成功", Toast.LENGTH_SHORT).show();
}
@Override
public void onLoginFailed() {
}
}
3.6.2【activity_main.xml】
<?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">
<EditText
android:id="@+id/account"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="30dp"
android:layout_marginBottom="20dp"
android:hint="@string/please_input_phone"
android:inputType="text"
android:autofillHints=""
app:layout_constraintBottom_toTopOf="@id/password" />
<EditText
android:id="@+id/password"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="30dp"
android:layout_marginBottom="50dp"
android:hint="@string/please_input_pwd"
android:inputType="text"
app:layout_constraintBottom_toTopOf="@id/confirmed_btn"/>
<TextView
android:id="@+id/confirmed_btn"
android:layout_width="match_parent"
android:layout_height="50dp"
android:text="@string/confirmed_login"
android:textColor="@color/white"
android:background="@color/black"
android:layout_marginHorizontal="30dp"
android:gravity="center"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
到此为止,rxjava+retrofit+okhttp的网络架构就全部搭建好了。最后附上github地址https://github.com/shijia2118/demo