I'm fairly new to Android and Java / Kotlin so I've been struggling to implement cookies in the recommended architecture. I looked in many places, read the documentation and watched many videos and everyone had such different ways to implement things that I was still confused. How does it all fit together?
I would have thought this was such a common use case that I can't believe the answer isn't all over the net, but I've had to work hard to put all the pieces together. Below is what worked for me from the Repository down. I haven't included the database side of things since that is well documented in many places and I found it easy enough to follow (if anyone needs me to include that, let me know). I switched to Kotlin part way through because I could only find some parts of the answer in Java. My example is to log in a user and get basic profile details.
Repository sends login details to server and saves response in database then pulls that info to save as LiveData
package com.example.myapplication
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.example.myapplication.*
import com.example.myapplication.asDomainModel
import com.example.myapplication.asDBEntity
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.IOException
import javax.inject.Inject
class LoginRepository @Inject constructor(
private val myDao: MyDao,
private val myNetwork: Network
) {
private val _profile: MutableLiveData<Profile> = MutableLiveData()
val profile: LiveData<Profile>
get() = _profile
suspend fun login(name: String, password: String) {
withContext(Dispatchers.IO) {
// log in to server and get profile data
val profileNWEntity = myNetwork.login("login", name, password)
// process response
when (profileNWEntity.status) {
"PROFLOGINOK" -> {
// save profile in database then retrieve
myDao.insertProfile(profileNWEntity.asDBEntity())
_profile.postValue(myDao.getProfile(profileNWEntity.user).asDomainModel())
}
else -> {
throw IOException (profileNWEntity.status)
}
}
}
}
}
Retrofit endpoint defines the login process
package com.example.myapplication
import com.example.myapplication.ProfileNWEntity
import retrofit2.http.Field
import retrofit2.http.FormUrlEncoded
import retrofit2.http.POST
interface Network {
@FormUrlEncoded
@POST("server_api")
suspend fun login(
@Field("action") action: String,
@Field("name") name: String,
@Field("pass") password: String
): ProfileNWEntity
}
Entity - used by Gson to parse the network response and by the repository to adapt for the database
package com.example.myapplication
import com.example.myapplication.AccountDBEntity
import com.example.myapplication.ProfileDBEntity
/**
* Base profile response from network query
*/
data class ProfileNWEntity(
val user: Int,
val name: String,
val status: String
)
// map the profile from network to database format
fun ProfileNWEntity.asDBEntity(): ProfileDBEntity {
return ProfileDBEntity(
id = user,
name = name
)
}
Retrofit class to enable inclusion of cookies (together with the interceptors included below, this comes from the work of tsuharesu and Nikhil Jha found at https://gist.github.com/nikhiljha/52d45ca69a8415c6990d2a63f61184ff)
package com.example.myapplication
import android.content.Context
import dagger.hilt.android.qualifiers.ApplicationContext
import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import javax.inject.Inject
class RetrofitWithCookie @Inject constructor(
context: Context, // uses Hilt to inject the context to be passed to the interceptors
gson: Gson
) {
private val mContext = context
private val gson = gson
fun createRetrofit(): Retrofit {
val client: OkHttpClient
val builder = OkHttpClient.Builder()
builder.addInterceptor(AddCookiesInterceptor(mContext)) // VERY VERY IMPORTANT
builder.addInterceptor(ReceivedCookiesInterceptor(mContext)) // VERY VERY IMPORTANT
client = builder.build()
return Retrofit.Builder()
.baseUrl("myServer URL") // REQUIRED
.client(client) // VERY VERY IMPORTANT
.addConverterFactory(GsonConverterFactory.create(gson))
.build() // REQUIRED
}
}
Receiving Interceptor catches the inbound cookies and saves them in sharedpreferences
package com.example.myapplication
import android.content.Context
import androidx.preference.PreferenceManager
import okhttp3.Interceptor
import okhttp3.Response
import java.io.IOException
import java.util.*
// Original written by tsuharesu
// Adapted to create a "drop it in and watch it work" approach by Nikhil Jha.
// Just add your package statement and drop it in the folder with all your other classes.
class ReceivedCookiesInterceptor(context: Context?) : Interceptor {
private val context: Context?
@Throws(IOException::class)
override fun intercept(chain: Interceptor.Chain): Response {
val originalResponse = chain.proceed(chain.request())
if (!originalResponse.headers("Set-Cookie").isEmpty()) {
val cookies = PreferenceManager.getDefaultSharedPreferences(context)
.getStringSet("PREF_COOKIES", HashSet()) as HashSet<String>?
for (header in originalResponse.headers("Set-Cookie")) {
cookies!!.add(header)
}
val memes = PreferenceManager.getDefaultSharedPreferences(context).edit()
memes.putStringSet("PREF_COOKIES", cookies).apply()
memes.commit()
}
return originalResponse
}
init {
this.context = context
} // AddCookiesInterceptor()
}
AddCookies interceptor adds the cookie back into future requests
package com.example.myapplication
import android.content.Context
import androidx.preference.PreferenceManager
import dagger.hilt.android.qualifiers.ActivityContext
import okhttp3.Interceptor
import okhttp3.Response
import timber.log.Timber
import java.io.IOException
import java.util.*
// Original written by tsuharesu
// Adapted to create a "drop it in and watch it work" approach by Nikhil Jha.
// Just add your package statement and drop it in the folder with all your other classes.
/**
* This interceptor put all the Cookies in Preferences in the Request.
* Your implementation on how to get the Preferences may ary, but this will work 99% of the time.
*/
class AddCookiesInterceptor(@ActivityContext context: Context?) : Interceptor {
// We're storing our stuff in a database made just for cookies called PREF_COOKIES.
// I reccomend you do this, and don't change this default value.
private val context: Context?
@Throws(IOException::class)
override fun intercept(chain: Interceptor.Chain): Response {
val builder = chain.request().newBuilder()
val preferences = PreferenceManager.getDefaultSharedPreferences(context).getStringSet(
PREF_COOKIES, HashSet()
) as HashSet<String>?
// Use the following if you need everything in one line.
// Some APIs die if you do it differently.
/*String cookiestring = "";
for (String cookie : preferences) {
String[] parser = cookie.split(";");
cookiestring = cookiestring + parser[0] + "; ";
}
builder.addHeader("Cookie", cookiestring);
*/for (cookie in preferences!!) {
builder.addHeader("Cookie", cookie)
Timber.d("adding cookie %s", cookie)
}
return chain.proceed(builder.build())
}
companion object {
const val PREF_COOKIES = "PREF_COOKIES"
}
init {
this.context = context
}
}
Hilt Module to tie it all together
package com.example.myapplication
import android.content.Context
import com.example.myapplication.Network
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import javax.inject.Singleton
@InstallIn(SingletonComponent::class)
@Module
class NetworkModule {
@Singleton
@Provides
fun provideNetwork(retrofit: Retrofit)
: Network = retrofit.create(Network::class.java)
@Singleton
@Provides
fun provideRetrofitWithCookie(
@ApplicationContext context: Context,
gson: Gson
): Retrofit = RetrofitWithCookie(context, gson).createRetrofit()
@Singleton
@Provides
fun provideGson(): Gson = GsonBuilder()
.setDateFormat("yyyy-MM-dd'T'HH:mm:ssZ") // used for parsing other responses
.create()
}