В 2018 году произошли серьезные изменения мира Android, особенно касательно Android Networking. Многие перешли от использования RxJava к Kotlin Coroutines, для обработки многопоточности в Android.
Поговорим о том, как выполнить вызов Android Networking API с помощью Retrofit2 и Kotlin Coroutines, и сделаем сетевой вызов API TMDB для получения популярных фильмов.
Вкратце об Android Networking
В сущности, android networking, как и любой другой networking, работает следующим образом:
- Request — Выполнение HTTP-запроса к URL-адресу (конечной точке — endpoint) с соответствующими заголовками. При необходимости используется ключ авторизации.
- Response — Запрос возвращает ответ, содержащий error или success. В случае успешного завершения запроса, ответ содержит содержимое endpoint (обычно в формате JSON)
- Parse & Store — Парсинг JSON, получение необходимых значений и размещение их в классе данных.
В Android используются:
- Okhttp — Для создания HTTP-запроса с соответствующими заголовками.
- Retrofit — Для создания запроса
- Moshi / GSON — Для парсинга данных JSON
- Kotlin Coroutines — Для создания сетевых запросов, не блокирующих основной поток.
- Picasso / Glide —Для скачивания и установки изображения в ImageView.
Начнем
В API TMDb (The Movie Database) находится список всех популярных, недавно вышедших, а также готовящихся к выпуску фильмов и сериалов. Это один из самых популярных API для различных экспериментов.
Для создания запросов в API TMDB требуется ключ API. Для этого нужно:
- Создать аккаунт на TMDB
- Выполнить следующие действия для регистрации ключа API.
Скрытие ключа API в Version Control (Не обязательное, но рекомендуемое действие)
После получения ключа API выполните следующие действия, чтобы скрыть его в VCS.
- Добавьте ключ в local.properties, находящийся в корневой папке (root folder).
- Получите доступ к ключу в 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.
- Сначала создаем Network Interceptor, чтобы добавить api_key во все запросы в качестве authInterceptor.
- Затем создаем сетевого клиента с помощью OkHttp и добавляем authInterceptor.
- Теперь соединяем все вместе, чтобы создать строителя (builder) и обработчика (handler) HTTP-запроса с помощью Retrofit. Здесь добавляем ранее созданного сетевого клиента, базовый URL, а также конвертер и адаптер.
MoshiConverter участвует в парсинге JSON и конвертирует ответ JSON в класс данных Kotlin, при необходимости используя селективный парсинг.
CoroutineCallAdaptor — этоCallAdapter.Factory
Retrofit2 для значенияDeferred
Kotlin coroutine. - Наконец создаем tmdbApi, передав ссылку на класс TmdbApi (Создание с следующем пункте) к ранее созданному классу retrofit.
Поиск по API Tmdb
Получаем следующий ответ для endpoint /movie/popular. Ответ возвращает результаты — массив объекта 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"
}
]
}
Теперь создаем класс данных Movie и класс MovieResponse в соответствии с json.
// 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. В интерфейс TmdbApi добавляем требуемые вызовы 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 Result Class
Класс, который используется для обработки ответа сети. Он может иметь значение Success с необходимыми данными или Error с исключением.
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>()
}
Построение BaseRepository для обработки safeApiCall
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();
}
}
Создание View Model для получения данных
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()
}
Использование ViewModel в Activity для обновления 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
})
}
}
Мы рассмотрели базовый полнофункциональный вызов API для Android. Больше примеров вы найдете здесь.
Удачного программирования!
Перевод статьи Navendra Jha: Android Networking in 2019 — Retrofit with Kotlin’s Coroutines