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 release NetworkOnMainThreadException. Always use Coroutines, RxJava or AsyncTask (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:

  1. Add in build.gradle (Module: app) dependencies for Retrofit, OkHttp and Gson:
    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'

  2. Specify internet access permission in AndroidManifest.xml:
    <uses-permission android:name="android.permission.INTERNET" />
  3. For Android 10 (API 29) and above, add android:usesCleartextTraffic="true" to the tag <application>if you use HTTP (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.

๐Ÿ“Š Which library do you use to work with the API?
Retrofit
Volley
OkHttp without Retrofit
Ktor
Other
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 Found or 500 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 Cache or Room 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 WorkManager to synchronize data in the background

An example of setting up a cache in OkHttp:

val cacheSize = 10  1024  1024 // 10 MB

val 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:

  1. Use HTTPS โ€” Android by default blocks unsecured HTTPconnections starting from API 28.
  2. Keep API keys secure โ€”do not hardcode them in the code! Use local.properties or Android Secrets Gradle Plugin.
  3. Use OAuth 2.0 or JWT for authentication.
  4. Check certificates โ€” configure CertificatePinner in OkHttp to 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:

@Test

fun `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 dependency retrofit2:adapter-rxjava2 and use Single or Observable.
  • C Callbacks โ€”specify Callback<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, use OkHttp with source().
  • 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 from git).
  • 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 ones request/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.