Create a callback function with another callback inside - kotlin

My apologies for the bad title, I'm fairly new to callbacks and I'm not sure how to explain what I'm trying to achieve.
I have a class called MyClass that has a function connectToService inside of it.
The function connectToService does some calculations and then calls a function with a callback, like this:
fun connectToService() {
//Whatever calculations
val a = 7
var b = 3
var c = a + b
val token = MyToken()
token.actionCallback = object: SuperSecretObject {
override fun onSuccess(asyncActionToken: MyToken) {
c++
}
override fun onFailure(asyncActionToken: MyToken) {
c--
}
}
}
I want to create another class, YourClass which creates an object of MyClass and then calls the connectToService function. When the connectToService function finishes either the onSuccess or onFailurefunctions, I want to do something depending on which one was triggered (something different each time, thats why I can't put it inside the onSuccess or onFailure blocks of code).
Something like this:
//Inside `yourClass`
private fun myFunc() {
val yourClassObj = YourClass()
youClassObj.connectToService {
if(onSuccess)
reinventTheWheel()
else
squareIt()
}
youClassObj.connectToService {
combAWatermelon()
}
youClassObj.connectToService {
sharpenMyHammer()
}
}
Is this possible? If so, how can I achieve it? If it's not, what would be the closest solution to this requirement?
EDIT:
More detailed information has been requested, so while I can't provide exact details, I'll do my best to explain what's going on.
I'm basically working on a library to simplify petitions. For example, MQTT petitions. This is something tht resembles what I want to achieve:
/**
* Subscribes to a list of topics and handles the results
*/
fun subscribe(client: MqttAndroidClient, list: MutableList<String>, onMsg: ((String, MqttMessage)->Unit)?=null, conLost: ((Throwable)->Unit)?=null, delComp: ((IMqttDeliveryToken)->Unit)?=null) {
if (client.isConnected) { //Assert connection
for(x in list.iterator()) { //Subscribe to events
client.subscribe(x, 0)
}
client.setCallback(object : MqttCallback {
override fun connectionLost(cause: Throwable) { //Lost connection
Log.i("TAG", "Connection lost")
conLost?.let { it(cause) }
}
#Throws(java.lang.Exception::class)
override fun messageArrived(topic: String, message: MqttMessage) { //Arrived message
Log.i("TAG", "Message arrived: topic => $topic, message => $message")
onMsg?.let { it(topic, message) }
}
override fun deliveryComplete(token: IMqttDeliveryToken) { //Delivery complete
Log.i("TAG", "Delivery complete")
delComp?.let { it(token) }
}
})
}
}
The messageArrived function must have a behaviour that can be customized depending on the app it's being used on.
For example, on one app I want the onMsg() function to be like this:
when(topic) {
"firstTopic" -> {
localVariable++
}
"secondTopic" -> {
localMethod()
}
"thirdTopic" -> {
localClass.variable.method()
}
}
If I'm using it on an Android device, I'd like to be able to update the interface, doing Android API calls, etc.

I'm not sure I got your question correctly. I think what you are looking for is passing lambdas.
fun connectToService(onSucc: ()->Unit, onFail: ()->Unit) {
//Whatever calculations
MyToken().actionCallback = object: SuperSecretObject {
override fun onSuccess(asyncActionToken: MyToken) {
onSucc()
}
override fun onFailure(asyncActionToken: MyToken) {
onFail()
}
}
}
Then you can call the function like this:
connectToService({ /* Something */ }, { /* Something else */ })

Related

Getting data from Datastore for injection

I am trying to retrieve the base url from my proto datastore to be used to initialize my ktor client instance I know how to get the data from the datastore but I don't know how to block execution until that value is received so the client can be initialized with the base url
So my ktor client service asks for a NetworkURLS class which has a method to return the base url
Here is my property to retrieve terminalDetails from my proto datastore
val getTerminalDetails: Flow<TerminalDetails> = cxt.terminalDetails.data
.catch { e ->
if (e is IOException) {
Log.d("Error", e.message.toString())
emit(TerminalDetails.getDefaultInstance())
} else {
throw e
}
}
Normally when I want to get the values I would do something like this
private fun getTerminalDetailsFromStore() {
try {
viewModelScope.launch(Dispatchers.IO) {
localRepository.getTerminalDetails.collect {
_terminalDetails.value = it
}
}
} catch(e: Exception) {
Log.d("AdminSettingsViewModel Error", e.message.toString()) // TODO: Handle Error Properly
}
}
but in my current case what I am looking to do is return terminalDetails.backendHost from a function and that where the issue comes in I know I need to use a coroutine scope to retrieve the value so I don't need to suspend the function but how to a prevent the function returning until the coroutine scope has finished?
I have tried using async and runBlocking but async doesn't work the way I would think it would and runBlocking hangs the entire app
fun backendURL(): String = runBlocking {
var url: String = "localhost"
val job = CoroutineScope(Dispatchers.IO).async {
repo.getTerminalDetails.collect {
it.backendHost
}
}
url
}
Can anyone give me some assistance on getting this to work?
EDIT: Here is my temporary solution, I do not intend on keeping it this way, The issue with runBlocking{} turned out to be the Flow<T> does not finish so runBlocking{} continues to block the app.
fun backendURL(): String {
val details = MutableStateFlow<TerminalDetails>(TerminalDetails.getDefaultInstance())
val job = CoroutineScope(Dispatchers.IO).launch {
repo.getTerminalDetails.collect {
details.value = it
}
}
runBlocking {
delay(250L)
}
return details.value.backendHost
}
EDIT 2: I fully fixed my issue. I created a method with the same name as my val (personal decision) which utilizes runBlocking{} and Flow<T>.first() to block while the value is retrieve. The reason I did not replace my val with the function is there are places where I need the information as well where I can utilize coroutines properly where I am not initializing components on my app
val getTerminalDetails: Flow<TerminalDetails> = cxt.terminalDetails.data
.catch { e ->
if (e is IOException) {
Log.d("Error", e.message.toString())
emit(TerminalDetails.getDefaultInstance())
} else {
throw e
}
}
fun getTerminalDetails(): TerminalDetails = runBlocking {
cxt.terminalDetails.data.first()
}

Change source Flow for LiveData

I try to to use Flow instead of LiveData in repos.
In viewModel:
val state: LiveData<StateModel> = stateRepo
.getStateFlow("euro")
.catch {}
.asLiveData()
Repository:
override fun getStateFlow(currencyCode: String): Flow<StateModel> {
return serieDao.getStateFlow(currencyCode).map {with(stateMapper) { it.fromEntityToDomain() } }
}
It works fine if currCode if always the same during viewModel's lifetime, for example euro
but what to do if currCode is changed to dollar?
How to make state to show a Flow for another param?
You need to switchMap your repository call.
I imagine you could dosomething like this:
class SomeViewModel : ViewModel() {
private val currencyFlow = MutableStateFlow("euro");
val state = currencyFlow.switchMap { currentCurrency ->
// In case they return different types
when (currentCurrency) {
// Assuming all of these database calls return a Flow
"euro" -> someDao.euroCall()
"dollar" -> someDao.dollarCall()
else -> someDao.elseCall()
}
// OR in your case just call
serieDao.getStateFlow(currencyCode).map {
with(stateMapper) { it.fromEntityToDomain() }
}
}
.asLiveData(Dispatchers.IO); //Runs on IO coroutines
fun setCurrency(newCurrency: String) {
// Whenever the currency changes, then the state will emit
// a new value and call the database with the new operation
// based on the neww currency you have selected
currencyFlow.value = newCurrency
}
}

How can I override logRequest/logResponse to log custom message in Ktor client logging?

Currently, the ktor client logging implementation is as below, and it works as intended but not what I wanted to have.
public class Logging(
public val logger: Logger,
public var level: LogLevel,
public var filters: List<(HttpRequestBuilder) -> Boolean> = emptyList()
)
....
private suspend fun logRequest(request: HttpRequestBuilder): OutgoingContent? {
if (level.info) {
logger.log("REQUEST: ${Url(request.url)}")
logger.log("METHOD: ${request.method}")
}
val content = request.body as OutgoingContent
if (level.headers) {
logger.log("COMMON HEADERS")
logHeaders(request.headers.entries())
logger.log("CONTENT HEADERS")
logHeaders(content.headers.entries())
}
return if (level.body) {
logRequestBody(content)
} else null
}
Above creates a nightmare while looking at the logs because it's logging in each line. Since I'm a beginner in Kotlin and Ktor, I'd love to know the way to change the behaviour of this. Since in Kotlin, all classes are final unless opened specifically, I don't know how to approach on modifying the logRequest function behaviour. What I ideally wanted to achieve is something like below for an example.
....
private suspend fun logRequest(request: HttpRequestBuilder): OutgoingContent? {
...
if (level.body) {
val content = request.body as OutgoingContent
return logger.log(value("url", Url(request.url)),
value("method", request.method),
value("body", content))
}
Any help would be appreciative
No way to actually override a private method in a non-open class, but if you just want your logging to work differently, you're better off with a custom interceptor of the same stage in the pipeline:
val client = HttpClient(CIO) {
install("RequestLogging") {
sendPipeline.intercept(HttpSendPipeline.Monitoring) {
logger.info(
"Request: {} {} {} {}",
context.method,
Url(context.url),
context.headers.entries(),
context.body
)
}
}
}
runBlocking {
client.get<String>("https://google.com")
}
This will produce the logging you want. Of course, to properly log POST you will need to do some extra work.
Maybe this will be useful for someone:
HttpClient() {
install("RequestLogging") {
responsePipeline.intercept(HttpResponsePipeline.After) {
val request = context.request
val response = context.response
kermit.d(tag = "Network") {
"${request.method} ${request.url} ${response.status}"
}
GlobalScope.launch(Dispatchers.Unconfined) {
val responseBody =
response.content.tryReadText(response.contentType()?.charset() ?: Charsets.UTF_8)
?: "[response body omitted]"
kermit.d(tag = "Network") {
"${request.method} ${request.url} ${response.status}\nBODY START" +
"\n$responseBody" +
"\nBODY END"
}
}
}
}
}
You also need to add a method from the Ktor Logger.kt class to your calss with HttpClient:
internal suspend inline fun ByteReadChannel.tryReadText(charset: Charset): String? = try {
readRemaining().readText(charset = charset)
} catch (cause: Throwable) {
null
}

Android Kotlin Coroutines: what is the difference between flow, callbackFlow, channelFlow,... other flow constructors

I have code that should change SharedPreferences into obsarvable storage with flow so I've code like this
internal val onKeyValueChange: Flow<String> = channelFlow {
val callback = SharedPreferences.OnSharedPreferenceChangeListener { _, key ->
coroutineScope.launch {
//send(key)
offer(key)
}
}
sharedPreferences.registerOnSharedPreferenceChangeListener(callback)
awaitClose {
sharedPreferences.unregisterOnSharedPreferenceChangeListener(callback)
}
}
or this
internal val onKeyValueChange: Flow<String> = callbackFlow {
val callback = SharedPreferences.OnSharedPreferenceChangeListener { _, key ->
coroutineScope.launch {
send(key)
//offer(key)
}
}
sharedPreferences.registerOnSharedPreferenceChangeListener(callback)
awaitClose {
sharedPreferences.unregisterOnSharedPreferenceChangeListener(callback)
}
}
Then I observe this preferences for token, userId, companyId and then log into but there is odd thing as I need to build app three times like changing token not causes tokenFlow to emit anything, then second time new userId not causes userIdFlow to emit anything, then after 3rd login I can logout/login and it works. On logout I am clearing all 3 properties stores in prefs token, userId, companyId.
For callbackFlow:
You cannot use emit() as the simple Flow (because it's a suspend function) inside a callback. Therefore the callbackFlow offers you a synchronized way to do it with the trySend() option.
Example:
fun observeData() = flow {
myAwesomeInterface.addListener{ result ->
emit(result) // NOT ALLOWED
}
}
So, coroutines offer you the option of callbackFlow:
fun observeData() = callbackFlow {
myAwesomeInterface.addListener{ result ->
trySend(result) // ALLOWED
}
awaitClose{ myAwesomeInterface.removeListener() }
}
For channelFlow:
The main difference with it and the basic Flow is described in the documentation:
A channel with the default buffer size is used. Use the buffer
operator on the resulting flow to specify a user-defined value and to
control what happens when data is produced faster than consumed, i.e.
to control the back-pressure behavior.
The trySend() still stands for the same thing. It's just a synchronized way (a non suspending way) for emit() or send()
I suggest you to check Romans Elizarov blog for more detailed information especially this post.
Regarding your code, for callbackFlow you wont' be needing a coroutine launch:
coroutineScope.launch {
send(key)
//trySend(key)
}
Just use trySend()
Another Example, maybe much concrete:
private fun test() {
lifecycleScope.launch {
someFlow().collectLatest {
Log.d("TAG", "Finally we received the result: $it")
// Cancel this listener, so it will not be subscribed anymore to the callbackFlow. awaitClose() will be triggered.
// cancel()
}
}
}
/**
* Define a callbackFlow.
*/
private fun someFlow() = callbackFlow {
// A dummy class which will run some business logic and which will sent result back to listeners through ApiCallback methods.
val service = ServiceTest() // a REST API class for example
// A simple callback interface which will be called from ServiceTest
val callback = object : ApiCallback {
override fun someApiMethod(data: String) {
// Sending method used by callbackFlow. Into a Flow we have emit(...) or for a ChannelFlow we have send(...)
trySend(data)
}
override fun anotherApiMethod(data: String) {
// Sending method used by callbackFlow. Into a Flow we have emit(...) or for a ChannelFlow we have send(...)
trySend(data)
}
}
// Register the ApiCallback for later usage by ServiceTest
service.register(callback)
// Dummy sample usage of callback flow.
service.execute(1)
service.execute(2)
service.execute(3)
service.execute(4)
// When a listener subscribed through .collectLatest {} is calling cancel() the awaitClose will get executed.
awaitClose {
service.unregister()
}
}
interface ApiCallback {
fun someApiMethod(data: String)
fun anotherApiMethod(data: String)
}
class ServiceTest {
private var callback: ApiCallback? = null
fun unregister() {
callback = null
Log.d("TAG", "Unregister the callback in the service class")
}
fun register(callback: ApiCallback) {
Log.d("TAG", "Register the callback in the service class")
this.callback = callback
}
fun execute(value: Int) {
CoroutineScope(Dispatchers.IO).launch {
if (value < 2) {
callback?.someApiMethod("message sent through someApiMethod: $value.")
} else {
callback?.anotherApiMethod("message sent through anotherApiMethod: $value.")
}
}
}
}

Kotlin - Trying to factorize code with high-order function

I'm quite new to Kotlin and I'd like to see if using high-order functions can help in my case.
My use-case is that I need to call the methods of an IInterface derived class to send events to one or more components. And I'd like to make this generic, and I want to check if a high-order funtion can help. A sample of code will help to understand (well, I hope so!).
private val eventListeners = mutableListOf<IEventInterface>() // List filled somewhere else!
private fun sendConnectionEvent(dummyString: String) {
val deadListeners = mutableListOf<IEventInterface>()
eventListeners.forEach {
try {
it.onConnectionEvent(dummyString)
} catch (e: DeadObjectException) {
Log.d(TAG, "Removing listener - Exception ${e.message}")
deadListeners.add(it)
}
}
deadListeners.forEach { it ->
eventListeners.remove(it)
}
}
private fun sendWonderfulEvent(dummyString: String, dummyInt: Int) {
val deadListeners = mutableListOf<IEventInterface>()
eventListeners.forEach {
try {
it.onWonderfulEvent(dummyString, dummyInt)
} catch (e: DeadObjectException) {
Log.d(TAG, "Removing listener - Exception ${e.message}")
deadListeners.add(it)
}
}
deadListeners.forEach { it ->
eventListeners.remove(it)
}
}
I added 2 similar methods (I will have many more in the real use case) and I think (I hope!) that something could be done but I can't make high-order function works in this case because:
I want to call the same method on several instances, and not 'just' a basic function
To make things even worse, the methods I need to call don't have the same prototype (that would have been too easy!).
Hope this is clear enough.
Thanks for your help!
VR
Here is how it can be done
fun onEvent(body: (IEventInterface) -> Unit) {
val deadListeners = mutableListOf<IEventInterface>()
eventListeners.forEach {
try {
body(it)
} catch (ex: DeadObjectException) {
Log.d(TAG, "Removing listener - Exception ${e.message}")
deadListeners.add(it)
}
}
deadListeners.forEach { it ->
eventListeners.remove(it)
}
}
Supposing an interface like this:
interface IEventInterface {
fun onConnectionEvent(dummyString: String)
fun onWonderfulEvent(dummyString: String, dummyInt: Int)
}
Define an generic type that implements your defined interface ( <T : IEventInterface>)
Define an mutable list of this type to receive your implementation (MutableList<T>.removeIfThrows)
Expect an extension function for you type that will do your specific validation (and custom parameters if you want)
Using an apply and returning the instance you can run your code like a pipeline
Executing the custom validation when you want
private fun <T : IEventInterface> MutableList<T>.removeIfThrows(validation: T.() -> Unit, customLogMessage: String? = null): MutableList<T> {
return apply {
removeIf {
it.runCatching {
validation()
}.onFailure { error ->
print(customLogMessage ?: "Removing listener - Exception ${error.message}")
}.isFailure
}
}
}
Define your specific implementation passing just the function with custom validation as an parameter
private fun <T : IEventInterface> MutableList<T>.sendConnectionEvent(dummyString: String) = removeIfThrows({
onConnectionEvent(dummyString)
})
private fun <T : IEventInterface> MutableList<T>.sendWonderfulEvent(dummyString: String, dummyInt: Int) = removeIfThrows({
onWonderfulEvent(dummyString, dummyInt)
})
Now you can run your code like an pipeline modifying your original object like this
private fun nowYouCanDoSomethingLikeThis() {
eventListeners
.sendConnectionEvent("some dummy string")
.sendWonderfulEvent("some another dummy string", 123)
}