- 原文地址:Android Networking in 2019 — Retrofit with Kotlin’s Coroutines
- 原文做者:Navendra Jha
- 译文出自:掘金翻译计划
- 本文永久连接:github.com/xitu/gold-m…
- 译者:feximin
2018 年,Android 圈发生了许多翻天覆地的变化,尤为是在 Android 网络方面。稳定版本的 Kotlin 协程的发布极大地推进了 Android 在处理多线程方面从 RxJava 到 Kotlin 协程的发展。 本文中,咱们将讨论在 Android 中使用 Retrofit2 和 Kotlin 协程 进行网络 API 调用。咱们将调用 TMDB API 来获取热门电影列表。html
若是你在 Android 网络方面有经验而且在使用 Retrofit 以前进行过网络调用,但可能使用的是 RxJava 而不是 Kotlin 协程,而且你只想看看实现方式,请查看 Github 上的 readme 文件。前端
简而言之,Android 网络或者任何网络的工做方式以下:java
Android 中,咱们使用:android
显然这些只是一些热门的库,也有其余相似的库。此外这些库都是由 Square 公司 的牛人开发的。点击 Square 团队的开源项目 查看更多。ios
Movie Database(TMDb)API 包含全部热门的、即将上映的、正在上映的电影和电视节目列表。这也是最流行的 API 之一。git
TMDB API 须要 API 密钥才能请求。为此:github
获取 API 密钥后,按照下述步骤将其在 VCS 中隐藏。编程
//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" } 复制代码
//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
CallAdapter.Factory
,用于处理 Kotlin 协程中的 Deferred
。调用 /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 中进行此调用。
这是用来处理网络响应的类。它可能成功返回所需的数据,也可能发生异常而出错。
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>() } 复制代码
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")) } } 复制代码
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(); } } 复制代码
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() } 复制代码
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 连接。
掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 Android、iOS、前端、后端、区块链、产品、设计、人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划、官方微博、知乎专栏。