天天看點

Android Compose 新聞App(一)網絡架構搭建

Compose 新聞App(一)網絡架構搭建

  • ​​前言​​
  • ​​正文​​
  • ​​一、項目建立​​
  • ​​二、依賴配置​​
  • ​​三、資料API​​
  • ​​四、網絡架構建構​​
  • ​​五、項目配置​​
  • ​​六、網絡請求​​
  • ​​七、源碼​​

前言

  要去學習新的知識,光是簡單的使用還是不夠的,最好是有一個項目讓你去了解和學習,在開發中去增加你的使用,并且以後回頭來看很快就能用上,哪怕你現在用不上,知識的儲備是非常要必要的,能給你的未來更多機會。

正文

  最近覺得Compose很有意思,想要去寫一個關于Compose的系列文章,做一個簡單的新聞App,話不多說,我們建立一個項目吧。

一、項目建立

Android Compose 新聞App(一)網絡架構搭建

這裡選擇的是Empty Compose Activity,點選Next。

Android Compose 新聞App(一)網絡架構搭建

就命名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'      
Android Compose 新聞App(一)網絡架構搭建

然後Sync Now。

三、資料API

  現在免費的API資料接口實在太少了,聚合的每天免費次數也隻供測試的,因為我重新找了一個API接口,就是​​天行資料​​,點選進入完成注冊登入以及實名制。

然後我們可以進入我的控制台

Android Compose 新聞App(一)網絡架構搭建

點選這裡申請接口,這裡我選擇了一個抗擊疫情的接口,點選申請,可以看到這裡有免費的調用次數,建議開發者使用自己的Key去調用。

Android Compose 新聞App(一)網絡架構搭建

下面回到我的控制台,然後是我申請的接口,找到抗擊疫情,點選立即調試。

Android Compose 新聞App(一)網絡架構搭建

在這裡可以看到請求位址和請求參數,我們點選測試請求按鈕。

Android Compose 新聞App(一)網絡架構搭建

這裡我們就拿到了傳回的資料,通過傳回的資料去建構Kotlin的Data類。

Android Compose 新聞App(一)網絡架構搭建

這裡我推薦一個AS插件,很好用,點選File,然後Settings… ,選擇Plugins,輸入Generate Kotlin data classes from JSON

Android Compose 新聞App(一)網絡架構搭建

安裝好插件之後,我們來使用它。在com.llw.goodnews包下建立一個bean包,滑鼠右鍵點選Generate class from GSON。

Android Compose 新聞App(一)網絡架構搭建

輸入資料類名稱,然後将JSON格式資料粘貼到下方,點選OK。

Android Compose 新聞App(一)網絡架構搭建

生成了這麼多個資料類,我們看一下EpidemicNews

Android Compose 新聞App(一)網絡架構搭建

它裡面包裹了一個清單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中去配置,如下圖所示:

Android Compose 新聞App(一)網絡架構搭建

現在萬事具備,隻差請求了。

六、網絡請求

進入到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")
                        }
                    }      

添加的位置為下圖所示:

Android Compose 新聞App(一)網絡架構搭建

這裡就是通過請求傳回資料,然後列印一下資料,下面來運作一下:

Android Compose 新聞App(一)網絡架構搭建

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)      

改完之後删除其他的類,隻保留一個

Android Compose 新聞App(一)網絡架構搭建

再運作一下看看效果如何

Android Compose 新聞App(一)網絡架構搭建