Compose 新聞App(一)網絡架構搭建
- 前言
- 正文
- 一、項目建立
- 二、依賴配置
- 三、資料API
- 四、網絡架構建構
- 五、項目配置
- 六、網絡請求
- 七、源碼
前言
要去學習新的知識,光是簡單的使用還是不夠的,最好是有一個項目讓你去了解和學習,在開發中去增加你的使用,并且以後回頭來看很快就能用上,哪怕你現在用不上,知識的儲備是非常要必要的,能給你的未來更多機會。
正文
最近覺得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,新增如下代碼:
.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)
改完之後删除其他的類,隻保留一個
再運作一下看看效果如何