Implement shared Flowable in RxKotlin with dynamic parameters - android

Nothing I've tried seems to solve my problem.
I have three buttons with with onClick behavior. Each of these buttons calls the same method launchActivity but with different parameters. launchActivity does some IO with the variables received from the onClick methods and then returns an intent. I would like to be able to implement a RxKotlin/Java Flowable to handle backpressure collectively across the three onClick methods so that I can implement BackpressureStrategy.DROP. So if onClick1 was initiated onClick2 would be dropped if initated while launchActivity was still processing onClick1 on the io() thread.
class ActivityLauncher {
fun onClick1() {
val intent = launchActivity(inFile1, outFile1)
startActivity(intent)
}
fun onClick2() {
val intent = launchActivity(inFile2, outFile2)
startActivity(intent)
}
fun onClick3() {
val intent = launchActivity(inFile3, outFile3)
startActivity(intent)
}
fun launchActivity(in: File, out: File): Intent {
// do IO with in and out files and create an intent
return intent
}
}
If I was to implement this as say a Single, I'd implement the onClick methods somewhat like:
fun onClick() {
Single.fromCallable(launchActivity(inFile, outFile)
.observeOn(scheduler.io())
.subscribeOn(scheduler.ui())
.subscribe { i -> startActivity(i) }
}
But I can't figure out how to call launchActivity from a shared Flowable that is accessible to all three onClick methods while still allowing them to pass in their unique inFile and outFile variables and enforcing backpressure.
The basic criteria is:
Ensure launchActivity is run on the io() thread
Pass the unique arguments from each of the onClick methods to launchActivity each time onClick[#] is run.
BackpressureStrategy.DROP is used to ensure only the first click in a series is processed in launchActivity
The resulting intent from launchActivity is passed to startActivity
How do I implement a Flowable to allow this behavior?

This doesn't really need to be done in a reactive way, seems like you're using it because of the convenience of threading - nothing wrong with that, however it brings complications when you try and model your situation using Rx.
Single is the correct operator to use - you only want 1 emission (BackpressureStrategy.DROP in a Flowable will still emit items of downstream if they can keep up). You just need to make your buttons isClickable = false at the start of your onClick(), and set then back to isClickable = true - something like :
Single.fromCallable { launchActivity(inFile, outFile) }
.doOnSubscribe { disableButtonsFunction() }
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doFinally { enableButtonsFucntion() }
.subscribe { i -> startActivity(i) }

Related

How can I get data from ViewModel to Activity or Fragment in clean and simple way?

I have a question... sometimes, I need to get data from ViewModel directly. For example, Let's say there's a isChecked() method in ViewModel. And I want to use it in the if condition.
if(viewModel.isChecked()){
// TODO:
}
So, what I am doing right now is:
fun isChecked(): Boolean = runBlocking {
val result = dbRepo.getData()
val response = apiRepo.check(result)
return response.isSuccessful
}
It uses runBlocking. So, it runs on MainThread. I don't think it's a good way because it can freeze the screen. But yes, if the condition needs to run, it needs to wait it until it gets the data from DB and Network.
Another way that I can think of is using LiveData. However, I can't use it in the condition. So, I needs to move the condition in the observer block. But sometimes, this can't be done because there can be something before the condition. And it doesn't seem to look direct but writing code here and there and finally get that data.
So, Is there any simpler way than this?
Your best bet if you have something slow or blocking like that is to rethink how you are using the data entirely. Instead of trying to return it, use LiveData or callbacks to handle the response asynchronously without causing your UI to hang or become laggy. In these cases you really only have three options:
Use a callback to handle when the response is received
Use observable data like LiveData to handle when the response is received
Change the method to a suspend function and call it from a coroutine
Forcing a method to wait to return on the main thread without using one of these is going to cause the app to hang.
Callback to get state
It's hard to say definitely what the best solution for you is without more details about how you are using isChecked(), but one pattern that could work would be to use a callback to handle what you were formerly putting in the if statement, like this (in the ViewModel):
fun getCheckedState(callback: (Boolean)->Unit) {
viewModelScope.launch {
// do long-running task to get checked state,
// using an appropriate dispatcher if needed
val result = dbRepo.getData()
val response = apiRepo.check(result)
// pass "response.isSuccessful" to the callback, to be
// used as "isChecked" below
callback(response.isSuccessful)
}
}
You would call that from the activity or fragment like this:
viewModel.getCheckedState { isChecked ->
if( isChecked ) {
// do something
}
else {
// do something else
}
}
// CAUTION: Do NOT try to use variables you set inside
// the callback out here!
A word of caution - the code inside the callback you pass to getCheckedState does not run right away. Do not try to use things you set inside there outside the callback scope or you fall into this common issue
Simpler Callback
Alternately, if you only want to run some code when isChecked is true, you could simplify the callback like this
fun runIfChecked(callback: ()->Unit) {
viewModelScope.launch {
// do long-running task to get checked state,
// using an appropriate dispatcher if needed
val result = dbRepo.getData()
val response = apiRepo.check(result)
// only call the callback when it's true
if( response.isSuccessful ) {
callback()
}
}
}
and call it with
viewModel.runIfChecked {
// do something
}
// Again, don't try to use things from the callback out here!
Use lifecyclescope.launch(Dispatcher.IO) instead of runblocking
Try this code on your ViewModel class:
suspend fun isChecked(): Boolean {
val response: Response? = null
viewModelScope.launch(Dispatchers.IO) {
val result = dbRepo.getData()
response = apiRepo.check(result)
}.join()
return response?.isSuccessful
}
From Activity:
// Suppose you have a button
findViewById<Button>(R.id.btn).setOnClickListener({
CoroutineScope(Dispatchers.Main).launch {
if (viewModel.isChecked()) {
Log.d("CT", "Do your others staff")
}
}
})
Hope it work file. If no let me comment

Android Kotlin - Unit test a delayed coroutine that invokes a lambda action when delay is finished

Ive been struggling with this for quite some time now, perhaps someone could help...
I have this function in my class under test:
fun launchForegroundTimer(context: Context) {
helper.log("AppRate", "[$TAG] Launching foreground count down [10 seconds]")
timerJob = helper.launchActionInMillisWithBundle(Dispatchers.Main, TimeUnit.SECOND.toMillis(10), context, this::showGoodPopupIfAllowed)
}
So in that function, I first write to some log and then I call a coroutine function that expects a Dispatcher param, how long to wait before running the action, Any object that I would like to pass on to the action and the actual action function that is invoked when time has passed.
So in this case, the this::showGoodPopupIfAllowed which is a private method in the class, gets called when the 10,000 ms have passed.
Here is that function:
private fun showGoodPopupIfAllowed(context: Context?) {
if (isAllowedToShowAppRate()) {
showGoodPopup(context)
}
}
In that first if, there are a bunch of checks that occur before I can call showGoodPopup(context)
Now, here is the helper.launchActionInMillisWithBundle function:
fun <T> launchActionInMillisWithBundle(dispatcher: CoroutineContext, inMillis: Long, bundle: T, action: (T) -> Unit): Job = CoroutineScope(dispatcher).launchInMillisWithBundle(inMillis, bundle, action)
And here is the actual CoroutineScope extension function:
fun <T> CoroutineScope.launchInMillisWithBundle(inMillisFromNow: Long, bundle: T, action: (T) -> Unit) = this.launch {
delay(inMillisFromNow)
action(bundle)
}
What I am trying to achieve is a UnitTest that calls the launchForegroundTimer function, calls the helper function with the appropriate arguments and also continue through and call that lambda showGoodPopupIfAllowed function where I can also provide mocked behaviour to all the IF statments that occur in isAllowedToShowAppRate.
Currently my test stops right after the launchActionInMillisWithBundle is called and the test just ends. I assume there is no real call to any coroutine because I am mocking the helper class... not sure how to continue here.
I read a few interesting articles but none seems to resolve such state.
My current test function looks like this:
private val appRaterManagerHelperMock = mockkClass(AppRaterManagerHelper::class)
private val timerJobMock = mockkClass(Job::class)
private val contextMock = mockkClass(Context::class)
#Test
fun `launch foreground timer`() {
every { appRaterManagerHelperMock.launchActionInMillisWithBundle(Dispatchers.Main, TimeUnit.SECOND.toMillis(10), contextMock, any()) } returns timerJobMock
val appRaterManager = AppRaterManager(appRaterManagerHelperMock)
appRaterManager.launchForegroundTimer(contextMock)
verify(exactly = 1) { appRaterManagerHelperMock.log("AppRate", "[AppRaterManager] Launching foreground count down [10 seconds]") }
}
I'm using mockk as my Mocking lib.
AppRaterManager is the class under test
I'd like to also mention that, in theory I could have moved the coroutine invocation outside the class under test. So an external class like activity.onResume() could launch some sort of countdown and then call directly a function that checks showGoodPopupIfAllowed(). But currently, please assume that I do not have any way to change the calling code so the timer and coroutine should remain in the class under test domain.
Thank you!
Alright, I read a bit deeper into capturing/answers over at https://mockk.io/#capturing and saw there is a capture function.
So I captured the lambda function in a slot which enables me invoke the lambda and then the actual code continues in the class under test. I can mock the rest of the behavior from there.
Here is my test function for this case (for anyone who gets stuck):
#Test
fun `launch foreground timer, not participating, not showing good popup`() {
val slot = slot<(Context) -> Unit>()
every { appRaterManagerHelperMock.launchActionInMillisWithBundle(Dispatchers.Main, TimeUnit.SECOND.toMillis(10), contextMock, capture(slot)) } answers {
slot.captured.invoke(contextMock)
timerJobMock
}
every { appRaterManagerHelperMock.isParticipating() } returns false
val appRaterManager = AppRaterManager(appRaterManagerHelperMock)
appRaterManager.launchForegroundTimer(contextMock)
verify(exactly = 1) { appRaterManagerHelperMock.log("AppRate", "[AppRaterManager] Launching foreground count down [10 seconds]") }
verify(exactly = 1) { appRaterManagerHelperMock.isParticipating() }
verify(exactly = 0) { appRaterManagerHelperMock.showGoodPopup(contextMock, appRaterManager) }
}
So what's left now is how to test the coroutine actually invokes the lambda after the provided delay time is up.

How to Pass finish() from Activity class as a function parameter (So the code can be reused in Kotlin)?

I have to use this peice of code twice in two different places in two different activites. No good programmer would willingly want to use same code in multiple places without reusing it.
//when back key is pressed
override fun onBackPressed() {
dialog.setContentView(twoBtnDialog.root)
twoBtnDialog.title.text = getString(R.string.warning)
twoBtnDialog.msgDialog.text = getString(R.string.backPressWarning)
twoBtnDialog.ok.text = getString(R.string.exit)
twoBtnDialog.cancel.text = getString(R.string.cancel)
twoBtnDialog.ok.setOnClickListener {
//do nav back
finish()
dialog.dismiss()
}
twoBtnDialog.cancel.setOnClickListener {
dialog.dismiss() //just do nothing
}
dialog.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
dialog.show()
}
I can move it to one place, but the problem is I have to pass in the finish() function from Activity Class to close the calling activity.
My simple question is how can I resue it ? Or How can I pass this function (finish()) to a different class (which is in some other file).
Take a function type parameter in your method.
fun doBackPress(finish: () -> Unit) {
// you need to invoke the finish method when necessary
finish.invoke()
}
Then you need to call the method and have to pass the finish() method from any other activity or fragment method like bellow.
override fun onBackPressed() {
doBackPress { finish() }
}
You could make an interface and extension function, which I think is less messy than trying to pass everything you need as parameters to a function, because it communicates intent better and makes it harder to do something wrong.
interface MyDialogOwner {
val dialog: Dialog
val twoBtnDialog: MyDialogBinding
fun Activity.handleBackPress() {
//the exact same content you have in your function now.
}
}
// In Activity:
override fun onBackPressed() = handleBackPress()
Your Activities should implement the interface, using your existing properties for dialog and twoBtnDialog (just add override in front of their declarations).
I'm assuming twoBtnDialog is a view binding.

How to minimize the number of webservice calls using Kotlin coroutines?

In my Android Kotlin project, I call a webservice in a coroutine (myWebservice is just a custom class that manages webservice calls):
fun searchForItems(userInput: String)
{
CoroutineScope(Dispatchers.IO + Job()).launch {
val listOfItems = myWebService.call(userInput)
}
}
That method is called everytime a user types a character in an EditText, so the app calls a webservice that returns a list of items matching his request. But I want to optimize that.
Let's say that the user types the word: "apple". In order to minimise the number of webservice calls, here is what I want to achieve:
when the user types the first letter (a), the webservice is called
when the user types the next letters, there is no new webservice call as long as the first called hasn't returned (let's assume that he has enough time to type the next letters (pple))
when the first webservice call is done, a new call is done automatically with the new user input (apple)
What would be the best practices to achieve that?
Or is there a better way to minimize the number of webservice calls?
Thanks.
Using Kotlin coroutines I solved it like this:
class SomeViewModel : ViewModel() {
private var searchJob: Job? = null
fun search(userInput: String) {
searchJob?.cancel() // cancel previous job when user enters new letter
searchJob = viewModelScope.launch {
delay(300) // add some delay before search, this function checks if coroutine is canceled, if it is canceled it won't continue execution
val listOfItems = myWebService.call(userInput)
...
}
}
}
When user enters first letter search() function is called, coroutine is launched and Job of this coroutine is saved to searchJob. Then delay(300) function is called to wait for another user input before calling the WebService. If user enters another letter before 300 milliseconds expire search() function will be called again and previous coroutine will be cancelled using searchJob?.cancel() function and WebService will not be called in the first coroutine.
You need debouncing. Coroutines or not, you shouldn't call the web service with each letter for every active user. That will eventually DDOS the web service if your app is used by many people at once.
Since you are using Kotlin, instead of coroutines you can use Flow. It comes with a built in debounce method. Also the stream of letters is easily modelled as a flow. It would be something like this (I'm not sure this even runs, but you get the idea):
textFlow = flow {
myTextView.doOnTextChanged { text, start, count, after -> emit(text)}
}.debounce(1000)
The more complex alternative is RxJava's debounce operator.
You can also try LiveData with lupajz's debounce extension
Or you can also roll your own solution.
To optimize web service calls you can use StateFlow and its debounce operator. For example in ViewModel:
val inputFlow = MutableStateFlow("")
init {
inputFlow
.debounce(300) // filters out values that are followed by the newer values within the given timeout. The latest value is always emitted.
.filterNot { userInput -> userInput.isEmpty() } // filter the unwanted string like an empty string in this case to avoid the unnecessary network call.
.distinctUntilChanged() // to avoid duplicate network calls
.flowOn(Dispatchers.IO) // Changes the context where this flow is executed to Dispatchers.IO
.onEach { userInput -> // go through each filtered userInput
val listOfItems = myWebService.call(userInput)
// do sth with listOfItems
}
.launchIn(viewModelScope)
}
fun searchForItems(userInput: String) {
inputFlow.value = userInput
}
You can achieve the same functionality using Rx Java's debounce operator. every time you will enter a text in Edit Text Rx Java's debounce will call the webservice and quickly will produce result and if Again user enter another text it will then call the web service.
Please refer the below link for batter understaing , So you can modify your code and achieve same functinolaity
https://blog.mindorks.com/implement-search-using-rxjava-operators-c8882b64fe1d
RxSearchObservable.fromView(searchView)
.debounce(300, TimeUnit.MILLISECONDS)
.filter(new Predicate<String>() {
#Override
public boolean test(String text) {
if (text.isEmpty()) {
textViewResult.setText("");
return false;
} else {
return true;
}
}
})
.distinctUntilChanged()
.switchMap(new Function<String, ObservableSource<String>>() {
#Override
public ObservableSource<String> apply(String query) {
return dataFromNetwork(query)
.doOnError(throwable -> {
// handle error
})
// continue emission in case of error also
.onErrorReturn(throwable -> "");
}
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Consumer<String>() {
#Override
public void accept(String result) {
textViewResult.setText(result);
}
});
var myApiUpdateTask: Deferred<Unit>? = null
fun getApiResponse() = viewModelScope.launch {
myApiUpdateTask?.cancel()
myApiUpdateTask = async {
delay(500)
// TODO: your api request
}
}
Using this structure inside the viewmodel; You can wait for 500 milliseconds and send the request you will send consecutively.

Best way of creating an Observable in RxJava 2 to be able to use the emitter for mvvm

My intention is to create an Observable that can be made to emit an item inside a class.
My goal is to create a ViewModel, which can expose a stream to a View in Android, reactively delivering items.
My approach is as follows:
class MyMVVM {
private lateinit var timeSelectedEmitter: ObservableEmitter<Int>
val timeSelectedObservable: Observable<Int> = Observable.create { emitter -> timeSelectedEmitter = emitter }
}
This should, in theory and as far as I understand it currently, give me access to the emitter, which then can be called at any point to deliver time-events to subscribers of timeSelectedObservable (subscribers which are inside the View)
1) Is this a correct approach or should this be done in another way?
2) Is there a way for the emitter to be created on Observable creation instead of when a subscriber is subscribing to the Observable (to avoid nullpointer exceptions or Kotlin exceptions)?
Thanks.
I usually create the Observable once and return the same Observable to anything that wants to subscribe to it. This way, if there are multiple subscribers, a new Observable doesn't need to be created each time.
So something like:
class ViewModel {
private val timeObservable = PublishSubject.create<Int>()
fun timeObservable(): Observable<Int> = timeObservable
fun update() {
// ...
timeObservable.onNext(time)
}
}
class Activity {
...
viewModel.timeObservable()
.subscribe {
time -> ...
})
}

Categories

Resources