Internet

Most Android apps do HTTP requests to some servers (database/...). Most of the time, there is an API in place instead of direct access. API calls are usually done in a ViewModel class, inside a method starting a new thread/coroutine - to avoid blocking the main thread.

Add the permission in your AndroidManifest.xml (above application) ⭐

<uses-permission android:name="android.permission.INTERNET" />
Note about localhost

⚠️ Note that localhost/127.0.0.1 isn't available from your Android application. You should use 10.0.2.2 instead.

You will also have to edit AndroidManifest.xml and allow HTTP

    <application
+        android:usesCleartextTraffic="true"

There are many libraries that you may use at some point

  • retrofit (40.9k ⭐): HTTP library
  • Moshi (8.7k ⭐): JSON library
  • Gson (21.7k ⭐): JSON library
  • okhttp (43.3k ⭐): HTTP client used by retrofit/fuel/...
  • fuel (4.3k ⭐, πŸ‘»): HTTP library
  • volley (3.3k ⭐, πŸ‘»): HTTP library

What I defined as HTTP libraries are libraries that provide an interface to an HTTP client, so they aren't the ones doing the request.


Retrofit

Retrofit is the most used HTTP client.

implementation "com.squareup.retrofit2:retrofit:2.9.0"
implementation "com.squareup.retrofit2:converter-scalars:2.9.0"

Use the API

In an Activity, you will be able to fetch results with the code below.

CoroutineScope(Dispatchers.IO).launch {
    Log.d("TAG", RetrofitService.instance.getAllPosts())
}

But, you should do it inside a ViewModel with viewModelScope

viewModelScope.launch {
    Log.d("TAG", RetrofitService.instance.getAllPosts())
}

interface xxxAPI

In the example, this interface is called JsonPlaceholderAPI. Name it however you want, but inside you will declare the methods that you will be able to use in the code. See the documentation.

// create an interface with the routes+methods
interface JsonPlaceholderAPI {
    // GET /posts
    @GET("posts") suspend fun getAllPosts() : String
    // GET /posts?id=value
    @GET("posts") suspend fun getPost(@Query("id") id: Int) : String
    // GET /posts/:id
    @GET("posts/{id}") suspend fun getPost(@Path("id") id: Int) : String
    // POST /posts with parameters wrapped in a class
    @POST("posts") suspend fun getPost(@Body body: String) : String
    // POST /posts with parameters provided one by one
    @POST("posts") @FormUrlEncoded suspend fun login(@Field("xxx") xxx: String/*, ...*/) : String
}

You can remove the return type if the result does not interest you.

RetrofitService

package xxx

import retrofit2.Retrofit
import retrofit2.converter.scalars.ScalarsConverterFactory
import retrofit2.http.GET

private const val BASE_URL = "https://jsonplaceholder.typicode.com"

private val retrofit = Retrofit.Builder()
    .addConverterFactory(ScalarsConverterFactory.create())
    .baseUrl(BASE_URL)
    .build()

// create an interface with the routes+methods
interface JsonPlaceholderAPI {
    @GET("posts")
    suspend fun getAllPosts() : String
}

object RetrofitService {
    val instance : JsonPlaceholderAPI by lazy {
        retrofit.create(JsonPlaceholderAPI::class.java) }
}
Make this code yours
- package xxx
+ package com.my.app

import retrofit2.Retrofit
import retrofit2.converter.scalars.ScalarsConverterFactory
import retrofit2.http.GET

- private const val BASE_URL = "https://jsonplaceholder.typicode.com"
+ private const val BASE_URL = "https://MY_URL_HERE"

private val retrofit = Retrofit.Builder()
    .addConverterFactory(ScalarsConverterFactory.create())
    .baseUrl(BASE_URL)
    .build()

- interface JsonPlaceholderAPI {
+ interface MyAPI {
-    @GET("posts")
-    suspend fun getAllPosts() : String
+    // add your methods here
}

object RetrofitService {
-    val instance : JsonPlaceholderAPI by lazy {
-        retrofit.create(JsonPlaceholderAPI::class.java) }
+    val instance : MyAPI by lazy {
+        retrofit.create(MyAPI::class.java) }
}

Expected result

Any request will return a big string with the result inside. The JSON isn't converted to Kotlin as we haven't used any converted yet.

➑️That's why every method inside "xxxAPI" returns a String.

➑️That's why the POST's request Body has the type String.

The output is available in the logcat tab after a few seconds.


Retrofit - Moshi

Moshi is one of the converters supported by Retrofit. It's very similar to the popular Gson converter.

Replace the previous imports with the two below

implementation 'com.squareup.moshi:moshi-kotlin:1.13.0'
implementation 'com.squareup.retrofit2:converter-moshi:2.9.0'

Then, you need to replace the previous "default" converter

- import retrofit2.converter.scalars.ScalarsConverterFactory
+ import com.squareup.moshi.Moshi
+ import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
+ import retrofit2.converter.moshi.MoshiConverterFactory

+ private val moshi = Moshi.Builder()
+    .add(KotlinJsonAdapterFactory())
+    .build()

private val retrofit = Retrofit.Builder()
-    .addConverterFactory(ScalarsConverterFactory.create())
+    .addConverterFactory(MoshiConverterFactory.create(moshi))
    .baseUrl(BASE_URL)
    .build()

Then, you need to define a class to store the JSON responses

// { "userId": 1, "id": 1, "title": "xxx", "body": "yyy" }
data class Post(val userId: Int,
                val id: Int,
                val title: String,
                val body: String)

Then, you can replace the return type of your methods

interface JsonPlaceholderAPI {
    @GET("posts")
-    suspend fun getAllPosts() : String
+    suspend fun getAllPosts() : List<Post>
}

And now we can adapt our code.

CoroutineScope(Dispatchers.IO).launch {
-    Log.d("TAG", RetrofitService.instance.getAllPosts())
+    val posts = RetrofitService.instance.getAllPosts()
+    posts.forEach {
+        Log.d("TAG", "Post title is: "+it.title)
+    }
}

And the output in the console will be

Post title is: sunt aut [...]
Post title is: qui est esse [...]
[...]

Useful stuff

Different names for API fields and attributes
// { player_name: "toto" }
// store "player_name" inside "playerName" 
data class Player(
    @Json(name = "player_name") val playerName: String)
Multiple response formats

After looking around, I found a lot of alternatives, but I didn't like them, so I made my own.

private val moshi = Moshi.Builder()
+    .add(XXXXJsonAdapter)
    .add(KotlinJsonAdapterFactory())
    .build()

Then, create an Adapter that will map to results to one data class

data class Post(val userId: Int,
                val id: Int,
                val title: String,
                val body: String,
+               val error: String?)
// OK: { "userId": 1, "id": 1, "title": "xxx", "body": "yyy" }
// ERROR: { "message": "xxx" }
object XXXJsonAdapter {
    @Suppress("unused") @FromJson
    fun parse(json: Map<*, *>) : Post {
        // error result
        if (json.containsKey("message")) {
            return Post(
                -1, -1, "", "", json["message"] as String
            )
        }
        // normal result
        return Post(
            json["userId"] as Int,
            json["id"] as Int,
            json["title"] as String,
            json["body"] as String,
            null // no error
        )
    }
}

Then in the code, simply check if error is null or not.

Prepare/Format results

See Custom Type Adapters for better examples. See also the example above for multiple response formats.

private val moshi = Moshi.Builder()
+    .add(XXXXJsonAdapter)
    .add(KotlinJsonAdapterFactory())
    .build()
// mochi will first parse the result in XXX
data class XXX(@Json(name = "type") val type : Int)
// but, we want to use an enum instead of an Int
enum class YYY {
    AAA,
    BBB,
    CCC
}
object XXXJsonAdapter {
    @Suppress("unused") @FromJson
    fun parse(xxxJson: XXX): YYY {
        return when(xxxJson.type) {
            1 -> YYY.AAA
            2 -> YYY.BBB
            3 -> YYY.CCC
        }
    }
}
// in your interface xxxAPI
suspend fun getXXX() : YYY

Retrofit: cookies/sessions

By default, Retrofit does not store/load cookies. It can be solved easily by creating a CookieJar.

+ import okhttp3.OkHttpClient

private val retrofit = Retrofit.Builder()
    .addConverterFactory(MoshiConverterFactory.create(moshi))
+    .client(OkHttpClient().newBuilder().cookieJar(SimpleCookieJar()).build())
    .baseUrl(BASE_URL)
    .build()

The SimpleCookieJar is a class that stores cookies received from the server, and loads them in the next requests.

import okhttp3.Cookie
import okhttp3.CookieJar
import okhttp3.HttpUrl

class SimpleCookieJar : CookieJar {
    private var _cookies : MutableList<Cookie> = mutableListOf()

    override fun saveFromResponse(url: HttpUrl, cookies: MutableList<Cookie>) {
        _cookies = cookies
    }

    override fun loadForRequest(url: HttpUrl): MutableList<Cookie> {
        return _cookies
    }
}

Coil: Load images from the internet

You can use the coil (8.8k ⭐) library.

implementation "io.coil-kt:coil:1.1.1"

Example of working imageURL

val imageURL = "https://hdqwalls.com/download/far-cry-5-australian-cattle-dog-5k-du-2160x3840.jpg"

And we will consider an ImageView with the Id "image_view".


Use with findViewById

view.findViewById<ImageView>(R.id.image_view).load(imageURL)

Use with View Binding

binding.imageView.load(imageURL)

Add a placeholder/handle loading errors

imgView.load(imageURL) {
    placeholder(R.drawable.a_loading_image)
    error(R.drawable.a_broken_image)
}

Use with Data Binding

I assume here that you're already familiar with data binding.

<!-- xxx is a variable in <data> with an attribute imageURL -->
<!--You must import app (xmlns:app="http://schemas.android.com/apk/res-auto" in <layout>)  -->
<!-- we will create loadImage -->
<ImageView
    [...]
    app:loadImage="@{xxx.imageURL}" />
// see binding adapter, apply for 'kotlin-kapt'
object BindingAdapters {
    // you can change the first/second argument name/type
    // according to the component on which you use it
    // and the type of the value you are passing to it
    @BindingAdapter("app:loadImage") @JvmStatic
    fun someMethodName(imageView: ImageView, imageUrl: String) {
        imageView.load(imageUrl)
    }
}

Convert any URL to HTTPS

val imgUri = imageURL.toUri().buildUpon().scheme("https").build()
imgView.load(imgUri)

πŸ‘» To-do πŸ‘»

Stuff that I found, but never read/used yet.

suspend fun xxx(YYY): Response<List<XXX>>

val response = XXX.xxx.xxx()
if (!response.isSuccessful) {
    /* ... */
    return@launch
}
val result = response.body()!!
/* ... */
@Multipart @POST("xxx")
suspend fun xxx(@Part avatar: MultipartBody.Part): YYY
val okHttpClient = OkHttpClient.Builder()
    .addInterceptor(HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY))
    .addInterceptor { chain ->
        val newRequest = chain.request().newBuilder()
            .addHeader("XXX", "YYY")
            .build()
        chain.proceed(newRequest)
   }
   .build()

.client(okHttpClient)
val jsonSerializer = Json {
    ignoreUnknownKeys = true
    coerceInputValues = true
}
.addConverterFactory(jsonSerializer.asConverterFactory("application/json".toMediaType()))