The Professional Developer Guide to Modern Android Development — Unicorn Fullstack Coding Bootcamp Handbook Series — Chapter 1
The Professional Developer Developer Guide to Modern Android Development is a coding bootcamp handbook for beginner, intermediate and advanced developers.
Chapter 0 — Preface
The Professional Developer Guide to Modern Android Development — Edition One — Unicorn Fullstack Professional Coding Bootcamp Handbook covers modern Kotlin concepts as well as core Modern Android Development concepts such as Jetpack Compose, Coroutines, Material Design, and all the various Jetpack libraries needed for a fully functional, well designed Modern Android app, developed using the Jetpack Compose, Kotlin, Clean Architecture, and Modern Android Development best practices, as recommended by Android.
Chapter 1 — Starter Kit
In order to start implementing Unicorn Commerce app, we will need a starting point. In thi case, the starting point will be the Unicorn Starter Kit, which may be downloaded from Github from the Starter Kit Part 1 release from the releases link below
Alternatively, the feature/develop_starter_kit_part1 branch may be cloned as well
This gives us a starting point to start implementing the various screens of the Unicorn Commerce Android app.
First, we need to cover a bit of the basics.
State Management
State Management in Jetpack Compose is done using the remember API and the state API in the Compose library
Declaring State and Delegates
Let’s take a look at how state is declared in Jetpack Compose
A state is just a variable which can mutate.
Compose holds the value of such a state variable in an object called the MutableState object.
To define a mutable state, we call a Compose built-in function called MutableStateOf() and pass in, as a parameter, the value that we want it to hold.
The MutableStateOf() function creates the MutableState object and holds the value
MutableState [object] <=[returns]= MutableStateOf(/* value to hold */)
The MutableState object is an observable object and any change to that MutableState object is observed by Compose.
Observing the MutableState object means that Compose keeps track of the value associated with the MutableState object and notifies whoever reads or displays that value of the change.
UI elements and Composables are typically the elements that read and display the values.
When the value changes, the composable functions have to be called again and this process is called Recomposition.
When a variable is defined inside a Composable function, then a re-call or recomposition would reset the value of the variable to the initial value.
We need the variable to retain the value after recomposition.
To support this, Compose provides a special Composable function called Remember.
Another declaration is to use the by keyword in Kotlin.
var value by remember { mutableStateOf( /* some value*/) }
For example, we may use the by remember delegate to keep track of the number of items in the shopping cart. We may modify the ItemOrder Composable in the BrowseScreen to allow the count of items to be persisted using the remember delegate as below
@Composable
private fun ItemOrder(
navController: NavController
) {
var count by remember {
mutableStateOf(0)
}
Column(
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
modifier = Modifier.clickable {
navController.navigate(Screen.HomeDetailScreen.route) {
popUpTo(Screen.HomeDetailScreen.route) {
inclusive = true
}
}
},
text = "Browse",
color = Color.White,
fontSize = MaterialTheme.typography.titleSmall.fontSize,
fontWeight = FontWeight.Bold
)
Row(
Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically
) {
IconButton(
onClick = { count-- }
) {
Icon(
imageVector = Icons.Default.Remove,
contentDescription = "Remove"
)
}
Text(
text = "$count",
fontSize = 28.sp,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(8.dp)
)
IconButton(
onClick = { count++ }
) {
Icon(
imageVector = Icons.Default.Add,
contentDescription = "Add"
)
}
}
}
}
Recomposition
As state changes take place, composable functions need to be re-executed or re-composed. Recomposition is therefore, the process of updating the Composable to reflect any state changes.
In order to persist the value of mutableState objects across Activity recomposition, one may need to use the rememberSaveable() delegate function as follows
var count by rememberSaveable() {
mutableStateOf(0)
}
This preserves the value across Activity recompositions such as screen rotation.
State Hoisting
One may have Stateful and Stateless Composables. In the above example, ItemOrder is a Stateful Composable as it manages its state internally. However, in order to make the ItemOrder Composable reusable, it may make sense to move the state to the caller and make the ItemOrder Composable stateless, thereby making the ItemOrder Composable reusable.
We may therefore, hoist the state into the BrowseScreen Composable as below
package app.unicornapp.mobile.android.unicorn.ui.screens
// Imports
@Composable
fun BrowseScreen(
navController: NavController
) {
var count by rememberSaveable() {
mutableStateOf(0)
}
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Image(
painterResource(
id = R.drawable.banner_bg_6),
contentDescription = "",
contentScale = ContentScale.FillBounds,
modifier = Modifier.matchParentSize()
)
ItemOrder(
count,
{ count++ },
{ count --},
navController = navController
)
}
}
@Composable
private fun ItemOrder(
count: Int,
onIncrement: () -> Unit,
onDecrement: () -> Unit,
navController: NavController
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
modifier = Modifier.clickable {
navController.navigate(Screen.HomeDetailScreen.route) {
popUpTo(Screen.HomeDetailScreen.route) {
inclusive = true
}
}
},
text = "Browse",
color = Color.White,
fontSize = MaterialTheme.typography.titleSmall.fontSize,
fontWeight = FontWeight.Bold
)
Row(
Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically
) {
IconButton(
onClick = { onDecrement() }
) {
Icon(
imageVector = Icons.Default.Remove,
contentDescription = "Remove",
tint = Color.White
)
}
Text(
text = "$count",
color = Color.White,
fontSize = 28.sp,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(8.dp)
)
IconButton(
onClick = { onIncrement() }
) {
Icon(
imageVector = Icons.Default.Add,
contentDescription = "Add",
tint = Color.White
)
}
}
}
}
@Preview
@Composable
fun BrowseScreenPreview() {
BrowseScreen(navController = rememberNavController())
}
Design System, UI Design Patterns, Scaffolds
The material design system provides reusable material design components such as Button, TextField, Checkbox, Card, Slider components as part of the Jetpack Compose material design components.
UI Design Patterns are recurring patterns of using commonly used UI elements such as TopBar, BottomBar, NavigationDrawer, SnackBar and FABs.
Jetpack Compose provides a special Composable called Scaffold which allows us to combine and customize the UI design components and patterns to customize the layouts and navigation for the app. Scaffolds are customizable layouts that provide support to various UI structures and components, which include AppBars, SnackBars, NavigationDrawer, Floating Action Buttons. Besides the main body of the Scaffold, the Scaffold can be configured to combine some or all of these UI structures.
The following properties of a Scaffold can be used to provide relevant material components to a Scaffold, along with the relevant Composables —
topBar, bottomBar, SnackBarHost, floatingActionButton, drawerContent
Scaffold is a Composable, and a material component in Compose. It provides slots to plugin other UI structures such as top and bottom bars, navigation drawer, floating action button and snackbar. Thus, Scaffold acts like a container to these UI components.
ScaffoldState
ScaffoldState is a class in Compose that holds basic screen state, like the sizes of the components and stores.
class ScaffoldState {
val drawerState: DrawerState
val snackbarHostState: SnackBarHostState
}
To remember the state, we call the rememberScaffoldState() function to get a scaffoldState instance, and pass it to the Scaffold
val scaffoldState = rememberScaffoldState()
Scaffold(
scaffoldState = scaffoldState,
//
) {
//
}
When we define the drawer, the DrawerState object maintains the state of the drawer, which can be either open or closed.
The drawerState can be changed by callings its public functions, open or closed, that correspond to the opening or closing of the drawer.
Example — To open the drawer, we use the scaffoldState object to call the drawerState object and then call open() using the drawerState object
scaffoldState.drawerState.open()
Since open is a suspend function, the function must be called within a Coroutine.
A coroutine is a program component that takes a block of code and runs it concurrently with the rest of the code
Thus we have
val scope = rememberCoroutineScope()
scope.launch {
scaffoldState.drawerState.close()
}
A suspend function is simply a function that can be paused and resumed later.
A suspend function can only be called within a coroutine.
Launch builds a coroutine. It launches a coroutine together with the rest of the code.
Launch is declared in the coroutine scope.
We continue the implementation in Chapter 2 (Part 2) of this series.