- 原文地址: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、前端、後端、區塊鏈、產品、設計、人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。