0% found this document useful (0 votes)
6 views81 pages

Jetpack Compose UI Development Guide

The lecture covers the declarative programming paradigm and its application in mobile network programming using Jetpack Compose for UI development. It explains the benefits of using composable functions, modifiers, and state management in building dynamic user interfaces. Additionally, it discusses techniques for displaying long lists and persisting state across configuration changes.

Uploaded by

alwyn.s.x.tan
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
6 views81 pages

Jetpack Compose UI Development Guide

The lecture covers the declarative programming paradigm and its application in mobile network programming using Jetpack Compose for UI development. It explains the benefits of using composable functions, modifiers, and state management in building dynamic user interfaces. Additionally, it discusses techniques for displaying long lists and persisting state across configuration changes.

Uploaded by

alwyn.s.x.tan
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd

IEMS5722 Mobile Network Programming and Distributed

Server Architecture
Lecture 3

Lecturer: Ivan Ng

Department of Information Engineering


Topics
● Declarative Programming Paradigm
● Jetpack Compose
● UI using Jetpack Compose
● Navigation using Jetpack Compose
● App Architecture Overview
● App Architecture UI Layer Example
● App Architecture Data Layer Example
● Assignment 2
Last lesson recap
Declarative
Programming
Paradigm
The declarative programming paradigm
● An Android view hierarchy has
been representable as a tree of
UI widgets.
● As the state of the app changes,
the most common way is to walk
the tree using functions like
findViewById() and update by
[Link](String) and so
forth.
The Problem
● If a piece of data is rendered in multiple places, it’s easy to forget to update
one of the views that shows it.
● In general, the software maintenance complexity grows with the number of
views that require updating.
Declarative UI model
● The entire industry has started shifting to a declarative UI model.
● Compose is a declarative UI framework.
● Compose intelligently chooses which parts of the UI need to be redrawn at
any given time.
Jetpack Compose
What is a User Interface (UI)?
● The user interface (UI) of an app is what you see on the screen: text, images,
buttons, and many other types of elements, and how it's laid out on the screen.
UI Component
● Each of these elements is called
a UI component.
● They can be interactive, like a
clickable button or an editable
input field, or they can be
decorative images.
What is Jetpack Compose?

[Link]
What is Jetpack Compose?
● A modern, fully declarative UI toolkit for building native Android apps.
● Simplifies UI development by using Kotlin to create UI components
programmatically.
● With Compose, you can build your UI by defining a set of functions, called
composable functions, that take in data and describe UI elements.
Why Use Jetpack Compose?
● Reduces development time.
● Encourages clean, maintainable code.
● Future-proof for modern Android app design.
Composable functions
● Composable functions are the basic building block of a UI in Compose.
● A composable function:
○ Describes some part of your UI.
○ Doesn't return anything.
○ Takes some input and generates what's shown on the screen.
● A Composable function is called such because it represents a reusable,
self-contained piece of UI that can be composed together with other
composables to create complex interfaces.
● That’s why it’s called composable
Composable functions
● The Composable function is annotated with the @Composable annotation.
● All composable functions must have this annotation.
● This annotation informs the Compose compiler that this function is intended to
convert data into UI.
Composable functions
● This code snippet is an example of a simple composable function that is
passed data (the name function parameter) and uses it to render a text
element on the screen.

@Composable
fun Greeting(name: String) {
Text(text = "Hello $name!")
}
Annotations
● Annotations are means of attaching extra information to code.
● This information helps tools like the Jetpack Compose compiler, and other
developers understand the app's code.
● An annotation is applied by prefixing its name (the annotation) with the @
character at the beginning of the declaration you are annotating.
● Different code elements, including properties, functions, and classes, can be
annotated.
Annotations
● Jetpack Compose includes a wide range of built-in annotations, you have
already seen @Composable
Preview
● Android Studio lets you preview your @Composable
composable functions within the IDE, fun Greeting(name: String) {
instead of installing the app to an Android Text(text = "Hello $name!")
device or emulator. }
● The composable function must provide
default values for any parameters to @Preview(showBackground = true)
preview it. @Composable
● For this reason, it is recommended not to fun BirthdayCardPreview() {
preview the Greeting() function directly.
HappyBirthdayTheme {
● Instead, you need to add another function,
Greeting("Android")
the BirthdayCardPreview() function in this
}
case, that calls the Greeting() function with
}
an appropriate parameter.
Preview
The Overall Calling Flow
● The setContent function acts as a
bridge between the Android
framework and Jetpack Compose.
● It initializes the Compose runtime
within the activity and starts the
composition process (the process
of rendering and managing the UI
tree).
● It can call composables (e.g.
Greeting) and only composables
can call composables
subsequently
UI using Jetpack
Compose
Modifiers
● Most Compose UI elements such as Surface and Text accept an optional
modifier parameter.
● The padding modifier will apply an amount of space around the element it
decorates. You can create a padding modifier with Modifi[Link]()

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
Surface(color = [Link]) {
Text(
text = "Hello $name!",
modifier = [Link]([Link])
)
}
}
Modifiers
● There are many modifiers
● Reference: [Link]
Creating Columns and Rows
● The three basic standard layout elements in Compose are Column, Row and Box.
Creating Columns and Rows
● Most Compose UI elements such as Surface and Text accept an optional
modifier parameter.
● The padding modifier will apply an amount of space around the element it
decorates. You can create a padding modifier with Modifi[Link]()

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
Surface(color = [Link]) {
Column {
Text("Hello")
Text("Android")
}
}
}
@Composable

Creating Columns and Rows fun MyApp(


modifier: Modifier = Modifier,
names: List<String> = listOf("World", "Compose")
) {
● Instantiate multiple composable Column(modifier = [Link](vertical = [Link])) {
for (name in names) {
● Use modifiers for with adjustment Greeting(name = name)
}
}
}

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
Surface(
color = [Link],
modifier = [Link](vertical = [Link], horizontal = [Link])
) {
Column(modifier = [Link]().padding([Link])) {
Text(text = "Hello ")
Text(text = name)
}
}
}
@Composable
fun MyApp(
modifier: Modifier = Modifier,

Adding a button ) {
names: List<String> = listOf("World", "Compose")

Column(modifier = [Link](vertical = [Link])) {


for (name in names) {
Greeting(name = name)
● onClick is a callback (to be }
}
used later) }

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
Surface(
color = [Link],
modifier = [Link](vertical = [Link], horizontal = [Link])
) {
Row(modifier = [Link]([Link])) {
Column(modifier = [Link](1f)) {
Text(text = "Hello ")
Text(text = name)
}
ElevatedButton(
onClick = { /* TODO */ }
) {
Text("Show more")
}
}
}
}
Adding a button
● There are different types of buttons.
State in Compose
● The state of the item: Store some value somewhere that indicates whether
each item is expanded or not
@Composable
fun MyApp(
modifier: Modifier = Modifier,

State in Compose ) {
names: List<String> = listOf("World", "Compose")

Column(modifier = [Link](vertical = [Link])) {


for (name in names) {
Greeting(name = name)
● Make changes like this? }
}
● However, this does not }

work as intended @Composable


fun Greeting(name: String, modifier: Modifier = Modifier) {
● This happens because the var expanded = false
Surface(
variable is not tracked by color = [Link],
modifier = [Link](vertical = [Link], horizontal = [Link])
Compose ) {
Row(modifier = [Link]([Link])) {
Column(modifier = [Link](1f)) {
Text(text = "Hello ")
Text(text = name)
}
ElevatedButton(
onClick = { expanded = !expanded }
) {
Text("Show more")
}
}
}
}
@Composable
fun MyApp(
modifier: Modifier = Modifier,

State in Compose ) {
names: List<String> = listOf("World", "Compose")

Column(modifier = [Link](vertical = [Link])) {


for (name in names) {
Greeting(name = name)
● We will further change it }
}
● To add internal state to a }

composable, we use @Composable


fun Greeting(name: String, modifier: Modifier = Modifier) {
mutableStateOf function var expanded by mutableStateOf(false)
Surface(
color = [Link],
modifier = [Link](vertical = [Link], horizontal = [Link])
) {
Row(modifier = [Link]([Link])) {
Column(modifier = [Link](1f)) {
Text(text = "Hello ")
Text(text = name)
}
ElevatedButton(
onClick = { expanded = !expanded }
) {
Text("Show more")
}
}
}
}
@Composable
fun MyApp(
modifier: Modifier = Modifier,

State in Compose ) {
names: List<String> = listOf("World", "Compose")

Column(modifier = [Link](vertical = [Link])) {


for (name in names) {
Greeting(name = name)
● As recomposition can }
}
happen any time which }

would call the composition @Composable


fun Greeting(name: String, modifier: Modifier = Modifier) {
again, to preserve the var expanded by remember {mutableStateOf(false)}
Surface(
state across color = [Link],
modifier = [Link](vertical = [Link], horizontal = [Link])
recompositions, remember ) {
Row(modifier = [Link]([Link])) {

the mutable state Column(modifier = [Link](1f)) {


Text(text = "Hello ")

● remember is a guard Text(text = name)


}

against recomposition ElevatedButton(


onClick = { expanded = !expanded }
) {
Text("Show more")
}
}
}
}
State in Compose
● About recomposition
● Recomposition is the core mechanism that makes your Jetpack Compose UI
dynamic and reactive.
● When the state changes (e.g. a MutableState variable), Compose
"recomposes" the relevant parts of the UI, ensuring it always reflects the
most up-to-date information without you having to manually manage UI
updates.
@Composable
fun MyApp(
modifier: Modifier = Modifier,

State in Compose ) {
names: List<String> = listOf("World", "Compose")

Column(modifier = [Link](vertical = [Link])) {


for (name in names) {
Greeting(name = name)
● We can also assign an }
}
action reflecting the }

change @Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
● This can be done by a var expanded by remember {mutableStateOf(false)}
Surface(
lambda function color = [Link],
modifier = [Link](vertical = [Link], horizontal = [Link])
) {
Row(modifier = [Link]([Link])) {
Column(modifier = [Link](1f)) {
Text(text = "Hello ")
Text(text = name)
}
ElevatedButton(
onClick = { expanded = !expanded }
) {
Text(if (expanded) "Show less" else "Show more")
}
}
}
}
@Composable
fun MyApp(
modifier: Modifier = Modifier,

State in Compose ) {
names: List<String> = listOf("World", "Compose")

Column(modifier = [Link](vertical = [Link])) {


for (name in names) {
Greeting(name = name)

● We can add a bit padding }


}

when it is expanded }

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
var expanded by remember {mutableStateOf(false)}
var extraPadding = if (expanded) [Link] else [Link]
Surface(
color = [Link],
modifier = [Link](vertical = [Link], horizontal = [Link])
) {
Row(modifier = [Link]([Link])) {
Column(modifier = [Link](1f).padding(bottom = extraPadding)
) {
Text(text = "Hello ")
Text(text = name)
}
ElevatedButton(
onClick = { expanded = !expanded }
) {
Text(if (expanded) "Show less" else "Show more")
}
}
}
}
Display a long list
import [Link]
● So far, we displayed two import [Link]
greetings. // ...

● How about displaying a @Composable


long list? private fun Greetings(
modifier: Modifier = Modifier,
● We can use LazyColumn,
names: List<String> = List(1000) { "$it" }
which renders only the ) {
LazyColumn(modifier = [Link](vertical = [Link])) {
visible items on screen.
items(items = names) { name ->
Greeting(name = name)
}
}
}
Display a long list
● Note:
● LazyColumn doesn't recycle its children like RecyclerView.
● RecyclerView provides more flexibility in terms of customizing item layouts, handling
different item types, and integrating complex animations.

● For reference:
● [Link]
Persisting State
● The remember function works when the composable is kept in the Composition.
● When you rotate, the whole activity is restarted so all state is lost.
● Instead, you can use rememberSaveable.
● This will save each state surviving configuration changes (such as rotations) and process
death.

import [Link]
// ...

var shouldShowOnboarding by rememberSaveable { mutableStateOf(true) }


Difference: remember versus rememberSaveable
● remember is used to store an object or state only during the lifecycle of a single
composition. The state is preserved across recompositions, but it is not retained during
configuration changes (e.g., screen rotation) or process death.

● rememberSaveable works like remember, but it also saves the state in a Bundle using
the SavedStateHandle or the saved instance state mechanism of Android. This means
the state is retained during configuration changes (like screen rotation) and process
recreation.
Difference: = remember versus by remember
When you use = remember, you are assigning the @Composable
fun ExampleWithEqualSign() {
result of the remember function to a variable or val state = remember { mutableStateOf(0) } // Assigning remember to a variable
property directly. You need to access or modify the
Column {
value explicitly.
Text("Value: ${[Link]}") // Access [Link] explicitly
Button(onClick = { [Link]++ }) {
Text("Increment")
}
}
}

When you use by remember, you are using Kotlin's @Composable


fun ExampleWithByKeyword() {
property delegation syntax. A delegate (in this case, var state by remember { mutableStateOf(0) } // Delegating remember to the property
remember) manages the getter and setter for the
Column {
property, so you can interact with the state directly
Text("Value: $state") // No need for .value
without needing to explicitly call .value. Button(onClick = { state++ }) {
Text("Increment")
}
}
}
Navigation using
Jetpack Compose
Navigate Multiple Screens
● Up to now, we talk about UI on a single screen.
● Many apps you use probably have multiple screens.
● Using Jetpack Navigation component.
Navigate Multiple Screens

Start Flavor Pickup Summary


StartOrderScreen composable in SelectOptionScreen composable in SelectOptionScreen composable in OrderSummaryScreen
[Link] [Link] [Link] composable in [Link]

The list of quanty options is stored as a The list of possible flavors is stored as a list of Pickup options come from a list returned
list of pairs in [Link]. string resource IDs in [Link]. by the pickupOptions() function in
OrderViewModel.

The full source code can be found here:


[Link]
Navigate Multiple Screens
● Navigation component has three main parts

NavHost Composable acting as a container for displaying the current


destination.

NavController Responsible for navigating between destinations—that is, the


screens in your app.
Navigate Multiple Screens
Route
● Fundamental concepts of navigation in a Compose app is the route.
● A route is a string that corresponds to a destination.
● This idea is similar to the concept of a URL.
● A route is a string that maps to a destination and serves as its unique
identifier.
● A destination is typically a single Composable or group of Composables.
● We have the start order screen, the flavor screen, the pickup date screen, and
the order summary screen.
Route
● You can define an app's routes using an enum class.
○ Start: Select the quantity of cupcakes from one of three buttons.
○ Flavor: Select the flavor from a list of choices.
○ Pickup: Select the pickup date from a list of choices.
○ Summary: Review the selections and either send or cancel the order.
● Add an enum class named CupcakeScreen
enum class CupcakeScreen() {
Start,
Flavor,
Pickup,
Summary
}
NavHost
● A NavHost is a Composable that displays other composable destinations,
based on a given route.

If the route is Start, display the Start screen.

If the route is Flavor, display the Flavor screen.

If the route is Pickup, display the Pickup screen.

If the route is Summary, display the Summary screen.


NavHost and NavController - The code in action

UI State from the ViewModel is collected and will be


updated within the composable (to be explained later)

The collectAsState() function subscribes to the


StateFlow (or Flow) in the uiState property of the
viewModel.

When the state in [Link] changes,


Compose automatically recomposes any UI
components that depend on uiState.

The UI updates automatically when the StateFlow


emits a new value.
NavHost and NavController - The code in action

The Cupcake Start screen is initiated first in NavHost


NavHost and NavController - The code in action

After user’s selection of quantity (in Start screen), the


user click will be collected via uiState and
navController will navigate to the Flavor screen.

Similarly, after user’s selection of flavor (in Flavor


screen), the user click will be collected via uiState and
navController will navigate to the Pickup screen.
NavHost and NavController - The code in action

Finally, after the user’s selection of pickup (in Pickup


screen), the user click will be collected via uiState and
navController will navigate to the Summary screen.
App Architecture
Overview
App Architecture Basics
● Considering the common architectural principles mentioned in the previous
section, each application should have at least two layers:
○ The UI layer that displays application data on the screen.
○ The Data layer that contains the business logic of your app and exposes application data.
App Architecture Basics - UI Layer
● The most important principle to follow is separation of concerns.
● It's a common mistake to write all your code in an Activity (or a Fragment).
● These UI-based classes should only contain logic that handles UI and
operating system interactions.
App Architecture Basics - Data Layer
● Another important principle is that you should drive your UI from data models,
preferably persistent models.
● Data models represent the data of an app.
● They're independent from the UI elements and other components in your
app.
App Architecture Basics - Persistence
● Persistent models are ideal for the following reasons:
● Your users don't lose data if the Android OS destroys your app to free up
resources.
● Your app continues to work in cases when a network connection is flaky or not
available.
App Architecture
The UI Layer
Example
UI as UI Elements and UI State
● UI is what the user sees, the UI state is what the app says they should see.
● Any changes to the UI state are immediately reflected in the UI.
What does UI layer do?
● Transform underlying data into UI-renderable data for UI to render.
● Consume UI-renderable data and transform it into UI elements for
presentation to the user.
● Consume user input events from those assembled UI elements and reflect
their effects in the UI-renderable data as needed.
● Repeat steps 1 through 3 for as long as necessary.
What does UI layer do?
● ViewModel exposes UI state
● UI notifies ViewModel of
events
● ViewModel updates state and
is consumed by the UI
● Repeat as necessary
Revisit the CupCake Example
View Model
data class OrderUiState( fun setQuantity
fun setFlavor
/** Selected cupcake quantity (1, 6, 12) */
fun setDate
val quantity: Int = 0, fun resetOrder
/** Flavor of the cupcakes in the order (such as fun calculatePrice
"Chocolate", "Vanilla", etc..) */ fun pickupOptions
val flavor: String = "",
/** Selected date for pickup (such as "Jan 1") */
OrderUiState Upon UI Event, the ViewModel will be updated via
val date: String = "",
/** Total price for the order */ the functions exposed by the ViewModel
val price: String = "", For example, when the user chooses the quantity in
/** Available pickup dates for the order*/
the Start screen, the UI will call setQuantity to change
val pickupOptions: List<String> = listOf()
)
the UI state

UI Elements

The UI state wil be collected via


val uiState by [Link]()
Self-Study
Self-Study
● The below example is very good for your learning of navigation.
● Please practise the lab yourself.
● [Link]
on#1
Self-Study
● At the end of the exercise, the order is sent
to another app.
● It is implemented using intent.
Self-Study
● When the Send button is clicked, the shareOrder() function is called
● In the shareOrder() function, an implicit Intent is created
App Architecture
The Data Layer
The Data Layer
● While the UI layer contains
UI-related state and UI logic,
the data layer contains
application data and business
logic.
● This separation of concerns
allows the data layer to be
used on multiple screens.
The Data Layer
● The data layer is made of repositories that each can contain zero to many
data sources.
● You should create a repository class for each different type of data you
handle in your app.
The Data Layer
● Repository classes are responsible for the following tasks:
○ Exposing data to the rest of the app.
○ Centralizing changes to the data.
○ Resolving conflicts between multiple data sources.
○ Abstracting sources of data from the rest of the app.
○ Containing business logic.
App Architecture
The Data Layer
Example
App Architecture - Data Layer - Example
● Will be revisited when we talk about loading data via HTTP
Self-Study
Self-Study
● The below example is very good for your learning of app architecture.
● Please practise the lab yourself.
● [Link]
Assignment 2
Assignment 2
● Follow the lab below to make an Android application with a simple UI
● [Link]
● Change the data to show a fixed list of car parks
● Show the id, name and address of each car park
● Experience some styling and theme you want
Assignment 2
● AI is not allowed
● Go through all the steps by yourself
● Remind to sign the originality declaration for written assessment and submit
the signed document together with your assignment each time.
● Your work should be submitted to CUHK e-Learning system before the
deadline
Resources
Resources
● Jetpack Compose Basics
[Link]

● Jetpack Compose Navigation


[Link]
on
End of Lecture

You might also like