Calls made by Ktor fails first most of the time and then use the refreshTokens to get a token and re-try the calls..
This is not acceptable as I see a LOT of Unauthenticated calls, therefore most calls I get is made 2x, this effectively almost double my network call count.
The ONLY time the calls is authenticated properly is when the previous call was made to the same end point.
Question:
How can I improve this situation so that the token is provided ONCE to all Ktor end-points and
until the token is replaced (via valid login)
I use androidx.datastore:datastore-preferences to store my token and settingsRepository.getAuthToken() correctly retreive it
My DI
I use Dagger Hilt to inject my Bearer token this way
#Provides
#Singleton
fun provideBearerAuthProvider(
settingsRepository: SettingsRepository
) = BearerAuthProvider(
realm = null,
loadTokens = {
val token = settingsRepository.getAuthToken()
if (token == null) {
BearerTokens(accessToken = "fake", refreshToken = "")
} else {
BearerTokens(accessToken = token, refreshToken = "")
}
},
refreshTokens = {
val token = settingsRepository.requireAuthToken()
token?.let { BearerTokens(accessToken = it, refreshToken = "") }
},
sendWithoutRequestCallback = { httpRequestBuilder ->
httpRequestBuilder.url.host == USER_LOGIN
}
).also { bearerAuthProvider ->
settingsRepository.addOnClearListener(bearerAuthProvider::clearToken)
}
#Provides
#Singleton
fun provideApiClient(
bearerAuthProvider: BearerAuthProvider,
) = HttpClient(Android) {
install(Logging) {
logger = Logger.ANDROID
LogLevel.ALL
}
install(ContentNegotiation) {
gson()
}
install(Auth) {
providers += bearerAuthProvider
}
}
My ApiService
This is where the HttpClient is injected
abstract class BaseApiService(
protected val httpClient: HttpClient,
protected val baseUrl: String,
) {
protected suspend inline fun <reified T> get(endpoint: String, block: HttpRequestBuilder.() -> Unit = {}): T {
return httpClient.get(urlString = baseUrl + endpoint, block = block).body()
}
protected suspend inline fun <reified T> post(endpoint: String, block: HttpRequestBuilder.() -> Unit): T {
return httpClient.post(urlString = baseUrl + endpoint, block = block).body()
}
protected suspend inline fun <reified T> put(endpoint: String, block: HttpRequestBuilder.() -> Unit = {}): T {
return httpClient.put(urlString = baseUrl + endpoint, block = block).body()
}
protected suspend inline fun <reified T> patch(endpoint: String, block: HttpRequestBuilder.() -> Unit): T {
return httpClient.patch(urlString = baseUrl + endpoint, block = block).body()
}
protected suspend inline fun <reified T> delete(endpoint: String, block: HttpRequestBuilder.() -> Unit = {}): T {
return httpClient.delete(urlString = baseUrl + endpoint, block = block).body()
}
}
UserApiService
class UserApiService #Inject constructor(
httpClient: HttpClient
) : BaseApiService(httpClient, BASE_URL) {
suspend fun authenticate(): BasicApiResponse<Unit> =
get(endpoint = USER_AUTHENTICATE)
suspend fun login(loginRequest: LoginRequest): BasicApiResponse<AuthApiResponse> =
post(endpoint = USER_LOGIN) {
contentType(ContentType.Application.Json)
setBody(loginRequest)
}
suspend fun updateUserProfile(updateProfileRequest: UpdateProfileRequest): BasicApiResponse<UserApiResponse> =
patch(endpoint = USER) {
contentType(ContentType.Application.Json)
setBody(updateProfileRequest)
}
}
Notes:
I use data classes passing variables to the json payloads e.g.
data class LoginRequest(
val email: String? = null,
val password: String
)
In above code I also included the sendWithoutRequestCallback endpoint
sendWithoutRequestCallback = { httpRequestBuilder ->
httpRequestBuilder.url.host == USER_LOGIN
}
Related
I have installed my_client_cert.p12 to Android.
And then I can use this to get the X509Certificate.
try {
val cert = getCert()
} catch (e: Exception) {
val keyChainAliasCallback =
KeyChainAliasCallback { s ->
val cert = getCert()
}
KeyChain.choosePrivateKeyAlias(
this,
keyChainAliasCallback,
null,
null,
null,
-1,
"my_client_cert"
)
}
fun getCert(): X509Certificate? {
try {
val chain = KeyChain.getCertificateChain(applicationContext, "my_client_cert")
if (chain == null || chain.size == 0) {
return null
}
for (certificate in chain!!) {
return certificate
}
} catch (e: Exception) {
throw e
}
return null
}
I can also use this to set the my_client_cert to okhttp if I put the my_client_cert.p12 to res/raw
val keyStore = KeyStore.getInstance("PKCS12");
val fis = resources.openRawResource(R.raw.my_client_cert)
keyStore.load(fis, "password".toCharArray());
val kmf = KeyManagerFactory.getInstance("X509")
kmf.init(keyStore, "password".toCharArray())
val keyManagers = kmf.keyManagers
val sslContext = SSLContext.getInstance("TLS")
sslContext.init(keyManagers, trustAllCerts, SecureRandom())
val builder = OkHttpClient.Builder()
builder.sslSocketFactory(
sslContext.getSocketFactory(),
trustAllCerts[0] as X509TrustManager
)
builder.hostnameVerifier { hostname, session -> true }
val request = Request.Builder()
.url("https://1.2.3.4/clientauth")
.build()
val client: OkHttpClient = builder.build()
client.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
}
override fun onResponse(call: Call, response: Response) {
}
})
var trustAllCerts = arrayOf<TrustManager>(
object : X509TrustManager {
override fun checkClientTrusted(chain: Array<X509Certificate>, authType: String) {}
override fun checkServerTrusted(chain: Array<X509Certificate>, authType: String) {}
override fun getAcceptedIssuers(): Array<X509Certificate> {
return arrayOf()
}
}
)
But I don't want to copy the cert into APK, how can I set X509Certificate to okhttp?
I have a class that looks like this:
object SomeRepository {
private val logger = Logger(this)
private val flow: MutableStateFlow<List<SomeClass>> = MutableStateFlow(listOf())
init {
GlobalScope.launch(Dispatchers.IO) {
// some code
}
}
fun list() = flow.value
fun observe(): StateFlow<List<SomeClass>> = flow
}
It is recommended to inject dispatchers in tests.
Also, there is a risk to get weird problems such as AppNotIdleException if not following this practice.
One option would be to make these into variables and add setters, but not very nice. Also it creates a race condition (init-block vs setters):
object SomeRepository {
private val logger = Logger(this)
private val flow: MutableStateFlow<List<SomeClass>> = MutableStateFlow(listOf())
private var coroutineScope: CoroutineScope = GlobalScope
private var dispatcher: CoroutineDispatcher = Dispatchers.IO
init {
dispatcher.launch(coroutineScope) {
// some code
}
}
fun list() = flow.value
fun observe(): StateFlow<List<SomeClass>> = flow
#VisibleForTesting
fun setDispatcher(dispatcher: CoroutineDispatcher) {
this.dispatcher = dispatcher
}
#VisibleForTesting
fun setCoroutineScope(coroutineScope: CoroutineScope) {
this.coroutineScope = coroutineScope
}
}
Version that avoids the race condition (test needs to explicitly invoke init()):
object SomeRepository {
private val logger = Logger(this)
private val flow: MutableStateFlow<List<SomeClass>> = MutableStateFlow(listOf())
private var coroutineScope: CoroutineScope = GlobalScope
private var dispatcher: CoroutineDispatcher = Dispatchers.IO
init {
// var isInTesting = Build.FINGERPRINT == "robolectric"
if (!isInTesting) {
init()
}
}
#VisibleForTesting
fun init() {
dispatcher.launch(coroutineScope) {
// some code
}
}
fun list() = flow.value
fun observe(): StateFlow<List<SomeClass>> = flow
#VisibleForTesting
fun setDispatcher(dispatcher: CoroutineDispatcher) {
this.dispatcher = dispatcher
}
#VisibleForTesting
fun setCoroutineScope(coroutineScope: CoroutineScope) {
this.coroutineScope = coroutineScope
}
}
How can I inject a coroutine scope and dispatcher to my tests and avoid variables?
I have a ViewModel
#HiltViewModel
class LoginViewModel #Inject constructor(
private val apiRepository: ApiRepository
) : ViewModel() {
private val account = MutableLiveData<String>("123")
private val password = MutableLiveData<String>("123")
val message: MutableLiveData<String> = MutableLiveData()
var loginResult: LiveData<Resource<UserInfo>> = MutableLiveData()
fun signIn() {
if (TextUtils.isEmpty(account.value)) {
message.postValue("Please enter your account")
return
}
if (TextUtils.isEmpty(password.value)) {
message.postValue("please enter your password")
return
}
// In this code, it doesn’t work. I think it’s because I didn’t observe it.
// Is there any better way to write it here?
loginResult = apiRepository.signIn(account.value!!, password.value!!)
}
fun inputAccount(accountValue: String) {
account.value = accountValue
}
fun inputPassword(passwordValue: String) {
password.value = passwordValue
}
}
This is my interface
#AndroidEntryPoint
class LoginActivity : BaseActivity<ActivityLoginBinding>() {
private val viewModel: LoginViewModel by viewModels()
......
override fun initEvent() {
binding.account.editText!!.addTextChangedListener { viewModel.inputAccount(it.toString()) }
binding.password.editText!!.addTextChangedListener { viewModel.inputPassword(it.toString()) }
binding.signIn.setOnClickListener {
viewModel.signIn()
}
}
override fun setupObservers() {
viewModel.message.observe(this) {
Snackbar.make(binding.root, it, Snackbar.LENGTH_SHORT).show()
}
/**
* There will be no callback here, I know it’s because I’m observing
* `var loginResult: LiveData<Resource<UserInfo>> = MutableLiveData()`
* instead of `apiRepository.signIn(account.value!!, password.value!!)`
* because it was reassigned
*/
viewModel.loginResult.observe(this) {
Log.d("TAG", "setupObservers: $it")
}
}
}
So I adjusted the code a bit
LoginViewModel.signIn
fun signIn(): LiveData<Resource<UserInfo>>? {
if (TextUtils.isEmpty(account.value)) {
message.postValue("Please enter your account")
return null
}
if (TextUtils.isEmpty(password.value)) {
message.postValue("please enter your password")
return null
}
return apiRepository.signIn(account.value!!, password.value!!)
}
LoginActivity.initEvent
override fun initEvent() {
binding.signIn.setOnClickListener {
viewModel.signIn()?.observe(this) {
Log.d("TAG", "setupObservers: $it")
}
}
}
I have checked the official documents of LiveData, and all call livedata{} during initialization. There has been no re-assignment, but if you log in, you cannot directly start the application and request the network.
coroutines doucument
Although I finally achieved my results, I think this is not the best practice, so I want to ask for help!
Supplementary code
ApiRepository
class ApiRepository #Inject constructor(
private val apiService: ApiService
) : BaseRemoteDataSource() {
fun signIn(account: String, password: String) =
getResult { apiService.signIn(account, password) }
}
BaseRemoteDataSource
abstract class BaseRemoteDataSource {
protected fun <T> getResult(call: suspend () -> Response<T>): LiveData<Resource<T>> =
liveData(Dispatchers.IO) {
try {
val response = call.invoke()
if (response.isSuccessful) {
val body = response.body()
if (body != null) emit(Resource.success(body))
} else {
emit(Resource.error<T>(" ${response.code()} ${response.message()}"))
}
} catch (e: Exception) {
emit(Resource.error<T>(e.message ?: e.toString()))
}
}
}
Or i write like this
fun signIn() {
if (TextUtils.isEmpty(account.value)) {
message.postValue("Please enter your account")
return
}
if (TextUtils.isEmpty(password.value)) {
message.postValue("please enter your password")
return
}
viewModelScope.launch {
repository.signIn(account.value, password.value).onEach {
loginResult.value = it
}
}
}
But I think this is not perfect
I am trying to insert data into the room database using the kotlin coroutine. But I always get an exception java.lang.IllegalStateException: Method addObserver must be called on the main thread
But I don't have an observer in this code, the insert call is called from launch with Dispatchers IO
DocumentDao.kt
#Dao
interface DocumentDao {
#Insert
suspend fun insertDocument(document: Document): Long
}
Repository.kt
class Repository#Inject constructor(val db: MyDB) {
suspend fun demoInsert(
uri: String,
albumId: Long
): Long {
val newDoc = Document(0, albumId, rawUri = uri)
return db.documentDao().insertDocument(newDoc)
}
}
MyViewModel.kt
#HiltViewModel
class MyViewModel#Inject constructor(val repo: Repository) : ViewModel() {
suspend fun demoInsert(
uri: String,
albumId: Long
): Long {
return repo.demoInsert(uri, albumId)
}
}
MyFrag.kt
#AndroidEntryPoint
class MyFrag: Fragment() {
val viewModel: MyViewModel by viewModels()
....
....
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.insert.setOnClickListener {
lifecycleScope.launch(Dispatchers.IO) {
val res = viewModel.demoInsert("test", Random.nextLong(500))
Log.d(TAG, "onViewCreated: $res")
}
}
........
.......
}
}
what is wrong with this code? please help me
I'm not sure about this but you can launch coroutine inside listener with Main Dispatcher and later use withContext inside DB function, to change context.
I was facing the same issue and I solved yhis way :
private fun insertAllItemsInDb(data : List<PostResponse>){
val listPost = data.map { it.toUI() }
val scope = CoroutineScope(Job() + Dispatchers.Main)
scope.launch {
localViewModel.insertAllPosts(listPost)
}
}
ViewModel:
fun insertAllPosts(posts: List<PostItem>) {
viewModelScope.launch {
dbRepository.insertAllPosts(posts)
}
}
Creating view model with:
val viewModel: MyViewModel by viewModels()
Will result in lazy creating. Creation of real object will be performed when you access your object for first time. This happens inside:
lifecycleScope.launch(Dispatchers.IO) {
val res = viewModel.demoInsert("test", Random.nextLong(500))
Log.d(TAG, "onViewCreated: $res")
}
And since implementation of method viewModels<>() looks like this:
#MainThread
public inline fun <reified VM : ViewModel> Fragment.viewModels(
noinline ownerProducer: () -> ViewModelStoreOwner = { this },
noinline factoryProducer: (() -> Factory)? = null
): Lazy<VM> = createViewModelLazy(VM::class, { ownerProducer().viewModelStore }, factoryProducer)
You are getting
Method addObserver must be called on the main thread
You should be able to fix this with something like this.
lifecycleScope.launch(Dispatchers.IO) {
val res = withContext(Dispatchers.Main + lifecycleScope.coroutineContext){}.demoInsert("test", Random.nextLong(500))
Log.d(TAG, "onViewCreated: $res")
}
MyViewModel.kt
#HiltViewModel
class MyViewModel#Inject constructor(val repo: Repository) : ViewModel() {
suspend fun demoInsert(
uri: String,
albumId: Long
): Long {
viewModelScope.launch {
repo.demoInsert(uri, albumId)
}
}
}
MyFrag.kt
#AndroidEntryPoint
class MyFrag: Fragment() {
val viewModel: MyViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.insert.setOnClickListener {
lifecycleScope.launch(Dispatchers.Main) {
viewModel.demoInsert("test", Random.nextLong(500))
}
}
}
}
I am using reactor to create an infinite flux,
once I make it parallel, the stream gets stuck after the first passed value, can't figure out why
val source = source().parallel().runOn(Schedulers.parallel())
.map(this::toUpperCase)
.subscribe(sink())
private fun sink() = SimpleSink<SimpleDaoModel>()
private fun toUpperCase(simpleDaoModel: SimpleDaoModel) = simpleDaoModel.copy(stringValue = simpleDaoModel.stringValue.toUpperCase())
private fun source() = Flux.create { sink: FluxSink<SimpleDaoModel> ->
fun getNextAsync(): Job = GlobalScope.launch(Dispatchers.Default) {
val task = customSimpleModelRepository.getNextTask()
if (task != null) {
logger.info("emitting next task")
sink.next(task)
} else {
logger.info("No more tasks")
Timer("nextTaskBackoff", false).schedule(1000) {
getNextAsync()
}
}
}
sink.onRequest { getNextAsync() }
}
class SimpleSink<T> : BaseSubscriber<T>() {
public override fun hookOnSubscribe(subscription: Subscription) {
println("Subscribed")
request(1)
}
public override fun hookOnNext(value: T) {
println(value)
request(1)
}
}
If I remove the parallel operator, everything works like a charm.
Note: getNextTask is a suspended function