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.
- glide (33.2k β, images)
- Fuel example (stripe)
- Retrofit errors
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()))