Recreate object in kotlin - android

I have simple issue: need recreate a object with other parameters.
Have this object:
object NetworkClient {
var BASE_URL = "http://google.ru" //we can take this from another class or Pref's
const val API_BASE_URL = "$BASE_URL/api/"
val httpClient = OkHttpClient.Builder()
val client: Client
var retrofit: Retrofit
init {
val builder = Retrofit.Builder()
.baseUrl(API_BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
retrofit = builder.client(httpClient.build()).build()
client = retrofit.create<PapaJobsClient>(Client::class.java)
}
}
In rare cases you need to change the BASE_URL on the fly to another String and recreate client and etc. I know how to make it, but my resolution so hard - need rework all places where i use this class but i want create this object with concrete parameter. How you think about this problem?

You need to make a separate class for that, since singletons (object in Kotlin) cannot have a constructor:
class NetworkClient(val baseUrl: String) {
const val API_BASE_URL = "$baseUrl/api/"
val httpClient = OkHttpClient.Builder()
val client: Client
var retrofit: Retrofit
init {
// ...
}
}
Use an object to have the current networkClient instance always at hand:
object NetworkClientProvider {
var networkClient = NetworkClient("http://google.ru")
}
Usage, if you need to create a new NetworkClient:
NetworkClientProvider.networkClient = NetworkClient("http://someOtherDomain.com")

You're misusing object here. This should be used if you really have a single instance of a class, it's the Kotlin built-in feature for applying the Singleton pattern.
Fix it by making it a regular class:
class NetworkClient(val baseurl: String) {
const val API_BASE_URL = "$baseurl/api/"
val httpClient = OkHttpClient.Builder()
//...
}
Then it’s possible to create objects with different baseurls as shown:
val russian = NetworkClient("http://google.ru")
val com = NetworkClient("http://google.com")

Related

Android - Retrofit2 Authorization with Bearer Token stored in SharedPreferences

I'm trying to use Retrofit2 in Android app, I'd like to add Bearer Token to http request header which stored with SharedPreferences.
But the below code return the error when launch app.
java.lang.NullPointerException: Attempt to invoke virtual method 'android.content.Context android.content.Context.getApplicationContext()' on a null object reference
object RetrofitInstance : Application() {
private const val API_BASE_URL = BuildConfig.API_BASE_URL
private val sharedPreferences: SharedPreferences by lazy {
applicationContext.getSharedPreferences(
applicationContext.getString(R.string.preference_file_key),
Context.MODE_PRIVATE
)
}
private val httpBuilder: OkHttpClient.Builder
get() {
val httpClient = OkHttpClient.Builder()
.addInterceptor(Interceptor { chain ->
val original = chain.request()
val bearerToken = sharedPreferences.getString(
applicationContext.getString(R.string.preference_token_data_key),
null
)
val request = original.newBuilder()
.header("Accept", "application/json")
.header("Authorization", "Bearer $bearerToken")
.build()
return#Interceptor chain.proceed(request)
})
return httpClient
}
private val retrofit by lazy {
Retrofit.Builder()
.baseUrl(API_BASE_URL)
.client(httpBuilder.build())
.addConverterFactory(
MoshiConverterFactory.create(
Moshi.Builder().add(KotlinJsonAdapterFactory()).build()
)
)
.addCallAdapterFactory(ResultCallAdapterFactory())
.build()
}
val api: TestService by lazy {
retrofit.create(TestService::class.java)
}
}
Is there any way to use the value stored with SharedPreferences in Singleton Object?
The Application class in your Android App is Singleton; what you need to do is change the object to class, add this class to the AndroidManifest.xml
like that android:name=".RetrofitInstance", and then get access to that class in your activities or fragments like the following
(application as RetrofitInstance).api
But I don't think this is the right place for such a thing, usually we create separate files that handle the creation and configuration for retrofit, a separate class that wraps the SharedPreferences, and have another implementation entity that integrates these two (get the data from shared preferences and pass them to the Retrofit OkHttpBuilder).

How the lazy declaration is working here?

I am following a course named "Getting Data from the Internet" from the official android developers' website.
It shows how to get data from a URL using Retrofit and Moshi library. They throw this code and I am not able to understand how the code inside object MarsApi{..} is working. Can anyone here explain this to me?
private const val BASE_URL = "https://android-kotlin-fun-mars-server.appspot.com"
private val moshi = Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
private val retrofit = Retrofit.Builder()
.addConverterFactory(MoshiConverterFactory.create(moshi))
.baseUrl(BASE_URL)
.build()
interface MarsApiService{
#GET("photos")
suspend fun getPhotos():List<MarsPhoto>
}
object MarsApi {
val retrofitService : MarsApiService by lazy {
retrofit.create(MarsApiService::class.java)
}
//Why I can't write it as:
// val retrofitService2 : MarsApiService = object : MarsApiService{
// override suspend fun getPhotos(): List<MarsPhoto> {
// return retrofit.create(MarsApiService::class.java)
// }
// }
}
By using "lazy" keyword, the service will be initialised only the first time you call it, so you can later use the same value.
More details about lazy in official docs:
https://kotlinlang.org/docs/delegated-properties.html#lazy-properties

Kotlin : Okhttpclient correct way of creating single instance? OkHttpClient doesn't go through server ip check?

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.

Dynamic urls with Koin ans Retrofit

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.

Retrofit : Url is being changed

Currently I am facing some issues with Retrofit. The URL, I am providing to RetrofitInstance is changing for the second request. Here are the codes:
object RetrofitClientInstance{
private var retrofit: Retrofit? = null
//private const val BASE_URL = "http://api.sample.com/req/";
private const val BASE_URL = "http://test.sample.com/req/"
private const val BASE_URL_VERSION = "v1/"
fun getRetrofitInstance() : Retrofit {
if (retrofit == null) {
retrofit = Retrofit.Builder()
.baseUrl(BASE_URL + BASE_URL_VERSION)
.addConverterFactory(GsonConverterFactory.create())
.build()
}
return this!!.retrofit!!
}
}
Here are the interface methods for different API requests:
class UserLoginResponseReceiver{
interface GetDataService {
#FormUrlEncoded
#POST(UrlEndPoints.USER_LOGIN_BY_FACEBOOK)
fun loginUserByFacebook(#Field("access_token") last: String): Call<FbLoginResponse>
#GET(UrlEndPoints.ALL_POSTS)
fun getAllPosts() : Call<AllPostsResponse>
}
}
UrlEndPoints.kt
object UrlEndPoints {
const val USER_LOGIN_BY_FACEBOOK = "user/auth/facebook"
const val ALL_POSTS = "post"
}
For the first request (loginUserByFacebook), the URL I am getting by debugging my response is:
http://test.sample.com/req/v1/user/auth/facebook
Which is fine and working perfectly. But for the second request (getAllPosts()) I am getting the following URL:
http://test.sample.com/post
The "req/v1" part is completely cut off! As a result I don't get the desired response. What's the problem here actually? Please help me.
I can't reproduce this, you should try cleaning / rebuilding your project, the code you've provided here has no errors in it.
This is the code I added to test it:
class AllPostsResponse
class FbLoginResponse
fun main(args: Array<String>) {
val service = RetrofitClientInstance.getRetrofitInstance()
.create(UserLoginResponseReceiver.GetDataService::class.java)
val url = service.getAllPosts().request().url()
println(url)
}
The URL returned here is the following, as expected:
http://test.sample.com/req/v1/post

Categories

Resources