• 带你封装自己的MVP+Retrofit+RxJava2框架(一)


    前言

    文本已经收录到我的Github个人博客,欢迎大佬们光临寒舍:我的GIthub博客

    看完本篇文章的,可以看下带你封装自己的MVP+Retrofit+RxJava2框架(二),里面封装得到了改进

    本篇文章需要已经具备的知识:

    • MVP的概念和基本使用
    • Retrofit框架的基本使用
    • RxJava2框架的基本使用
    • ButterKnife框架的基本使用
    • Base基类的概念

    学习清单:

    • ActivityFragment基类的封装
    • MVP的封装使用

    一.为什么要封装这套框架呢?

    在搞清楚这个问题之前,我们回顾一下基本概念

    RxJava: ReactiveXJVM上的一个实现,ReactiveX使用Observable序列组合异步和基于事件的程序;掌握了它,你可以优美地处理异步任务和事件的回调

    Retrofit:一个 RESTfulHTTP网络请求框架的封装,网络请求的工作本质上是OkHttp 完成,而 Retrofit仅负责 网络请求接口的封装:掌握了它,你能优美地进行网络请求。

    MVP:一种解耦模型和视图的模式,是现在很多公司的主流模式。

    由此可见,在平时的开发中熟练运用这种模式,不仅可以满足生活中大部分应用程序的场景,还可以为将来的工作积攒宝贵的实战经验。

    二.核心用法

    本项目基于Android X 进行构建,完整代码可在我的Github上下载:带你封装自己的MVP+Retrofit+RxJava2框架

    首先,看一下我们项目的基本结构,下面笔者将为大家详细介绍每个类的相关信息

    项目基本结构

    2.1 基类Base

    Base基类是封装了一些基类,方便后面新建新的Activity或者Fragment,减少耦合

    2.1.1 BaseActivity

    这个类是Activity的基类,注意与下面的BaseMvpActivity区分开

    /**
     * Description : BaseActivity 基类活动
     *
     * @author XuCanyou666
     * @date 2020/2/2
     */
    
    
    public abstract class BaseActivity extends AppCompatActivity {
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(getLayoutId());
            initPresenter();
            initViews();
            ButterKnife.bind(this);
        }
    
    
        /**
         * 抽象方法:实例化Presenter
         */
        protected abstract void initPresenter();
    
        /**
         * 抽象方法:初始化控件,一般在BaseActivity中通过ButterKnife来绑定,所以该方法内部一般我们初始化界面相关的操作
         *
         * @return 控件
         */
        protected abstract void initViews();
    
    
        /**
         * 抽象方法:得到布局id
         *
         * @return 布局id
         */
        protected abstract int getLayoutId();
    
    
        /**
         * 启动Fragment
         *
         * @param id       id
         * @param fragment 碎片
         */
        protected void startFragment(int id, Fragment fragment) {
            FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
            fragmentTransaction.add(id, fragment);
            fragmentTransaction.commit();
        }
    
    }
    
    

    2.1.2 BaseView

    一个接口,说明了每一个View基本需要的一些操作

    package com.users.xucanyou666.rxjava2_retrofit_mvp.base;
    
    
    /**
     * created by xucanyou666
     * on 2020/1/31 18:26
     * email:913710642@qq.com
     */
    public interface BaseView {
    
        /**
         * 显示进度框
         */
        void showProgressDialog();
    
    
        /**
         * 关闭进度框
         */
        void hideProgressDialog();
    
    
        /**
         * 出错信息的回调
         *
         * @param result 错误信息
         */
        void onError(String result);
    
    
    
    }
    
    

    2.1.3 BaseMvpActivity

    • MVP活动的基类

    • 继承自BaseActivity,它是MVP活动的基类,封装好了Presenter的相关操作

    package com.users.xucanyou666.rxjava2_retrofit_mvp.base;
    
    
    /**
     * created by xucanyou666  MVP活动的基类,封装好了presenter的相关操作
     * on 2019/12/24 20:53
     * email:913710642@qq.com
     */
    
    public abstract class BaseMvpActivity<V extends BaseView, P extends BasePresenter> extends BaseActivity {
    
        private P presenter;
    
        /**
         * 初始化presenter
         */
        @Override
        protected void initPresenter() {
            presenter = createPresenter();
            if (presenter != null) {
                presenter.attachView((V) this);
            }
        }
    
        /**
         * 创建presenter
         *
         * @return Presenter
         */
        protected abstract P createPresenter();
    
    
        /**
         * 得到presenter
         *
         * @return presenter
         */
        protected P getPresenter() {
            return presenter;
        }
    
        /**
         * 销毁
         */
        @Override
        protected void onDestroy() {
            super.onDestroy();
            if (presenter != null) {
                presenter.detachView();
            }
        }
    
    
    }
    
    

    2.1.4 BaseFragment

    • Fragment的基类

    • 需要注意的是,这里用了ButterKnife框架,对碎片进行了绑定和解绑操作

    /**
     * Fragment的基类,封装了一些Fragment的相关操作
     * created by xucanyou666
     * on 2020/1/31 16:21
     * email:913710642@qq.com
     */
    public abstract class BaseFragment<T extends BasePresenter> extends Fragment implements BaseView {
        protected T mPresenter;
        protected Context mContext;
        protected Bundle mBundle;
        protected Unbinder unbinder;
        protected View view;
    
        /**
         * 恢复数据
         *
         * @param outState bundle
         */
        @Override
        public void onSaveInstanceState(@NonNull Bundle outState) {
            super.onSaveInstanceState(outState);
            if (mBundle != null) {
                outState.putBundle("bundle", mBundle);
            }
        }
    
        /**
         * 绑定activity
         *
         * @param context context
         */
        @Override
        public void onAttach(@NonNull Context context) {
            super.onAttach(context);
            mContext = context;
        }
    
        /**
         * 运行在onAttach之后,可以接收别人传递过来的参数,实例化对象
         * 可以解决返回的时候页面空白的bug
         *
         * @param savedInstanceState
         */
        @Override
        public void onCreate(@Nullable Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            if (savedInstanceState != null) {
                mBundle = savedInstanceState.getBundle("bundle");
            } else {
                mBundle = getArguments() == null ? new Bundle() : getArguments();
            }
            //初始化presenter
            mPresenter = initPresenter();
        }
    
        protected T getPresenter() {
            return mPresenter;
        }
    
    
        /**
         * 运行在onCreate之后,生成View视图
         *
         * @param inflater
         * @param container
         * @param savedInstanceState
         * @return
         */
        @Nullable
        @Override
        public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
            view = initView(inflater, container, savedInstanceState);
            unbinder = ButterKnife.bind(this, view);
            return view;
        }
    
        /**
         * 运行在onCreateView之后
         * 加载数据
         *
         * @param savedInstanceState
         */
        @Override
        public void onActivityCreated(@Nullable Bundle savedInstanceState) {
            super.onActivityCreated(savedInstanceState);
            mPresenter.attachView(this);
    
        }
    
    
        /**
         * 跳转Fragment
         *
         * @param toFragment 跳转去的fragment
         */
        public void startFragment(Fragment toFragment) {
            Log.d(TAG, "haha");
            startFragment(toFragment, null);
        }
    
        /**
         * 跳转Fragment
         *
         * @param toFragment 跳转到的fragment
         * @param tag        fragment的标签
         */
        public void startFragment(Fragment toFragment, String tag) {
            FragmentTransaction fragmentTransaction = getFragmentManager().beginTransaction();
            fragmentTransaction.hide(this).add(android.R.id.content, toFragment, tag);
            fragmentTransaction.addToBackStack(tag);
            fragmentTransaction.commitAllowingStateLoss();
        }
    
        @Override
        public void onDestroyView() {
            super.onDestroyView();
            unbinder.unbind();
        }
    
        /**
         * fragment进行回退
         * 类似于activity的OnBackPress
         */
        public void onBack() {
            getFragmentManager().popBackStack();
        }
    
        @Override
        public void onDetach() {
            mPresenter.detachView();
            super.onDetach();
        }
    
        /**
         * 初始化Fragment应有的视图
         *
         * @return view
         */
        public abstract View initView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState);
    
    
        /**
         * 创建presenter
         *
         * @return <T extends BasePresenter> 必须是BasePresenter的子类
         */
        public abstract T initPresenter();
    
        /**
         * 得到context
         *
         * @return context
         */
        @Override
        public Context getContext() {
            return mContext;
        }
    
        /**
         * 得到bundle
         *
         * @return bundle
         */
        public Bundle getBundle() {
            return mBundle;
        }
    
        /**
         * 得到fragment
         *
         * @return fragment
         */
        public Fragment getFragment() {
            return this;
        }
    
    }
    

    2.1.5 BasePresenter

    
    /**
     * created by xucanyou666
     * on 2020/1/16 17:12
     * email:913710642@qq.com
     */
    public abstract class BasePresenter<V extends BaseView> {
        //将所有正在处理的Subscription都添加到CompositeSubscription中。统一退出的时候注销观察
        private CompositeDisposable mCompositeDisposable;
        private V baseView;
    
        /**
         * 和View绑定
         *
         * @param baseView
         */
        public void attachView(V baseView) {
            this.baseView = baseView;
        }
    
        /**
         * 解绑View,该方法在BaseMvpActivity类中被调用
         */
        public void detachView() {
            baseView = null;
            // 在界面退出等需要解绑观察者的情况下调用此方法统一解绑,防止Rx造成的内存泄漏
            if (mCompositeDisposable != null) {
                mCompositeDisposable.dispose();
            }
        }
    
        /**
         * 获取View
         *
         * @return view
         */
        public V getMvpView() {
            return baseView;
        }
    
    
    
        /**
         * 将Disposable添加,在每次网络访问之前初始化时进行添加操作
         *
         * @param subscription subscription
         */
        public void addDisposable(Disposable subscription) {
            //csb 如果解绑了的话添加 sb 需要新的实例否则绑定时无效的
            if (mCompositeDisposable == null || mCompositeDisposable.isDisposed()) {
                mCompositeDisposable = new CompositeDisposable();
            }
            mCompositeDisposable.add(subscription);
        }
    
    }
    
    

    2.1.6 MyApplication

    • 封装了一个可以全局获取Context的方法,参考写法自:《第一行代码--第二版》
    • 注意:记得在AndroidManifest中注册Application
    package com.users.xucanyou666.rxjava2_retrofit_mvp.base;
    
    
    import android.app.Application;
    import android.content.Context;
    
    /**
     * 基类
     * created by xucanyou666
     * on 2019/11/2 14:46
     * email:913710642@qq.com
     * @author xucanyou666
     */
    public class MyApplication extends Application {
        private static Context context;
    
        @Override
        public void onCreate() {
            super.onCreate();
            context = getApplicationContext();
        }
    
        public static Context getContext() {
            return context;
        }
    }
    
    

    2.2 工具类 Util

    2.2.1 RetrofitManager

    Retrofit单例工具类

    /**
     * Retrofit单例工具类
     * created by xucanyou666
     * on 2020/1/16 16:38
     * email:913710642@qq.com
     */
    public class RetrofitManager {
        private Retrofit mRetrofit;
    
    
        //构造器私有,这个工具类只有一个实例
        private RetrofitManager() {
            OkHttpClient.Builder httpClientBuilder = new OkHttpClient.Builder();
            httpClientBuilder.connectTimeout(15, TimeUnit.SECONDS);
            mRetrofit = new Retrofit.Builder()
                    .client(httpClientBuilder.build())
                    .addConverterFactory(GsonConverterFactory.create())
                    .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
                    .baseUrl(BASE_URL)
                    .build();
        }
    
    
        /**
         * 静态内部类单例模式
         *
         * @return
         */
        public static RetrofitManager getInstance() {
            return Inner.retrofitManager;
        }
    
        private static class Inner {
            private static final RetrofitManager retrofitManager = new RetrofitManager();
        }
    
    
        /**
         * 利用泛型传入接口class返回接口实例
         *
         * @param ser 类
         * @param <T> 类的类型
         * @return Observable
         */
        public <T> T createRs(Class<T> ser) {
            return mRetrofit.create(ser);
        }
    }
    

    2.2.2 RxJavaUtil

    RxJava的工具类,执行线程调度工作

    /**
     * created by xucanyou666
     * on 2019/11/17 19:20
     * email:913710642@qq.com
     *
     * @author xucanyou666
     */
    public class RxJavaUtil {
        /**
         * 线程调度工作
         *
         * @param observable 被观察者
         * @param <T>        类型
         */
        public static <T> Observable  toSubscribe(Observable<T> observable) {
            return observable.subscribeOn(Schedulers.io())
                    .unsubscribeOn(Schedulers.io())
                    .observeOn(AndroidSchedulers.mainThread());
        }
    
    }
    
    

    2.3 常量类 Contant

    常量池,特别感谢api open网提供的免费API

    /**
     * created by xucanyou666
     * on 2019/11/17 19:01
     * email:913710642@qq.com
     */
    public class StaticQuality {
        public static final String BASE_URL="https://api.gushi.ci/";
    }
    

    2.4 接口管理器 Contract

    这里集中了一些Model层,Presenter层,View层的与诗歌相关的接口

    /**
     * 诗歌的接口管理器
     * created by xucanyou666
     * on 2020/2/2 15:33
     * email:913710642@qq.com
     */
    public interface IPoetryContract {
        interface IPoetryModel {
            /**
             * 得到诗歌
             *
             * @return 诗歌
             */
            Observable<PoetryEntity> getPoetry();
        }
    
        interface IPoetryPresenter {
            void getPoetry();
        }
    
        interface IPoetryView extends BaseView {
            /**
             * @param author 作者
             */
            void searchSuccess(String author);
        }
    }
    
    

    2.5 实体类 Entity

    
    
    /**
     * 诗歌的实体类
     * created by xucanyou666
     * on 2020/1/23 21:23
     * email:913710642@qq.com
     * API返回示例:
     * {
     * "content": "胡瓶落膊紫薄汗,碎叶城西秋月团。",
     * "origin": "从军行七首",
     * "author": "王昌龄",
     * "category": "古诗文-天气-月亮"
     * }
     */
    public class PoetryEntity {
        private String content; //诗歌内容
        private String origin; //来源
        private String author; //作者
        private String category; //分类
    
        public String getContent() {
            return content;
        }
    
        public void setContent(String content) {
            this.content = content;
        }
    
        public String getOrigin() {
            return origin;
        }
    
        public void setOrigin(String origin) {
            this.origin = origin;
        }
    
        public String getAuthor() {
            return author;
        }
    
        public void setAuthor(String author) {
            this.author = author;
        }
    
        public String getCategory() {
            return category;
        }
    
        public void setCategory(String category) {
            this.category = category;
        }
    }
    
    

    2.6 Retrofit接口 iApiService

    
    
    /**
     * retrofit接口
     * created by xucanyou666
     * on 2020/1/23 21:25
     * email:913710642@qq.com
     */
    public interface GetPoetryEntity {
        /**
         * 获取古诗词
         *
         * @return 古诗词
         */
        @GET("all.json")
        Observable<PoetryEntity> getPoetry();
    }
    

    2.7 视图层 View

    这里为了减少代码量,方便读者们掌握核心操作,故View层都是用的同一个PresenterModel,仅作学习参考

    2.7.1 MainActivity

    需要注意的是,这里BaseMvpActivity<activity, presenter>Activity填入的是当前的ActivityPresenter填入的是对应的Presenter

    
    /**
     * Description : MainActivity
     *
     * @author XuCanyou666
     * @date 2020/2/3
     */
    
    public class MainActivity extends BaseMvpActivity<MainActivity, PoetryPresenter> implements IPoetryContract.IPoetryView {
    
        @BindView(R.id.btn_get_poetry)
        Button btnGetPoetry;
        @BindView(R.id.tv_poetry_author)
        TextView tvPoetryAuthor;
        @BindView(R.id.btn_goto_fragment)
        Button btnGotoFragment;
        @BindView(R.id.ll)
        LinearLayout ll;
    
        @Override
        protected void initViews() {
    
        }
    
        @Override
        protected int getLayoutId() {
            return R.layout.activity_main;
        }
    
        @Override
        protected PoetryPresenter createPresenter() {
            return PoetryPresenter.getInstance();
        }
    
        @Override
        public void searchSuccess(String author) {
            tvPoetryAuthor.setText(author);
        }
    
        @Override
        public void showProgressDialog() {
    
        }
    
        @Override
        public void hideProgressDialog() {
    
        }
    
        @Override
        public void onError(String result) {
            Toast.makeText(MyApplication.getContext(), result, Toast.LENGTH_SHORT).show();
        }
    
    
        @OnClick({R.id.btn_get_poetry, R.id.btn_goto_fragment})
        public void onViewClicked(View view) {
            switch (view.getId()) {
                case R.id.btn_get_poetry:
                    getPresenter().getPoetry();
                    break;
                case R.id.btn_goto_fragment:
                    startFragment(R.id.ll, new MainFragment());
                    break;
                default:
                    break;
            }
        }
    }
    

    2.7.2 MainFragment

    
    /**
     * Description : MainFragment
     *
     * @author XuCanyou666
     * @date 2020/2/2
     */
    
    
    public class MainFragment extends BaseFragment<PoetryPresenter> implements IPoetryContract.IPoetryView {
    
        @BindView(R.id.btn_get_poetry)
        Button btnGetPoetry;
        @BindView(R.id.tv_poetry_author)
        TextView tvPoetryAuthor;
    
        @Override
        public View initView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
            return inflater.inflate(R.layout.fragment_main, container, false);
        }
    
        @Override
        public PoetryPresenter initPresenter() {
            return PoetryPresenter.getInstance();
        }
    
    
        @Override
        public void showProgressDialog() {
    
        }
    
        @Override
        public void hideProgressDialog() {
    
        }
    
        @Override
        public void onError(String result) {
            Toast.makeText(MyApplication.getContext(), result, Toast.LENGTH_SHORT).show();
    
        }
    
        @OnClick(R.id.btn_get_poetry)
        public void onViewClicked() {
            getPresenter().getPoetry();
        }
    
        @Override
        public void searchSuccess(String author) {
            tvPoetryAuthor.setText(author);
        }
    }
    

    2.8 Presenter

    
    
    /**
     * created by xucanyou666
     * on 2020/1/16 17:09
     * email:913710642@qq.com
     */
    public class PoetryPresenter extends BasePresenter<IPoetryContract.IPoetryView> implements IPoetryContract.IPoetryPresenter {
    
        private static final String TAG = "PoetryPresenter";
    
        private PoetryEntity mPoetryEntity;
        private PoetryModel mPoetryModel;
    
        private PoetryPresenter() {
            mPoetryModel = PoetryModel.getInstance();
        }
    
        public static PoetryPresenter getInstance() {
            return Inner.instance;
        }
    
        private static class Inner {
            private static final PoetryPresenter instance = new PoetryPresenter();
        }
    
        /**
         * 得到诗歌
         */
        @Override
        public void getPoetry() {
            Observable observable = mPoetryModel.getPoetry().doOnSubscribe(new Consumer<Disposable>() {
                @Override
                public void accept(Disposable disposable) throws Exception {
                    addDisposable(disposable);
                }
            });
            observable = RxJavaUtil.toSubscribe(observable);
            observable.subscribe(new Observer<PoetryEntity>() {
                @Override
                public void onSubscribe(Disposable d) {
                }
    
                @Override
                public void onNext(PoetryEntity poetryEntity) {
                    mPoetryEntity = poetryEntity;
                }
    
                @Override
                public void onError(Throwable e) {
                    getMvpView().onError(e.getMessage());
                    Log.d(TAG, "onError: " + e.getMessage());
                }
    
                @Override
                public void onComplete() {
                    if (mPoetryEntity != null) {
                        getMvpView().searchSuccess(mPoetryEntity.getAuthor());
                    }
                }
            });
    
        }
    }
    
    
    

    2.9 Model

    
    
    /**
     * created by xucanyou666
     * on 2020/1/16 17:06
     * email:913710642@qq.com
     */
    public class PoetryModel implements IPoetryContract.IPoetryModel {
    
    
        private PoetryModel() {
    
        }
    
        public static PoetryModel getInstance() {
            return Inner.instance;
        }
    
        private static class Inner {
            private static final PoetryModel instance = new PoetryModel();
        }
    
    
        /**
         * 获取古诗词
         *
         * @return 古诗词
         */
        @Override
        public Observable<PoetryEntity> getPoetry() {
            return RetrofitManager.getInstance().createRs(GetPoetryEntity.class).getPoetry();
        }
    }
    

    2.10 app.build.gradle

    apply plugin: 'com.android.application'
    
    android {
        compileSdkVersion 28
        compileOptions {
            sourceCompatibility JavaVersion.VERSION_1_8
            targetCompatibility JavaVersion.VERSION_1_8
        }
        defaultConfig {
            applicationId "com.users.xucanyou666.rxjava2_retrofit_mvp"
            minSdkVersion 19
            targetSdkVersion 28
            versionCode 1
            versionName "1.0"
            testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
        }
        buildTypes {
            release {
                minifyEnabled false
                proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
            }
        }
    }
    
    dependencies {
        // RxJava
        implementation 'io.reactivex.rxjava2:rxjava:2.1.12'
        implementation 'com.squareup.retrofit2:retrofit:2.6.0'
        // Retrofit和jxjava关联
        implementation 'com.squareup.retrofit2:adapter-rxjava2:2.4.0'
        // Retrofit使用Gson转换
        implementation 'com.squareup.retrofit2:converter-gson:2.4.0'
        // RxAndroid
        implementation 'io.reactivex.rxjava2:rxandroid:2.0.2'
        //引入ButterKnife
        implementation "com.jakewharton:butterknife:10.2.0"
        implementation 'androidx.legacy:legacy-support-v4:1.0.0'
        annotationProcessor "com.jakewharton:butterknife-compiler:10.2.0"
    
        implementation "com.google.android.material:material:1.0.0"
        implementation fileTree(dir: 'libs', include: ['*.jar'])
        implementation 'androidx.appcompat:appcompat:1.1.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'
    }
    
    

    三.我在使用中遇到的问题

    3.1 网络权限忘记授予

    • 解决措施:加上权限即可
    <uses-permission android:name="android.permission.INTERNET" />
    

    3.2 ButterKnife框架版本问题

    使用ButterKnife框架的时候

    当是androidX的时候,需要implementation 10.2.0版本的ButterKnife

    //引入ButterKnife
        implementation "com.jakewharton:butterknife:10.2.0"
        implementation 'androidx.legacy:legacy-support-v4:1.0.0'
        annotationProcessor "com.jakewharton:butterknife-compiler:10.2.0"
    

    当是android 28等其他版本的时候,可以导入8.4.0版本的ButterKnife(导入10.2.0版本会出错)

    implementation 'com.jakewharton:butterknife:8.4.0'
    annotationProcessor 'com.jakewharton:butterknife-compiler:8.4.0' 
    

    3.3 ButterKnife需要Java 1.8以上的支持

     compileOptions {
            sourceCompatibility JavaVersion.VERSION_1_8
            targetCompatibility JavaVersion.VERSION_1_8
        } 
    

    3.4 Fragment中点击事件失效的问题

    • 点击事件失效发生的场景:Fragment中初始化控件没有用ButterKnife框架

    解决措施如下:

    A:方法一:

    • 将控件的初始化放在onCreateView
    • 将控件的点击事件的代码放在onActivityCreated

    B:方法二:

    • Fragment中使用ButterKnife框架

    如果文章对您有一点帮助的话,希望您能点一下赞,您的点赞,是我前进的动力

    本文参考链接:

  • 相关阅读:
    10年学到的编程经验总结
    测试框架 Mocha 实例教程
    在 Flutter 中玩转 Objective-C Block
    使用Gulp压缩混淆JS的相关配置
    Gulp前端自动化构建工具
    常用的十大 NodeJS 框架
    如何使用Vue中的嵌套插槽(包括作用域插槽)
    通过Python代码操作MySQL:
    使用python发邮件:
    jquery:
  • 原文地址:https://www.cnblogs.com/xcynice/p/rxjava_retrofit_mvp1.html
Copyright © 2020-2023  润新知