第一次使用Retrofit引申的思考

背景


22年由嵌入式专业毕业转行搞车载Android,编程方面只有半桶水的C语言。入职后领导很好,每个新人都分配了导师,啥都不干光学习,从零开始学Java,学Android四大组件,存储,xml界面编写,多线程,IPC,MVVM,MVC框架。。。。。。车载特色的还有ADB调试,刷机,车载通信SOMEIP协议,以及一些FW的皮毛。由于本科学习根基之匮乏,加上Android已经走得太远,后面不大可能搞其他的方向了。

求变


车载开发的应用绝大多数是系统应用,一开始就有system权限,发布apk包后随系统一起编译。所以日常业务多是和同电气架构的下游ECU通信,由系统平台层封装好了接口方法供调用,还有一些数据展示和调试所需的业务。同时,为求稳定,target的系统版本是锁死的,屏幕尺寸也是固定的,应用所需权限全部是编译前就配好,所以手机上很多适配性的烦恼车机都不会遇到。

也正是因为这些原因,几乎所有项目的AGP,编译gradle版本,依赖的版本,使用架构,都比较落后。感觉代码基础已经掌握差不多了,看别人代码可以很快找到关键位置,增改需求,独立开发一些基本业务也可以把持住。也有自己的一套封装哲学。

最近开始头疼接下来往下进阶的路线了,郭神的《第三行代码》,任玉刚的《Android开发艺术探索》都概览了一遍,拿来查漏补缺还可以,里面的一些内容大多有些过时了。于是就想着,既然现在是学习Android,跟随Google官方走总没错的。有时趁工作间隙,游历于掘金,CSDN,公众号等媒体,粉上了像朱凯,郭神,张拭心等几位GRE(谷歌认证Android专家),他们也都推荐拥抱新技术,比如Kotlin,Compose,MVI架构等。

说干就干


首先花三天学了Kotlin语法基础,瞬间感觉官方推荐真不错,Kotlin的编写速度和代码量要优于Java一大截。直接把手里的一个功能简单的小项目全部改写成Kotlin,不用自动转换,手动一行一行的重构,期间写了很多Java风格的Kotlin代码,最喜欢用的是when,完败switch!然后每次了解到一个新特性,就去把老项目对应的业务逻辑改写成新的实现方式,不断优化代码质量。这时候没有往下深耕,一是深耕进阶容易打击积极性,二是要学的太多。我的计划是先求广,Kotlin,Jetpack,Compose等等。将所有学习的新技术都写一个Demo,了解基本使用方式,然后看能不能运用到老项目中去,再往下学下一个。

后面顺理成章地接着学Compose了,xml已经用的相当熟悉了,改成这种声明式UI框架太不习惯了。写起来之后,马上喜欢上了这个框架,比如列表直接用lazycolumn往里填设计好的Item,不用搞一个adapter写对应的逻辑。很快就发现要使用好Compose,掌握MVI式的响应式架构,还有众多Kotlin语言的高阶操作是必不可少的。于是上github上找了一个简单的图片获取展示的app,照着他的应用,将之前改成Kotlin的小项目重构成了MVI框架,看gitlab里的开发语言占比,真是好家伙,100%Kotlin!

说回标题


由于此前做的都是本地化的系统应用,几乎没有涉及到网络操作,老感觉这是一块坑,想将其填平,只知道retrofit + rxjava是几乎占领了100%市场的网络请求方案。但是rxjava又被很多唱衰,听说也很难学,了解到有很多可以替代rxjava的异步操作方案,比如协程,livedata,所以目前只学retrofit的基本操作。找了一个免费的API网站——聚合数据,随便选了一个随机笑话的api,看了下接口文档。然后就按照标准的三大步开始写请求。

一、先把注解Service接口写好

这一步只需要使用注解,就可以配置好一个请求方法.

先看api网站上的接口文档,需要使用Post请求方法,内容类型是urlencoded,查看书写规范,方法修饰使用@Post注解,方法传参使用@Field注解。

interface RandomJokesService {

    @FormUrlEncoded
    @POST("randJoke.php")
    fun getRandomJokes(@FieldMap paramsKey: Map<String, String>): Call<JokeModel>

}

再看返回数据实例,利用JsonToKotlinClass插件,直接将文档里的实例返回字符串,生成一个数据类data class,接口里的返回类型,要设置成Call请求对象,并填入这个数据类做泛型,后面转换后就可以直接将服务器的response数据转换成这个对象JokeModel,拿过来操作其变量获取需要的数据。

data class JokeModel(
    @SerializedName("error_code")
    val errorCode: Int,
    val reason: String,
    val result: List<Result>
)

data class Result(
    val content: String,
    val hashId: String,
    val unixtime: String
)

二、构建retrofit对象

这一步没有使用最简单的构建方式,而是按照郭神《第三行代码》里的方法封装了一个服务实例化的类。inline和reified不是很懂原理,只知道是泛型实化所需要的关键字,retrofit.create返回的是一个T泛型,经由这个inline方法处理后就可以获得服务实例,可以调用服务api里面的Call请求方法。

object ServiceCreator {

    private const val baseUrl = "http://v.juhe.cn/joke/"

    private val retrofit: Retrofit =
        Retrofit.Builder().baseUrl(baseUrl)
            .addConverterFactory(GsonConverterFactory.create())
            .build()

    fun <T> getService(serviceClass: Class<T>): T = retrofit.create(serviceClass)

    inline fun <reified T> createService(): T = getService(T::class.java)
}

这一步填入服务器的通用baseurl,添加response和Json的转换器,其实写法比较死板,经由其他项目了解到可以自己配置okhttp的client,设置拦截器等灵活操作,这些后续再扩展使用。

三、调用方法

传统的方法中,在其他Thread里通过excute()同步调用,直接拿取数据,回到主线程更新UI,或则和使用enqueue(Callback)异步调用,在callback的response回调里处理数据更新UI,这个回调已经由retrofit使用handler将数据发送到主线程了。

这里由于学了Kotlin的协程,我采用协程的调度器切换,使用Dispatchers.IO切换到分支线程中处理suspend的网络请求,再使用Dispatchers.Main切换回主线程更新UI。每次返回了6,7个小笑话,所以中途调整了一下格式,填到ScrollView中去进行展示,为了专注于请求逻辑,界面层面尽量做到最简单。

  // 网络请求request
        val scope = CoroutineScope(Dispatchers.IO).launch {
            val resultString = StringBuilder()
            val data = ServiceCreator.createService<RandomJokesService>()
                .getRandomJokes(mapOf("key" to "api网站个人中心提供的key")).execute()
            data.body()?.result?.forEach {
                resultString.append(it.content)
                resultString.append("\n\r\n")
            }
            withContext(Dispatchers.Main){
                binding.tvHello.text = resultString
            }
        }

进一步优化代码风格

withContext的切换,看起来像嵌套一样,而协程最爽的就是可以以同步的格式来书写代码。现在将IO线程和主线程的切换,在调用的地方省略掉,IO线程切换可以放到请求方法里。

以另外一个天气请求api为例,viewModel的代码如下:

class WeatherViewModel : ViewModel() {

    suspend fun getWeather() = withContext(Dispatchers.IO) {
        debugLog("thread name: ${Thread.currentThread().name}")
        ServiceCreator.createService<WeatherService>()
            .getWeather("武汉", "api网站个人中心提供的key")
            .execute()
            .body()
    }
}

在Activity里调用时,就可以按顺序的方式来书写代码了。

CoroutineScope(Dispatchers.Main).launch {
            // 这里getWeather是suspend函数,会切到IO线程
            val weatherModel = weatherViewModel.getWeather()
            // 自动切回主线程
            debugLog("thread name: ${Thread.currentThread().name}")
            val city = weatherModel?.result?.city
            val date = weatherModel?.result?.future?.get(0)?.date
            val weather = weatherModel?.result?.future?.get(0)?.weather
            getBinding().tvCity.text = "城市:$city"
            getBinding().tvDate.text = "日期:$date"
            getBinding().tvWeather.text = "天气:$weather"
        }

查看打印的thread.name,可以看到这种写法,协程确实也能自动帮我们切换线程。

思考


当我的Pixel 5成功显示了一堆文字之后,我感觉我终于与世界连接上了,村里有网了家人们。这次Demo编写体验,也是小小地补足了一块盲区。

最近一年的小目标,后面的计划还是循序渐进地把Jetpack里其他热门的前沿库都使用一遍,做好Demo记录,后面有使用场景的时候,再看自己的Demo写法,根据具体业务进行扩展与优化,在实战中提升经验。

然后做Kotlin进阶,通读官方文档,买本书把语言工具上的短板补足下。

最后再实行MVI架构,重构小项目。逐步构建起高效优雅的代码编写方式。

在技术上,我信奉的学习格言是“在行中知,在知中行”,最近感觉行的有点快,有点懵。也需要更加注重知的板块,知行合一。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值