jetpack+mvvm+协程封装网络框架

2021-04-30  本文已影响0人  书生也很芍

最近学习完flutter基础后,心血来潮,把kotlin的协程学习了下,又把项目的网络框架用协程重写了,分享给你大家

一、gradel插件相关配置

因为协程,需要用到kotlin

(1)根gradel配置

buildscript {
    ext {
        kotlin_version = '1.3.72'
    }
    repositories {
        google()
        jcenter()
        mavenCentral()
        ext {
            kotlin_version = '1.3.72'
        }
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:4.1.0'
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.3.72"
        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}

allprojects {
    repositories {
        google()
        jcenter()
        maven { url 'https://jitpack.io' }
        mavenCentral()
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}

(2)module gradel配置

apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
dependencies {
    /*viewmodle 绑定*/
    api 'androidx.lifecycle:lifecycle-extensions:2.2.0'
    //kt协程
    api "androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.0-rc01"
    api 'com.squareup.retrofit2:retrofit:2.7.0'
    api 'com.squareup.okhttp3:logging-interceptor:4.4.0'
    api 'com.squareup.retrofit2:converter-gson:2.7.0'
    api 'com.squareup.retrofit2:adapter-rxjava2:2.7.0'
}

二、kotlin写Retrofit

(1)基础的RetrofitClient

object RetrofitClient {
    //请求的地址
    private const val BASE_URL = "http://xxxx.xxxx.net.cn/"

    //retrofit对象
    private var retrofit: Retrofit? = null

    //请求的api,可以根据不同的场景设置多个
    val service: ApiService by lazy {
        getRetrofit().create(ApiService::class.java)
    }

    private fun getRetrofit(): Retrofit {
        if (retrofit == null) {
            retrofit = Retrofit.Builder()
                    .baseUrl(BASE_URL)
                    .client(getOkHttpClient())
                    .addConverterFactory(GsonConverterFactory.create())
                    .build()
        }
        return retrofit!!
    }

    /**
     * 获取 OkHttpClient
     */
    private fun getOkHttpClient(): OkHttpClient {
        val builder = OkHttpClient().newBuilder()
        val loggingInterceptor: HttpLoggingInterceptor
        if (BuildConfig.DEBUG) {
            val logInterceptor = HttpLoggingInterceptor(HttpLogger())
            logInterceptor.level = HttpLoggingInterceptor.Level.BODY
            builder.addNetworkInterceptor(logInterceptor)
        }

        builder.run {
            connectTimeout(HttpConstant.DEFAULT_TIMEOUT, TimeUnit.SECONDS)
            readTimeout(HttpConstant.DEFAULT_TIMEOUT, TimeUnit.SECONDS)
            writeTimeout(HttpConstant.DEFAULT_TIMEOUT, TimeUnit.SECONDS)
            retryOnConnectionFailure(true) // 错误重连
            // cookieJar(CookieManager())
        }
        return builder.build()
    }

(2)ApiService

因为用到了协程,接口需要挂在任务中去,一般用suspend与CoroutineScope一起使用

interface ApiService {

    companion object {
 
        const val JOIN_URL = "?c=600&v=100"
    }

    /**
     * 商城首页轮播
     */
    @GET("xxx/xxx/xxx/banner/list$JOIN_URL")
    suspend fun getBanners(@QueryMap map: Map<String, String>?): ApiFrameResult<BasePageEntity<MallBannerBean>>

    /**
     * 订单状态数量
     */
    @GET("xxx/xxx/xxx/order/orderCount$JOIN_URL")
    suspend fun getMallOrderCount(@QueryMap map: Map<String, String>?):ApiFrameResult<MallOrderCount>

}

(3)是否需要加密

这里的加密规则按照后台规范要求来操作,我这里采取了项目的一些代码块,EncryptionUtil,JsonUtil工具类问度娘

open class HttpClient  {
    /**
     * 是否需要加密
     *
     * @return 加密
     */
    protected val isNeedEncrypt: Boolean
        protected get() = false

    /**
     * 组装query形式的请求参数
     * @param parameter 业务参数
     * @return query 参数
     */
    protected open fun composeQueryParameter(parameter: Any?): Map<String, String>? {
        val uid: String = BaseCacheHelper.getUid()
        val map: MutableMap<String, String> = HashMap(6)
        map["uid"] = uid
        val json = handelParameterQ(parameter)
        map["q"] = json
        if (isNeedEncrypt) {
            map["sign"] = EncryptionUtil.encryptSgin(json)
        }
        return map
    }


    /**
     * 组装post请求体中业务参数
     * @param parameter 业务参数
     * @return 请求体
     */
    protected open fun composePostBody(parameter: Any?): RequestBody? {
        val mapOut: MutableMap<String, Any> = HashMap(1)
        mapOut["q"] = handelParameterQ(parameter)
        return create("application/json; charset=utf-8".toMediaTypeOrNull(), JsonUtil.string(mapOut))
    }


    /**
     * 处理请求参数
     *
     * @param parameter 业务参数
     */
    protected open fun handelParameterQ(parameter: Any?): String {
        var json = if (parameter == null) "{}" else JsonUtil.string(parameter)
        if (isNeedEncrypt) {
            json = EncryptionUtil.encrypt(json)
        }
        return json
    }

}

(4)业务逻辑请求类

协程中执行的任务,挂起通过挂起函数,实现非阻塞挂起,suspend用来修饰函数,作用是提醒这是一个挂起函数,协程运行到该函数的时候,会挂起

class CommonClient : HttpClient() {

    companion object {
        @Volatile
        private var INSTANCE: CommonClient? = null
        fun getInstance(): CommonClient = INSTANCE ?: synchronized(this) {
            INSTANCE ?: CommonClient()
        }
    }


    /**
     * @description 获取banner数据
     */
    suspend fun getBanner(params: Map<String?, Any?>?): ApiFrameResult<BasePageEntity<MallBannerBean>> {
      return  RetrofitClient.service.getBanners(composeQueryParameter(params))

    }


    /**
     * @description 返回订单数量
     */
    suspend fun getMallOrderCount(params: Map<String?, Any?>?): ApiFrameResult<MallOrderCount> {
        return  RetrofitClient.service.getMallOrderCount(composeQueryParameter(params))
    }

三、MVVM封装

(1)BaseViewModel对协程的封装

此处launchData放在基类,让子类去实现,参数详解
api: suspend CoroutineScope.() -> ApiFrameResult<T>这个参数,是一个协程接口方法,需要用suspend 来修饰
withContext 任务是串行的, 且 withContext可直接返回耗时任务的结果,直到withConext执行完成后再执行下面,在子线程和主线程来回切换
liveData.value,error.value,熟悉mvvm原理的,我不赘述,会在view层做监听处理,后面有贴代码
ApiRequestSubscriber这个根据后台的返回code自行封装

open class BaseViewModel : ViewModel(), LifecycleObserver {
    @JvmField
    val loadingShowLD = MutableLiveData<Boolean>() //请求接口的loading

    val error = MutableLiveData<Int>()

    /**
     * @description 处理异常
     */
    fun handleException(status: Int) {
        error.value = status
    }

    /**
     * 注意此方法传入的参数:api是以函数作为参数传入
     * api:即接口调用方法
     * error:可以理解为接口请求失败回调
     * ->数据类型,表示方法返回该数据类型
     * ->Unit,表示方法不返回数据类型
     */
    fun <T> launchData(
            api: suspend CoroutineScope.() -> ApiFrameResult<T>,//请求接口方法,T表示data实体泛型,调用时可将data对应的bean传入即可
            liveData: MutableLiveData<T>,
            isShowLoading: Boolean = false) {
        if (isShowLoading) {
            //loading
            loadingShowLD.value = true
        }
        viewModelScope.launch {
            try {
                withContext(Dispatchers.IO) {//异步请求接口
                    val result = api()
                    withContext(Dispatchers.Main) {
                        if (result.status == ApiRequestSubscriber.SERVER_SUCCESS) {//请求成功
                            LogUtils.d("http  成功--- " + result.status)
                            liveData.value = result.content.data
                        } else {
                            LogUtils.d("http  服务器返回异常--- ")
                            handleException(result.status)
                        }
                    }
                }
            } catch (e: Throwable) {//接口请求失败
                LogUtils.e(e.message)
            } finally {//请求结束
                LogUtils.d("http  请求结束--- ")
                //解除loading
                loadingShowLD.value = false
            }
        }
    }
}

贴出数据ApiFrameResult类

data class ApiFrameResult<T>(
        val status: Int,
        val msg: String,
        val uid: String,
        val content: ApiBusinessResult<T>
)

/**
 * 业务返回的数据
 */
data class ApiBusinessResult<T>(
        val code: Int,
        val codeDesc: String,
        val data: T

)

data class BasePageEntity<T>(
        val list: List<T>?,
        val total: Int,
        val totalPage: Int,
        val pageNo: Int,
        val pageSize: Int,
        val isLastPage: Boolean
)

data class MallBannerBean(
        val bannerId: String,
        val bannerType: Int,
        val imgUrl: String,//图片地址
        val imgLink: String//图片跳转链接
)

data class MallOrderCount(
        val toBeDeliveredCount: Int,
        val toPayCount: Int,
        val toSendCount: Int
)

(2)View层的BaseActivity

public abstract class BaseActivity extends AppCompatActivity {
    protected BaseViewModel viewModel;
    protected ViewModelProvider viewModelProvider;
    protected ViewModelProvider.Factory factory;
    private LoadingDialog loadingDialog;


    protected abstract void initView();

    protected abstract void initLogic();


    protected BaseViewModel createViewModel() {
        return null;
    }


    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        viewModel = createViewModel();
        initView();
        observeLiveData();
        initLogic();
    }


    /**
     *请求网络数据的错误回调
     */
    protected void observeLiveData() {
        if (viewModel != null) {
            viewModel.loadingShowLD.observe(this, this::showLoading);
            viewModel.getError().observe(this, status -> {
                switch (status) {
                    case ApiRequestSubscriber.TOKEN_FAILURE:
                        ToastUtils.showShort("登录失效");
                        break;
                    case ApiRequestSubscriber.SERVER_ERROR:
                        ToastUtils.showShort("服务器异常");
                        break;
                    case ApiRequestSubscriber.NET_ERROR:
                        ToastUtils.showShort("网络错误");
                        break;
                    case ApiRequestSubscriber.TIME_OUT:
                        ToastUtils.showShort("网络超时");
                        break;
                    case ApiRequestSubscriber.LOGIN_FAILURE:
                        ARouter.getInstance().build(PageRouter.PAGE_lOGIN).navigation();
                        ToastUtils.showShort("请重新登录");
                        break;

                }

            });
        }
    }


    /**
     * 是否显示loading
     */
    protected void showLoading(boolean showLoading) {
        if (showLoading) {
            if (loadingDialog == null) {
                loadingDialog = new LoadingDialog(this);
                loadingDialog.setCanceledOnTouchOutside(false);
            }

            if (!loadingDialog.isShowing() && !isFinishing()) {
                loadingDialog.show();
            }
        } else {
            if (loadingDialog != null) {
                if (loadingDialog.isShowing()) {
                    loadingDialog.dismiss();
                    loadingDialog = null;
                }

            }
        }


    }


    protected ViewModelProvider getViewModelProvider() {
        if (viewModelProvider == null) {
            viewModelProvider = new ViewModelProvider(this, getFactory());
        }
        return viewModelProvider;
    }

    private ViewModelProvider.Factory getFactory() {
        Application application = getApplication();
        if (application == null) {
            throw new IllegalStateException("Your activity/fragment is not yet attached to "
                    + "Application. You can't request ViewModel before onCreate call.");
        }
        if (factory == null) {
            factory = ViewModelProvider.AndroidViewModelFactory.getInstance(application);
        }
        return factory;
    }

(3)model层的业务实现CommonViewModel

class CommonViewModel : BaseViewModel() {
    /**
     * 轮播图数据监听
     */
    val bannerLivaData: MutableLiveData<BasePageEntity<MallBannerBean>> = MutableLiveData()
    val mallOrderCount: MutableLiveData<MallOrderCount> = MutableLiveData()

    fun getBannerData(): MutableLiveData<BasePageEntity<MallBannerBean>> {
        return bannerLivaData
    }

    /**
     * @description 轮播图
     * @time 2021/4/14 13:33
     */
    fun getBanner() {
        launchData({ CommonClient.getInstance().getBanner(null) }, bannerLivaData, true)
    }

    /**
     * @description 获取配送的数量
     * @time 2021/4/14 13:50
     */
    fun getMallOrderCount() {
        launchData({ CommonClient.getInstance().getMallOrderCount(HashMap()) }, mallOrderCount, true)
    }
}

(4)View层的页面实现

class CoroutineActivity : BaseActivity() {
....省略一些代码....
override fun createViewModel(): BaseViewModel {
        commonViewModel = getViewModelProvider().get(CommonViewModel::class.java)
        return commonViewModel

    }
//回调数据
   override fun observeLiveData() {
        super.observeLiveData()
        commonViewModel.getBannerData().observe(this, Observer {
            binding.kotlin.text = "banner数据"
            LogUtils.d("http  bannerLivaData-- " + it.list?.size)
        })
    }
}

调试


QQ图片20210430113606.png

四、协程相关知识

创建协程的方式就有了五种:
GlobalScope.launch{} :非阻塞的
launch{}
runBlocking{} : 是阻塞的
coroutineScope{}
async{}

//Dispatchers.Main:使用这个调度器在 Android 主线程上运行一个协程。可以用来更新UI 。在UI线程中执行
//Dispatchers.IO:这个调度器被优化在主线程之外执行磁盘或网络 I/O。在线程池中执行
    /**
     * @description ,协程体里的任务时就会先挂起(suspend),让CoroutineScope.launch后面的代码继续执行,
     * 直到协程体内的方法执行完成再自动切回来所在的上下文回调结果。
     */

    fun lauch() {
        CoroutineScope(Dispatchers.IO).launch {
            delay(500)     //延时500ms
            LogUtils.d(LogCat.COROUT_LOG + "1.执行CoroutineScope.... [当前线程为:${Thread.currentThread().name}]")
        }
        LogUtils.d(LogCat.COROUT_LOG + "2.BtnClick.... [当前线程为:${Thread.currentThread().name}]")
    }

    /**
     * @description 实际开发中很少会用到runBlocking,阻塞主线程
     * 等协程体完成才会执行下一个
     */
    fun runBlocking() {
        runBlocking {
            delay(500)     //延时500ms
            LogUtils.d(LogCat.COROUT_LOG + "1.执行CoroutineScope.... [当前线程为:${Thread.currentThread().name}]")
        }
        LogUtils.d(LogCat.COROUT_LOG + "2.BtnClick.... [当前线程为:${Thread.currentThread().name}]")
    }

    /**
     * @description 返回耗时任务的执行结果
    多个 withContext 任务是串行的, 且 withContext可直接返回耗时任务的结果,直到withConext执行完成后再执行下面
    用在一个请求结果依赖另一个请求结果的这种情况
     */
    fun withContext() {
        CoroutineScope(Dispatchers.IO).launch {
            val time1 = System.currentTimeMillis()

            val task1 = withContext(Dispatchers.IO) {
                delay(2000)
                LogUtils.d(LogCat.COROUT_LOG + "1.执行task1.... [当前线程为:${Thread.currentThread().name}]")
                "one"  //返回结果赋值给task1
            }

            val task2 = withContext(Dispatchers.IO) {
                delay(1000)
                LogUtils.d(LogCat.COROUT_LOG + "2.执行task2.... [当前线程为:${Thread.currentThread().name}]")
                "two"  //返回结果赋值给task2
            }

            LogUtils.d(LogCat.COROUT_LOG + "task1 = $task1  , task2 = $task2 , 耗时 ${System.currentTimeMillis() - time1} ms  [当前线程为:${Thread.currentThread().name}]")
        }
    }

    /**
     * @description  处理多个耗时任务,且这几个任务都无相互依赖时,可以使用 async ...
     */
    fun async() {
        CoroutineScope(Dispatchers.IO).launch {
            val time1 = System.currentTimeMillis()

            val task1 = async(Dispatchers.IO) {
                delay(2000)
                LogUtils.d(LogCat.COROUT_LOG + "1.执行task1.... [当前线程为:${Thread.currentThread().name}]")
                "one"  //返回结果赋值给task1
            }

            val task2 = async(Dispatchers.IO) {
                delay(1000)
                LogUtils.d(LogCat.COROUT_LOG + "2.执行task2.... [当前线程为:${Thread.currentThread().name}]")
                "two"  //返回结果赋值给task2
            }

            LogUtils.d(LogCat.COROUT_LOG + "task1 = $task1  , task2 = $task2 , 耗时 ${System.currentTimeMillis() - time1} ms  [当前线程为:${Thread.currentThread().name}]")
        }
    }
``
上一篇下一篇

猜你喜欢

热点阅读