1 of 49

Intro to Android: Week 5!

Dumb Components & UIEvents

2 of 49

Announcements

Assignments

  • A4
    • Late deadline tomorrow night! (4/15 @ 11:59pm)
  • A5 (Rate my Vibe) Released!
    • Due Sunday 4/20 @ 11:59pm (Late Deadline 4/22)

Hack Challenge Timeline

  • Hack Challenge Mixer (Optional)
    • Tomorrow, 4/15 @ 5-6pm in Gates G01
    • Meet other students to form Hack Challenge Groups!
  • Hack Challenge Kickoff (Required!)
    • Friday, 4/18 @ 5-6pm also in Gates G01
    • Meet your teams and learn about the challenge!

3 of 49

Announcements

See Ed

for more

info!

4 of 49

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!

5 of 49

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!

6 of 49

Dumb Components

Revamped Component Design for MVVM

7 of 49

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!

8 of 49

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

9 of 49

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)

10 of 49

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

11 of 49

demo time!

<lec5>

(Code Walkthrough)

12 of 49

Problem Statement (lec5 demo)

Here, TickerRow keeps track of its own price state:

This has the following pros and cons!

13 of 49

Problem Statement (lec5 demo)

Pros:

  • We don’t have to manually implement add/subtract.
  • No need for onClick.
  • Only takes in an initial price argument.

Cons:

  • The ViewModel cannot dynamically update the ticker.

That’s really bad.

14 of 49

ViewModel Control

Cons:

  • The ViewModel cannot dynamically update the ticker.

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.

15 of 49

demo time!

<lec5>

(TickerViewModel, TickerRepository)

16 of 49

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.

17 of 49

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.”

  • Viewmodel Brain!

18 of 49

demo time!

<lec5>

(lec5soln)

19 of 49

Now it’s the ViewModel’s responsibility to, at ALL points, determine what the price to show is!

This means:

  • Pro: We can dynamically update the price with calls!
  • Con: We now have to implement trivial addition/subtraction in the VM.

But that pro is BIG: now we can build a real app!

Why does this help?

ViewModel

View (UI)

20 of 49

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)

21 of 49

stopping point

any questions?

22 of 49

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!

23 of 49

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.

24 of 49

LaunchedEffect Example

Ex: This LaunchedEffect will fire every time the value of state changes to delayed update the “spinningDelayedState

25 of 49

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.

26 of 49

LaunchedEffect Example

wait 100ms

27 of 49

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:

  • rememberLazyListState() scrolling
  • rememberNavController() navigation

As a limitation, these [remember…] calls need to be called in @Composable scope and thus in the UI layer.

wait 100ms

28 of 49

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:

  • rememberLazyListState() scrolling
  • rememberNavController() navigation

As a limitation, these [remember…] calls need to be called in @Composable scope and thus in the UI layer.

29 of 49

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.

30 of 49

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!

31 of 49

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.

32 of 49

All App Logic

all your app logic, broken down

UI Logic

One-Off UI Logic

(LaunchedEffect)

(LaunchedEffect + UiEvent)

33 of 49

(excluding UI Logic)

ViewModel Logic

all your app logic, broken down

(NOTE: the vast majority of app logic should be in the VM!)

34 of 49

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

  • You want this to fire once, and then never again!
  • You also want to be able to fire multiple of these events in a row, and have them NOT be ignored by recomposition.

35 of 49

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

36 of 49

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

37 of 49

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

38 of 49

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

39 of 49

UIEvent Breakdown

What does this type mean? Let’s break it down.

  • This code represents a flow that emits UIEvents (or null) to the UI.

  • UIEvents optionally contain a package of data—a “payload.” However, a scroll to the top event will always just scroll to the top, so it does not need data.
  • Hence, we say it carries Unit, which essentially means no payload.

  • Then, we initialize the flow with null because there is no event yet.

val scrollEventFlow = MutableStateFlow<UIEvent<Unit>?>(null)

ViewModel

40 of 49

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

41 of 49

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)

42 of 49

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)

43 of 49

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

44 of 49

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

}

}

45 of 49

stopping point

any questions?

46 of 49

quick demo!

<lec5>

47 of 49

bonus demo (?)

<lec5 TickerText>

48 of 49

Attendance Form

49 of 49