[译] 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前端后端区块链产品设计人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏

相关文章
相关标签/搜索