[譯] 2019 年的 Android 網絡 —— Retrofit 與 Kotlin 協程

2019 年的 Android 網絡 —— Retrofit 與 Kotlin 協程

2018 年,Android 圈發生了許多翻天覆地的變化,尤爲是在 Android 網絡方面。穩定版本的 Kotlin 協程的發佈極大地推進了 Android 在處理多線程方面從 RxJava 到 Kotlin 協程的發展。 本文中,咱們將討論在 Android 中使用 Retrofit2Kotlin 協程 進行網絡 API 調用。咱們將調用 TMDB API 來獲取熱門電影列表。html

概念我都懂,給我看代碼!!

若是你在 Android 網絡方面有經驗而且在使用 Retrofit 以前進行過網絡調用,但可能使用的是 RxJava 而不是 Kotlin 協程,而且你只想看看實現方式,請查看 Github 上的 readme 文件前端

Android 網絡簡述

簡而言之,Android 網絡或者任何網絡的工做方式以下:java

  • 請求 —— 使用正確的頭信息向一個 URL(終端)發出一個 HTTP 請求,若有須要,一般會攜帶受權的 Key。
  • 響應 —— 請求會返回錯誤或者成功的響應。在成功的狀況下,響應會包含終端的內容(一般是 JSON 格式)。
  • 解析和存儲 —— 解析 JSON 並獲取所需的值,而後將其存入數據類中。

Android 中,咱們使用:android

  • Okhttp —— 用於建立具備合適頭信息的 HTTP 請求。
  • Retrofit —— 發送請求。
  • Moshi/ GSON —— 解析 JSON 數據。
  • Kotlin 協程 —— 用於發出非阻塞(主線程)的網絡請求。
  • Picasso / Glide —— 下載網絡圖片並將其設置給 ImageView。

顯然這些只是一些熱門的庫,也有其餘相似的庫。此外這些庫都是由 Square 公司 的牛人開發的。點擊 Square 團隊的開源項目 查看更多。ios

開始吧

Movie Database(TMDb)API 包含全部熱門的、即將上映的、正在上映的電影和電視節目列表。這也是最流行的 API 之一。git

TMDB API 須要 API 密鑰才能請求。爲此:github

在版本控制系統中隱藏 API 密鑰(可選但推薦)

獲取 API 密鑰後,按照下述步驟將其在 VCS 中隱藏。編程

  • 將你的密鑰添加到根目錄下的 local.properties 文件中。
  • build.gradle 中用代碼來訪問密鑰。
  • 以後在程序中經過 BuildConfig 就可使用密鑰了。
//In local.properties
tmdb_api_key = "xxxxxxxxxxxxxxxxxxxxxxxxxx"

//In build.gradle (Module: app)
buildTypes.each {
        Properties properties = new Properties()
        properties.load(project.rootProject.file("local.properties").newDataInputStream())
        def tmdbApiKey = properties.getProperty("tmdb_api_key", "")

        it.buildConfigField 'String', "TMDB_API_KEY", tmdbApiKey
        
        it.resValue 'string', "api_key", tmdbApiKey

}

//In your Constants File
var tmdbApiKey = BuildConfig.TMDB_API_KEY
複製代碼

設置項目

爲了設置項目,咱們首先會將全部必需的依賴項添加到 build.gradle (Module: app) 文件中:後端

// build.gradle(Module: app)
dependencies {

    def moshiVersion="1.8.0"
    def retrofit2_version = "2.5.0"
    def okhttp3_version = "3.12.0"
    def kotlinCoroutineVersion = "1.0.1"
    def picassoVersion = "2.71828"

     
    //Moshi
    implementation "com.squareup.moshi:moshi-kotlin:$moshiVersion"
    kapt "com.squareup.moshi:moshi-kotlin-codegen:$moshiVersion"

    //Retrofit2
    implementation "com.squareup.retrofit2:retrofit:$retrofit2_version"
    implementation "com.squareup.retrofit2:converter-moshi:$retrofit2_version"
    implementation "com.jakewharton.retrofit:retrofit2-kotlin-coroutines-adapter:0.9.2"

    //Okhttp3
    implementation "com.squareup.okhttp3:okhttp:$okhttp3_version"
    implementation 'com.squareup.okhttp3:logging-interceptor:3.11.0'
    
     //Picasso for Image Loading
    implementation ("com.squareup.picasso:picasso:$picassoVersion"){
        exclude group: "com.android.support"
    }

    //Kotlin Coroutines
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlinCoroutineVersion"
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinCoroutineVersion"

   
}
複製代碼

如今建立咱們的 TmdbAPI 服務

//ApiFactory to create TMDB Api
object Apifactory{
  
    //Creating Auth Interceptor to add api_key query in front of all the requests.
    private val authInterceptor = Interceptor {chain->
            val newUrl = chain.request().url()
                    .newBuilder()
                    .addQueryParameter("api_key", AppConstants.tmdbApiKey)
                    .build()

            val newRequest = chain.request()
                    .newBuilder()
                    .url(newUrl)
                    .build()

            chain.proceed(newRequest)
        }
  
   //OkhttpClient for building http request url
    private val tmdbClient = OkHttpClient().newBuilder()
                                .addInterceptor(authInterceptor)
                                .build()


  
    fun retrofit() : Retrofit = Retrofit.Builder()
                .client(tmdbClient)
                .baseUrl("https://api.themoviedb.org/3/")
                .addConverterFactory(MoshiConverterFactory.create())
                .addCallAdapterFactory(CoroutineCallAdapterFactory())
                .build()   

  
   val tmdbApi : TmdbApi = retrofit().create(TmdbApi::class.java)

}
複製代碼

看一下咱們在 ApiFactory.kt 文件中作了什麼。api

  • 首先,咱們建立了一個用以給全部請求添加 api_key 參數的網絡攔截器,名爲 authInterceptor
  • 而後咱們用 OkHttp 建立了一個網絡客戶端,並添加了 authInterceptor。
  • 接下來,咱們用 Retrofit 將全部內容鏈接起來構建 Http 請求的構造器和處理器。此處咱們加入了以前建立好的網絡客戶端、基礎 URL、一個轉換器和一個適配器工廠。 首先是 MoshiConverter,用以輔助 JSON 解析並將響應的 JSON 轉化爲 Kotlin 數據類,若有須要,可進行選擇性解析。 第二個是 CoroutineCallAdaptor,它的類型是 Retorofit2 中的 CallAdapter.Factory,用於處理 Kotlin 協程中的 Deferred
  • 最後,咱們只需將 TmdbApi 類(下節中建立) 的一個引用傳入以前建好的 retrofit 類中就能夠建立咱們的 tmdbApi。

探索 Tmdb API

調用 /movie/popular 接口咱們獲得了以下響應。該響應中返回了 results,這是一個 movie 對象的數組。這正是咱們關注的地方。

{
  "page": 1,
  "total_results": 19848,
  "total_pages": 993,
  "results": [
    {
      "vote_count": 2109,
      "id": 297802,
      "video": false,
      "vote_average": 6.9,
      "title": "Aquaman",
      "popularity": 497.334,
      "poster_path": "/5Kg76ldv7VxeX9YlcQXiowHgdX6.jpg",
      "original_language": "en",
      "original_title": "Aquaman",
      "genre_ids": [
        28,
        14,
        878,
        12
      ],
      "backdrop_path": "/5A2bMlLfJrAfX9bqAibOL2gCruF.jpg",
      "adult": false,
      "overview": "Arthur Curry learns that he is the heir to the underwater kingdom of Atlantis, and must step forward to lead his people and be a hero to the world.",
      "release_date": "2018-12-07"
    },
    {
      "vote_count": 625,
      "id": 424783,
      "video": false,
      "vote_average": 6.6,
      "title": "Bumblebee",
      "popularity": 316.098,
      "poster_path": "/fw02ONlDhrYjTSZV8XO6hhU3ds3.jpg",
      "original_language": "en",
      "original_title": "Bumblebee",
      "genre_ids": [
        28,
        12,
        878
      ],
      "backdrop_path": "/8bZ7guF94ZyCzi7MLHzXz6E5Lv8.jpg",
      "adult": false,
      "overview": "On the run in the year 1987, Bumblebee finds refuge in a junkyard in a small Californian beach town. Charlie, on the cusp of turning 18 and trying to find her place in the world, discovers Bumblebee, battle-scarred and broken. When Charlie revives him, she quickly learns this is no ordinary yellow VW bug.",
      "release_date": "2018-12-15"
    }
  ]
}
複製代碼

所以如今咱們能夠根據該 JSON 建立咱們的 Movie 數據類和 MovieResponse 類。

// Data Model for TMDB Movie item
data class TmdbMovie(
    val id: Int,
    val vote_average: Double,
    val title: String,
    val overview: String,
    val adult: Boolean
)

// Data Model for the Response returned from the TMDB Api
data class TmdbMovieResponse(
    val results: List<TmdbMovie>
)

//A retrofit Network Interface for the Api
interface TmdbApi{
    @GET("movie/popular")
    fun getPopularMovie(): Deferred<Response<TmdbMovieResponse>>
}
複製代碼

TmdbApi 接口:

建立了數據類後,咱們建立 TmdbApi 接口,在前面的小節中咱們已經將其引用添加至 retrofit 構建器中。在該接口中,咱們添加了全部必需的 API 調用,若有必要,能夠給這些調用添加任意參數。例如,爲了可以根據 id 獲取一部電影,咱們在接口中添加了以下方法:

interface TmdbApi{

    @GET("movie/popular")
    fun getPopularMovies() : Deferred<Response<TmdbMovieResponse>>

    @GET("movie/{id}")      
    fun getMovieById(@Path("id") id:Int): Deferred<Response<Movie>>

}
複製代碼

最後,進行網絡調用

接着,咱們最終發出一個用以獲取所需數據的請求,咱們能夠在 DataRepository 或者 ViewModel 或者直接在 Activity 中進行此調用。

密封 Result 類

這是用來處理網絡響應的類。它可能成功返回所需的數據,也可能發生異常而出錯。

sealed class Result<out T: Any> {
    data class Success<out T : Any>(val data: T) : Result<T>()
    data class Error(val exception: Exception) : Result<Nothing>()
}
複製代碼

構建用來處理 safeApiCall 調用的 BaseRepository

open class BaseRepository{

    suspend fun <T : Any> safeApiCall(call: suspend () -> Response<T>, errorMessage: String): T? {

        val result : Result<T> = safeApiResult(call,errorMessage)
        var data : T? = null

        when(result) {
            is Result.Success ->
                data = result.data
            is Result.Error -> {
                Log.d("1.DataRepository", "$errorMessage & Exception - ${result.exception}")
            }
        }


        return data

    }

    private suspend fun <T: Any> safeApiResult(call: suspend ()-> Response<T>, errorMessage: String) : Result<T>{
        val response = call.invoke()
        if(response.isSuccessful) return Result.Success(response.body()!!)

        return Result.Error(IOException("Error Occurred during getting safe Api result, Custom ERROR - $errorMessage"))
    }
}
複製代碼

構建 MovieRepository

class MovieRepository(private val api : TmdbApi) : BaseRepository() {
  
    fun getPopularMovies() : MutableList<TmdbMovie>?{
      
      //safeApiCall is defined in BaseRepository.kt (https://gist.github.com/navi25/67176730f5595b3f1fb5095062a92f15)
      val movieResponse = safeApiCall(
           call = {api.getPopularMovie().await()},
           errorMessage = "Error Fetching Popular Movies"
      )
      
      return movieResponse?.results.toMutableList();
    
    }

}
複製代碼

建立 ViewModel 來獲取數據

class TmdbViewModel : ViewModel(){
  
    private val parentJob = Job()

    private val coroutineContext: CoroutineContext
        get() = parentJob + Dispatchers.Default

    private val scope = CoroutineScope(coroutineContext)

    private val repository : MovieRepository = MovieRepository(ApiFactory.tmdbApi)
    

    val popularMoviesLiveData = MutableLiveData<MutableList<ParentShowList>>()

    fun fetchMovies(){
        scope.launch {
            val popularMovies = repository.getPopularMovies()
            popularMoviesLiveData.postValue(popularMovies)
        }
    }


    fun cancelAllRequests() = coroutineContext.cancel()

}
複製代碼

在 Activity 中使用 ViewModel 更新 UI

class MovieActivity : AppCompatActivity(){
    
    private lateinit var tmdbViewModel: TmdbViewModel
  
     override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_movie)
       
        tmdbViewModel = ViewModelProviders.of(this).get(TmdbViewModel::class.java)
       
        tmdbViewModel.fetchMovies()
       
        tmdbViewModel.popularMovies.observe(this, Observer {
            
            //TODO - Your Update UI Logic
        })
       
     }
  
}
複製代碼

本文是 Android 中一個基礎但卻全面的產品級別的 API 調用的介紹。更多示例,請訪問此處

祝編程愉快!

若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。


掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索