本篇文章是針對上一篇文章:帶你封裝本身的MVP+Retrofit+RxJava2框架(一)的進一步封裝改進,建議在看完上一篇文章後,再食用本文效果更佳!java
文本已經收錄到個人Github我的博客,歡迎大佬們光臨寒舍:個人GIthub博客react
如上一篇文章所說,在MVP模式日漸流行的時候,封裝一套MVP框架,不只對平常的開發大大便利,還能提早積累一下將來在實際工做中的技巧,而且,良好的封裝和規範使用還能減小開發中的各類使人頭疼的BUG。android
有人可能會問:「你上一篇不是也寫了MVP框架嗎?你這篇難道仍是同樣的嗎?難道你是換湯不換藥嗎?」git
其實,一開始筆者也覺得我上一篇文章封裝的MVP框架已經夠不錯了,可是,在筆者某天看了yechaoa大神玩安卓java的源碼後,被其封裝的MVP框架的所折服,所以第一時間寫這篇文章,想向你們分享下,筆者從中吸收的經驗,但願可以幫助到各位!github
本文相對上一篇文章的改進地方有下面幾點:json
本項目基於Android X 進行構建,完整代碼可在個人github上下載:帶你封裝本身的MVP+Retrofit+RxJava2框架(二)api
首先,能夠看一下筆者項目的基本結構服務器
爲了給你們模擬帶自動獲取Cookie的功能,因此筆者設計了一個具備登錄,註冊,收藏功能的Democookie
筆者在Demo中用到的框架以下
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
implementation 'com.google.android.material:material:1.1.0'
//cardView
implementation 'androidx.cardview:cardview:1.0.0'
/*retrofit、rxjava*/
implementation 'com.squareup.retrofit2:retrofit:2.6.2'
implementation 'com.squareup.retrofit2:adapter-rxjava2:2.4.0'
implementation 'com.squareup.retrofit2:converter-gson:2.4.0'
implementation 'io.reactivex.rxjava2:rxandroid:2.0.2'
implementation 'com.squareup.okhttp3:logging-interceptor:3.4.1'
/*glide*/
implementation 'com.github.bumptech.glide:glide:4.10.0'
annotationProcessor 'com.github.bumptech.glide:compiler:4.10.0'
/*butterknife*/
implementation 'com.jakewharton:butterknife:10.2.0'
annotationProcessor 'com.jakewharton:butterknife-compiler:10.2.0'
/*YUtils*/
implementation 'com.github.yechaoa:YUtils:2.1.0'
/*BRVAH*/
implementation 'com.github.CymChad:BaseRecyclerViewAdapterHelper:2.9.50'
/*banner*/
implementation 'com.youth.banner:banner:1.4.10'
複製代碼
下面筆者將爲你們詳細介紹每一個類的相關信息
BaseActivity相對於筆者上一個版本的MVP框架,改進的地方是:將兩個基類Activity合併爲一個BaseActivity,而且在其中封裝了進度條的顯示和隱藏的方法
/** * Description : BaseActivity * * @author XuCanyou666 * @date 2020/2/7 */
public abstract class BaseActivity<P extends BasePresenter> extends AppCompatActivity implements BaseView {
protected P presenter;
protected abstract P createPresenter();
protected abstract int getLayoutId();
protected abstract void initView();
protected abstract void initData();
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//設置豎屏
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
setContentView(LayoutInflater.from(this).inflate(getLayoutId(), null));
ButterKnife.bind(this);
presenter = createPresenter();
initView();
initData();
}
@Override
protected void onResume() {
super.onResume();
initListener();
}
@Override
protected void onDestroy() {
super.onDestroy();
//銷燬時,解除綁定
if (presenter != null) {
presenter.detachView();
}
}
protected void initListener() {
}
@Override
public void showLoading() {
YUtils.showLoading(this, "加載中");
}
@Override
public void hideLoading() {
YUtils.dismissLoading();
}
/** * 能夠處理異常 */
@Override
public void onErrorCode(BaseBean bean) {
}
/** * 啓動activity * * @param activity 當前活動 * @param isFinish 是否結束當前活動 */
public void startActivity(Class<?> activity, boolean isFinish) {
Intent intent = new Intent(this, activity);
startActivity(intent);
if (isFinish) {
finish();
}
}
}
複製代碼
/** * Description : BaseFragment * * @author XuCanyou666 * @date 2020/2/7 */
public abstract class BaseFragment<P extends BasePresenter> extends Fragment implements BaseView {
private Unbinder unbinder;
protected Context mContext;
protected P presenter;
protected abstract P createPresenter();
protected abstract int getLayoutId();
protected abstract void initView();
protected abstract void initData();
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(getLayoutId(), container, false);
unbinder = ButterKnife.bind(this, view);
//獲得context,在後面的子類Fragment中均可以直接調用
mContext = ActivityUtil.getCurrentActivity();
presenter = createPresenter();
initView();
initData();
return view;
}
@Override
public void onResume() {
super.onResume();
initListener();
}
@Override
public void onDestroyView() {
super.onDestroyView();
//do something
unbinder.unbind();
//銷燬時,解除綁定
if (presenter != null) {
presenter.detachView();
}
}
private void initListener() {
}
@Override
public void onErrorCode(BaseBean bean) {
}
/** * 顯示加載中 */
@Override
public void showLoading() {
YUtils.showLoading(ActivityUtil.getCurrentActivity(), "加載中");
}
/** * 隱藏加載中 */
@Override
public void hideLoading() {
YUtils.dismissLoading();
}
}
複製代碼
BasePresenter相對於筆者上一個版本的MVP框架,改進的地方是:將線程的調度寫入了addDisposable中,並改寫了addDisposable方法,使得調用方式更加簡單優美
/** * Description : BasePresenter * * @author XuCanyou666 * @date 2020/2/7 */
public class BasePresenter<V extends BaseView> {
private CompositeDisposable compositeDisposable;
public V baseView;
/** * 這個後面能夠直接用 Example:apiServer.login(username, password); */
protected API.WAZApi apiServer = RetrofitService.getInstance().getApiService();
public BasePresenter(V baseView) {
this.baseView = baseView;
}
/** * 解除綁定 */
public void detachView() {
baseView = null;
removeDisposable();
}
/** * 返回 view */
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));
}
private void removeDisposable() {
if (compositeDisposable != null) {
compositeDisposable.dispose();
}
}
}
複製代碼
Observer的基類,提供了自動顯示和自動隱藏進度條的方法,對內處理了onStart,onError,onComplete方法,對外只提供了onSuccess和onError方法,符合使用習慣
/** * Description : BaseObserver * * @author XuCanyou666 * @date 2020/2/7 */
public abstract class BaseObserver<T> extends DisposableObserver<T> {
protected BaseView view;
private boolean isShowDialog;
public BaseObserver(BaseView view) {
this.view = view;
}
/** * 帶進度條的初始化方法 * * @param view view * @param isShowDialog 是否顯示進度條 */
public BaseObserver(BaseView view, boolean isShowDialog) {
this.view = view;
this.isShowDialog = isShowDialog;
}
@Override
protected void onStart() {
if (view != null && isShowDialog) {
view.showLoading();
}
}
@Override
public void onNext(T o) {
onSuccess(o);
}
@Override
public void onError(Throwable e) {
if (view != null && isShowDialog) {
view.hideLoading();
}
BaseException be;
if (e != null) {
//自定義異常
if (e instanceof BaseException) {
be = (BaseException) e;
//回調到view層 處理 或者根據項目狀況處理
if (view != null) {
// 處理登陸失效 更新
view.onErrorCode(new BaseBean(be.getErrorCode(), be.getErrorMsg()));
} else {
onError(be.getErrorMsg());
}
//系統異常
} else {
if (e instanceof HttpException) {
//HTTP錯誤
be = new BaseException(BaseException.BAD_NETWORK_MSG, e);
} else if (e instanceof ConnectException || e instanceof UnknownHostException) {
//鏈接錯誤
be = new BaseException(BaseException.CONNECT_ERROR_MSG, e);
} else if (e instanceof InterruptedIOException) {
//鏈接超時
be = new BaseException(BaseException.CONNECT_TIMEOUT_MSG, e);
} else if (e instanceof JsonParseException || e instanceof JSONException || e instanceof ParseException) {
//解析錯誤
be = new BaseException(BaseException.PARSE_ERROR_MSG, e);
} else {
be = new BaseException(BaseException.OTHER_MSG, e);
}
}
} else {
be = new BaseException(BaseException.OTHER_MSG);
}
onError(be.getErrorMsg());
}
@Override
public void onComplete() {
if (view != null && isShowDialog) {
view.hideLoading();
}
}
public abstract void onSuccess(T o);
public abstract void onError(String msg);
}
複製代碼
異常的基類
/** * Description : BaseException * * @author XuCanyou666 * @date 2020/2/7 */
public class BaseException extends IOException {
/** * 解析數據失敗 */
public static final String PARSE_ERROR_MSG = "解析數據失敗";
/** * 網絡問題 */
public static final String BAD_NETWORK_MSG = "網絡問題";
/** * 鏈接錯誤 */
public static final String CONNECT_ERROR_MSG = "鏈接錯誤";
/** * 鏈接超時 */
public static final String CONNECT_TIMEOUT_MSG = "鏈接超時";
/** * 未知錯誤 */
public static final String OTHER_MSG = "未知錯誤";
private String errorMsg;
private int errorCode;
public String getErrorMsg() {
return errorMsg;
}
public int getErrorCode() {
return errorCode;
}
public BaseException(String message) {
this.errorMsg = message;
}
public BaseException(String errorMsg, Throwable cause) {
super(errorMsg, cause);
this.errorMsg = errorMsg;
}
public BaseException(int errorCode, String message) {
this.errorMsg = message;
this.errorCode = errorCode;
}
}
複製代碼
實體類的基類,方便處理返回的json數據,具體的寫法根據每一個API而定
/** * Description : BaseBean 實體類的基類 * * @author XuCanyou666 * @date 2020/2/7 */
public class BaseBean<T> implements Serializable {
/** * data : * errorCode : 0 * errorMsg : */
public T data;
public int errorCode;
public String errorMsg;
public BaseBean(int code, String data) {
this.errorCode = code;
this.data = (T) data;
}
}
複製代碼
/** * Description : BaseView * * @author XuCanyou666 * @date 2020/2/7 */
public interface BaseView {
void showLoading();
void hideLoading();
void onErrorCode(BaseBean bean);
}
複製代碼
持久化cookie,由於代碼太多,這裏只展現一個類的代碼,詳細代碼請前往個人Github查看
package com.users.xucanyou666.rxjava2_retrofit_mvp2.http.cookie;
import android.content.Context;
import java.util.List;
import okhttp3.Cookie;
import okhttp3.CookieJar;
import okhttp3.HttpUrl;
/** * Created by yechao on 2019/11/19/019. * Describe : */
public class CookiesManager implements CookieJar {
private final PersistentCookieStore cookieStore;
public CookiesManager(Context context) {
cookieStore = new PersistentCookieStore(context);
}
@Override
public void saveFromResponse(HttpUrl url, List<Cookie> cookies) {
if (cookies.size() > 0) {
for (Cookie item : cookies) {
cookieStore.add(url, item);
}
}
}
@Override
public List<Cookie> loadForRequest(HttpUrl url) {
return cookieStore.get(url);
}
}
複製代碼
重寫ResponseBodyConverter對json預處理,這裏只展現一個類的代碼,詳細代碼請前往個人Github查看
/** * Created by yechao on 2019/11/18/018. * Describe : 重寫ResponseBodyConverter對json預處理 */
public class BaseResponseBodyConverter<T> implements Converter<ResponseBody, T> {
private final Gson gson;
private final TypeAdapter<T> adapter;
BaseResponseBodyConverter(Gson gson, TypeAdapter<T> adapter) {
this.gson = gson;
this.adapter = adapter;
}
@Override
public T convert(ResponseBody value) throws IOException {
String jsonString = value.string();
try {
JSONObject object = new JSONObject(jsonString);
int code = object.getInt("errorCode");
if (0 != code) {
String data;
//錯誤信息
if (code == -1001) {//失效
data = "登陸失效,請從新登陸";
} else {
data = object.getString("errorMsg");
}
//異常處理
throw new BaseException(code, data);
}
//正確返回整個json
return adapter.fromJson(jsonString);
} catch (JSONException e) {
e.printStackTrace();
//數據解析異常即json格式有變更
throw new BaseException(BaseException.PARSE_ERROR_MSG);
} finally {
value.close();
}
}
}
複製代碼
隨着項目日漸龐大,請求也愈來愈多,不可能每一個請求都使用一個接口,這樣不只形成浪費,更是不方便管理,所以,新建一個API做爲Retrofit的管理類,用一個接口管理全部網絡請求,能夠有效改善代碼質量
/** * Description : API * 接口的管理類 * * @author XuCanyou666 * @date 2020/2/7 */
public class API {
static final String BASE_URL = "https://www.wanandroid.com/";
public interface WAZApi {
//-----------------------【首頁相關】----------------------
//首頁文章列表 這裏的{}是填入頁數
@GET("article/list/{page}/json")
Observable<BaseBean<Article>> getArticleList(@Path("page") Integer page);
//-----------------------【登陸註冊】----------------------
//登陸
@FormUrlEncoded
@POST("user/login")
Observable<BaseBean<User>> login(@Field("username") String username, @Field("password") String password);
//註冊
@FormUrlEncoded
@POST("user/register")
Observable<BaseBean<User>> register(@Field("username") String username, @Field("password") String password, @Field("repassword") String repassword);
//-----------------------【 收藏 】----------------------
//收藏站內文章
@POST("lg/collect/{id}/json")
Observable<BaseBean> collectIn(@Path("id") Integer id);
//取消收藏---文章列表
@POST("lg/uncollect_originId/{id}/json")
Observable<BaseBean> uncollect(@Path("id") Integer id);
}
}
複製代碼
Retrofit的配置類,在裏面初始化了apiServer對象,並配置了日誌信息,超時時間,Cookie持久化,用了靜態內部類的單例模式
* Description : RetrofitService
*
* @author XuCanyou666
* @date 2020/2/8
*/
public class RetrofitService {
private static RetrofitService apiRetrofit;
private API.WAZApi apiServer;
//單例調用
public static RetrofitService getInstance() {
if (apiRetrofit == null) {
synchronized (Object.class) {
if (apiRetrofit == null) {
apiRetrofit = new RetrofitService();
}
}
}
return apiRetrofit;
}
//獲取api對象
public API.WAZApi getApiService() {
return apiServer;
}
//初始化retrofit
private RetrofitService() {
//配置okhttp並設置時間、日誌信息和cookies
HttpLoggingInterceptor httpLoggingInterceptor = new HttpLoggingInterceptor();
httpLoggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
OkHttpClient okHttpClient = new OkHttpClient.Builder()
.addInterceptor(httpLoggingInterceptor)
//設置超時時間
.connectTimeout(15, TimeUnit.SECONDS)
//設置Cookie持久化
.cookieJar(new CookiesManager(YUtils.getApplication()))
.build();
//關聯okhttp並加上rxjava和gson的配置和baseurl
Retrofit retrofit = new Retrofit.Builder()
.client(okHttpClient)
.addConverterFactory(BaseConverterFactory.create())
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.baseUrl(API.BASE_URL)
.build();
apiServer = retrofit.create(API.WAZApi.class);
}
}
複製代碼
這裏的帶有嵌套的實體類看似很複雜,其實能夠經過一個在線JSON字符串轉Java實體類的工具進行生成,須要注意的是,不要將BaseBean的那一層導入到實體類中
文章內容的實體類
/** * Description : Article * * @author XuCanyou666 * @date 2020/2/8 */
public class Article {
/** * curPage : 2 * datas : [{"apkLink":"","author":"葉應是葉","chapterId":67,"chapterName":"網絡基......."}] * offset : 20 * over : false * pageCount : 62 * size : 20 * total : 1224 */
public int curPage;
public int offset;
public boolean over;
public int pageCount;
public int size;
public int total;
public List<DataDetailBean> datas;
public static class DataDetailBean {
/** * apkLink : * author : 葉應是葉 * chapterId : 67 * chapterName : 網絡基礎 * collect : false * courseId : 13 * desc : * envelopePic : * fresh : false * id : 2809 * link : https://www.jianshu.com/p/6d2f324c8f42 * niceDate : 2018-04-12 * origin : * projectLink : * publishTime : 1523532264000 * superChapterId : 98 * superChapterName : 網絡訪問 * tags : [] * title : 在 Android 設備上搭建 Web 服務器 * type : 0 * visible : 1 * zan : 0 */
public String apkLink;
public String author;
public int chapterId;
public String chapterName;
public boolean collect;
public int courseId;
public String desc;
public String envelopePic;
public boolean fresh;
public int id;
public int originId;
public String link;
public String niceDate;
public String origin;
public String projectLink;
public long publishTime;
public int superChapterId;
public String superChapterName;
public String title;
public int type;
public int visible;
public int zan;
public List<?> tags;
}
}
複製代碼
/** * GitHub : https://github.com/yechaoa * CSDN : http://blog.csdn.net/yechaoa * <p> * Created by yechao on 2018/5/2. * Describe : */
public class User {
/** * collectIds : [] * email : * icon : * id : 3 * password : 111111 * type : 0 * username : 111111 */
public String email;
public String icon;
public int id;
public String password;
public int type;
public String username;
public List<?> collectIds;
public String repassword;
}
複製代碼
這裏分爲每一個模塊進行管理,本Demo有Login,Register,Home總共3個模塊,在這裏僅說明一個模塊,其餘的模塊的寫法也是相似的,具體其餘模塊的寫法,能夠上Github查看
LoginView層的接口
/** * Description : ILoginView * * @author XuCanyou666 * @date 2020/2/8 */
public interface ILoginView extends BaseView {
/** * 顯示登錄成功 * * @param successMessage 成功信息 */
void showLoginSuccess(String successMessage);
/** * 顯示登錄失敗 * * @param errorMessage 失敗信息 */
void showLoginFailed(String errorMessage);
void doSuccess(BaseBean<User> user);
}
複製代碼
這裏由於RxJava通過封裝後,model層的代碼太少,因此將Model直接寫入Presenter中
/** * Description : LoginPresenter * * @author XuCanyou666 * @date 2020/2/8 */
class LoginPresenter extends BasePresenter<ILoginView> {
LoginPresenter(ILoginView baseView) {
super(baseView);
}
void login(String username, String password) {
addDisposable(apiServer.login(username, password), new BaseObserver<BaseBean<User>>(baseView, true) {
@Override
public void onSuccess(BaseBean<User> bean) {
baseView.showLoginSuccess("登陸成功( ̄▽ ̄)");
baseView.doSuccess(bean);
}
@Override
public void onError(String msg) {
baseView.showLoginFailed(msg + "(°∀°)ノ");
}
});
}
}
複製代碼
登錄界面輸入框的監聽器
/** * TextInputLayout監聽器 * created by xucanyou666 * on 2020/2/7 18:09 * email:913710642@qq.com */
public class LoginTextWatcher implements android.text.TextWatcher {
private TextInputLayout mTilUsername;
private TextInputLayout mTilPassword;
LoginTextWatcher(TextInputLayout username, TextInputLayout password) {
mTilUsername = username;
mTilPassword = password;
}
@Override
public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
}
@Override
public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
}
@Override
public void afterTextChanged(Editable s) {
checkInput(mTilUsername);
checkInput(mTilPassword);
}
/** * 判斷輸入內容是否合法 * * @param textInputLayout textInputLayout */
public static void checkInput(TextInputLayout textInputLayout) {
if (textInputLayout != null) {
if (textInputLayout.getEditText().getText().length() > textInputLayout.getCounterMaxLength()) {
textInputLayout.setError("輸入內容超過上限");
} else if (textInputLayout.getEditText().getText().length() < textInputLayout.getCounterMaxLength() / 2) {
textInputLayout.setError("最少6位");
} else {
textInputLayout.setError(null);
}
}
}
}
複製代碼
/** * Description : LoginActivity * * @author XuCanyou666 * @date 2020/2/8 */
public class LoginActivity extends BaseActivity<LoginPresenter> implements ILoginView {
@BindView(R.id.et_username)
EditText mEtUsername;
@BindView(R.id.til_username)
TextInputLayout mTilUsername;
@BindView(R.id.et_password)
EditText mEtPassword;
@BindView(R.id.til_password)
TextInputLayout mTilPassword;
@BindView(R.id.btn_login)
Button mBtnLogin;
@BindView(R.id.btn_register)
Button mBtnRegister;
private String mUsername;
private String mPassword;
@Override
protected LoginPresenter createPresenter() {
return new LoginPresenter(this);
}
@Override
protected int getLayoutId() {
return R.layout.activity_login;
}
@Override
protected void initData() {
}
@Override
protected void initView() {
LoginTextWatcher textWatcher = new LoginTextWatcher(mTilUsername, mTilPassword);
mEtUsername.addTextChangedListener(textWatcher);
mEtPassword.addTextChangedListener(textWatcher);
}
@Override
public void showLoginSuccess(String successMessage) {
ToastUtil.showToast(successMessage);
}
@Override
public void showLoginFailed(String errorMessage) {
ToastUtil.showToast(errorMessage);
}
@Override
public void doSuccess(BaseBean<User> user) {
//存進sp裏面
SpUtil.setBoolean(GlobalConstant.IS_LOGIN, true);
SpUtil.setString(GlobalConstant.USERNAME, user.data.username);
SpUtil.setString(GlobalConstant.PASSWORD, user.data.password);
startActivity(MainActivity.class, true);
}
/** * 判斷帳號和密碼輸入是否正確 * * @return */
private boolean isValid() {
mUsername = mEtUsername.getText().toString().trim();
mPassword = mEtPassword.getText().toString().trim();
return check(mUsername, mTilUsername) && check(mPassword, mTilPassword);
}
/** * 判斷輸入是否正確 * * @param string 輸入的內容 * @param textInputLayout textInputLayout控件 * @return */
private boolean check(String string, TextInputLayout textInputLayout) {
return !TextUtils.isEmpty(string) && string.length() <= textInputLayout.getCounterMaxLength() && textInputLayout.getCounterMaxLength() / 2 <= string.length();
}
@OnClick({R.id.btn_login, R.id.btn_register})
public void onViewClicked(View view) {
switch (view.getId()) {
case R.id.btn_login:
YUtils.closeSoftKeyboard();
if (isValid()) {
presenter.login(mUsername, mPassword);
} else {
ToastUtil.showToast("填寫錯誤 (°∀°)ノ");
}
break;
case R.id.btn_register:
YUtils.closeSoftKeyboard();
startActivity(RegisterActivity.class, false);
break;
default:
break;
}
}
}
複製代碼
有一天, 當我點開個人colors.xml資源文件的時候,發現是下圖這個樣子
而後當鼠標的光標移動到紅色標記處,發現
The color 「colorPrimary」 in values has no declaration in the base values folder; this can lead to crashes when the resource is queried in a configuration that does not match this qualifier less…
接着我翻譯了一下:
值中的顏色「colorPrimary」在基本值folde中沒有聲明
懵逼了,我不是聲明瞭嗎....最後仍是百度到告終果
解決方式是:先把colors文件剪切下來,再粘回去。
感受是AS的BUG....我用的AS版本是3.5.1
固然百度了一下,解決方式是:在根目錄的build.gradle中添加maven
就是下面這樣
發生問題的場景,就是我在presenter中設置了請求文章列表的數據的時候,會自動顯示和隱藏進度條,可是請求完文章列表後,不能自動隱藏
通過瀏覽代碼,發現,個人請求文章列表的方法寫多了一次,解決方法:只保存onResume裏面的一次
若是文章對您有一點幫助的話,但願您能點一下贊,您的點贊,是我前進的動力
本文參考連接: