前言
要去学习新的知识,光是简单的使用还是不够的,最好是有一个项目让你去了解和学习,在开发中去增加你的使用,并且以后回头来看很快就能用上,哪怕你现在用不上,知识的储备是非常要必要的,能给你的未来更多机会。
正文
最近觉得Compose很有意思,想要去写一个关于Compose的系列文章,做一个简单的新闻App,话不多说,我们新建一个项目吧。
一、项目创建
这里选择的是Empty Compose Activity,点击Next。
就命名GoodNews吧,开发语言就是Kotlin,我这里用的是当前最新版本的AS,点击Finish完成项目创建。
二、依赖配置
作为一个新闻App,新闻数据的获取是通过网络API,那么我们需要先构建一个网络框架。之前用Java写网络框架时是通过Okhttp、Retrofit、rxJava、那么在Kotlin中就使用Retrofit和协程来操作,在app的build.gradle的dependencies{}闭包中添加如下代码:
//Retrofit implementation 'com.squareup.retrofit2:retrofit:2.9.0' implementation 'com.squareup.retrofit2:converter-gson:2.9.0' //LiveData、ViewModel implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1' implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.4.0-alpha02'
然后Sync Now。
三、数据API
现在免费的API数据接口实在太少了,聚合的每天免费次数也只供测试的,因为我重新找了一个API接口,就是天行数据,点击进入完成注册登录以及实名制。
然后我们可以进入我的控制台
点击这里申请接口,这里我选择了一个抗击疫情的接口,点击申请,可以看到这里有免费的调用次数,建议开发者使用自己的Key去调用。
下面回到我的控制台,然后是我申请的接口,找到抗击疫情,点击立即调试。
在这里可以看到请求地址和请求参数,我们点击测试请求按钮。
这里我们就拿到了返回的数据,通过返回的数据去构建Kotlin的Data类。
这里我推荐一个AS插件,很好用,点击File,然后Settings… ,选择Plugins,输入Generate Kotlin data classes from JSON
安装好插件之后,我们来使用它。在com.llw.goodnews包下新建一个bean包,鼠标右键点击Generate class from GSON。
输入数据类名称,然后将JSON格式数据粘贴到下方,点击OK。
生成了这么多个数据类,我们看一下EpidemicNews
它里面包裹了一个列表NewslistItem,你看到类都是这种情况,数据是很多的,所以每一层都有一个data类。现在数据有了,下面就是通过这个接口去进行网络请求了。
四、网络框架构建
做网络请求肯定不能够随便写,要考虑实用性,这个网络框架我也是在《第一行代码》中学到的,建议有些不知道的地方可以看看这本书,这里就拿来用,稍微有一点变化,不过不大。在com.llw.goodnews包下新建一个network包,包下新建一个ServiceCreator类,代码如下:
object ServiceCreator { private const val baseUrl = "http://api.tianapi.com" private fun getRetrofit() : Retrofit = Retrofit.Builder() .baseUrl(baseUrl) .addConverterFactory(GsonConverterFactory.create()) .build() fun <T> create(serviceClass: Class<T>): T = getRetrofit().create(serviceClass) inline fun <reified T> create(): T = create(T::class.java) }
这里的代码就很简单了,通过网络地址构建一个Retrofit,然后根据传入的Service去访问接口,这里还有一个内联函数。
下面我们来构建这个服务接口,在此之前先在com.llw.goodnews包下新建一个utils包,包下新建一个Constant的类,里面的代码如下:
object Constant { /** * 天行数据Key,请使用自己的Key */ const val API_KEY = "d8bc937c366fcd1629e00f19105db258" /** * 请求接口成功状态码 */ const val CODE = 200 /** * 请求接口成功状态描述 */ const val SUCCESS = "success" }
这里就是一个常量类,我们在请求API接口时会用到的一些不变的值就放这里。然后我们在network包下新建一个ApiService接口,代码如下:
interface ApiService { /** * 获取新闻数据 */ @GET("/ncov/index?key=$API_KEY") fun getEpidemicNews(): Call<EpidemicNews> }
下面我们在network包下新建一个发起请求的NetworkRequest类,代码如下:
object NetworkRequest { /** * 创建服务 */ private val service = ServiceCreator.create(ApiService::class.java) //通过await()函数将getNews()函数也声明成挂起函数。使用协程 suspend fun getEpidemicNews() = service.getEpidemicNews().await() /** * Retrofit网络返回处理 */ private suspend fun <T> Call<T>.await(): T = suspendCoroutine { enqueue(object : Callback<T> { //正常返回 override fun onResponse(call: Call<T>, response: Response<T>) { val body = response.body() if (body != null) it.resume(body) else it.resumeWithException(RuntimeException("response body is null")) } //异常返回 override fun onFailure(call: Call<T>, t: Throwable) { it.resumeWithException(t) } }) } }
这段代码解释一下:首先我们使用ServiceCreator创建了一个ApiService接口的动态代理对象,然后定义了一个getEpidemicNews函数,调用刚刚在ApiService中定义的getEpidemicNews方法,以发起疫情新闻数据请求。这里简化了Retrofit回调的写法,这里定义了一个await()函数,它是一个挂起函数,我们给它声明了一个泛型T,并将await()函数定义成了Call< T >的扩展函数,这样所有返回值是Call类型的Retrofit网络请求接口都可以直接调用await()函数了。
接着,await()函数中使用了suspendCoroutine函数来挂起当前协程,并且由于扩展函数的原因,我们现在拥有了Call对象的上下文,那么这里就可以直接调用enqueue()方法让Retrofit发起网络请求。
最后我们在存储库中发起数据请求,在com.llw.goodnews下创建一个repository包,包下新建一个BaseRepository,里面的代码如下:
open class BaseRepository { fun <T> fire(context: CoroutineContext, block: suspend () -> Result<T>) = liveData(context) { val result = try { block() } catch (e: Exception) { Result.failure(e) } //通知数据变化 emit(result) } }
这里的fire()函数,按照liveData()函数的参数接收标准定义的一个高阶函数。在fire()函数的内部会先调用一下liveData()函数,然后在liveData()函数的代码块中统一进行try catch处理,并在try语句中调用传入的Lambda表达式中的代码,最终Lambda表达式的执行结果并调用emit()方法发射出去。
然后我们在repository包下再新建一个EpidemicNewsRepository类,用于请求疫情新闻数据,继承自BaseRepository,里面的代码如下:
object EpidemicNewsRepository : BaseRepository() { fun getEpidemicNews() = fire(Dispatchers.IO) { val epidemicNews = NetworkRequest.getEpidemicNews() if (epidemicNews.code == CODE) Result.success(epidemicNews) else Result.failure(RuntimeException("getNews response code is ${epidemicNews.code} msg is ${epidemicNews.msg}")) } }
这里我们调用父类的fire()函数,将liveData()函数的线程参数类型指定成了Dispatchers.IO,这样的代码块中的所有代码都是运行在子线程中,如果请求状态码是200,则表示成功,那么就使用Kotlin内置的Result.success()方法来包装获取的疫情新闻数据,然后就调用Result.failure()方法来包装一个异常信息。
那么到这里为止,网络框架就搭建完成了,要使用的话还需要一些配置:
五、项目配置
这里我们在com.llw.goodnews包下自定义一个App类,继承自Application,代码如下:
class App : Application() { companion object { @SuppressLint("StaticFieldLeak") lateinit var context: Context } override fun onCreate() { super.onCreate() context = applicationContext } }
然后因为我们访问的API是http开头的,在Android9.0及以上版本中默认访问https,因此我们需要打开对http的网络访问,在res文件夹下新建一个xml文件夹,在xml文件夹下创建一个network_config.xml,里面的代码如下:
<?xml version="1.0" encoding="utf-8"?> <network-security-config> <base-config cleartextTrafficPermitted="true"/> </network-security-config>
然后我们在AndroidManifest.xml中去配置,如下图所示:
现在万事具备,只差请求了。
六、网络请求
进入到MainActivity,新增如下代码:
EpidemicNewsRepository.getEpidemicNews().observe(this@MainActivity) { result -> val epidemicNews = result.getOrNull() if (epidemicNews != null) { Log.d("TAG", "onCreate: ${epidemicNews.code}") Log.d("TAG", "onCreate: ${epidemicNews.msg}") Log.d("TAG", "onCreate: ${epidemicNews.newslist?.get(0)?.news?.get(0)?.title}") Log.d("TAG", "onCreate: ${epidemicNews.newslist?.get(0)?.news?.get(0)?.summary}") } else { Log.e("TAG", "onCreate: null") } }
添加的位置为下图所示:
这里就是通过请求返回数据,然后打印一下数据,下面来运行一下:
OK,网络框架就没有啥问题了,主要有一个点不爽,就是这里的bean里面太多类,我们写到一个类里面,修改一下EpidemicNews.kt,里面的代码如下:
data class EpidemicNews(val msg: String = "", val code: Int = 0, val newslist: List<NewslistItem>?) data class NewslistItem(val news: List<NewsItem>?, val desc: Desc, val riskarea: Riskarea) data class NewsItem(val summary: String = "", val sourceUrl: String = "", val id: Int = 0, val title: String = "", val pubDate: Long = 0, val pubDateStr: String = "", val infoSource: String = "") data class Desc(val curedCount: Int = 0, val seriousCount: Int = 0, val currentConfirmedIncr: Int = 0, val midDangerCount: Int = 0, val suspectedIncr: Int = 0, val seriousIncr: Int = 0, val confirmedIncr: Int = 0, val globalStatistics: GlobalStatistics, val deadIncr: Int = 0, val suspectedCount: Int = 0, val currentConfirmedCount: Int = 0, val confirmedCount: Int = 0, val modifyTime: Long = 0, val createTime: Long = 0, val curedIncr: Int = 0, val yesterdaySuspectedCountIncr: Int = 0, val foreignStatistics: ForeignStatistics, val highDangerCount: Int = 0, val id: Int = 0, val deadCount: Int = 0, val yesterdayConfirmedCountIncr: Int = 0) data class Riskarea(val high: List<String>?, val mid: List<String>?) data class GlobalStatistics(val currentConfirmedCount: Int = 0, val confirmedCount: Int = 0, val curedCount: Int = 0, val currentConfirmedIncr: Int = 0, val confirmedIncr: Int = 0, val curedIncr: Int = 0, val deadCount: Int = 0, val deadIncr: Int = 0, val yesterdayConfirmedCountIncr: Int = 0) data class ForeignStatistics(val currentConfirmedCount: Int = 0, val confirmedCount: Int = 0, val curedCount: Int = 0, val currentConfirmedIncr: Int = 0, val suspectedIncr: Int = 0, val confirmedIncr: Int = 0, val curedIncr: Int = 0, val deadCount: Int = 0, val deadIncr: Int = 0, val suspectedCount: Int = 0)
改完之后删除其他的类,只保留一个
再运行一下看看效果如何
OK,木有问题。
七、源码
GitHub:GoodNews
CSDN:GoodNews_1.rar
CSDN的源码rar文件表示的是当前这篇文章的源码,后面即使更新文章,这个源码不会变,而GitHub上的源码是会是最新的源码。