How to verify state change in Compose? - android-jetpack-compose

Say in a composable I have two states:
var stateA by remember { mutableStateOf(varA) }
var stateB by remember { mutableStateOf(varB) }
varA and varB are class variables of type Int and are set elsewhere in the code.
Then somewhere in the composable, in the same scope, I have
processA(stateA)
processB(stateB)
processA and processB are not composable functions.
So after initial rendering, if neither state changes, then nothing is further processed, that is cool.
Then if say stateB is changed, then both process statements get called. But I hope only to call processB in the case and not processA. How can I detect which of the states has changed?

You should not run heavy processing directly from Composable functions. More information can be found in side effects documentation.
In this case, you can use LaunchedEffect. Using snapshotFlow, you can create a flow that emits values every time a state changes, so you can process it. You can have a flow for each state, so they will be processed independently.
LaunchedEffect(Unit) {
launch {
snapshotFlow { stateA }
.collect(::processA)
}
launch {
snapshotFlow { stateB }
.collect(::processB)
}
}

Related

LaunchedEffect vs rememberCoroutineScope. This explanation makes me confused. Please make it clear to me

I am following the codelab Advanced State and Side Effects in Jetpack Compose. It says that if we use rememberCoroutineScope instead of LaunchEffect in this case, it seems to work, but it is not correct. "As explained in the Thinking in Compose documentation, composables can be called by Compose at any moment. LaunchedEffect guarantees that the side-effect will be executed when the call to that composable makes it into the Composition. If you use rememberCoroutineScope and scope.launch in the body of the LandingScreen, the coroutine will be executed every time LandingScreen is called by Compose regardless of whether that call makes it into the Composition or not. Therefore, you'll waste resources and you won't be executing this side-effect in a controlled environment."
#Composable
private fun MainScreen(onExploreItemClicked: OnExploreItemClicked) {
Surface(color = MaterialTheme.colors.primary) {
var showLandingScreen by remember { mutableStateOf(true) }
if (showLandingScreen) {
LandingScreen(onTimeout = { showLandingScreen = false })
} else {
CraneHome(onExploreItemClicked = onExploreItemClicked)
}
}
}
#Composable
fun LandingScreen(modifier: Modifier = Modifier, onTimeout: () -> Unit) {
Box(modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
// TODO Codelab: LaunchedEffect and rememberUpdatedState step
// TODO: Make LandingScreen disappear after loading data
val onCurrentTimeout by rememberUpdatedState(newValue = onTimeout)
LaunchedEffect(Unit) {
delay(SplashWaitTime)
onCurrentTimeout()
}
Image(painterResource(id = R.drawable.ic_crane_drawer), contentDescription = null)
}
}
I do not understand the explanation. The phrases "when the call to that composable makes it into the Composition" and "regardless of whether that call makes it into the Composition or not" make me confused. I think when a composable is called by Compose, it will be in the Composition. How can it be not in the Composition after being called? Please show me what I miss here.
When a Composable enters composition LaunchedEffect() gets triggered. If LaunchedEffect has key or keys it can be triggered on recompositions when key values change.
#Composable
private fun MyTestComposable() {
var counter by remember { mutableStateOf(0) }
Column(modifier = Modifier.fillMaxSize()) {
LaunchedEffect(key1 = counter>4) {
println("counter: $counter")
}
Button(onClick = { counter++ }) {
Text("counter: $counter")
}
}
}
For instance, LaunchedEffect block gets triggered when key change. First when MyTestComposable enters composition with false flag and when counter is 5 with true flag.
rememberCoroutineScope on the other hand launches every time your function is run. It's very very likely to enter composition but there might be times state that triggers composition change fast so current composition gets canceled and new one is scheduled
Recomposition starts whenever Compose thinks that the parameters of a
composable might have changed. Recomposition is optimistic, which
means Compose expects to finish recomposition before the parameters
change again. If a parameter does change before recomposition
finishes, Compose might cancel the recomposition and restart it with
the new parameter.
When recomposition is canceled, Compose discards the UI tree from the
recomposition. If you have any side-effects that depend on the UI
being displayed, the side-effect will be applied even if composition
is canceled. This can lead to inconsistent app state.
Ensure that all composable functions and lambdas are idempotent and
side-effect free to handle optimistic recomposition.
https://developer.android.com/jetpack/compose/mental-model#optimistic

Compose - pass composable block in function after coroutine result

I have a problem that I still can't solve and it just doesn't want to work. Basically I have to convert a function into a composable.
In the old function I launched a Coroutine and at the result, I changed context and then continued with my processes. In compose I don't understand how I have to "change context" in order to continue.
Old code:
fun getMyView( activity: Activity
) {
backgroundCoroutineScope.launch {
//some stuff here
withContext(coroutineContext) {
startSearchView(
activity
)
}
}
}
New not working code:
#Composable
fun getMyView( content: #Composable() () -> Unit) {
LaunchedEffect(key1 = Unit) {
//some stuff here like old funciont
//here I don't know how to change context, wait the end and go ahead. startSearchViewis a composable function too
// i want to use it to populate my screen
startSearchView(
content
)
}
}
How can I solve it? Thanks
Seems like you are trying to asynchronously "create" composable function, but UI emitting doesn't work this way. Like #PylypDukhov suggested, you should keep a mutable state holding nullable result of your async action. After loading the data set this state. Then in composable just do something like:
if (data != null) {
SearchComposable(data)
}
This way the composable will be emitted after the data is loaded

If variable is not set, wait for variable to be set

I have 2 delegate methods that are being called by notifications from a 3rd party library.
Method 1:
mediaContentWasUpdated()
Method 2:
adMediaDidBeginPlaying()
In Method 1, a key variable (adDuration) is set from a parameter that is passed in with the notification. As far as I can see this is the only place to get this information.
In Method 2, we check the adDuration and if it is greater than 0 then we update the UI to reflect that we are in fact play an ad.
A bug has appeared where sometimes these two methods are called in the wrong order. Meaning the adDuration is not set and Method 2 thinks there is no ad media to be played and does not update the UI accordingly.
My current attempt at a solution is to make adDuration optional and use an NSCondition to cause Method 2 to wait for Method 1 to set adDuration and then proceed.
var adDuration : Double?
let condition = NSCondition()
func mediaContentWasUpdated(notification: NSNotificiation) {
condition.lock()
if(notificationHasAdDurationInfo(notification)) {
self.adDuration = getAdDuration(notification)
condition.signal()
}
condition.unlock()
}
func adMediaDidBeginPlaying(notification: NSNotification) {
condition.lock()
while adDuration == nil {
condition.wait()
}
if adDuration! > Double(0) {
updateUIForAd()
}
condition.unlock()
}
This is my first time trying something like this and I worry I am doing something wrong. I also have some concerns about locking and unlocking threads needlessly (which would happen in a well timed run, or if there were no ad content to be played).
Outside factors are hindering my ability to test and I wanted to get some input to see if I am heading in the right direction while I wait for those issues to be resolved.
Your discussion of NSCondition got me on the same track with you, and I built two or three solutions using DispatchGroup (which is the better tool for this), but they always had little corner cases that could behave badly, and didn't really capture the intent.
(If you're interested in the DispatchGroup solutions, they're of the form: call .enter() in init, call .leave() when the duration comes in, call notify() when the playing starts. It works fine, but it introduces corner cases that can crash, just like NSCondition.)
Getting back to the real intent:
Update the UI when the duration is known and the ad has started playing.
There's no concurrency going on here. So pulling out GCD is not just overkill; it actually makes things worse because it introduces lots of complicated corner cases.
So I thought about how I'd have solved this back before GCD. And the answer is obvious: just check if you have the data you want, and then do the thing. (Reading through the comments, I see Paulw11 pointed this out as well.)
Personally I like to pull this kind of thing into its own type to make things more self-contained. I hate some of the names here, but the idea should be clear:
class AdPlayer {
private var readyToPlay = false
private var duration: Double = 0.0
private let completion: (Double) -> Void
func setDuration(from notification: Notification) {
if(notificationHasAdDurationInfo(notification)) {
duration = getAdDuration(notification)
}
playIfReady()
}
func play() {
readyToPlay = true
playIfReady()
}
private func playIfReady() {
if duration > 0 && readyToPlay {
completion(duration)
}
}
init(completion: #escaping (Double) -> Void) {
self.completion = completion
}
}
When you set each thing, see if you're ready to update, and if so, update. I've gotten rid of the optional as well, since I believe the intent is "0 duration is always wrong." But you could use an Optional so you could detect actually receiving a 0 from the notification.
With that, you just set up a player property:
player = AdPlayer(completion: updateUIForAd)
(Note that the above might be creating a retain loop, depending on what updateUIForAd is; you may need a [weak self] closure or the like here.)
And then update it as needed:
func mediaContentWasUpdated(notification: NSNotificiation) {
player.setDuration(from: notification)
}
func adMediaDidBeginPlaying(notification: NSNotification) {
player.play()
}
A big advantage of creating the AdPlayer type is that it's easy to reset the system when the ad is done (or if something goes wrong). Just throw away the whole object and create another one.

Ambiguous reference to member 'subscribe' Swift 3

I am new to Reactive programming, and I'm trying to observe a boolean value from my ViewModel in order to let my ViewController know when to start/stop the app's loader screen.
It's fairly simple and I want to use this method to avoid unnecessary delegates, since my ViewModel holds the business logic and my ViewController handles the UI.
My problem is this compiler error: Ambiguous reference to member 'subscribe'.
It also adds the two possible candidates, as you can see in the image below:
In my ViewModel, I've declared the observable as PublishSubject:
let done = PublishSubject<Bool>()
And I use it while observing another stream:
func subscribe() {
done.onNext(false)
anotherObservable.subscribe(
// other events observed here but not relevant to this matter
onCompleted: {
self.done.onNext(true)
}).addDisposableTo(rx_disposeBag)
}
And, finally, this is how I'm trying to handle it in the ViewController:
self.model.done.subscribe(
.onNext { isDone in
if isDone {
self.removeLoader()
}
}).addDisposableTo(rx_disposeBag)
I believe there is something simple I'm probably missing, so any help is appreciated.
In your second subscribe should be:
self.model.done.subscribe(onNext: { isDone in
if isDone {
self.removeLoader()
}
}).addDisposableTo(rx_disposeBag)

Proper way to dispose a one-off observable in RxSwift

I have an observable that I only want to kick off once. The docs say:
Using dispose bags or takeUntil operator is a robust way of making sure resources are cleaned up. We recommend using them in production even if the sequences will terminate in finite time.
My observable terminates after just one event
let observable = Observable.create() { observer in
webservice.makeHTTPRequestWithCompletionBlock {
if something {
observer.on(.Next(...))
observer.onCompleted()
} else {
observer.on(.Error(...))
}
}
}
Say I wasn't interested in cancelling subscribers to this observable, I just want it run once and complete. I want the lifecycle of this observable to end when the work itself is completed. Meaning there are no good candidates for disposeBag that I can see. takeUntil also expects an 'event', and there are no good ones that I can see.
Right now I just solve the warning by throwing away the disposable:
_ = observeable.subscribeNext { ... }
Is there a way to do this, or a different paradigm that I should use?
Both DiposeBag and takeUntil are used to cancel a subscription prior to receiving the .Completed/.Error event.
When an Observable completes, all the resources used to manage subscription are disposed of automatically.
As of RxSwift 2.2, You can witness an example of implementation for this behavior in AnonymousObservable.swift
func on(event: Event<E>) {
switch event {
case .Next:
if _isStopped == 1 {
return
}
forwardOn(event)
case .Error, .Completed:
if AtomicCompareAndSwap(0, 1, &_isStopped) {
forwardOn(event)
dispose()
}
}
}
See how AnonymousObservableSink calls dispose on itself when receiving either an .Error or a .Completed event, after forwarding the event.
In conclusion, for this use case, _ = is the way to go.

Resources