Using Retrofit for network calls and Koin for dependency injection in an Android app, how to support dynamic url change?
(while using the app, users can switch to another server)
EDIT: network module is declared like this:
fun networkModule(baseUrl: String) = module {
single<Api> {
Retrofit.Builder()
.baseUrl(baseUrl)
.client(OkHttpClient.Builder().readTimeout(30, TimeUnit.SECONDS)
.connectTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.build())
.build().create(Api::class.java)
}
I am starting Koin in the Aplication class onCreate like this:
startKoin {
if (BuildConfig.DEBUG) AndroidLogger() else EmptyLogger()
androidContext(this#App)
modules(listOf(networkModule(TEST_API_BASE_URL), storageModule, integrationsModule, appModule))
}
I faced the same problem recently. The most convenient way is to use a Interceptor to change the baseUrl dynamically.
class HostSelectionInterceptor(defaultHost: String? = null, defaultPort: Int? = null) : Interceptor {
#Volatile var host: String? = null
#Volatile var port: Int? = null
init {
host = defaultHost
port = defaultPort
}
#Throws(IOException::class)
override fun intercept(chain: Interceptor.Chain): okhttp3.Response {
var request = chain.request()
this.host?.let {host->
val urlBuilder = request.url().newBuilder()
urlBuilder.host(host)
this.port?.let {
urlBuilder.port(it)
}
request = request.newBuilder().url(urlBuilder.build()).build()
}
return chain.proceed(request)
}
}
Initialize it with your default url.
single { HostSelectionInterceptor(HttpUrl.parse(AppModuleProperties.baseUrl)?.host()) }
single { createOkHttpClient(interceptors = listOf(get<HostSelectionInterceptor>()))}
And add this interceptor when creating your OkHttpClient.
val builder = OkHttpClient().newBuilder()
interceptors?.forEach { builder.addInterceptor(it) }
To change the url you only have to update the interceptors member.
fun baseUrlChanged(baseUrl: String) {
val hostSelectionInterceptor = get<HostSelectionInterceptor>()
hostSelectionInterceptor.host = baseUrl
}
I've tried with Koin loading/unloading modules..and for a short period of time it worked, but later, after a minimal change I wasn't able to make it reload again.
At the end, I solved it with wrapper object:
class DynamicRetrofit(private val gson: Gson) {
private fun buildClient() = OkHttpClient.Builder()
.build()
private var baseUrl = "https://etc..." //default url
private fun buildApi() = Retrofit.Builder()
.baseUrl(baseUrl)
.addConverterFactory(GsonConverterFactory.create(gson))
.client(buildClient())
.build().create(MyApi::class.java)
var api: MyApi = buildApi()
private set
fun setUrl(url: String) {
if (baseUrl != url)
baseUrl = url
api = buildApi()
}}
I declare it in within Koin module like this:
single<DynamicRetrofit>()
{
DynamicRetrofit(get(), get())
}
and use it in pretty standard way:
dynamicRetrofit.api.makeSomeRequest()
It was good solution for my case since I change baseUrl very rarely. If you need to make often and parallel calls to two different servers it will probably be inefficient since you this will recreate HTTP client often.
Related
I have a class that creates the RetrofitInstance in a very basic way, and I want to test that it is working correctly by running a dummy api against a mockedWebServer but for some reason Instead of getting a succesfull 200 response I get a 0.
fun createRetrofitInstance(baseUrl: String, client: OkHttpClient): Retrofit {
return Retrofit.Builder().baseUrl(baseUrl)
.addCallAdapterFactory(callAdapterFactory)
.addConverterFactory(converterFactory)
.client(client)
.build()
}
and I want to test it using a DummyApi
#Test
fun `should return successful response`() {
val mockedWebServer = MockWebServer()
val mockedResponse = MockResponse().setResponseCode(200)
mockedWebServer.enqueue(mockedResponse)
mockedWebServer.start()
mockedWebServer.url("/")
val retrofit = tested.createRetrofitInstance(mockedWebServer.url("/").toString(), client)
val testApi = retrofit.create(TestApi::class.java)
val actualResponseCall: Call<Any> = testApi.getTestApi()
assertEquals(200, actualResponseCall.execute().code())
mockedWebServer.shutdown()
}
DummyApi
interface TestApi {
#GET("/")
fun getTestApi() : Call<Any>
}
You should read through one of the excellent tutorials on MockWebServer out there. Too much information for just this answer. I think in this case you are just missing the setBody call.
https://medium.com/android-news/unit-test-api-calls-with-mockwebserver-d4fab11de847
val mockedResponse = MockResponse()
mockedResponse.setResponseCode(200)
mockedResponse.setBody("{}") // sample JSON
I have a basic retrofit setup in kotlin.
val BASE_URL: String = "http://10.0.2.2:5000/"
private val moshi = Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
private val interceptor: HttpLoggingInterceptor = HttpLoggingInterceptor().apply {
this.level = HttpLoggingInterceptor.Level.BODY
}
private val client: OkHttpClient = OkHttpClient.Builder().apply {
this.addInterceptor(interceptor)
}.build()
private val retrofit = Retrofit.Builder()
.addConverterFactory(MoshiConverterFactory.create(moshi))
.baseUrl(BASE_URL)
.client(client)
.build()
val service: Api by lazy {
retrofit.create(Api::class.java)
}
I want to check if the server I'm fetching my data from is running - if its not I want to fall back on the local DB for basic functionality. I tried something similar at first but there's a couple of things that are wrong with this approach. First of all the request timeout period is 10 seconds long, which is a little bit more than you'd want it to be for an app. Second, well, it doesn't really work, it'll still throw an exception if the server is offline.
fun serverReachable(): Boolean {
return try {
GlobalScope.async {
// call whatever api function here
}
true
} catch (e: Exception) {
false
}
}
Is there are quick and dirty version of checking if the server is up?
I'm adding DI to the existing project, in process I faced problem that header Authorization disappears from request. There is no any exceptions or logs from Retrofit/OkHttp. My dependencies are:
implementation 'com.squareup.retrofit2:retrofit:2.6.0'
implementation 'com.squareup.okhttp:okhttp:2.7.5'
implementation 'com.squareup.okhttp3:logging-interceptor:3.10.0'
implementation 'org.koin:koin-android:2.1.3'
I create http client using provideClient:
class OkHttpProvider private constructor() {
companion object {
fun provideClient(credentials: UsernamePasswordCredentials? = null, context: Context): OkHttpClient {
val client = OkHttpClient.Builder()
// logs
if (BuildConfig.DEBUG) {
client.addInterceptor(
HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY)
)
}
if (credentials != null) {
val creds = Credentials.basic(credentials.userName, credentials.password)
val headerInterceptor = Interceptor { chain ->
var request = chain.request()
val headers = request
.headers()
.newBuilder()
.add("Authorization", creds)
.build()
request = request.newBuilder().headers(headers).build()
chain.proceed(request)
}
//client.addInterceptor(AccessTokenInterceptor(credentials))
client.addInterceptor(headerInterceptor)
}
client
.callTimeout(60L, TimeUnit.SECONDS)
.connectTimeout(10L, TimeUnit.SECONDS)
.readTimeout(60L, TimeUnit.SECONDS)
.writeTimeout(60L, TimeUnit.SECONDS)
.sslSocketFactory(getSslContext().socketFactory).hostnameVerifier { _, _ -> true }
client.addInterceptor(ChuckInterceptor(context))
return client.build()
}
private fun getSslContext(): SSLContext {
...implementation...
}
}
}
My modules for http client and Retrofit are below:
object HttpClientModule {
val module = module {
single(named(COMMON)) {
OkHttpProvider.provideClient(
get<SharedPreferenceManager>().getUserCredentials(),
androidContext()
)
}
...other versions...
}
const val COMMON = "common"
}
object ApiModule {
val module = module {
single {
RetrofitFactory.getServiceInstance(
ApiService::class.java,
get<SharedPreferenceManager>().getString(LocalDataSource.BUILD_OPTION_API, ""),
get(named(HttpClientModule.COMMON))
)
}
...other apis...
}
}
object RetrofitFactory {
const val GEO_URL = "http://maps.googleapis.com/maps/api/"
fun <T> getServiceInstance(
clazz: Class<T>,
url: String = GEO_URL,
client: OkHttpClient
): T = getRetrofitInstance(url, client).create(clazz)
private fun getRetrofitInstance(
url: String,
client: OkHttpClient
) = Retrofit.Builder()
.baseUrl(url)
.addConverterFactory(GsonConverterFactory.create())
.client(client)
.addCallAdapterFactory(CoroutineCallAdapterFactory())
.build()
}
App starts to work with "admin" user and has some credentials saved in shared preferences, when user starts login with phone and sms and requests are sent with "admin" Authorization header, when user inputs code from sms and his new user credentials are saved in shared preferences. After that app sends two requests and Authorization header isn't presented in them. I saw it in Chuck, I even rechecked it using Charles.
To fix this problem I tried few solutions. Firstly, I changed inject for http client from single to factory, that didn't work. Secondly, I googled the problem, but I didn't mentions of this phenomenon. Thirdly, I wrote AccessTokenInterceptor according to this article and also cover everything with logs. I noticed that interceptor works fine in normal cases, but when Authorization header is missing method intercept is not called. This might be reason why default headerInterceptor also not working. Fourthly, I upgraded versions of Retrofit and OkHttp, this also didn't helped.
I noticed interesting thing about that bug: if I restart app after Retrofit lost Authorization header, app works fine test user is properly logged with correct token. Any attempts to relog without restarting the app fails. Maybe someone had similar problem or knows what is happening here, any ideas are welcomed.
I finally find solution to this problem. The problem was user credentials was passed to provideClient only once, when it's created. At that moment user was logged as admin, and standard user credentials was empty, so http client for ApiService was created without Authorization header.
To solve this I changed AccessTokenInterceptor form article (HttpClientType is a enum to select which credentials need to use):
class AccessTokenInterceptor(
private val sharedPreferenceManager: SharedPreferenceManager,
private val clientType: OkHttpProvider.HttpClientType
) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val credentials = getUserCredentials(clientType)
if (credentials != null) {
val accessToken = Credentials.basic(credentials.userName, credentials.password)
val request = newRequestWithAccessToken(chain.request(), accessToken)
return chain.proceed(request)
} else {
return chain.proceed(chain.request())
}
}
private fun getUserCredentials(clientType: OkHttpProvider.HttpClientType): UsernamePasswordCredentials? {
return when (clientType) {
OkHttpProvider.HttpClientType.COMMON -> sharedPreferenceManager.getUserCredentials()
OkHttpProvider.HttpClientType.ADMIN -> ServiceCredentialsUtils.getCredentials(sharedPreferenceManager)
}
}
private fun newRequestWithAccessToken(#NonNull request: Request, #NonNull accessToken: String): Request {
return if (request.header("Authorization") == null) {
request.newBuilder()
.header("Authorization", accessToken)
.build()
} else {
request
}
}
}
Now each time request is sending, Interceptor gets user's credentials and adds header to request.
I'm new at android kotlin development and currently trying to solve how to correctly create a single instance of OkHttpClient for app-wide usage. I've currently sort-of* created a single instance of client and using it to communicate with the server, however currently the back-end server is not using token/userid for validation but IP check. I can log in the user no problem, but after going to another activity trying to call api, I'm being blocked access by server because apparently IP is not the same. I've used POSTMAN as well as already created a same functioning iOS app that is working with no issue. So my question is am i creating the single instance of OkHttpClient wrong? Or is OkHttpClient not suitable for this kind of ipcheck system? Should i use other library, and if yes, any suggestion and examples?
Thanks in advance
Currently i tried creating it like this :
class MyApplication: Application(){
companion object{
lateinit var client: OkHttpClient
}
override fun onCreate(){
super.onCreate()
client = OkHttpClient()
}
}
Then i created a helper class for it :
class OkHttpRequest {
private var client : OkHttpClient = MyApplication.client
fun POST(url: String, parameters: HashMap<String, String>, callback: Callback): Call {
val builder = FormBody.Builder()
val it = parameters.entries.iterator()
while (it.hasNext()) {
val pair = it.next() as Map.Entry<*, *>
builder.add(pair.key.toString(), pair.value.toString())
}
val formBody = builder.build()
val request = Request.Builder()
.url(url)
.post(formBody)
.build()
val call = client.newCall(request)
call.enqueue(callback)
return call
}
fun GET(url: String, callback: Callback): Call {
val request = Request.Builder()
.url(url)
.build()
val call = client.newCall(request)
call.enqueue(callback)
return call
}
}
Finally I'm using it like this :
val loginUrl = MyApplication.postLoginUrl
var userIdValue = user_id_textfield.text.toString()
var passwordValue = password_textfield.text.toString()
val map: HashMap<String, String> = hashMapOf("email" to userIdValue, "password" to passwordValue)
var request = OkHttpRequest()
request.POST(loginUrl, map, object : Callback {
val responseData = response.body?.string()
// do something with response Data
}
And on another activity after user log in :
val getPaidTo = MyApplication.getPaidTo
var request = OkHttpRequest()
request.GET(getPaidTo, object: Callback{
//do something with data
}
First, don't use your OkHttpClient directly in every Activity or Fragment, use DI and move all of your business logic into Repository or some source of data.
Here I will share some easy way to make REST request with Retrofit, OkHttpClient and Koin, if you want use the same:
WebServiceModule:
val webServiceModule = module {
//Create HttpLoggingInterceptor
single { createLoggingInterceptor() }
//Create OkHttpClient
single { createOkHttpClient(get()) }
//Create WebServiceApi
single { createWebServiceApi(get()) }
}
/**
* Setup a Retrofit.Builder and create a WebServiceApi instance which will hold all HTTP requests
*
* #okHttpClient Factory for HTTP calls
*/
private fun createWebServiceApi(okHttpClient: OkHttpClient): WebServiceApi {
val retrofit = Retrofit.Builder()
.baseUrl(BuildConfig.REST_SERVICE_BASE_URL)
.client(okHttpClient)
.addCallAdapterFactory(CoroutineCallAdapterFactory())
.build()
return retrofit.create(WebServiceApi::class.java)
}
/**
* Create a OkHttpClient which is used to send HTTP requests and read their responses.
*
* #loggingInterceptor logging interceptor
*/
private fun createOkHttpClient(
loggingInterceptor: HttpLoggingInterceptor
): OkHttpClient {
return OkHttpClient.Builder()
.addInterceptor(loggingInterceptor)
.readTimeout(defaultTimeout, TimeUnit.SECONDS)
.connectTimeout(defaultTimeout, TimeUnit.SECONDS)
.build()
}
And now you can inject your WebServiceApi everywhere, but better inject it in your Repository and then use it from some ViewModel
ViewModelModule:
val viewModelModule = module {
//Create an instance of MyRepository
single { MyRepository(webServiceApi = get()) }
}
Hope this help somehow
Okay, after i check with the back-end developer, i figured out the problem wasn't the ip address(it stays the same) but that the cookie was not saved by okhttp, both POSTMan and xcode automatically save the token returned into cookie so i never noticed that was the problem. So after googling a-bit, the solution can be as easy as this:
class MyApplication : Application(){
override fun onCreate(){
val cookieJar = PersistentCookieJar(SetCookieCache(),SharedPrefsCookiePersistor(this))
client = OkHttpClient.Builder()
.cookieJar(cookieJar)
.build()
}
}
With adding persistentCookieJar to gradle.
I am developing an app using the MVP architecture. I am trying to test the Interactors of my app using MockWebServer. Well, I have this test:
#RunWith(RobolectricTestRunner::class)
#Config(constants = BuildConfig::class, manifest = "src/main/AndroidManifest.xml", packageName = "br.com.simplepass.simplepassnew", sdk = intArrayOf(23))
class LoginInteractorImplTest {
lateinit var mLoginInteractor : LoginInteractor
lateinit var mServer: MockWebServer
#Before
fun setUp(){
mLoginInteractor = LoginInteractorImpl()
mServer = MockWebServer()
mServer.start()
}
#Test
fun loginTest(){
mServer.url("http://192.168.0.10:8080/login")
val testSubscriber = TestSubscriber.create<User>()
mLoginInteractor.login("31991889992", "lala").subscribe(testSubscriber)
testSubscriber.assertNoErrors()
// testSubscriber.assertCompleted()
}
#After
fun tearDown(){
mServer.shutdown()
}
}
But, when I uncomment the assertCompleted on the TestSubscriber, I always get assertionError... I know the TestSubscriber works, because I use it in other tests.
Here is my ApiCall:
#GET("login")
fun login() : Observable<User>
My NetModule:
#Module
class NetModule(val mBaseUrl: String) {
#Provides
#Singleton
fun provideHttpCache(application: Application): Cache {
val cacheSize = 10 * 1024 * 1024
return Cache(application.cacheDir, cacheSize.toLong())
}
#Provides
#Singleton
fun provideOkhttpClient(cache: Cache) : OkHttpClient {
val client = OkHttpClient.Builder()
val interceptor = HttpLoggingInterceptor()
interceptor.level = HttpLoggingInterceptor.Level.BODY
client.addInterceptor(interceptor)
return client.cache(cache).build()
}
#Provides
#Singleton
fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit {
return Retrofit.Builder()
.baseUrl(mBaseUrl)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
.client(okHttpClient)
.build()
}
}
And my base URL (There's no backend server... could be anything):
<string name="api_base_url">http://192.168.0.12:8080</string>
So, What am I missing? This code should be working...
Any help is welcome!
EDIT:
So, I changed the code to this:
mLoginInteractor = LoginInteractorImpl()
mServer = MockWebServer()
mServer.enqueue(MockResponse()
.setResponseCode(200)
.setBody(Gson().toJson(User(1, "991889992", "Leandro", "123"))))
mServer.start()
val client = OkHttpClient.Builder()
val cacheSize = 10 * 1024 * 1024
client.cache(Cache(application.cacheDir, cacheSize.toLong())).build()
mLoginInteractor.setRetrofit(Retrofit.Builder()
.baseUrl(mServer.url("/"))
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
.client(client.cache(Cache(application.cacheDir, cacheSize.toLong())).build())
.build())
And this:
val testSubscriber = TestSubscriber.create<User>()
mLoginInteractor.login("31991889992", "lala").subscribe(testSubscriber)
testSubscriber.assertNoErrors()
testSubscriber.assertReceivedOnNext(listOf(User(1, "991889992", "Leandro", "123")))
testSubscriber.assertCompleted()
But I still get this error:
Number of items does not match. Provided: 1 Actual: 0.
Provided values: [User(id=1, phoneNumber=991889992, name=Leandro, password=123)]
Actual values: []
There are a couple of things going on here. First, MockWebServer.url() resolves the given url against the mock server's base url, it does not set the url. If you want to set the url, you'll need to pass it to the start() method. Generally, you configure your retrofit to call the server's endpoint --
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(server.url("/"))
// Other builder methods.
.build();
Second, to get responses from the mock web server, you need to enqueue the expected responses as MockResponses. Otherwise it doesn't know what to send back. Do something like the following before making your request --
server.enqueue(new MockResponse().setBody("Success!"));
You'll need to build your response to mirror the expected response.
See the README for some more examples.