Today we will see how to use Retrofit to call an API multiple times and cancel all previous calls when making a new call. In addition, we will see how to use coroutines with Retrofit.
Use-Case: When implementing the search function in the App – we request the API every time the user enters something in EditText (Trying to get the search done in real time) and cancels all API calls before.
First, we need to add the Retrofit
and Coroutines
to the Gradle file. Add these lines to App-level build.gradle
:
1 2 3 4 5 6 7 8 9 | // Retrofit implementation 'com.squareup.retrofit2:retrofit:2.6.1' implementation 'com.squareup.retrofit2:converter-gson:2.6.1' implementation 'com.squareup.okhttp3:logging-interceptor:4.1.1' // Kotlin Coroutines implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.3' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.3' |
Now create an interface for retrofit named ApiService.
1 2 3 4 5 6 7 8 9 10 | interface ApiService { companion object { val BASE_URL: String = "https://demo.dataverse.org/api/" } @GET("search") suspend fun getResult( @Query("q") query: String?): Response<SearchResultModel?> } |
We use the demo API from https://demo.dataverse.org
, this is a free demo API service we use for testing. It will provide search results according to the user’s query.
We use the Kotlin suspend
function instead of the normal function because we will use coroutines to manage the API.
Now create a data class
for this API named SearchResultModel
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | data class SearchResultModel( val `data`: Data?, val status: String? ) { data class Data( val count_in_response: Int?, val items: List<Item?>?, val q: String?, val spelling_alternatives: SpellingAlternatives?, val start: Int?, val total_count: Int? ) { data class Item( val checksum: Checksum?, val dataset_citation: String?, val dataset_id: String?, val dataset_name: String?, val dataset_persistent_id: String?, val file_content_type: String?, val file_id: String?, val file_type: String?, val md5: String?, val name: String?, val published_at: String?, val size_in_bytes: Int?, val type: String?, val url: String? ) { data class Checksum( val type: String?, val value: String? ) } class SpellingAlternatives( ) } } |
Now, create an API provider object
file named ApiProvider
, this object will be used to create a retrofit instance:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | object ApiProvider { private val retrofit = Retrofit.Builder() .baseUrl(ApiService.BASE_URL) .client(getHttpClient()) .addConverterFactory(GsonConverterFactory.create()) .build() private fun getHttpClient(): OkHttpClient { val logging = HttpLoggingInterceptor() logging.level = HttpLoggingInterceptor.Level.BODY val httpClient = OkHttpClient.Builder() httpClient.addInterceptor(logging) return httpClient.build() } fun <S> createService(serviceClass: Class<S>?): S { return retrofit.create(serviceClass) } } |
Here we are creating the OkHttp client to log requests and response APIs and set up Retrofit. The retrofit
variable, used to get an instance of the Retrofit client.
We will use repository classes / objects to process data.
So, we create an object called DataRepository
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | object DataRepository { var apiService: ApiService? = null var searchResultsLiveData: MutableLiveData<GenericDataModel<SearchResultModel>?> = MutableLiveData() init { apiService = ApiProvider.createService( ApiService::class.java ) } suspend fun getSearchResults(searchQuery: String?) { searchResultsLiveData.postValue(null) val result = apiService?.getResult(searchQuery) val genericDataModel = GenericDataModel( result?.isSuccessful, result?.body(), result?.message() ) searchResultsLiveData.postValue(genericDataModel) } } |
Here, we first created an instance of APIService in the init
block, to initialize retrofit
whenever the DataRepository
was initialized.
Note: Since we are using an object instead of a class, only one instance will be created and shared on the application.
Now we will create a generic data class
, this will be a wrapper class to receive all responses from API calls. We use a wrapper class with LiveData
so we can notify our views whenever there’s data from the API.
Create a data class
named GenericDataModel
.
1 2 | data class GenericDataModel<T>(val isSuccess: Boolean?, val data: T?, val message: String?) |
Also, note that we are declaring the genetic T
data type so we can reuse this class for all Responses types.
Now this is the last part of the API call in practice. In Activity, create a function named startSearching
pass the parameter of type String
. This method will be called each time when the user types anything to search:
1 2 3 | private fun startSearching(searchQuery: String?) { } |
Now we will create a Coroutine
in function startSearching
, it will call the API and perform the rest of the process in a background thread, and finally it sends the results back to the main thread.
1 2 3 4 5 6 7 8 | private fun startSearching(searchQuery: String?) { CoroutineScope(Dispatchers.IO).launch { DataRepository.getSearchResults(searchQuery) withContext(Dispatchers.Main) { } } } |
In the above method, we can see that we have provided Dispatchers.IO
for coroutine scope, which means that everything inside will be called in the background thread. Also, the withContext(Dispatchers.Main)
function inside Coroutine will run anything inside it back to the main thread.
Now, take an instance of coroutine and create a global variable to store that instance. This is done to avoid creating multiple coroutines for each call and canceling the previous request before performing a new search. Create a global variable of type Job
and return an instance of coroutine to this variable in the startSearching
method.
1 2 | private var apiJob: Job? = null |
Now here all the important things happen, we don’t need to manage anything here. We don’t have to wait for a response from the API, no need to use any callbacks. All of this is managed by retrofit itself because they support coroutines with suspend
functions.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | private fun startSearching(searchQuery: String?) { apiJob = CoroutineScope(Dispatchers.IO).launch { DataRepository.getSearchResults(searchQuery) withContext(Dispatchers.Main) { DataRepository.searchResultsLiveData.observe( <a class="__cf_email__" href="/cdn-cgi/l/email-protection">[email protected]</a> , Observer { genericDataModel: GenericDataModel<SearchResultModel>? -> run { if (genericDataModel?.isSuccess == true) { val data = genericDataModel.data if (data?.status.equals("OK", true)) { resultTextView?.text = data.toString() resultTextView?.visibility = View.VISIBLE progressLoading?.visibility = View.GONE } } else { resultTextView?.text = "No data" resultTextView?.visibility = View.VISIBLE progressLoading?.visibility = View.GONE } } }) } } } |
Now, add TextWatcher
on EditText
to listen to all the facts whenever users start typing to begin the Search Query API calls. Alternatively, we can create an extension
function for this.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | searchEditText?.addTextChangedListener(object: TextWatcher { override fun afterTextChanged(s: Editable?) { resultTextView?.visibility = View.GONE progressLoading?.visibility = View.VISIBLE apiJob?.cancel() startSearching(s.toString()) } override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) { } override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { } }) |
Note that inside afterTextChanged
, we canceled CoroutineJob
earlier so it automatically canceled the previous API calls. Then call startSearching
method so the new API call will be made.
This is how we can manage the cancellation of previous API calls when making new calls to only get results for the very latest and last API calls.
Ref: https://proandroiddev.com/retrofit-cancelling-multiple-api-calls-4dc6b7dc0bbd