Using Cookies with Retrofit and Hilt and recommended architecture - android

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()
}

Related

How do I make a post in xml using a serial number and api key and receive back json?

I am using an existing API that requires a POST in XML. That XML will contain an API key and a serial number and the response will be in JSON. I am using Android Studio and Kotlin to write the app. I am also trying to use Retrofit2.
My problem is that I can not find how to use both XML and JSON. I know this code is incomplete but I would be interested in a good source of information on these topics. If you have any good lessons please share.
This is my Interface
package com.example.project
import retrofit2.Call
import retrofit2.http.Body
import retrofit2.http.Headers
import retrofit2.http.POST
interface APIInterface {
#POST("APIEndpont")
fun submitSerNum(#Body serNum: SerialNumber): Call<MyData>
}
This is where I try to just log the JSON as a string
val retrofit = ServiceBuilder.buildService(APIInterface::class.java)
val obj = SerialNumber(ser = "000")
retrofit.submitSerNum(obj).enqueue(
object:Callback<MyData>{
override fun onResponse(call: Call<MyData>, response: Response<MyData>) {
Log.d("TAG", "${response.body().toString()}")
}
override fun onFailure(call: Call<MyData>, t: Throwable) {
Log.d("Tag Failure", "Failure")
}
}
)
This is my service builder
package com.example.project
import android.util.Log
import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
object ServiceBuilder {
private val client = OkHttpClient.Builder().build()
private val retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.client(client)
.build()
fun <T> buildService(service: Class<T>): T {
return retrofit.create(service)
Log.d("Creating Service", "Service Created");
}
}
I have followed a few tutorials on how to do API calls mostly GETs though. I can not figure out how to adapt the code to my needs.

mocking actual API call during unittesting retrofit interface

learning about retrofit but couldn't write the tests for it. I came from jest background and struggling to test two things:
that the call was making to a specific end point and its status.
import okhttp3.ResponseBody
import retrofit2.Response
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.http.GET
import retrofit2.http.Query
interface WeatherAPI {
#GET("current.json")
suspend fun getCurrentWeatherData(#Query("key") apiKey: String, #Query("q") cityName: String, #Query("qui") quiValue: String): Response<ResponseBody>
companion object {
private const val BASE_URL = "http://api.weatherapi.com/v1/"
val instance: WeatherAPI by lazy {
Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(WeatherAPI::class.java)
}
}
}
Test file:
class WeatherAPITest {
#Test
fun WeatherAPI_getCurrentWeather_apiKey_city(){
runBlocking {
val res = WeatherAPI.instance.getCurrentWeatherData("123", "London", "no")
assertThat(res.code()).isEqualTo(200)
}
}
}
it fails because there is no token. How can i mock the actual api call to return say 200 on the test and confirm something like this:
assertThat(url).isEqualTo('http:....?key=123&q=London')
assertThat(responseCode).isEqualTo(200)

Another case with Exception: java.lang.IllegalArgumentException: No Retrofit annotation found. (parameter #2) in ApiService.login

I'm stuck at Exception that appears in the title. I checked similar topics, but they were specific cases that didn't apply to my situation. Below you could see my models, retrofit setup and its usage. I tried removing Body Class, Response Class, just to check if they are culprits, but unfortunately those weren't the case. Maybe someone will be able to figure out what I'm doing wrong?
Stack Trace:
java.lang.IllegalArgumentException: No Retrofit annotation found. (parameter #2)
for method ApiService.login
at retrofit2.Utils.methodError(Utils.java:52)
at retrofit2.Utils.methodError(Utils.java:42)
at retrofit2.Utils.parameterError(Utils.java:61)
at retrofit2.RequestFactory$Builder.parseParameter(RequestFactory.java:311)
at retrofit2.RequestFactory$Builder.build(RequestFactory.java:182)
at retrofit2.RequestFactory.parseAnnotations(RequestFactory.java:65)
at retrofit2.ServiceMethod.parseAnnotations(ServiceMethod.java:25)
at retrofit2.Retrofit.loadServiceMethod(Retrofit.java:168)
at retrofit2.Retrofit$1.invoke(Retrofit.java:147)
at java.lang.reflect.Proxy.invoke(Proxy.java:1006)
at $Proxy1.login(Unknown Source)
at com.rudearts.cyber2020.services.NetworkService$login$1.invokeSuspend(NetworkService.kt:17)
at com.rudearts.cyber2020.services.NetworkService$login$1.invoke(Unknown Source:10)
at kotlinx.coroutines.flow.SafeFlow.collect(Builders.kt:56)
at kotlinx.coroutines.flow.internal.ChannelFlowOperatorImpl.flowCollect(ChannelFlow.kt:144)
at kotlinx.coroutines.flow.internal.ChannelFlowOperator.collectTo$suspendImpl(ChannelFlow.kt:111)
at kotlinx.coroutines.flow.internal.ChannelFlowOperator.collectTo(Unknown Source:0)
at kotlinx.coroutines.flow.internal.ChannelFlow$collectToFun$1.invokeSuspend(ChannelFlow.kt:33)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:56)
at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:571)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:738)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:678)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:665)
Models:
import com.google.gson.annotations.SerializedName
data class LoginRequest(
#SerializedName("pin") val pin:String,
#SerializedName("pushId") val pushId:String)
import com.google.gson.annotations.SerializedName
data class UserJson (
#SerializedName("id") val id:Long,
#SerializedName("name") val name:String?,
#SerializedName("access_rights") val accessRights:String?)
Retrofit Builder
import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
object RetrofitBuilder {
private const val BASE_URL = "<url>"
private val client = OkHttpClient.Builder().build()
private fun getRetrofit(): Retrofit {
return Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.client(client)
.build()
}
val apiService: ApiService = getRetrofit().create(ApiService::class.java)
}
import com.rudearts.cyber2020.model.LoginRequest
import com.rudearts.cyber2020.model.UserJson
import retrofit2.Response
import retrofit2.http.Body
import retrofit2.http.Headers
import retrofit2.http.POST
interface ApiService {
#Headers("Content-Type: application/json")
#POST("login.php")
suspend fun login(#Body request: LoginRequest):Response<UserJson>
}
Usage in other class:
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import java.util.*
class NetworkService {
private val repoService by lazy { RepoService.instance }
private val apiService = RetrofitBuilder.apiService
fun login(pin:String, token:String): Flow<NetworkResult<Boolean>> = flow {
emit(Loading)
try {
val userJson = apiService.login(LoginRequest(pin,token)).body()
userJson?.let {
val user =
User(userJson.id, userJson.name ?: "", emptyList(), userJson.accessRights ?: "")
repoService.user = user
}
emit(NetworkSuccess(userJson != null))
} catch (throwable: Throwable) {
emit(NetworkError(throwable))
}
}
}
It could be that you are using an older version of okhttp / retrofit as you are using suspend function it requires the latest version of both libraries.
Try call the function like this
fun login(LoginRequest(pin,token))
instead of this
fun login(pin:String, token:String)
the exception says the error is on the second parameter

how to extract Json data without having a query in Retrofit

When I was having an Api Key then I used the following below code to extract the Json data from it.
Now I want to fetch Json data from https://api.coingecko.com/api/v3/exchanges and I don't have any Api Key or query to pass.How can I do it using RetroFit?
import retrofit2.Call
import retrofit2.Retrofit
import retrofit2.http.GET
import retrofit2.create
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.http.Query
const val BASE_URL = "https://newsapi.org/"
const val API_KEY = "5f60ae62gcbc4bdaa0d15164d7f1275b"
interface NewsInterface {
#GET("v2/top-headlines?apiKey=$API_KEY")
fun getHeadLines(#Query("country")country:String): Call<News>
}
object NewsService {
val newsInstance :NewsInterface
init {
val retrofit: Retrofit= Retrofit.Builder()
.baseUrl(BASE_URL).addConverterFactory(GsonConverterFactory.create()).build()
newsInstance = retrofit.create(NewsInterface::class.java)
}
}
You API fun should be as,
#GET("api/v3/exchanges")
fun getExchanges(): Call<Response>

Android. Could not instantiate Worker

I want to pre-populate my Room database from the json file in the assets folder. I follow the Google Sunflower sample. I copied the SeedDatabaseWorker class:
import android.content.Context
import android.util.Log
import androidx.work.Worker
import androidx.work.WorkerParameters
import com.dmitrysimakov.gymlab.data.GymLabDb
import com.dmitrysimakov.gymlab.data.entity.Training
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import com.google.gson.stream.JsonReader
import javax.inject.Inject
class SeedDatabaseWorker(val context: Context, workerParams: WorkerParameters) : Worker(context, workerParams) {
private val TAG = SeedDatabaseWorker::class.java.simpleName
#Inject lateinit var database: GymLabDb
override fun doWork(): Worker.Result {
val plantType = object : TypeToken<List<Training>>() {}.type
var jsonReader: JsonReader? = null
return try {
val inputStream = context.assets.open("training.json")
jsonReader = JsonReader(inputStream.reader())
val plantList: List<Training> = Gson().fromJson(jsonReader, plantType)
database.trainingDao().insert(plantList)
Worker.Result.SUCCESS
} catch (ex: Exception) {
Log.e(TAG, "Error seeding database", ex)
Worker.Result.FAILURE
} finally {
jsonReader?.close()
}
}
}
I'm using Dagger 2, so instead of doing this: Sunflower AppDatabase, I do this:
import android.arch.persistence.db.SupportSQLiteDatabase
import android.arch.persistence.room.Room
import android.arch.persistence.room.RoomDatabase
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
import com.dmitrysimakov.gymlab.GymLabApp
import com.dmitrysimakov.gymlab.data.GymLabDb
import com.dmitrysimakov.gymlab.workers.SeedDatabaseWorker
import dagger.Module
import dagger.Provides
import javax.inject.Singleton
#Module(includes = [ViewModelModule::class])
class AppModule {
#Singleton
#Provides
fun provideDb(app: GymLabApp): GymLabDb {
return Room
.databaseBuilder(app, GymLabDb::class.java, "gymlab.db")
.addCallback(object : RoomDatabase.Callback() {
override fun onCreate(db: SupportSQLiteDatabase) {
super.onCreate(db)
val request = OneTimeWorkRequestBuilder<SeedDatabaseWorker>().build()
WorkManager.getInstance().enqueue(request)
}
})
.fallbackToDestructiveMigration()
.build()
}
#Singleton
#Provides
fun provideTrainingDao(db: GymLabDb) = db.trainingDao()
}
But I can't inject the database that has not yet been created. So, how can I access the dao?
The problem was that I couldn't inject my database into the Worker. I found the solution here: AndroidWorkerInjection
Your issue is that SeedDatabaseWorker is still based on Worker() which is deprecated now, so you need to use Worker(Context, WorkerParameters) this constructor.
Check my answer from another post, it'll help you understand WorkManager library.
Edit :
You can now check Worker from that Sunflower demo, it's updated.

Categories

Resources