Development of mobile applications on Android today is impossible without integration with external services - be it weather data, social networks or cloud storage. API (Application Programming Interface) becomes a bridge between your application and remote resources, allowing you to exchange data in real time. However, working with APIs in Android Studio requires not only knowledge of syntax, but also an understanding of network protocols, asynchronous processing and security.
This article will help you figure out how to properly organize network requests in Androidprojects, avoiding common mistakes. We will look at modern approaches using Retrofit, OkHttp and Kotlin Coroutines, and also touch on issues of caching, error handling and performance optimization. If you are just starting to work with APIs or want to systematize your knowledge, this material is for you.
What is an API and why do you need it in Android applications
API (Application Programming Interface) is a set of rules and protocols that allows different apps to interact with each other. In the context of Androiddevelopment, an API is most often RESTful serviceproviding data in the format JSON or XML via the HTTP protocol.
Examples of using the API in mobile applications:
- ๐ฆ๏ธ Obtaining a weather forecast from services like OpenWeatherMap or AccuWeather
- ๐ฑ User authorization via Google Sign-In or Facebook Login
- ๐ Displaying currency or stock rates in financial applications
- ๐ต Streaming music or video from YouTube API or Spotify API
Without the API, many functions of modern applications would be impossible. For example, instant messengers use APIs to send messages, maps to display routes, and social networks to download posts. It is important to understand that working with the API always involves asynchronous data processing, since network requests should not block the main thread (UI-thread) of the application.
โ ๏ธ Attention: Starting from Android 9 (API 28), applications by default cannot perform network requests in the main stream. This will lead to a releaseNetworkOnMainThreadException. Always useCoroutines,RxJavaorAsyncTask(obsolete) to work with the network.
Project preparation: dependencies and permissions
Before you start working with the API, you must configure the project in Android Studio. This includes adding libraries for network requests and specifying permissions in AndroidManifest.xml.
Basic steps:
- Add in
build.gradle (Module: app)dependencies forRetrofit,OkHttpandGson:implementation 'com.squareup.retrofit2:retrofit:2.9.0'implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
implementation 'com.squareup.okhttp3:logging-interceptor:4.9.3' - Specify internet access permission in
AndroidManifest.xml:<uses-permission android:name="android.permission.INTERNET" /> - For Android 10 (API 29) and above, add
android:usesCleartextTraffic="true"to the tag<application>if you useHTTP(not recommended for production).
It is also recommended to add logging-interceptor for debugging network requests. This will help you see complete request/response logs Logcat.
| Library | Advantages | Disadvantages |
|---|---|---|
Retrofit |
Convenient syntax, support Coroutines, converters for JSON/XML |
Requires additional dependencies for logging |
Volley |
Built-in Android, simple for basic queries | Not supports Coroutines, limited functionality |
OkHttp |
Low-level control, support WebSockets |
More complex syntax compared to Retrofit |
Creating a data model and API interface
Before sending requests, you must determine data modelwhich will correspond to the structure of the response from the server. For example, if the API returns information about the user in the format:
{"id": 1,
"name": "John Doe",
"email": "john@example.com"
}
Then Kotlin it will look like this:
data class User(val id: Int,
val name: String,
val email: String
)
Next we create API interfacethat describes the available endpoints. For example, for a service JSONPlaceholder:
interface ApiService {@GET("users/{id}")
suspend fun getUser(@Path("id") userId: Int): Response<User>
@GET("posts")
suspend fun getPosts(@Query("userId") userId: Int): Response<List<Post>>
}
Pay attention to the annotations:
@GETโ indicates the type of HTTP request@Pathโ for dynamic parameters in the URL (for example,/users/{id})@Queryโ for query parameters (for example,?userId=1)suspendโallows the function to be used in coroutines
Always check the API documentation for required headers (for example, Authorization or Content-Type). They can be added using an annotation. @Headers or interceptor in OkHttp.
Initializing Retrofit and executing requests
Now that the model and interface are ready, you need to initialize RetrofitThis is usually done in a separate singleton class:
object RetrofitClient {private const val BASE_URL = "https://jsonplaceholder.typicode.com/"
private val okHttpClient = OkHttpClient.Builder()
.addInterceptor(HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY
})
.build()
val apiService: ApiService by lazy {
Retrofit.Builder()
.baseUrl(BASE_URL)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(ApiService::class.java)
}
}
To execute the request, use Coroutines. For example, to get user data in ViewModel:
viewModelScope.launch {try {
val response = RetrofitClient.apiService.getUser(1)
if (response.isSuccessful) {
val user = response.body()
// We update the UI via LiveData or StateFlow
} else {
// Error processing (for example, response.code() == 404)
}
} catch (e: Exception) {
// Handling network errors (for example, there is no Internet)
}
}
Coroutines s viewModelScope or lifecycleScope is a modern and recommended way.
Check your internet connection|Add error handling (404, 500, etc.)|Use try-catch for exceptions|Update UI only in the main thread (withContext(Dispatchers.Main))
-->
Handling errors and exceptions
Errors when working with the API are divided into several categories:
- ๐ด HTTP errors (codes 4xx, 5xx) - for example,
404 Not Foundor500 Internal Server Error - ๐ Network errors โno internet, connection timeout
- ๐ฑ Parsing errors โinconsistency in the response structure of the data model
- ๐ Authentication errors โinvalid or expired token
API-key
An example of error handling in Retrofit:
try {val response = apiService.getUser(1)
if (!response.isSuccessful) {
when (response.code()) {
401 -> { / Authorization error / }
404 -> { / User not found / }
else -> { / Other errors / }
}
}
} catch (e: IOException) {
// Network error (for example, no internet)
} catch (e: HttpException) {
// HTTP error (for example, 500)
}
โ ๏ธ Attention: Never ignore errors in production! The user should see a clear message (for example, "Check your Internet connection"), not application crash. Use try-catch even for "reliable" APIs.
To simplify processing, you can create an extension for Response:
fun <T> Response<T>.handleResponse(): Result<T> {return if (isSuccessful) {
Result.Success(body()!!)
} else {
Result.Error(code(), message())
}
}
Caching and optimizing network requests
Frequent network requests can slow down the application and increase traffic consumption Solutions:
- ๐ Caching โsaving API responses on the device using
OkHttp CacheorRoom Database - ๐ Update by timer โrequest data no more than once every N minutes
- ๐ฆ Pagination โ loading data in portions (for example, 20 elements at a time)
- ๐ถ Offline mode โ use
WorkManagerto synchronize data in the background
An example of setting up a cache in OkHttp:
val cacheSize = 10 1024 1024 // 10 MBval cache = Cache(context.cacheDir, cacheSize.toLong())
val okHttpClient = OkHttpClient.Builder()
.cache(cache)
.build()
To implement an offline mode, you can use the strategy "Cache-First":
@GET("posts")fun getPosts(
@Query("userId") userId: Int,
@Header("Cache-Control") cacheControl: String = "public, max-stale=2419200"
): Response<List<Post>>
โ ๏ธ Attention: Caching sensitive data (for example, personal information of users) may violate GDPR other privacy laws. Always encrypt the cache or store only non-critical data.
Security: HTTPS, tokens and API keys
Security is a critical aspect when working with APIs. Basic measures:
- Use HTTPS โ Android by default blocks unsecured
HTTPconnections starting from API 28. - Keep API keys secure โdo not hardcode them in the code! Use
local.propertiesor Android Secrets Gradle Plugin. - Use
OAuth 2.0orJWTfor authentication. - Check certificates โ configure
CertificatePinnerinOkHttpto protect against MITM attacks.
Example of adding a token to the headers:
@Headers("Authorization: Bearer {token}")@GET("private/data")
suspend fun getPrivateData(): Response<Data>
For To dynamically add a token, use the interceptor:
class AuthInterceptor(private val token: String) : Interceptor {override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request().newBuilder()
.addHeader("Authorization", "Bearer $token")
.build()
return chain.proceed(request)
}
}
Don't forget about Android Keystore to store sensitive data (for example, tokens). The library AndroidX Security provides a convenient EncryptedSharedPreferences for these purposes.
What is a MITM attack?
Man-in-the-Middle (MITM) is a type of cyber attack in which an attacker intercepts and possibly modifies messages between two parties (for example, your application and the server). For protection, use HTTPS certificate verification and Certificate Pinning.
Testing and debugging of API requests
Testing network requests is a mandatory stage of development. Tools that will help:
- ๐ MockWebServer โ for creating a โfakeโ server in tests
- ๐ Charles Proxy or Fiddler โ for intercepting and analyzing traffic
- ๐ Postman or Insomnia โ for manual testing endpoints
- ๐ค Espresso + IdlingResource โ for UI tests with network requests
Example test with MockWebServer:
@Testfun `getUser returns correct data`() = runTest {
val mockResponse = """{"id":1,"name":"Test User"}"""
mockWebServer.enqueue(
MockResponse()
.setBody(mockResponse)
.setResponseCode(200)
)
val response = apiService.getUser(1)
assertTrue(response.isSuccessful)
assertEquals("Test User", response.body()?.name)
}
For debugging on a real device, it is useful to use Stetho โ a library from Facebook, which allows you to view network requests right in Chrome DevTools.
Always test API requests on real devices with different connection types (Wi-Fi, 4G, offline). The emulator does not always show real network problems.
โ How to check if the API supports compression (gzip)?
Many APIs support compression of responses to reduce traffic. To test this, add data-i="204">to the request headers and look at the response headers. If the server returns Accept-Encoding: gzip and look at the response headers. If the server returns Content-Encoding: gzip, then the compression is working. In OkHttp compression is turned on automatically.
โ Is it possible to use Retrofit without Kotlin Coroutines?
Yes, Retrofit supports other methods of asynchronous processing:
- C
RxJavaโ add a dependencyretrofit2:adapter-rxjava2and useSingleorObservable. - C
CallbacksโspecifyCallback<T>in the interface method (outdated approach).
However, Coroutines is the most modern and recommended option for new projects.
โ How to process a large response from the API (for example, 10,000 records)?
For large data, use:
- Pagination โ request data in chunks (for example, 50 records at a time).
- Stream processing โ if the API supports
streaming, useOkHttpwithsource(). - Lazy loading โ in RecyclerView load elements as you scroll.
Avoid loading all the data at once - this can lead to OutOfMemoryError.
โ Do you need to encrypt API keys in the application?
Yes, hardcoding API keys in code is a bad practice. Alternatives:
- Store keys in
local.properties(exclude the file fromgit). - Use Firebase Remote Config to dynamically issue keys.
- Use Android NDK to hide keys in native code (difficult to reverse engineer).
Remember: any key built into the APK can be extracted by decompilation.. For critical data, use a backend proxy.
โ How to debug requests if the server requires specific headers?
If the API requires complex headers (for example, a request signature), use:
OkHttp Logging Interceptorโ to see the full onesrequest/response.Postmanโ for manual testing of headers before integration into the code.MockWebServerโ to simulate server responses in tests.
For dynamic headers (for example, a signature), write custom interceptors in OkHttp.