How to implement an optional callback from scratch in Kotlin? - android

I have this function, which works wonderfully.
inline fun <reified T:Any>String.parse() : T {
return GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create().fromJson<T>(this, T::class.java)
}
fun request (callback: (MyClass)->Unit) {
val url = URL("someurl").readText()
val myObject : MyClass = str.parse()
callback(myObject)
}
net.request {
it.myFunction()
println (it.myString)
}
myObject automatically filled with an object of type MyClass, and returned correctly to the callback.
Now I want to catch the error, and have the callback can return it as well.
fun request (callback: (MyClass?, Exception?)->Unit) {
try {
val url = URL("someurl").readText()
val myObject : MyClass = str.parse()
callback(myObject, null)
}
catch (e: Exception) {
callback(null, e)
}
}
net.request { response, error ->
if (response != null ) { // do something }
else { // report something }
}
But this is ugly, because no matter what, I will have to force the callback to always have two parameters, but only one is present at a time. So I'm searching for optional callback methods. I want to be able to call the method like this:
net.request {
onSuccess { response -> // do something }
onError { error -> // report something }
}
Or probably:
net.request
.onSuccess { response -> // do something }
.onError { error -> // report something }
If I don't want to handle the error, I simply make a call like this:
net.request {
onSuccess { // do something with 'it' }
}
What I can found over the internet is overwriting the existing callback methods like this. This is not what I want. I want to write that callback from scratch. Looking at the source code sometimes doesn't help either because the code is in Java, and I don't understand Java. Not yet.
And I understand that major library in Kotlin like retrofit or JavaRx probably already implement something like this, but I just want to know the bare minimum code needed to do this, as this is how I learn. I just can't find the correct tutorial for this.

You can try this library out. It helps model success/failure operations concisely. Your callback can then take in your MyClass object wrapped this way Result<MyClass, Exception> or just Result.
To pass a value to your callback, you then do Callback(Result.of(MyClass)) for a successful operation or a Callback(Result.of(Exception())) in case of a failure.
You can then consume the callback by using any of the functions below
//if successful
result.success {
}
//if failed
result.failure {
}
//fold is there, if you want to handle both success and failure
result.fold({ value ->
//do something with value
}, { error ->
//do something with error
})
Hope this helps :)
Edit: As #user2340612 pointed out, Result has been added to the Kotlin stdlib. See here for more details

Related

Is it necessary to call .isSuccess?

I need to read the content of a collection in real-time. Here is what I have tried:
override fun getItems() = callbackFlow {
val listener = db.collection("items").addSnapshotListener { snapshot, e ->
val response = if (snapshot != null) {
val items = snapshot.toObjects(Item::class.java)
Response.Success(items)
} else {
Response.Error(e)
}
trySend(response).isSuccess //???
}
awaitClose {
listener.remove()
}
}
And it works fine. The problem is that I don't understand the purpose of .isSuccess. Is it mandatory to be added?
trySend() returns a ChannelResult object which contains the result of the operation. If ChannelResult.isSuccess returns true then the response had been successfully sent, otherwise the operation has been failed for some reason (maybe because of the buffer overflow) or because of a coroutine had been finished. You may handle it if you want, but usually it's omitted. Or you may log this result.

Is it necessary to use .flowOn(Dispatchers.IO) inside repository class?

I'm trying to implement One Tap, so I have created this function:
override fun oneTapSgnInWithGoogle() = flow {
try {
emit(Result.Loading)
val result = oneTapClient.beginSignIn(signInRequest).await()
emit(Result.Success(result))
} catch (e: Exception) {
emit(Result.Error(e.message))
}
}
//.flowOn(Dispatchers.IO)
And some programmer told me that I need to add .flowOn(Dispatchers.IO) to the above function, so it can be correct. My code work correct without it. Here is how I call this function in the ViewModel:
fun oneTapSignIn() = viewModelScope.launch {
repo.oneTapSignInWithGoogle().collect { response ->
oneTapSignInResponse = response
}
}
Is it really necessary to do that? I'm really confused.
You're calling beginSignIn which returns a Task, so it does its own stuff in the background. Now Task.await is suspending, not blocking, so it won't block the current thread while waiting for the task.
Therefore, the body of your flow doesn't contain any blocking stuff, so there is no reason to use flowOn(Dispatchers.IO) here.

Kotlin Flow not collected anymore after working initially

Basically I want to make a network request when initiated by the user, collect the Flow returned by the repository and run some code depending on the result. My current setup looks like this:
Viewmodel
private val _requestResult = MutableSharedFlow<Result<Data>>()
val requestResult = _requestResult.filterNotNull().shareIn(
scope = viewModelScope,
started = SharingStarted.WhileViewSubscribed,
replay = 0
)
fun makeRequest() {
viewModelScope.launch {
repository.makeRequest().collect { _requestResult.emit(it) }
}
}
Fragment
buttonLayout.listener = object : BottomButtonLayout.Listener {
override fun onButtonClick() {
viewModel.makeRequest()
}
}
lifecycleScope.launchWhenCreated {
viewModel.requestResult.collect { result ->
when (result) {
Result.Loading -> {
doStuff()
}
is Result.Success -> {
doDifferentStuff(result.data)
}
is Result.Failure -> {
handleError()
}
}
}
}
The first time the request is made everything seems to work. But starting with the second time the collect block in the fragment does not run anymore. The request is still made, the repository returns the flow as expected, the collect block in the viewmodel runs and emit() also seems to be executed successfully.
So what could be the problem here? Something about the coroutine scopes? Admittedly I lack any sort of deeper understanding of the matter at hand.
Also is there a more efficient way of accomplishing what I'm attempting using Kotlin Flows in general? Collecting a flow and then emitting the same flow again seems a bit counterintuitive.
Thanks in advance:)
According to the documentation there are two recommended alternatives:
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
//your thing
}
}
I rather the other alternative:
viewLifecycleOwner.lifecycleScope.launch {
viewModel.makeReques().flowWithLifecycle(viewLifecycleOwner.lifecycle, Lifecycle.State.STARTED)
.collect {
// Process the value.
}
}
I like the flowWithLifecycle shorter syntax and less boiler plate. Be carefull thar is bloking so you cant have anything after that.
The oficial docs
https://developer.android.com/topic/libraries/architecture/coroutines
Please be aware you need the lifecycle aware library.

Is there any way to return value and end a function in callback?

I am struggling with Firestore.
suspend fun writeClientMemo(userId: String, memo: Memo): MemoWriteResponse {
val response = MemoWriteResponse()
val docRef = Firebase.firestore
.collection(COLLECTION_USER)
.document(userId)
val result = Tasks.await(docRef.get())
if (result.exists() && result.data != null) {
Log.d("memo", "data exists ")
val res = docRef.update(FIELD_MEMO_LIST, FieldValue.arrayUnion(memo))
res.addOnSuccessListener {
Log.d("memo", "success")
response.isSuccessful = true
// What I want to do is something like these:
// 1. return#addOnSuccessListener response
// 2. return response
}.addOnFailureListener { e ->
Log.d("memo", "failed")
response.isSuccessful = false
response.errorMessage = "${e.toString}"
}
}
return response // At the moment this always get false(default is false)
}
I tried to make this synchronous but didn't work.
when {
res.isSuccessful -> {
response.isSuccessful = true
Log.d("memo", "true: ${response.toString()}")
}
res.isCanceled -> {
response.run {
isSuccessful = false
errorMessage = "ADD: Error writing document}"
}
Log.d("memo", "false: ${response.toString()}")
}
res.isComplete -> {
Log.d("memo", "false2: ${response.toString()}")
}
else->{
Log.d("memo", "false3: ${response.toString()}")
}
}
I always get false3 here.
Is there any way to return value and end a function in a callback?
No, you cannot simply return a value of an asynchronous operation as a result of a method. Firebase API, as most modern cloud/ APIs are asynchronous. This means that it takes some time until the operation completes. Since you are using the Kotlin programming language, I recommend you consider using Kotlin Coroutines and call .await() like this:
docRef.get().await();
This means that you suspend the operation until the data is available. This means that this value can be returned as a result of a method.
For more information you can check my answers from the following post:
How to return a list from Firestore database as a result of a function in Kotlin?
Or read the following article:
How to read data from Cloud Firestore using get()?
Where I have explained four ways in which you can deal with the Firestore using get().
P.S.
I tried to make this synchronous but didn't work
Never try to do that. It's best to get used to dealing with Firestore in such a way. Hopefully, my explanation from the above answer and article can help you a little bit.
You can achieve this using suspendCoroutine() utility function. It is a common practice to use it to convert asynchronous callback-based API to synchronous suspend API. You use it like this:
suspend fun loadSomeData(): String {
return suspendCoroutine { cont ->
loadSomeDataAsync(
onSuccess = {
cont.resume("ok!")
},
onFailure = {
cont.resume("failed!")
// OR:
cont.resume(null)
// OR:
cont.resumeWithException(Exception("failed"))
}
)
}
}
suspendCoroutine() suspends execution in outer scope until one of resume*() function is called. Then it resumes with the provided result.

Kotlin - Avoid nested callbacks when fetching multiple JSON files with ION

I have some code that 1] fetches a JSON file and 2] depending on the content of that JSON file, fetches another JSON file. It uses the ion library.
The code roughly looks like this (Sidenote - this code is on the onCreate method of my activity and e is an object of the type Exception):
Ion.with(applicationContext)
.load("someURL")
.asJsonObject()
.setCallback { e, result->
//Do synchronous stuff with the "result" and "e" variables
//that determines whether boolean someCondition is true or false
if(somecondition) {
Ion.with(applicationContext)
.load("https://raw.githubusercontent.com/vedantroy/image-test/master/index.json")
.asJsonObject()
.setCallback { e, result ->
//Some synchronous code in here
}
}
}
This code is very ugly, and I'd like to improve it in order to get rid of the nesting. Is that possible, and if so, how would I do it?
I've been experimenting and it seems I can call the method .then() after setCallback like so, where .then() accepts a parameter that is a callback object:
Ion.with(applicationContext).load("someURL").setCallback { //Lambda Stuff }.then()
so maybe that will be part of the solution, but I'm not sure.
The Ion project has a module for Kotlin coroutines: https://github.com/koush/ion/tree/master/ion-kotlin, but it doesn't seem to be in a production state. Luckily, it is very simple to add your own bridge between any asynchronous API and Kotlin coroutines.
Here's an example of how to do it for Ion:
private suspend fun fetchIon(url: String): String = suspendCoroutine { cont ->
Ion.with(this)
.load(url)
.asString()
.setCallback { e, result ->
if (e != null) {
cont.resumeWithException(e)
} else {
cont.resume(result)
}
}
}
Now you can have code like this:
class MyActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
launch(UI) {
if (fetchIon("example.org/now") > fetchIon("example.org/deadline") {
...
}
}
}
As you can see, you execute asynchronous IO code just like it was blocking. To handle errors, just wrap it in a try-catch, as you would any "regular" code.

Categories

Resources