마스터Q&A 안드로이드는 안드로이드 개발자들의 질문과 답변을 위한 지식 커뮤니티 사이트입니다. 안드로이드펍에서 운영하고 있습니다. [사용법, 운영진]

hilt사용해서 baseUrl 주입방법

0 추천

현재 이 상태의 코드구요

@Module
@InstallIn(SingletonComponent::class)
object ModuleApiManager {

    @Singleton
    @Provides
    fun provideApiManager(): ApiService {
        val sUeserAgent = BasePreference.getInstance()[CommonInfo.USER_AGENT_KEY]
        var sLoginToken = ""
        val okHttp: OkHttpClient

        if (CommonData.getInstance().userInfo != null) {
            sLoginToken = CommonData.getInstance().userInfo.m_sToken
        }

        val logging = HttpLoggingInterceptor()
        logging.setLevel(HttpLoggingInterceptor.Level.BODY)

        okhttp.build() 
        return Retrofit.Builder()
            .baseUrl(CommonInfo.BASE_URL_SSL)
            .addConverterFactory(GsonConverterFactory.create())
            .client(okHttp)
            .build()
            .create(ApiService::class.java)
    }
}

여기서 

.baseUrl(CommonInfo.BASE_URL_SSL)

이부분이 baseUrl이 고정이 아니고, api호출때 baseUrl이 바뀌는경우가 있습니다

기존에 hilt없이 사용할때는 저 baseUrl을 생성자로 받아서, baseUrl을 저기에 넣어줬는데요
hilt에서는 저걸 어떻게 주입할 방법이 없나요?

글자수 제한때문에 okhttp,.build()부분은 cokhttpclient 생성코드로 저렇게 줄임말로 썼습니다
@quailfier 말고는 없을까요?

수원통학러 (3,570 포인트) 님이 3월 22일 질문
수원통학러님이 3월 22일 수정

3개의 답변

0 추천

OKHttp같은 경우는 Interceptor를 이용하는 방법이 하나 있습니다.  BaseUrlInterceptor를 inject하셔도 되고 OkHttpClient를 만드는 모듈에 SharePreferences를 inject해서 사용하실 수도 있겠죠. 물론 님이 BaseUrl을 저장하는 storage를 사용하셔야 하구요.

OkHttpClient.Builder()
      OkHttpClient.Builder()
               .addInterceptor(BaseUrlInterceptor())

     ..
        .build()



class BaseUrlInterceptor (
    private val preferences: SahredPreferences
) : Interceptor {

    override fun intercept(chain: Interceptor.Chain): okhttp3.Response {
        val request = buildUrl(chain.request().url)?.toHttpUrlOrNull()?.let { newUrl ->
            chain.request().newBuilder()
                .url(newUrl)
                .build()
        } ?: chain.request()

        return chain.proceed(request)
    }

    private fun buildUrl(url: okhttp3.HttpUrl): String? {
        return preferences.getBaseUrl().plus(url.toUrl().path.drop(1))
    }
}
spark (227,530 포인트) 님이 3월 22일 답변
잘 이해가 되지 않습니다..
혹시 좀더 쉬운방법없을까요? 주입이 아니어도 다른방법으로요
그럼 inject을 하지 마시고 사용하세요. Interceptor의 코드만 이해하시면 Interceptor를 Retrofit 만들때 생성하시면 됩니다.

@Module
@InstallIn(SingletonComponent::class)
object ModuleApiManager {

    @Singleton
    @Provides
    fun provideApiManager(): ApiService {
        ...

        okHttp.addInterceptor { chain ->
             // 여기서 base url 교체
             val baseUrl = getBaseUrl() // eg. BasePreference.getInstance()[BuildConifg.FLAVOUR + "baseUrl"]
            
             ...
             val request = ...
             chain.proceed(request)
        }
        .build()

        ...
    }
}

참고로, Hilt Module 안에서 작업을 하고 계시기 때문에  provideApiManager에 파라미터로 필요한 객체의 주입이 가능하고 이게 Hilt를 사용할 경우 더 나은 방법이라고 생각되지만, 어려움이 있으시니 위처럼 바로 객체를 생성하시면 됩니다.
0 추천

Hilt를 이용해서 인터셉터로 base url를 변경하는 방법을 코드를 보여드릴테니 참고하세요. 동작하는 코드이고 복잡하지 않으므로 코드를 보고 이애하시고 사용하시길 바라고 이 코드에 대한 추가적인 답은 하지 않겠습니다.

BaseURL을 개발환경에 따라 설정한다고 가정하고 아래처럼 개발환경을  enum으로 정의.

// StockExchange는 prod 환경만 존재하기 때문에 실제 테스트하면 DEV, STAGING은 에러가 나므로 BaseUlr이 잘 변경되었는지만 확인하시기 바람.
enum class BaseUrl(val value: String) {
    DEV("https://dev-api.stackexchange.com/"),
    STAGING("https://staging-api.stackexchange.com/"),
    PROD("https://api.stackexchange.com/");
}

 

Retrofit을 생성하는 Hilt 모듈
 

@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {

    private const val APP_SETTINGS = "appSettings"

    @Provides
    @Singleton
    fun providesAppSettings(@ApplicationContext context: Context): SharedPreferences {
        return context.getSharedPreferences(APP_SETTINGS, MODE_PRIVATE)
    }

    @Provides
    @Singleton
    fun provideOkHttpClient(loggingInterceptor: LoggingInterceptor, baseUrlInterceptor: BaseUrlInterceptor): OkHttpClient {
        return OkHttpClient.Builder()
            .addNetworkInterceptor(loggingInterceptor)
            .addInterceptor(baseUrlInterceptor)
            .build()
    }

    @Provides
    @Singleton
    fun providesRetrofit(okHttpClient: OkHttpClient): Retrofit {
        return Retrofit.Builder()
            .baseUrl(BaseUrl.PROD.value)
            .addConverterFactory(GsonConverterFactory.create())
            .client(okHttpClient)
            .build()
    }

    @Provides
    @Singleton
    fun providesStockExchangeApi(retrofit: Retrofit): StockExchangeApi {
        return retrofit.create(StockExchangeApi::class.java)
    }
}

private const val BASE_URL_KEY = "baseUrl"

fun SharedPreferences.getBaseUrlValue(): String {
    return getBaseUrl().value
}

fun SharedPreferences.getBaseUrl(): BaseUrl {
    val baseUrlName = getString(BASE_URL_KEY, BaseUrl.PROD.name) ?:  BaseUrl.PROD.name
    return BaseUrl.entries.find { it.name.equals(baseUrlName, ignoreCase = true) } ?: BaseUrl.PROD
}

fun SharedPreferences.setBaseUrl(baseUrl: BaseUrl) {
    edit()
        .putString(BASE_URL_KEY, baseUrl.name)
        .commit()
}

 

BaseInterceptor
 

@Singleton
class BaseUrlInterceptor @Inject constructor(
    private val preferences: SharedPreferences
) : Interceptor {

    override fun intercept(chain: Interceptor.Chain): okhttp3.Response {
        val request = buildUrl(chain.request().url).toHttpUrlOrNull()?.let { newUrl ->
            chain.request().newBuilder()
                .url(newUrl)
                .build()
        } ?: chain.request()

        return chain.proceed(request)
    }

    private fun buildUrl(url: okhttp3.HttpUrl): String {
        val baseUrl = preferences.getBaseUrlValue()
        return baseUrl.plus(url.toUrl().path.drop(1))
    }
}

 

StockExchangeApi
 

interface StockExchangeApi {

    @GET("2.3/posts?order=desc&sort=activity&site=stackoverflow")
    suspend fun getPosts(): Response<Posts>

}


data class Item(
    @SerializedName("content_license")
    val contentLicense: String,
    @SerializedName("creation_date")
    val creationDate: Int,
    @SerializedName("last_activity_date")
    val lastActivityDate: Int,
    @SerializedName("last_edit_date")
    val lastEditDate: Int,
    @SerializedName("link")
    val link: String,
    @SerializedName("owner")
    val owner: Owner,
    @SerializedName("post_id")
    val postId: Int,
    @SerializedName("post_type")
    val postType: String,
    @SerializedName("score")
    val score: Int
)

data class Owner(
    @SerializedName("accept_rate")
    val acceptRate: Int,
    @SerializedName("account_id")
    val accountId: Int,
    @SerializedName("display_name")
    val displayName: String,
    @SerializedName("link")
    val link: String,
    @SerializedName("profile_image")
    val profileImage: String,
    @SerializedName("reputation")
    val reputation: Int,
    @SerializedName("user_id")
    val userId: Int,
    @SerializedName("user_type")
    val userType: String
)


data class Posts(
    @SerializedName("has_more")
    val hasMore: Boolean,
    @SerializedName("items")
    val items: List<Item>,
    @SerializedName("quota_max")
    val quotaMax: Int,
    @SerializedName("quota_remaining")
    val quotaRemaining: Int
)

 

 

spark (227,530 포인트) 님이 3월 24일 답변
0 추천

테스트용 UI
 

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding

    @Inject
    lateinit var api: StockExchangeApi

    @Inject
    lateinit var settings: SharedPreferences

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        setupView()
    }

    private fun setupView() {
        binding.apply {
            when (val baseUrl = settings.getBaseUrl()) {
                BaseUrl.DEV -> devBtn.isChecked = true
                BaseUrl.STAGING -> stagingBtn.isChecked = true
                BaseUrl.PROD -> prodBtn.isChecked = true
            }

            baseUrlGroup.setOnCheckedChangeListener { _, checkedId ->
                baseUrlChanged(checkedId)
            }
            getPostsBtn.setOnClickListener {
                getPosts()
            }
        }
    }

    private fun baseUrlChanged(checkedId: Int) {
        when (checkedId) {
            R.id.devBtn -> {
                chaneBaseUrl(BaseUrl.DEV)
            }

            R.id.stagingBtn -> {
                chaneBaseUrl(BaseUrl.STAGING)
            }

            R.id.prodBtn -> {
                chaneBaseUrl(BaseUrl.PROD)
            }
        }
    }

    private fun chaneBaseUrl(baseUrl: BaseUrl) {
        settings.setBaseUrl(baseUrl)

        println("=========== BaseUrl changed: ${settings.getBaseUrl()}")
    }

    private fun getPosts() {
        lifecycleScope.launch {
            binding.loadingView.isVisible = true
            val response = api.getPosts()

            binding.resultTxt.text = response.toString()

            if (response.isSuccessful) {
                val posts = response.body()
                if (posts != null) {
                    binding.resultTxt.append("------------------------------------------\r\n")
                    val line = posts.items.joinToString("\r\n")
                    binding.resultTxt.append(line)
                }
            }

            binding.loadingView.isVisible = false
        }
    }
}

 

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="24dp"
    tools:context=".MainActivity">

    <RadioGroup
        android:id="@+id/baseUrlGroup"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginTop="40dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent">

        <RadioButton
            android:id="@+id/devBtn"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Dev"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <RadioButton
            android:id="@+id/stagingBtn"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Staging"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <RadioButton
            android:id="@+id/prodBtn"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:checked="true"
            android:text="Prod"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

    </RadioGroup>

    <com.google.android.material.progressindicator.CircularProgressIndicator
        android:id="@+id/loadingView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:indeterminate="true"
        android:visibility="gone"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <ScrollView
        app:layout_constraintBottom_toTopOf="@+id/getPostsBtn"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/baseUrlGroup"
        android:layout_width="0dp"
        android:layout_height="0dp">
        <TextView
            android:id="@+id/resultTxt"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginBottom="16dp" />
    </ScrollView>

    <com.google.android.material.button.MaterialButton
        android:id="@+id/getPostsBtn"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:text="Get Posts"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

 

StackExchange는 API key가 없이 콜을 여러번 하게 블락이 당하므로 참고하세요.

 

 

spark (227,530 포인트) 님이 3월 24일 답변
...