Android中Kotlin协程http并发请求的正确开启方式

关于Kotlin协程在实际使用中的一点个人经验总结,下面是一个协程使用案例,requestHeWeather6()方法中有详细说明,这个方法是使用Kotlin协程 + Retrofit同步实现并发请求。

package com.coolweather.android

import android.annotation.SuppressLint
import android.graphics.Color
import android.os.Build
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.preference.PreferenceManager
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.widget.Toast
import androidx.core.view.GravityCompat
import com.bumptech.glide.Glide
import com.coolweather.android.gson.HeWeather
import com.coolweather.android.retrofit.WeatherType
import com.coolweather.android.retrofit.requestAreaAndPictureUri
import com.coolweather.android.retrofit.requestWeather
import com.google.gson.Gson
import kotlinx.android.synthetic.main.activity_weather.*
import kotlinx.android.synthetic.main.aqi.*
import kotlinx.android.synthetic.main.forecast.*
import kotlinx.android.synthetic.main.forecast_item.view.*
import kotlinx.android.synthetic.main.now.*
import kotlinx.android.synthetic.main.suggestion.*
import kotlinx.android.synthetic.main.title.*
import kotlinx.coroutines.*
import java.lang.Exception
import kotlin.system.measureTimeMillis

class WeatherActivity : AppCompatActivity(), CoroutineScope by CoroutineScope(Dispatchers.IO) {
    private val TAG = WeatherActivity::class.java.simpleName
    private var mWeatherId: String? = null //用于下来刷新天气信息
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_weather)

        //============下拉刷新天气设置=====================
        swipe_refresh.setColorSchemeResources(R.color.colorPrimary)//设置下来刷新进度条颜色
        swipe_refresh.setOnRefreshListener {
            //设置刷新监听器
            mWeatherId?.let {
                requestHeWeather6(it)
            }
        }

        //============状态栏与背景图融合====================
        if (Build.VERSION.SDK_INT >= 21) {//如果Android版本大于5.0实现让背景图和状态栏融合到一起
            window.decorView.run {
                systemUiVisibility =
                    View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
            }
            window.statusBarColor = Color.TRANSPARENT
        }

        //============设置背景图片=====================
        val preferences = getSharedPreferences("PictureUri", MODE_PRIVATE)
        val bingPic = preferences.getString("bingPic", "")
        if (bingPic != null) {//判断缓存中的图片地址是否为空,不为空使用缓存中的图片地址直接加载
            Glide.with(this).load(bingPic).into(bing_pic_img)
        } else {//没有缓存图片地址的情况下通过API接口获取图片地址并加载显示
            loadBingPic()
        }

        //=============设置界面上显示的天气数据===============
        val weatherId = intent.getStringExtra("weather_id")
        if (weatherId == null) {//为null时加载缓存天气数据
            PreferenceManager.getDefaultSharedPreferences(this).run {
                val heWeather6String = getString("heWeather6", null)
                if (heWeather6String != null) {
                    // 有缓存时直接解析天气数据
                    Gson().fromJson(heWeather6String, HeWeather::class.java).run {
                        mWeatherId = heWeatherList[0].basic.cid
                        showHeWeatherInfo(this)
                    }
                } else {
                    Toast.makeText(
                        this@WeatherActivity, "缓存中无天气数据", Toast.LENGTH_LONG
                    ).show()
                }
            }
        } else {
            // 有ID时直接去服务器查询天气
            intent.getStringExtra("weather_id")?.run {
                weather_layout.visibility = View.INVISIBLE
                requestHeWeather6(this)
            }
        }

        //===========设置滑动菜单的逻辑处理=========================
        nav_button.setOnClickListener {
            drawer_layout.openDrawer(GravityCompat.START)
        }
    }

    /**
     * 访问API接口获取必应每天更新的背景图片地址
     */
    private fun loadBingPic() {
        launch {
            //TODO 这里启动async子协程不用切换到Dispatchers.IO调度器,这个活动所委托的对象的调度器就是Dispatchers.IO
            val job = async {
                requestAreaAndPictureUri("bing_pic")
            }
            val response = job.await()

            if (response.isSuccessful) {
                //此处要么返回null,要么返回获取到的图片uri
                response.body()?.bytes()?.run {
                    val bingPic = String(this)
                    runOnUiThread {
                        //Glide.with(this@WeatherActivity).load(R.drawable.bg).into(bing_pic_img)
                        Glide.with(this@WeatherActivity).load(bingPic).into(bing_pic_img)
                    }
                    getSharedPreferences("PictureUri", MODE_PRIVATE).edit().run {
                        putString("bingPic", bingPic)
                        apply()//把获取到的图片地址缓存起来
                    }
                }
            } else {
                Toast.makeText(
                    this@WeatherActivity,
                    "获取背景图片URI时候出现错误(${response.code()})!",
                    Toast.LENGTH_LONG
                )
                    .show()
            }
        }
    }

    /**
     * 根据天气id请求城市天气信息
     */
    fun requestHeWeather6(heWeatherId: String) {
        mWeatherId = heWeatherId

        /*TODO 注意,如果这里使用runBlocking来启动一个顶层协程,在runBlocking内部代码执行完之前都会阻塞主线程
        * (此时的主线程是IO线程), 这意味着活动界面此时是不可操作的,如果有刷新进度条,那么刷新进度条是静止
        * 的,没有进度,因为主线程此时被runBlocking阻塞了,界面上的所有功能都无法使用,这种体验是非常糟糕的。
        *
        * 解决方案:
        * 1、当前活动实现CoroutineScope接口,并委托给CoroutineScope(Dispatchers.IO)对象,这样就不用去实现接口
        * 的方法了,注意调度器使用Dispatchers.IO,如果使用Dispatchers.Mian是无法开启子协程的。
        *
        * 2、在需要开启协程的地方使用launch{ },async{ }等方式来开启子协程,因为活动实现了CoroutineScope接口,
        * 所以在活动内的任何地方都可以开启子协程。
        *
        * 3、现在在子协程的内部可以随意使用runBlocking了,此时并不会阻塞IO线程,所以不会影响活动界面的任何操作,
        * 因为实现协程接口时的委托对象使用的调度器是Dispatchers.IO,此时子协程是处于协程线程池中的一条线程里(
        * runBlocking当前阻塞的线程就是这条)。除了runBlocking等协程构建器,还可以使⽤ coroutineScope 构建器声明
        * ⾃⼰的作⽤域。它会创建⼀个协程作⽤域并且在所有已启动⼦协程执⾏完毕之前不会结束。runBlocking与coroutineScope
        * 的主要区别在于后者在等待所有⼦协程执⾏完毕时不会阻塞当前线程。
        *
        * 4、通过实现接口的方式来启动协程还有另外一个好处,就是可以避免内存溢出,只要在活动的onDestroy()方法中调用
        * cancel()方法来取消当前的顶层协程,所有活动范围内的子协程都会被取消。 */

        launch {
            //TODO 这里启动async子协程不用切换到Dispatchers.IO调度器,这个活动所委托的对象的调度器就是Dispatchers.IO
            val jobWeatherNow = async {
                requestWeather(WeatherType.WeatherNow, heWeatherId)
            }
            //TODO 这里启动async子协程不用切换到Dispatchers.IO调度器,这个活动所委托的对象的调度器就是Dispatchers.IO
            val jobWeatherForecast = async {
                requestWeather(WeatherType.WeatherForecast, heWeatherId)
            }
            //TODO 这里启动async子协程不用切换到Dispatchers.IO调度器,这个活动所委托的对象的调度器就是Dispatchers.IO
            val jobWeatherLifestyle = async {
                requestWeather(WeatherType.WeatherLifestyle, heWeatherId)
            }
            //TODO 这里启动async子协程不用切换到Dispatchers.IO调度器,这个活动所委托的对象的调度器就是Dispatchers.IO
            val jobAirNow = async {
                requestWeather(WeatherType.AirNow, heWeatherId)
            }

            val response1 = jobWeatherNow.await()
            val response2 = jobWeatherForecast.await()
            val response3 = jobWeatherLifestyle.await()
            val response4 = jobAirNow.await()

            if (response1.isSuccessful && response2.isSuccessful && response3.isSuccessful && response4.isSuccessful) {
                try {
                    response1.body()?.heWeatherList?.run {
                        get(0).apply {
                            //把分4次获取到的JSON合并到response1一个结果中去
                            if (status != "ok") {
                                throw MyException("response1天气请求失败,status = $status")
                            }
                            response2.body()?.run {
                                if (heWeatherList[0].status == "ok") {
                                    this@apply.dailyForecastList =
                                        heWeatherList[0].dailyForecastList
                                } else {
                                    throw MyException("response2天气请求失败,status = ${heWeatherList[0].status}")
                                }
                            }
                            response3.body()?.run {
                                if (heWeatherList[0].status == "ok") {
                                    this@apply.lifestyleList = heWeatherList[0].lifestyleList
                                } else {
                                    throw MyException("response3天气请求失败,status = ${heWeatherList[0].status}")
                                }
                            }
                            response4.body()?.run {
                                if (heWeatherList[0].status == "ok") {
                                    this@apply.air_now_city = heWeatherList[0].air_now_city
                                }
                                //空气质量只有市才能获取到数据,县获取不到
                                //throw MyException("response4天气请求失败,status = ${heWeatherList[0].status}")
                            }
                        }
                    }

                    //保存天气信息到缓存
                    PreferenceManager.getDefaultSharedPreferences(this@WeatherActivity)
                        .edit().run {
                            putString("heWeather6", Gson().toJson(response1.body()))
                            apply()
                        }
                    if (response1.body() != null) {
                        showHeWeatherInfo(response1.body()!!) //把JSON数据显示到界面中
                    } else {
                        Toast.makeText(
                            this@WeatherActivity, "获取天气信息失败,body对象为null", Toast.LENGTH_LONG
                        ).show()
                    }
                } catch (e: MyException) {
                    Toast.makeText(
                        this@WeatherActivity,
                        "获取天气信息失败,接口请求异常(${e.message})",
                        Toast.LENGTH_SHORT
                    ).show()
                }
            } else {
                Toast.makeText(this@WeatherActivity, "获取天气信息失败", Toast.LENGTH_LONG)
                    .show()
            }
            runOnUiThread {
                swipe_refresh.isRefreshing = false //当前更新完天气信息后停止刷新进度条
            }
        }
        loadBingPic()//在每次请求天气信息的时候同时也刷新背景图片
    }

    class MyException : Exception {
        constructor()
        constructor(message: String) : super(message)
    }

    /**
     * 处理并展示HeWeather6实体类的数据
     */
    @SuppressLint("SetTextI18n")
    private fun showHeWeatherInfo(heWeather: HeWeather) {
        val weather = heWeather.heWeatherList[0]
        val cityName = weather.basic.location
        val updateTime = weather.update.loc.split(" ")[1]
        val degree = "${weather.now.tmp}℃"
        val weatherInfo = weather.now.cond_txt
        runOnUiThread {
            title_city.text = cityName
            title_update_time.text = updateTime
            degree_text.text = degree
            weather_info_text.text = weatherInfo

            forecast_layout.removeAllViews()
            weather.dailyForecastList.forEach {
                LayoutInflater.from(this).inflate(R.layout.forecast_item, forecast_layout, false)
                    .run {
                        //注意这里设置的view是LayoutInflater加载返回的view对象里面的view
                        date_text.text = it.date
                        info_text.text = it.cond_txt_d
                        max_text.text = it.tmp_max
                        min_text.text = it.tmp_min
                        forecast_layout.addView(this)
                    }
            }

            if (weather.air_now_city != null) {//如果是市区API能获取到空气质量数据
                layout_aqi.visibility = View.VISIBLE //如果市县级以上城市显示空气质量
                aqi_text.text = weather.air_now_city!!.aqi
                pm25_text.text = weather.air_now_city!!.pm25
            } else {
                layout_aqi.visibility = View.GONE //如果市县级城市隐藏空气质量显示项
            }

            weather.lifestyleList.forEach {
                when (it.type) {
                    "comf" -> comfort_text.text = "舒适度:${it.txt}"
                    "cw" -> car_wash_text.text = "洗车指数:${it.txt}"
                    "sport" -> sport_text.text = "运动建议:${it.txt}"
                }
            }
            weather_layout.visibility = View.VISIBLE
        }
    }

    override fun onDestroy() {
        //继承CoroutineScope接口后,调用cancel()方法可以取消在当前作用域内启动的所有子协程,可以避免内存溢出
        cancel()
        super.onDestroy()
    }
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值