Intro to Android: Week 5!
Dumb Components & UIEvents
Announcements
Assignments
Hack Challenge Timeline
Announcements
See Ed
for more
info!
2
LaunchedEffect
Setting a trigger in the UI layer…
3
UIEvent
One-off events in the UI layer!
1
Dumb Components
Pulling out UI brains for the ViewModel!
2
LaunchedEffect
Setting a trigger in the UI layer…
3
UIEvent
One-off events in the UI layer!
1
Dumb Components
Pulling out UI brains for the ViewModel!
Dumb Components
Revamped Component Design for MVVM
Recap: Why MVVM?
Q: Why are we even using MVVM again?
It’s a lot to learn for… what?
Model
ViewModel
View (UI)
A: It allows us to build a real app!
=> Required for networking, user input, asynchronous loading, etc!
Recap: Why ViewModel?
Q: Why are we even using ViewModels?
Can’t I just use State variables to handle state?
ViewModel
A: It allows for centralized control of app logic!
It is the ultimate state holder…
Idea: ViewModel as ultimate state
ViewModel Key Idea:
The ViewModel should act as an ultimate State for its screen.
(Instead of using cluttered State variables all over the UI code, let the ViewModel handle it!)
ViewModel
Oftentimes, we need to thus pull out State from our components to reflect the VM’s role!
View (UI)
Example: TickerRow
Let’s dive into an example!
This Composable currently keeps track of its own State internally.
Let’s see why this seems convenient, but actually limits our ability to do networking.
TickerRow Composable
demo time!
<lec5>
(Code Walkthrough)
Problem Statement (lec5 demo)
Here, TickerRow keeps track of its own price state:
This has the following pros and cons!
Problem Statement (lec5 demo)
Pros:
Cons:
That’s really bad.
ViewModel Control
Cons:
That’s really bad.
Let’s take a look at what’s actually happening in our ViewModel…
it’s trying to do cool stuff, but simply CAN’T right now.
demo time!
<lec5>
(TickerViewModel, TickerRepository)
How to Fix?
Unintuitive Idea: Make TickerRow dumber.
=> When + is clicked, just tell the viewmodel.
=> When - is clicked, just tell the viewmodel.
=> Rely on the viewmodel to give the correct price to show at any point.
=> Do NO addition/subtraction logic on your own, even if it seems intuitive to do so.
Effect on TickerRow:
“I can determine the new price on my own!”
“I have no idea what my buttons do. I’ll just hope the ViewModel does it for me.”
demo time!
<lec5>
(lec5soln)
Now it’s the ViewModel’s responsibility to, at ALL points, determine what the price to show is!
This means:
But that pro is BIG: now we can build a real app!
Why does this help?
ViewModel
View (UI)
Because the ViewModel exists in a separate, non-UI lifecycle from everything else, it’s super easy to do logic calculations there!
If you tried to do the same in UI, you’d run into values getting lost and overwritten by recompositions, all over the place.
Why does this help?
ViewModel
View (UI)
stopping point
any questions?
2
LaunchedEffect
Setting a trigger in the UI layer…
3
UIEvent
One-off events in the UI layer!
1
Dumb Components
Pulling out UI brains for the ViewModel!
LaunchedEffect (Brief Summary)
LaunchedEffect defines a function dependent on a value that will fire whenever that value changes.
That function is passed in as an anonymous function (curly braces) following the LaunchedEffect block.
The LaunchedEffect takes in one parameter—the value to trigger on.
LaunchedEffect Example
Ex: This LaunchedEffect will fire every time the value of state changes to delayed update the “spinningDelayedState”
LaunchedEffect Example
This is an actual line of code from our app, Resell!
LaunchedEffect here allows us to perform some UI-level logic: whenever a button state goes from loading to not loading or vice versa, a delayed animation plays.
LaunchedEffect Example
wait 100ms
UI Logic
While most important app logic is done in the ViewModel, some animation / specific screen logic needs to be done in the @Composable scope.
Ex:
As a limitation, these [remember…] calls need to be called in @Composable scope and thus in the UI layer.
wait 100ms
UI Logic
While most important app logic is done in the ViewModel, some animation / specific screen logic needs to be done in the @Composable scope.
Ex:
As a limitation, these [remember…] calls need to be called in @Composable scope and thus in the UI layer.
LaunchedEffect -> UI Logic
A bit confusing? That’s okay!
We’ll dive into a simpler (and more useful) example with UIEvents!
LaunchedEffect overall allows you to write UI-level app logic.
This should be used sparingly, for the VM should handle 99% of app logic, and this should be reserved for specific UI-only use cases such as those shown on the previous slide.
2
LaunchedEffect
Setting a trigger in the UI layer…
3
UIEvent
One-off events in the UI layer!
1
Dumb Components
Pulling out UI brains for the ViewModel!
UIEvent Niche
LaunchedEffect with regular values and ViewModel combined cover most use cases of app logic.
But there are some specific cases that demand a final logic paradigm.
All App Logic
all your app logic, broken down
UI Logic
One-Off UI Logic
(LaunchedEffect)
(LaunchedEffect + UiEvent)
(excluding UI Logic)
ViewModel Logic
all your app logic, broken down
(NOTE: the vast majority of app logic should be in the VM!)
UIEvent Niche
Amongst the cases in which you have to use LaunchedEffect for @Composable limitations, lots of them need a one-off-event firing pattern:
Ex: Clicking a button to scroll to the top
UIEvent Idea
What if you had a StateFlow that emitted a Boolean value (or really any value) to trigger a scroll:
and in the UI, whenever you’d see this true value, you’d scroll?
val scrollEventFlow = MutableStateFlow(false)
// …
fun onScrollClick() {
scrollEventFlow.value = true
}
ViewModel
UIEvent Idea
Unfortunately, StateFlow only emits unique values. As such…
…will only emit one true, and ignore the other.
scrollEventFlow.value = true
// 2 seconds later…
scrollEventFlow.value = true
ViewModel
UIEvent Idea
Also, how do we know if this event is still live or not?
All we can see is that it is true… but what if the event has already been acted upon?
scrollEventFlow.value // Evaluates to true.
// 2 seconds later…
scrollEventFlow.value // Still evaluates to true…
ViewModel
UIEvent, Solution
Instead, for this final use case, we’ll use a UIEvent!
val scrollEventFlow = MutableStateFlow<UIEvent<Unit>?>(null)
// …
fun onScrollClick() {
scrollEventFlow.value = UIEvent(Unit)
}
ViewModel
UIEvent Breakdown
What does this type mean? Let’s break it down.
val scrollEventFlow = MutableStateFlow<UIEvent<Unit>?>(null)
ViewModel
UIEvent Breakdown
Then, these lines…
…create a new UIEvent (with no payload) and send it down to the UI!
fun onScrollClick() {
scrollEventFlow.value = UIEvent(Unit)
}
ViewModel
UIEvent Breakdown
Then, in our UI, we can collect as follows…
val scrollEvent =
viewModel.scrollEventFlow.collectAsState().value
LaunchedEffect(scrollEvent) {
scrollEvent?.consumeSuspend {
// Scroll to top after successful consumption
listState.animateScrollToItem(0)
}
}
View (UI)
UIEvent Breakdown
What’s special about this?
This code will “consume” the event.
After an event has been consumed, it will never be recognized by any further recompositions.
scrollEvent?.consumeSuspend { … }
View (UI)
UIEvent Idea
Because of some inner, complicated implementation details…
will actually cause two separate scroll events. Yay!
(You don’t have to know why this is.)
scrollEventFlow.value = UIEvent(Unit)
// 2 seconds later…
scrollEventFlow.value = UIEvent(Unit)
ViewModel
Our UIEvent Secret…
The UIEvent class is actually written by AppDev.
(not that it’s very complicated; most companies will surely write their own version of this.)
Here’s the entire source code in tiny font!
We’ll give this to you for download for your assignments.
/**
* A simple class that represents a UI event.
*
* Distinct from the payload alone since different events will still
* trigger LaunchedEffect.
*/
class UIEvent<T>(
val payload: T,
) {
private var isConsumed = false
/**
* Consume the event. An event may only be consumed once.
*/
fun consume(then: (T) -> Unit) {
if (isConsumed) {
return
}
then(payload)
isConsumed = true
}
/**
* Consume the event in a coroutine. An event may only be consumed once.
*/
suspend fun consumeSuspend(then: suspend (T) -> Unit) {
if (isConsumed) {
return
}
then(payload)
isConsumed = true
}
}
stopping point
any questions?
quick demo!
<lec5>
bonus demo (?)
<lec5 TickerText>
Attendance Form