将 Cookie 与 Retrofit 和 Hilt 以及推荐的架构一起使用

Posted

技术标签:

【中文标题】将 Cookie 与 Retrofit 和 Hilt 以及推荐的架构一起使用【英文标题】:Using Cookies with Retrofit and Hilt and recommended architecture 【发布时间】:2021-08-29 04:54:41 【问题描述】:

我对 android 和 Java / Kotlin 还很陌生,所以我一直在努力在推荐的架构中实现 cookie。我查看了很多地方,阅读了文档并观看了很多视频,每个人都有不同的实现方式,我仍然感到困惑。它们是如何组合在一起的?

【问题讨论】:

【参考方案1】:

我原以为这是一个如此常见的用例,以至于我不敢相信答案并非遍布整个网络,但我必须努力将所有部分组合在一起。以下是从存储库向下对我有用的方法。我没有包括数据库方面的东西,因为这在很多地方都有很好的记录,我发现它很容易理解(如果有人需要我包括,请告诉我)。我中途切换到 Kotlin,因为我只能在 Kotlin 中找到部分答案。我的示例是登录用户并获取基本的个人资料详细信息。

存储库将登录详细信息发送到服务器并将响应保存在数据库中,然后提取该信息以另存为 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)
                
            
        
    

改造端点定义登录过程

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

实体 - Gson 用来解析网络响应和存储库以适应数据库

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
    )

改造类以允许包含 cookie(连同下面包含的拦截器,这来自 tsuharesu 和 Nikhil Jha 在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
    

接收拦截器捕获入站 cookie 并将它们保存在 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 拦截器将 cookie 重新添加到未来的请求中

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 将它们结合在一起

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

【讨论】:

以上是关于将 Cookie 与 Retrofit 和 Hilt 以及推荐的架构一起使用的主要内容,如果未能解决你的问题,请参考以下文章

[如何在Webview和Retrofit2 Okhttp3之间共享cookie

基于Excel的HIL自动化测试工具VIAutoHIL

Retrofit2.0 ,OkHttp3完美同步持久Cookie实现免登录

Retrofit+OKHttp 教你怎么持久化管理Cookie

如何在Retrofit请求里添加Cookie

如何在Retrofit请求里添加Cookie