Starting with Compose

android2ee (Seguy Mathias)
10 min readMar 15, 2024
My first stupid Compose View

Compose is the “new” way to create views in Android and is a big change in the way to think about UI in android; mainly because we do not manage static views graph but dynamic views.

The second magic moment in compose is when you realize your view follow the observable pattern under the hood; your view will be redraw when the data displayed is updated. And this is a big change in our way to consider managing UI.

Add Compose in your project

(quick set up official Android link)


dependencies {
def composeBom = platform("androidx.compose:compose-bom:2024.02.01")
implementation composeBom
androidTestImplementation composeBom

// Material Design 3
implementation("androidx.compose.material3:material3")
// Android Studio Preview support
implementation("androidx.compose.ui:ui-tooling-preview")
debugImplementation("androidx.compose.ui:ui-tooling")

// UI Tests
androidTestImplementation("androidx.compose.ui:ui-test-junit4")
debugImplementation("androidx.compose.ui:ui-test-manifest")
// Optional - Included automatically by material, only add when you need
// the icons but not the material library (e.g. when using Material3 or a
// custom design system based on Foundation)
// Optional - Add full set of material icons
implementation 'androidx.compose.material:material-icons-extended'
// Optional - Add window size utils
implementation 'androidx.compose.material3:material3-window-size-class'
implementation 'androidx.compose.material:material-ripple'
implementation 'androidx.compose.foundation:foundation'
// Optional - Integration with LiveData
implementation 'androidx.compose.runtime:runtime-livedata'
// Optional - Integration with activities
implementation 'androidx.activity:activity-compose:1.8.2'
// Optional - Integration with ViewModels
implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0'

My first compose screen

We used to say, in your Activity put the code for displaying the screen and handling user’s actions and in you ViewModel put the code to manage the data displayed.

My feeling is that in Compose, you do not even put your “drawing” code in your activity and you extract it in specific files (with re-usability in mind … like fragments but in a simple way). The activity is now an empty shell only handling the life cycle’s events.

class MainActivity : ComponentActivity() {

@OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState)

val appContainer = (application as JetnewsApplication).container
setContent {
val widthSizeClass = calculateWindowSizeClass(this).widthSizeClass
JetnewsApp(appContainer, widthSizeClass)
}
}
}

This code is extracted from JetNews a compose sample (it’s Florina Muntenescu’s code)

Second mind change, you are back to linear layout and now you think your UI as an arrangement of Row and Column (LinearLayout’s legacy point of view with the Vertical and Horizontal orientation)

So now every think is in a Row or in a Column (well, ok, we have also Scaffeold and Box, but well, it’s details). When you create your Row or Column, in its constructor you provide the constraints and in its content you provide the content:

Row(
Modifier.fillMaxSize(1f),
horizontalArrangement = Arrangement.SpaceEvenly,
verticalAlignment = Alignment.CenterVertically
) {
Image(
painter = painterResource(R.drawable.ic_android),
contentDescription = "Contact profile picture",
modifier = Modifier
.clip(CircleShape)
.size(36.dp)
.background(Color.Green)
)
}

Last key point before digging in the concepts of compose, you need to define your UI into a @Composable function and by convention, your function name starts with an Uppercase:

@Composable
fun DisplayList(messages: State<List<String>>) {
LazyColumn() {
//messages.value can not be null, else you'll have an "expect Int" compilation error
items(messages.value) { message ->
if (message.length % 2 == 0) {
MessageRow(message)
} else {
MessageRowOdd(message)
}
}
}
}

Also as you see, we provide parameters to those functions, so when those states change, the framework knows that it has to redraw your UI. Magic.

Golden Rules for Compose functions

@Compose functions have to be

  • Fast, they are called to draw the code, as we know that the screen has to be drawn in a 16ms windows, no heavy code can run here. You read properties and draw, never calculate them.
  • Idempotent, can be called several times with the same parameter and always produce the same result

Another way to understand this concept is

  • Without side effects,
  • Without state,
  • Immutable,
  • Predictable

Single Source of truth and Patterns

A good practice to follow is :

  • Your business data has to be stored in the ViewModel and exposed to the View, either as a LiveData, a Flow or a State
  • Your graphical components states have to be kept in your view using the remember pattern (view states are the states of the graphical components, extended/visible…)

Single source of truth : Business data and ViewModel

Business Data at the ViewModel level

For example displaying a List (RecyclerView is dead… we have to say it) of data exposed by the ViewModel :

A simple ViewModel exposing a list of String :

class SimpleViewModel :ViewModel() {

val dataList : LiveData<List<String>>
get() = _dataList
private val _dataList =MutableLiveData<List<String>>()

fun init(){
val list =mutableListOf<String>()
for(i in 1..20) {
list.add("Goretti")
list.add("Jason")
}
_dataList.value=list
}
}

The composable function that consumes and displays those Strings:

@Composable
fun ArrayAdapterScreen(viewModel: SimpleViewModel) {
val firstNames = viewModel.dataList.observeAsState()
//Do A list
if (firstNames != null && firstNames.value != null) {
displayList(firstNames as State<List<String>>)
}
}

@Composable
fun displayList(messages: State<List<String>>) {
LazyColumn() {
//messages.value can not be null, else you'll have an "expect Int" compilation error
items(messages.value) { message ->
//two different layouts are used here...
if (message.length % 2 == 0) {
MessageRow(message)
} else {
MessageRowOdd(message)
}
}
}
}

Single source of truth: Components states and remember

Here are two examples of keeping the graphical component state.

The selectedTabs pattern remembers which is the selected tabs and survive to orientation changes. As this information as to be retained (across rotation changed, as long as the process stays in the LRU cache) the key word rememberSaveable has to be used. Under the hoods, it uses the SavedInstanceState pattern.

Sample of Tabs in the bottom bar
class TabbedActivity : ComponentActivity() {

val viewModel : SimpleViewModel by viewModels()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//onCreate is the right moment to intialize your viewModel
//(while onStart is the right moment to launch data loading)
viewModel.init()
setContent {
MaterialTheme {
ContentScreen()
}
}
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ContentScreen() {
/**Define your tabs state and has to be retained when rotation*/
val selectedTabs = rememberSaveable { mutableStateOf(0) }
//then create your UI using this information
Scaffold(
//In the example the bottom bar displays the navigation Tabs
bottomBar = {
BottomAppBar(
containerColor = MaterialTheme.colorScheme.primaryContainer,
contentColor = MaterialTheme.colorScheme.primary,
) {
BottomBarContent(selectedTabs,)
}
})
{ innerPadding ->
Box(modifier = Modifier
.padding(innerPadding)
.fillMaxSize(1F)
.background(MaterialTheme.colorScheme.tertiaryContainer),
Alignment.Center) {
// Define the content according to the selected tab
when (selectedTabs.value){
0-> FirstScreen()
1-> ArrayAdapterScreen(viewModel)
2-> ThirdScreen()
else->Text(text = "WTF" )
}
}
}
}

@Composable
fun BottomBarContent(selectedTabs: MutableState<Int>) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceAround,
verticalAlignment = Alignment.CenterVertically
) {//Add the Tabs
Button(onClick = { selectedTabs.value = 0 }) {
//manage the Tabs in a specific function
SelectedTabText(label = "Tab 0", selectedTabs.value == 0 )
}
Button(onClick = { selectedTabs.value = 1 }) {
SelectedTabText(label = "Tab 1", selectedTabs.value == 1 )
}
Button(onClick = { selectedTabs.value = 2 }) {
SelectedTabText(label = "Tab 2", selectedTabs.value == 2 )
}
}
}

/**
* https://developer.android.com/jetpack/compose/text/style-text
*/
@Composable
fun SelectedTabText(label:String, isSelected: Boolean){
//According to the selected state display the button in a selected style or not
if(isSelected){
Text( "❤\uFE0F $label ❤\uFE0F",color = Color.White, fontStyle = FontStyle.Italic,fontWeight = FontWeight.Bold)
}else{
Text(label,color = Color.Black, fontStyle = FontStyle.Normal,fontWeight = FontWeight.Light)
}
}
}

The isPressed and the interaction are used to track when a component is clicked and do actions according to it. As it’s a short-life information, the remember key word is enough.

@Composable
fun BasicData(){
//Remember using to share the "isPressed" state of a Button/Image
val interactionImageIcon = remember { MutableInteractionSource() }
val isPressed = interactionImageIcon.collectIsPressedAsState()
//a way to listen/track for the "isPressed" state on an Image
Image(
//using the isPressed state
painter = if (isImagePressed.value) painterResource(R.drawable.ic_android)
else painterResource( R.drawable.ic_launcher_background ),
contentDescription = "Clickable Picture",
modifier = Modifier
.clip(CircleShape)
.size(if (isImagePressed.value.not()) 36.dp else 96.dp)
.clickable(
onClick = {
Toast
.makeText(
currentContext,
"I am the Image Toast",
Toast.LENGTH_SHORT
)
.show()
},
//linking the Image component with the interaction object
interactionSource = interactionImageIcon,
indication = rememberRipple(false, 102.dp, Color.Blue)
)
)
}

Scaffold and Activity

First and before every think : Appcompat is dead, long live the ComponentActivity. This said, let’s see the usage of the Scaffold.

https://developer.android.com/jetpack/compose/components/scaffold

When writing an activity, we have the Scaffold layout, it helps us creating the activity view with TopBar (ActionBar), BottomBar, FloatingActionButton, Content

 Scaffold(
//Define your topBar (the actionBar) (you can use action, navigationIcon...)
topBar = {
TopAppBar(
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.primaryContainer,
titleContentColor = MaterialTheme.colorScheme.primary,
),
title = {
Text("Top app bar")
}
)
},
//Define the bottom bar (you can use action also and do not define the content, see the link)
bottomBar = {
BottomAppBar(
containerColor = MaterialTheme.colorScheme.primaryContainer,
contentColor = MaterialTheme.colorScheme.primary,
) {
Text(
modifier = Modifier
.fillMaxWidth(),
textAlign = TextAlign.Center,
text = "Bottom app bar",
)
}
},
//Define teh FAB
floatingActionButton = {
FloatingActionButton(onClick = { Log.e("FirstSimpleAct","FAB clicked")}) {
Icon(Icons.Default.Add, contentDescription = "Add")
}
})
{
//Your content goes here
innerPadding ->
Box(modifier = Modifier.padding(innerPadding)) {
BasicContent(User("Bob", "Toto"),isPressed,interactionImageIcon)
}
}
}

Real example

An old UI

For those who knows (back in 2011), let’s try to reproduce this U.I

Using Compose I obtain the following

Result using compose
@Composable
fun EmailSetupScreen() {
//use the Column (vertical layout)
Column(
modifier = Modifier
.fillMaxWidth()
) {
//Use a Row to display the Title (we could have used a Box)
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
modifier = Modifier
.background(Color.White)
.fillMaxWidth(1f)
.padding(0.dp, 5.dp)
) {
Text(
"Email Setup",
fontSize = 32.sp
)
}
//Use a Row to display the sentence (we could have used a Box)
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Start,
modifier = Modifier
.background(Color.White)
.fillMaxWidth(1f)
.padding(0.dp, 5.dp)
) {
Text(
"You can configure your email in just few steps",
fontSize = 16.sp
)
}

//Use a Row to display on the same line the Label and the EditText
//so called Text and TextField in compose
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Start,
modifier = Modifier
.background(Color.White)
.fillMaxWidth(1f)
.padding(0.dp, 5.dp)
) {
//Simple Text for the label constrained in size
Text(
"Email address:",
modifier = Modifier
.fillMaxWidth(0.33f)
.padding(start = 0.dp, top = 0.dp, end = 10.dp, bottom = 0.dp),
textAlign = TextAlign.End
)
//Simple text for the label also constrained in size
OutlinedTextField(
value = "text@gmail.com",
onValueChange = { updateEmailAddress(it) },
modifier = Modifier.fillMaxWidth(0.75f)
)
}

//Use a Row to display on the same line the Label and the EditText
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Start,
modifier = Modifier
.background(Color.White)
.fillMaxWidth(1f)
.padding(0.dp, 5.dp)
) {
Text(
"Password :",
modifier = Modifier
.fillMaxWidth(0.33f)
.padding(start = 0.dp, top = 0.dp, end = 10.dp, bottom = 0.dp),
textAlign = TextAlign.End
)
OutlinedTextField(//password visible
value = " ",
onValueChange = { updateEmailAddress(it) },
modifier = Modifier.fillMaxWidth(0.5f)
)
}
//use a Spacer to add some space between components
Spacer(
modifier = Modifier
.height(48.dp)
.fillMaxWidth(1f)
.background(Color.White)
)
//add the button in a the last line with an Arrangement.End
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.End,
modifier = Modifier
.background(Color.White)
.fillMaxWidth(1f)
.padding(start = 0.dp, top = 0.dp, end = 10.dp, bottom = 0.dp)
) {
OutlinedButton(onClick = { onNext() }) {
Text("Next")
}
}
}
}

The code is much more verbose than the one using GridLayout

Nowadays way to do

Let’s try to do the UI more in the “air du temps”:

Let’s define the following ViewModel exposing a function to create an user (using email and password):

class SimpleViewModel : ViewModel() {
data class User(val email: String, val password: String)
fun storeUser(email: String, password: String) {
val newUser=User(email,password)
}

Then we can use the following Compose code to create the UI

OptIn(ExperimentalMaterial3Api::class)
@Composable
fun NewEmailSetupScreen(viewModel:SimpleViewModel) {
//remember the email
var email by rememberSaveable { mutableStateOf("") }
//remember the password
var password by rememberSaveable { mutableStateOf("") }
//create the column (vertical layouting)
Column(
modifier = Modifier
.fillMaxWidth()
.padding(5.dp, 5.dp)
.background(Color.White)
) {

//Display the sentence
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Start,
modifier = Modifier
.background(Color.White)
.fillMaxWidth(1f)
.padding(5.dp, 5.dp)
) {
Text(
"You can configure your email in just few steps"
)
}

//Display the row which request the email
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Start,
modifier = Modifier
.background(Color.White)
.fillMaxWidth(1f)
.padding(0.dp, 5.dp)
) {
//use an OutlinedTextField for the EditText
OutlinedTextField(
value = email,
onValueChange = { email=it},
modifier = Modifier.fillMaxWidth(1f),
label = { Text("Email Address") }
)
}

//Display the row which request the password
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Start,
modifier = Modifier
.background(Color.White)
.fillMaxWidth(1f)
.padding(5.dp, 5.dp)
) {
//use a TextField for the EditText ro be able to display bullet
//instead of letter as a password
TextField(
value = password,
onValueChange = { password=it },
modifier = Modifier
.fillMaxWidth(1f)
.background(Color.White),
colors = TextFieldDefaults.textFieldColors(
containerColor = Color(0x00FF00FF)),
label = { Text("Password") },
visualTransformation = PasswordVisualTransformation(),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password)
)
}
//add a separator (called spacer)
Spacer(
modifier = Modifier
.height(48.dp)
.fillMaxWidth(1f)
.background(Color.White)
)

//add the Next button to submit the form
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.End,
modifier = Modifier
.background(Color.White)
.fillMaxWidth(1f)
.padding(5.dp, 5.dp)
) {
OutlinedButton(onClick = { onNext(email,password,viewModel) }) {
Text("Next")
}
}
}
}

/**Call the ViewModel to create the User */
fun onNext(email:String,password:String,viewModel:SimpleViewModel) {
viewModel.storeUser(email,password)
}

And to display the title in the top bar, I just use a Scaffold:

Scaffold(
topBar = {
CenterAlignedTopAppBar(
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.primaryContainer,
titleContentColor = MaterialTheme.colorScheme.primary,
),
title = {
Text("Email Setup")
}
)
}

Next steps :

I will explore the following concepts in Compose

  • Theming
  • Understanding State

Links

https://developer.android.com/courses/pathways/compose?hl=fr

Thinking in compose:

https://developer.android.com/jetpack/compose/mental-model?

Manage the state:

https://developer.android.com/jetpack/compose/state?hl=fr

--

--

android2ee (Seguy Mathias)

Android2ee alias Mathias Seguy. Android expert gives you some news on the Android world. mathias.seguy@android2ee.com