Search icon CANCEL
Subscription
0
Cart icon
Your Cart (0 item)
Close icon
You have no products in your basket yet
Save more on your purchases now! discount-offer-chevron-icon
Savings automatically calculated. No voucher code required.
Arrow left icon
Explore Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Conferences
Free Learning
Arrow right icon
Kickstart Modern Android Development with Jetpack and Kotlin
Kickstart Modern Android Development with Jetpack and Kotlin

Kickstart Modern Android Development with Jetpack and Kotlin: Enhance your applications by integrating Jetpack and applying modern app architectural concepts

eBook
€19.99 €28.99
Paperback
€35.99
Subscription
Free Trial
Renews at €18.99p/m

What do you get with eBook?

Product feature icon Instant access to your Digital eBook purchase
Product feature icon Download this book in EPUB and PDF formats
Product feature icon Access this title in our online reader with advanced features
Product feature icon DRM FREE - Read whenever, wherever and however you want
Product feature icon AI Assistant (beta) to help accelerate your learning
Table of content icon View table of contents Preview book icon Preview Book

Kickstart Modern Android Development with Jetpack and Kotlin

Chapter 1: Creating a Modern UI with Jetpack Compose

Jetpack libraries enable you to build and design high-quality, robust Android apps that have a reliable architecture and work consistently across different versions and devices. At the same time, the Jetpack suite allows you to eliminate boilerplate code and ultimately focus on what matters – building the necessary features.

In this chapter, we will tackle one of the most popular Jetpack libraries for building user interfaces (UIs), called Compose. Simply put, Jetpack Compose is a powerful modern toolkit that allows you to build a native UI in Android directly with Kotlin functions and APIs.

Compose accelerates and greatly simplifies UI development as it harnesses the power of declarative programming, combined with the ease of use of the Kotlin programming language. The new toolkit solely relies on Kotlin APIs when allowing you to construct UIs through declarative functions.

By the end of this chapter, you will know how building UIs on Android can be done with less code, powerful tools, intuitive APIs, and without the need for additional languages such as XML.

In the first section, Understanding the core concepts of Compose, we will explore the fundamental concepts behind Compose and understand how they are beneficial in helping us write better and cleaner UIs. We will see how UIs can be described with composable functions while also understanding how the new declarative way of building UIs on Android works. We will also explore how composition is favored over inheritance and how the data flow works in Compose. Finally, we will cover what recomposition is and see how essential it is to our declarative UI.

In the second section, Exploring the building blocks of Compose UIs, we will study the most important composable functions that Compose provides out of the box. Afterward, we will see how we can preview our Compose UI and how activities render it.

We will then put our knowledge to good use by creating our first Compose project about restaurants in the Building a Compose-based screen section. In the last section, entitled Exploring lists with Compose, we will learn how to correctly show more content in Compose with the help of lists.

To summarize, in this chapter, we're going to cover the following main topics:

  • Understanding the core concepts of Compose
  • Exploring the building blocks of Compose UIs
  • Building a Compose-based screen
  • Exploring lists with Compose

    Note

    As Compose is a dedicated native UI framework, we will only briefly cover the core concepts, common components, and usages of the toolkit without going into advanced topics.

Technical requirements

When building Compose-based Android projects, you usually require your day-to-day tools for Android development. However, to follow along smoothly, make sure you have the following:

  • The Arctic Fox 2020.3.1 version of Android Studio. You can also use a newer Android Studio version or even Canary builds but note that the IDE interface and other generated code files might differ from the ones used throughout this book.
  • The Kotlin 1.6.10 or newer plugin must be installed in Android Studio.
  • Jetpack Compose 1.1.1 or greater. You should follow this chapter and use the projects with this version. You can explore newer versions if you wish, though API differences might arise.

You can find the GitHub repository containing the source code for this book here:https://github.com/PacktPublishing/Kickstart-Modern-Android-Development-with-Jetpack-and-Kotlin/.

To access the code presented in this chapter, navigate to the Chapter_01 directory. The code snippets presented in the first two sections can be found in the ExamplesActivity.kt file, which is located in the root directory. The project coding solution for the Restaurants app, which we will develop in the last few sections of this chapter, can be found in the chapter_1_restaurants_app Android project directory.

Understanding the core concepts of Compose

Jetpack Compose dramatically changes the way we write UIs on Android. UIs are now developed with Kotlin, which enables a new declarative paradigm of writing layouts with widgets called composables.

In this section, we will understand what composable functions are and how they are used to write UIs. We will learn how the programming paradigm has shifted and how composition is now enforced, thereby increasing flexibility in the way we define UIs. We will also discuss the flow of data within UIs and what recomposition is while trying to understand the benefits that are brought by these new concepts.

To summarize, we will be covering the following topics:

  • Describing UIs with composable functions
  • The paradigm shift in creating UIs on Android
  • Favoring composition over inheritance
  • Unidirectional flow of data
  • Recomposition

So, let's get started.

Describing UIs with composable functions

Compose allows you to build UIs by defining and calling composable functions. Composable functions are regular functions annotated with the @Composable annotation that represent widgets on the screen.

Compose works with the help of a Kotlin compiler plugin in the type checking and code generation phase of Kotlin. The Compose compiler plugin makes sure that you can create composables.

For example, a composable that displays a piece of text may look like this:

@Composable
fun FriendlyMessage(name: String) {
   Text(text = "Greetings $name!")
}

In the preceding code block, we've defined the FriendlyMessage composable function by annotating it with the @Composable annotation. Looking at the function definition and body, we can easily deduce that it displays a greeting message.

It's important to note that any function annotated with @Composable can be rendered on the screen as it will produce a piece of UI hierarchy that displays content. In their true sense, composable functions emit UI widgets based on their definition.

In our case, the previous function should display a greeting message by concatenating the String value it receives as a parameter with a predefined message. As the function relies on its input parameters to show different messages on every usage, it's correct to say that composable functions are functions of data (presented as F(data) in the following diagram) that are converted into pieces of UI or widgets:

Figure 1.1 – In Compose, UI is a function of data

Figure 1.1 – In Compose, UI is a function of data

Later, in the Unidirectional flow of data subsection, we will understand why having functions to describe UI widgets is beneficial to our projects as it leads to a less bug-prone UI layer.

Getting back to our example, you might be wondering what the Text functional call represents. As with every other framework, Compose provides composable functions such as Text out of the box that we can use.

As its name suggests, the Text composable allows you to display some text on the screen. We will cover other composable functions provided by Compose in the Exploring the building blocks of Compose UIs section.

Until then, let's have another look at the previous code example and highlight the most important rules when it comes to defining a composable function:

  • It should be a regular function marked with the @Composable annotation.
  • Its UI output is defined by the data that's received through its input parameters. Composable functions should return Unit as they emit UI elements and do not return data as regular functions do. Most of the time, we omit defining the Unit return type or even returning Unit – as Kotlin marks it as redundant – just like in the previous example.
  • It can contain other composable functions or regular Kotlin code. In the previous example, the FriendlyMessage composable function makes use of another composable, called Text, but it could also call regular Kotlin code (we will tackle that in the upcoming sections).
  • It should be named as a noun or a noun preceded by a suggestive adjective (but never a verb). This way, composable functions envision widgets and not actions. Additionally, its name should respect the PascalCase naming convention, meaning that the first letter of each compound word in a variable is capitalized.
  • It's recommended that the function is public and not defined within a class but directly within a Kotlin file. This way, Compose promotes the reuse of composable functions.

Now that we understand what a composable function is and how one is defined, let's move on and explore the paradigm shift that Compose brings to Android UI development.

The paradigm shift in creating UIs on Android

Compose brings a new approach to Android UI development and that is providing a declarative way of describing your UI. Before trying to understand how the declarative approach works, we will learn how the traditional View System relies on a different paradigm – the imperative one.

The imperative paradigm

When describing your UI with XML, you represent the view hierarchy as a tree of widgets that are commonly known as views. Views, in the context of the traditional View System, are all the components that inherit from the android.view.View class, from TextView, Button, or ImageView to LinearLayout, RelativeLayout, and so on.

Yet what's essential for the View System is the imperative paradigm that it relies on. Because your application must know how to react to user interactions and change the state of the UI accordingly, you can mutate the state of your views by referencing them through findViewById calls and then update their values through calls such as setText(), setBackgroundResource(), and so on.

Since views maintain their internal state and expose setters and getters, you must imperatively set new states for each component, as the following diagram suggests:

Figure 1.2 – The Android View System features in the imperative paradigm

Figure 1.2 – The Android View System features in the imperative paradigm

Manually manipulating views' states increases the chance of bugs and errors in your UI. Because you end up treating multiple possible states and because chunks of data are displayed in several such states, it's relatively easy to mess up the outcome of your UI. Illegal states or conflicts between states can also arise relatively easily when your UI grows in complexity.

Moreover, since the layouts are defined in an additional component – that is, an XML file – the coupling between Activity, Fragment, or ViewModel and the XML-based UI increases. This means that changing something on the UI in the XML file will often lead to changes in Activity, Fragment, or ViewModel classes, which is where state handling happens. Not only that but cohesion is reduced because of language differences: one component is in Java/Kotlin, while the other one is in XML. This means that for the UI to function, it needs not only an Activity or Fragment but also XML.

The declarative paradigm

To address some of the issues within the standard View System, Compose relies on a modern declarative UI model, which drastically simplifies the process of building, updating, and maintaining UIs on Android.

If, in the traditional View System, the imperative paradigm described how the UI should change, in Compose, the declarative paradigm describes what the UI should render at a certain point in time.

Compose does that by defining the screen as a tree of composables. As in the following examples, each composable passes data to its nested composables, just like the FriendlyMessage composable passed a name to the Text composable in our code example from the previous section:

Figure 1.3 – Visualizing a tree of composable widgets and how data is passed downwards

Figure 1.3 – Visualizing a tree of composable widgets and how data is passed downwards

When the input arguments change, Compose regenerates the entire widget tree from scratch. It applies the necessary changes and eliminates the need and the associated complexity of manually updating each widget.

This means that in Compose, composables are relatively stateless and because of that, they don't expose getter and setter methods. This allows the caller to react to interactions and handle the process of creating new states separately. It does that by calling the same composables but with different argument values. As we discussed in the Describing UIs with composable functions section, the UI in Compose is a function of data. From this, we can conclude that if new data is passed to composables, new UI states can be produced.

Lastly, compared to the View System, Compose only relies on Kotlin APIs, which means that UIs can now be defined with a single technology, in a single component, thereby increasing cohesion and reducing coupling.

Now, let's look at another shift in design brought by Compose and discuss how composition yields more flexible ways of defining UIs than inheritance does.

Favoring composition over inheritance

In the Android View System, every view inherits functionality from the parent View class. As the system relies solely on inheritance, the task of creating custom views can only be done through defining elaborate hierarchies.

Let's take the Button view as an example. It inherits functionality from TextView, which, in turn, inherits from View:

Figure 1.4 – The class inheritance hierarchy for the Button view

Figure 1.4 – The class inheritance hierarchy for the Button view

This strategy is great for reusing functionality, but inheritance becomes difficult to scale and has little flexibility when trying to have multiple variations of one view.

Say you want the Button view to render an image instead of text. In the View System, you would have to create an entirely new inheritance hierarchy, as shown in the following hierarchy diagram:

Figure 1.5 – The class inheritance hierarchy for the ImageButton view

Figure 1.5 – The class inheritance hierarchy for the ImageButton view

But what if you need a button that accommodates both a TextView and an ImageView? This task would be extremely challenging, so it's easy to conclude that having separate inheritance hierarchies for each custom view is neither flexible nor scalable.

These examples are real, and they show the limitations of the View System. As we've previously seen, one of the biggest reasons for the lack of flexibility is the inheritance model of the View System.

To address this issue, Compose favors composition over inheritance. As shown in the following diagram, this means that Compose builds more complex UIs by using smaller pieces and not by inheriting functionality from one single parent:

Figure 1.6 – Inheritance versus composition

Figure 1.6 – Inheritance versus composition

Let's try to briefly explain our previous comparison between inheritance and composition:

  • With inheritance, you are limited to inheriting your parent, just like Button inherits only from TextView.
  • With composition, you can compose multiple other components, just like the Button composable contains both an Image composable and a Text composable, thereby giving you much more flexibility in building UIs.

Let's try to build a composable that features a button with an image and text. This was a huge challenge with inheritance, but Compose simplifies this by allowing you to compose an Image composable and a Text composable inside a Button composable:

@Composable
fun SuggestiveButton() {
    Button(onClick = { }) {
        Row() {
            Image(painter = 
                     painterResource(R.drawable.drawable),
                  contentDescription = "")
            Text(text = "Press me")
        }
    }
}

Now, our SuggestiveButton composable contains both Image and Text composables. The beauty of this is that it could contain anything else. A Button composable can accept other composables that it renders as part of its button's body. Don't worry about this aspect or about that weird composable called Row for now. The Exploring the building blocks of Compose UIs section will cover both of these aspects in more detail.

What's important to remember from this example is that Compose gives us the flexibility of building a custom UI with ease. Next, let's cover how data and events flow in Compose.

Unidirectional flow of data

Knowing that each composable passes data down to its children composables, we can deduct that the internal state is no longer needed. This also translates into a unidirectional flow of data because composables only expect data as input and never care about their state:

Figure 1.7 – Visualizing the unidirectional flow of data and events

Figure 1.7 – Visualizing the unidirectional flow of data and events

Similarly, with data, each composable passes down callback functions to its children composables. Yet this time, the callback functions are caused by user interactions, and they create an upstream of callbacks that goes from each nested composable to its parent and so on. This means that not only the data is unidirectional but also events, just in opposite ways.

From this, it's clear that data and events travel only in one direction, and that's a good thing because only one source of truth – ideally, ViewModel – is in charge of handling them, resulting in fewer bugs and easier maintenance as the UI scales.

Let's consider a case with another composable provided by Jetpack Compose called Button. As its name suggests, it emits a button widget on the screen, and it exposes a callback function called onClick that notifies us whenever the user clicks the button.

In the following example, our MailButton composable receives data as an email identifier, mailId, and an event callback as a mailPressedCallback function:

@Composable
fun MailButton(
    mailId: Int,
    mailPressedCallback: (Int) -> Unit
) {
    Button(onClick = { mailPressedCallback(mailId) }) {
        Text(text = "Expand mail $mailId")
    }
}

While it consumes the data it receives via mailId, it also sets the mailPressedCallback function to be called every time its button is clicked, thereby sending the event back up to its parent. This way, data flows downwards and the callback flows upwards.

Note

It is ideal to construct your Compose UI in such a way that data provided by the ViewModel flows from parent composables to children composables and events flow from each composable back up to the ViewModel. If you're not familiar with the ViewModel component, don't worry as will cover it in the upcoming Chapter 2, Handling UI State with Jetpack ViewModel.

Recomposition

We have already covered how composable functions are defined by their input data and stated that whenever the data changes, composables are rebuilt as they render a new UI state corresponding to the newly received data.

The process of calling your composable functions again when inputs change is called recomposition. When inputs change, Compose automatically triggers the recomposition process for us and rebuilds the UI widget tree, redrawing the widgets emitted by the composables so that they display the newly received data.

Yet recomposing the entire UI hierarchy is computationally expensive, which is why Compose only calls the functions that have new input while skipping the ones whose input hasn't changed. Optimizing the process of rebuilding the composable tree is a complex job and is usually referred to as intelligent recomposition.

Note

In the traditional View System, we would manually call the setters and getters of views, but with Compose, it's enough to provide new arguments to our composables. This will allow Compose to initiate the recomposition process for parts of the UI so that the updated values are displayed.

Before jumping into an actual example of recomposition, let's have a quick look at the lifecycle of a composable function. Its lifecycle is defined by the composition lifecycle, as shown here:

Figure 1.8 – The composition lifecycle of a composable function

Figure 1.8 – The composition lifecycle of a composable function

This means that a composable first enters composition, and before leaving this process, it can recompose as many times as needed – that is, before it disappears from the screen, it can be recomposed and rebuilt many times, each time possibly displaying a different value.

Recomposition is often triggered by changes within State objects, so let's look at an example to explore how seamlessly this happens with little intervention from our side. Say you have a TimerText composable that expects a certain number of elapsed seconds that it displays in a Text composable. The timer starts from 0 and updates every 1 second (or 1,000 ms), displaying the number of seconds that have elapsed:

var seconds by mutableStateOf(0)
val stopWatchTimer = timer(period = 1000) { seconds++ }
   ...
@Composable
fun TimerText(seconds: Int) {
   Text(text = "Elapsed: $seconds")
}

In the Defining and handling state with Compose section of Chapter 2, Handling UI State with Jetpack ViewModel, we will define the state in Compose in more detail, but until then, let's think of seconds as a simple state object (instantiated with mutableStateOf()) that has an initial value of 0 and that its value changes over time, triggering a recomposition each time.

Every time stopWatchTimer increases the value of the seconds state object, Compose triggers a recomposition that rebuilds the widget tree and redraws the composables with new arguments.

In our case, TimerText will be recomposed or rebuilt because it receives different arguments – the first time, it will receive 0, then 1, 2, and so on. This, in turn, triggers the Text composable to also recompose and that's why Compose redraws it on the screen to display the updated message.

Recomposition is a complex topic. As we will not be able to go into too much depth on it now, it's important to also cover more advanced concepts, as described in the documentation: https://developer.android.com/jetpack/compose/mental-model#any-order.

Now that we've covered what recomposition is and the core concepts behind Compose, it's time to have a better look at the composables that are used to build a Compose UI.

Exploring the building blocks of Compose UIs

We've only had a brief look at the Text and Button composables so far. That's why, in this section, we will not only understand how activities can render composables instead of XML and how we can preview them, but we will also have a better look at the most important and commonly used composable functions: from the ones we've seen, such as Text and Button, to new ones such as TextField, Image, Row, Column, and Box.

To summarize, this section will cover the following topics:

  • Setting content and previewing composables
  • Exploring core composables
  • Customizing composables with modifiers
  • Layouts in Compose

Let's jump in and understand how to render composable functions on the screen.

Setting content and previewing composables

We had a quick look at some composable functions, but we didn't quite touch on the aspect of making the application display Compose UIs.

Setting the composable content can easily be achieved and is encouraged to be done in your Activity class by simply replacing the traditional setContentView(R.layout.XML) call with setContent() and passing a composable function to it:

import androidx.activity.compose.setContent
class MainActivity : ComponentActivity() {
   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       setContent {
           Text("Hello world")
       }
   }
}

Because Compose no longer needs the AppCompat API for backward compatibility, we made our MainActivity inherit the base ComponentActivity class.

In the previous example, we called the setContent method in the onCreate callback of MainActivity and passed a Text composable function to it. If we run the app, we will see the "Hello world" message.

The setContent method is an extension function for ComponentActivity that composes the given composable into the given activity. It only accepts a @Composable function as a trailing lambda. The input composable function will become the root view of the activity and act as a container for your Compose hierarchy.

Note

You can add composable functions into fragments or activities that have an XML UI already defined with the help of the ComposeView class, but we will not go into too much detail as far as interoperability goes.

As XML provided us with a preview tool, a good question would be whether Compose also has one. Compose brings an even more powerful preview tool that allows us to skip running the application on the emulator or real devices every time we want to see how our UI evolves.

Previewing your composable is easy; just add the @Preview annotation to it:

@Preview(showBackground = true)
@Composable
fun FriendlyMessage() {
   Text(text = "Greetings!")
}

The IDE will automatically pick up that you want to preview this composable and show it on the right-hand side of the screen. Make sure that you rebuild your project and have the Split option enabled:

Figure 1.9 – Previewing composable functions in Android Studio

Figure 1.9 – Previewing composable functions in Android Studio

Optionally, you can specify for the preview to show a background for better visibility by passing the showBackground parameter with a value of true.

Note

Make sure that the composable function you are trying to preview has no input parameters. If it has, supply the default values for them so that the preview tools can work.

Yet this preview tool is much more powerful than this as it supports Interactive mode, which allows you to interact with the UI, and Live Edit of literals, which, if enabled, causes the preview to reload every time you change widths, heights, or others, just like a real UI would. You can see these two options in the following screenshot:

Figure 1.10 – Using the Preview feature in Compose

Figure 1.10 – Using the Preview feature in Compose

Note

To enable Interactive mode on Android Studio Arctic Fox, go to File | Settings | Experimental (Windows) or Android Studio | Preferences | Experimental (macOS).

Additionally, you can have multiple previews simultaneously if you annotate each function with the @Preview annotation. You can add names for each preview through the name parameter and even tell the preview tool which device it should display it on through the device argument:

@Preview(
    name = "Greeting preview",
    showSystemUi = true,
    device = Devices.PIXEL_2_XL
)
@Composable
fun FriendlyMessagePreview() { Text(text = "Greetings!") }
@Preview(
        showSystemUi = true,
        device = Devices.NEXUS_5)
@Composable
fun FriendlyMessagePreview2() { Text(text = "Goodbye!") }

Make sure that you also set showSystemUi to true to see the entire device.

Note

@Preview functions should have different names to avoid preview conflicts.

Now that we have learned how to set and preview Compose UI, it's time to explore new composables.

Exploring core composables

We've already had a quick look at some of the most basic composable functions: Text, Button, and Image. In this subsection, we will spend a bit more time exploring not only those composables but also new ones such as TextField.

Text

Text is the Compose version of our old and beloved TextView. Text is provided by Compose and achieves the most basic and yet important functionality in any application: the ability to display a piece of text. We've already used this composable in several examples:

Text(text = "Greetings $name!")

You might be wondering how we can customize it. Let's check out the source code or the documentation for Text to find the most basic and commonly used arguments for it:

  • text is the only required argument. It expects a String and sets the output text.
  • color specifies the color of the output text and expects a Color object.
  • fontSize of type TextUnit, fontStyle of type FontStyle, fontFamily of type FontFamily, and fontWeight of type FontWeight all allow you to customize the look and appearance of your text.
  • textAlign specifies the horizontal alignment of the text. It expects a TextAlign object.
  • maxLines expects an Int value that sets the maximum number of lines in the output text.
  • style expects a TextStyle object and allows you to define and reuse styles through themes.

Instead of going through all the arguments for Text, let's check out an example where we can customize the look of our Text composable function:

@Composable
fun MyAppText() {
   Text(
       text = stringResource(id = R.string.app_name),
       fontStyle = FontStyle.Italic,
       textAlign = TextAlign.Center,
       color = Color.Magenta,
       fontSize = 24.sp,
       fontWeight = FontWeight.ExtraBold)
}

Instead of passing some hardcoded text, we passed a string resource with the help of the built-in stringResource function and obtained the following result:

Figure 1.11 – Exploring a customized Text composable

Figure 1.11 – Exploring a customized Text composable

Now that we've learned how to display text with the Text composable, let's move on to buttons.

Button

Displaying text is essential in any application, yet having clickable buttons allows it to be interactive. We've used the Button composable (previously known in the View System as Button too) before and its main characteristic was the onClick callback function, which notified us when the user pressed the button.

While Button features plenty of customizing arguments, let's check out the most used parameters:

  • onClick is a mandatory parameter and it expects a function that will be called whenever the user presses the button.
  • colors expects a ButtonColors object that defines the content/background colors.
  • shape expects a custom/Material theme Shape object that sets the shape of the button.
  • content is a mandatory parameter that expects a composable function that displays the content inside this Button. We can add any composables here, including Text, Image, and more.

Let's try to build a Button function that makes use of these core arguments:

@Composable
fun ClickableButton() {
   Button(
       onClick = { /* callback */ },
       colors = ButtonDefaults.buttonColors(
           backgroundColor = Color.Blue,
           contentColor = Color.Red),
       shape = MaterialTheme.shapes.medium
   ) { Text("Press me") }
}

We've also passed a predefined MaterialTheme shape. Let's preview the resulting composable:

Figure 1.12 – Exploring a customized Button composable

Figure 1.12 – Exploring a customized Button composable

With that, we've seen how easy it is to create a custom button with the Button composable. Next up, let's try to play around with another composable function – TextField.

TextField

Adding buttons is the first step toward having an interactive UI, but the most important element in this area is the TextField composable, previously known in the View System as EditText. Just like EditText did, the TextField composable allows the user to enter and modify text.

While TextField has many arguments, the most important ones that it features are as follows:

  • value is a mandatory String argument as it's the displayed text. This value should change as we type inside it by holding it in a State object; more on that soon.
  • onValueChange is a mandatory function that triggers every time the user inputs new characters or deletes existing ones.
  • label expects a composable function that allows us to add a descriptive label.

Let's have a look at a simple usage of a TextField that also handles its own state:

@Composable
fun NameInput() {
   val textState = remember { mutableStateOf("") }
   TextField(
        value = textState.value,
        onValueChange = { newValue ->
            textState.value = newValue
        },
        label = { Text("Your name") })
}

It achieves this by defining a MutableState that holds the text displayed by TextField. This means that textState doesn't change across recompositions, so every time the UI updates because of other composables, textState should be retained. Moreover, we've wrapped the MutableState object in a remember block, which tells Compose that across recompositions, it should not revert the value to its initial value; that is, "".

To get or set the value of a State or MutableState object, our NameInput composable uses the value accessor. Because TextField accesses a MutableState object through the value accessor, Compose knows to retrigger a recomposition every time the textState value changes – in our case, in the onValueChange callback. By doing so, we ensure that as we input text in our TextField, the UI also updates with the new characters that have been added or removed from the keyboard.

Don't worry if these concepts about state in Compose don't make too much sense right now – we will cover how state is defined in Compose in more detail in Chapter 2, Handling UI State with Jetpack ViewModel.

Note:

Unlike EditText, TextField has no internal state. That's why we've created and handled it; otherwise, as we would type in, the UI would not update accordingly.

The resulting NameInput composable updates the UI correctly and looks like this:

Figure 1.13 – Exploring a TextField composable

Figure 1.13 – Exploring a TextField composable

Now that we've learned how to add input fields within a Compose-based app, it's time to explore one of the most common elements in any UI.

Image

Displaying graphical information in our application is essential and Compose provides us with a handy composable called Image, which is the composable version of the ImageView from the View System.

While Image features plenty of customizing arguments, let's check out the most used parameters:

  • painter expects a Painter object. This argument is mandatory as it sets the image resource. Alternatively, you can use the overloaded version of Image to directly pass an ImageBitmap object to its bitmap parameter.
  • contentDescription is a mandatory String that's used by accessibility services.
  • contentScale expects a ContentScale object that specifies the scaling of the picture.

Let's add an Image composable that displays the application icon using painterResource:

@Composable
fun BeautifulImage() {
    Image(
        painter =
           painterResource(R.drawable.ic_launcher_foreground),
        contentDescription = "My app icon",
        contentScale = ContentScale.Fit
    )
}

Finally, let's preview the BeautifulImage function and then move on to the next section:

Figure 1.14 – Exploring the Image composable

Figure 1.14 – Exploring the Image composable

We've also tried displaying images with Compose, yet you may still be wondering, how can we customize all these composable functions?

Customizing composables with modifiers

All the composables we've covered so far feature an argument that we haven't covered yet: modifier. This expects a Modifier object. In simple terms, modifiers tell a composable how to display, arrange, or behave within its parent composable. By passing a modifier, we can specify many configurations for a composable: from size, padding, or shape to background color or border.

Let's start with an example by using a Box composable and specifying a size modifier for it:

@Composable
fun ColoredBox() {
   Box(modifier = Modifier.size(120.dp))
}

We will cover the Box composable later but until then, you can think of it like a container that we will use to draw several shapes on the screen. What's important here is that we passed the Modifier.size() modifier, which sets the size of the box. It accepts a dp value that represents both the width and the height of the composable. You can also pass the width and height as parameters within the size() modifier or separately with the help of the height() and width() modifiers.

Specifying only one modifier for composables is usually not enough. That's why modifiers can be chained. Let's chain multiple modifiers by adding several other configurations to our Box:

@Composable
fun ColoredBox() {
   Box(modifier = Modifier
           .size(120.dp)
           .background(Color.Green)
           .padding(16.dp)
           .clip(RoundedCornerShape(size = 20.dp))
           .background(Color.Red))
}

As we mentioned previously, chaining modifiers is simple: start with an empty Modifier object and then chain new modifiers one after the other. We've chained several new modifiers, starting with background, then padding, clip, and finally another background. The modifiers, when combined, produce an output consisting of a green rectangle that contains a nested rounded corner rectangle that's red:

Figure 1.15 – Exploring chained modifiers

Figure 1.15 – Exploring chained modifiers

Note

The order of the modifiers in the chain matters because modifiers are applied from the outer layer to the inner layer. Each modifier modifies the composable and then prepares it for the upcoming modifier in the chain. Different modifier orders yield different results.

In the previous example, because modifiers are applied from the outermost layer to the innermost layer, the entire rectangular box is green because green is the first color modifier that's applied. Going inner, we applied a padding of 16 dp. Afterward, still going inner, the RoundedCornerShape modifier is applied. Finally, in the innermost layer, we applied another color modifier – this time, of the color red – and we got our final result.

Now that we've played around with the most common composables, it's time to start building actual layouts that make use of multiple composable functions.

Layouts in Compose

Often, building even a simple screen cannot be achieved by following the previous examples since most of them feature only one composable. For simple use cases, composable functions contain only one composable child.

To build more complex pieces of UI, layout components in Compose give you the option to add as many children composables as you need.

In this section, we will cover those composable functions that allow you to place children composables in a linear or overlayed fashion, such as the following:

  • Row for arranging children composables in a horizontal fashion
  • Column for arranging children composables vertically
  • Box for arranging children composables on top of each other

Following these definitions, let's envision the layout composables with the following diagram:

Figure 1.16 – Exploring Column, Row, and Box

Figure 1.16 – Exploring Column, Row, and Box

It's clear now that arranging children composables in different ways can easily be achieved with Column, Row, and Box, so it's time to look at them in more detail.

Row

Displaying multiple widgets on the screen is achieved by using a Row composable that arranges its children composables horizontally, just like the old LinearLayout with horizontal orientation did:

@Composable
fun HorizontalNumbersList() {
   Row(
       horizontalArrangement = Arrangement.Start,
       verticalAlignment = Alignment.CenterVertically,
       modifier = Modifier.fillMaxWidth()
   ) {
       Text("1", fontSize = 36.sp)
       Text("2", fontSize = 36.sp)
       Text("3", fontSize = 36.sp)
       Text("4", fontSize = 36.sp)
   }
}

We've set Row to only take the available width and added several Text functions as children composables. We specified a horizontalArrangement of Start so that they start from the left of the parent but also made sure that they are centered vertically by passing a CenterVertically alignment for the verticalAlignment argument. The result is straightforward:

Figure 1.17 – Exploring the Row composable

Figure 1.17 – Exploring the Row composable

Largely, the essential arguments for a Row composable are related to how children are arranged or aligned:

  • horizontalArrangement defines how the children are positioned horizontally both relative to each other and within the parent Row. Apart from Arragement.Start, you can also pass Center or End or SpaceBetween, SpaceEvenly, or SpaceAround.
  • verticalAlignment sets how the children are positioned vertically within the parent Row. Apart from Alignment.CenterVertically, you can pass Top or Bottom.

Now that we've arranged the children composables horizontally, let's try to arrange them vertically.

Column

Displaying a vertical list on the screen can be achieved by using a Column composable that arranges its children composables vertically, just like the old LinearLayout with vertical orientation did:

@Composable
fun NamesVerticalList() {
   Column(verticalArrangement = Arrangement.SpaceEvenly,
       horizontalAlignment = Alignment.CenterHorizontally,
       modifier = Modifier.fillMaxSize()
   ) {
       Text("John", fontSize = 36.sp)
       Text("Amanda", fontSize = 36.sp)
       Text("Mike", fontSize = 36.sp)
       Text("Alma", fontSize = 36.sp)
   }
}

We've set Column to take all the available space and added several Text functions as children composables. This time, we specified a verticalArrangement of SpaceEvenly so that children are spread out equally within the parent, but we also made sure they are centered horizontally by passing a CenterHorizontally alignment as horizontalAlignment:

Figure 1.18 – Exploring the Column composable

Figure 1.18 – Exploring the Column composable

Similar to Row, the essential arguments for a Column are also related to how children are arranged or aligned. This time, though, the arrangement is vertical instead of horizontal, and the alignment is horizontal instead of vertical:

  • verticalArrangement defines how the children are vertically positioned within the parent Column. The values are the same as the row's horizontalArrangement.
  • horizontalAlignment defines how the children are aligned within the parent Column. Apart from Alignment.CenterHorizontally, you can pass Start or End.

    Note

    If you're feeling brave, this is a great time for you to explore different alignments and arrangements and see how the UI changes. Make sure that you preview your composable functions with the @Preview annotation.

Box

So far, we've learned how to arrange children horizontally and vertically, but what if we want to place them on top of each other? The Box composable comes to our rescue as it allows us to stack children composables. Box also allows us to position the children relatively to it.

Let's try to build our own Floating Action Button (FAB) with the help of Box. We will stack two composables inside Box:

  • One green circle, which will be created with the help of Surface. The Surface composable allows you to easily define a material surface with a certain shape, background, or elevation.
  • One plus sign (+) added as text inside the Text composable, which is aligned in the center of its parent Box.

This is what the code will look like:

@Composable
fun MyFloatingActionButton() {
   Box {
       Surface(
           modifier = Modifier.size(32.dp),
           color = Color.Green,
           shape = CircleShape,
           content = { })
       Text(text = "+",
            modifier = Modifier.align(Alignment.Center))
   }
}

The Surface composable is defined with a mandatory content parameter that accepts another composable as its inner content. We don't want to add a composable inside of it. Instead, we want to stack a Text composable on top of it, so we passed an empty function to the content parameter.

The result is similar to the FAB we are all used to:

Figure 1.19 – Exploring the Box composable

Figure 1.19 – Exploring the Box composable

To take advantage of Box, you must keep the following in mind:

  • The order in which composables are added within Box defines the order in which they are painted and stacked on top of each other. If you switch the order of Surface and Text, the + icon will be painted beneath the green circle making it invisible.
  • You can align the children composables relative to the Box parent by passing different values for each of the child's alignment modifiers. That's why, apart from Alignment.Center, you can also position children composables with CenterStart, CenterEnd, TopStart, TopCenter, TopEnd, BottomStart, BottomEnd, or BottomCenter.

Now that we covered the basics, it's time to roll up our sleeves and create our first Compose project!

Building a Compose-based screen

Let's say we want to build an application that showcases some restaurants. We will build the UI with Compose and go through the steps of creating a new Compose project. We will then build a list item for such a restaurant and finally display a dummy list of such items.

To summarize, in this section, we will build our first Compose-based application: a restaurant explorer app! To achieve that, we must display some restaurants, which we will do by covering the following topics:

  • Creating your first Compose project
  • Building a restaurant element layout
  • Displaying a list of restaurants with Compose

Now that we have a clear path, let's get started.

Creating your first Compose project

To build a restaurant app, we have to create a new Compose-based project:

  1. Open Android Studio and select the New Project option:
Figure 1.20 – Starting a new project with Android Studio

Figure 1.20 – Starting a new project with Android Studio

If you already have Android Studio open, go to File, then New, and finally New Project.

Note

Make sure that you have Android Studio version Arctic Fox 2020.3.1 or newer. If you're using a newer version though, some files might have differences in the generated code.

  1. In the Phone and tablet template section, select Empty Compose Activity and then choose Next:

Figure 1.21 – Starting a new project with Android Studio

  1. Next, enter some details about your application. In the Name field, enter Restaurants app. Leave Kotlin as-is for Language and set Minimum SDK to API 21. Then, click Finish.

    Important note

    The upcoming step is an essential configuration step. It makes sure that the project Android Studio has configured for you the same versions of dependencies (from Compose, to Kotlin and other dependencies) that we use throughout the book. By doing so, you will be able to follow the code snippets and inspect the code source without any API differences.

  2. Inside the newly generated project, before inspecting the code, make sure that the generated project uses the versions of dependencies that are used throughout the book.

To do so, first go to the project-level build.gradle file and inside the dependencies block, make sure that the Kotlin version is set to 1.6.10:

buildscript {
    […]
    dependencies {
        classpath "com.android.tools.build:gradle:7.0.2"
        classpath "org.jetbrains.kotlin:kotlin-gradle- 
            plugin:1.6.10"
      […]
    }
}

Alternatively, if you're using a newer version of Android Studio, you might find the Kotlin version used in this project inside the plugins block, like so:

plugins {
    […]
    id 'org.jetbrains.kotlin.android' version '1.6.10' 
        apply false
}

If you haven't already, you might need to install the 1.6.10 plugin version of Kotlin in Android Studio. To do that, click on the Tools option of Android Studio on the Kotlin and on the Configure Kotlin Plugin Updates options. In the newly opened window, you can update your Kotlin version to 1.6.10.

Still in the project-level build.gradle file, because Compose is tied to the Kotlin version used in our project, make sure that the Compose version is set to 1.1.1 inside the ext { } block:

buildscript {
    ext {
        compose_version = '1.1.1'
    }
    repositories {…}
    dependencies {…}
}

Then, move into the app-level build.gradle file. First check that the composeOptions { } block looks like this:

plugins { ... }
android {
    [...]
    buildFeatures { compose true }
    composeOptions {
        kotlinCompilerExtensionVersion compose_version
    }
    packagingOptions { ... }
}

In some versions of Android Studio, the composeOptions { } block would add an outdated kotlinCompilerVersion '1.x.xx' line that should be removed.

Finally, make sure that the dependencies block of the app-level build.gradle file includes the following versions for its dependencies:

dependencies {
    implementation 'androidx.core:core-ktx:1.7.0'
    implementation 'androidx.appcompat:appcompat:1.4.1'
    implementation 'com.google.android.material:
        material:1.5.0'
    implementation "androidx.compose.ui:ui:
        $compose_version"
    implementation "androidx.compose.material:
        material:$compose_version"
    implementation "androidx.compose.ui:ui-tooling-
        preview:$compose_version"
    implementation 'androidx.lifecycle:lifecycle-
        runtime-ktx:2.4.1'
    implementation 'androidx.activity:activity-
        compose:1.4.0'
    testImplementation 'junit:junit:4.+'
    androidTestImplementation
        'androidx.test.ext:junit:1.1.3'
    androidTestImplementation 
        'androidx.test.espresso:espresso-core:3.4.0'
    androidTestImplementation "androidx.compose.ui:ui-
        test-junit4:$compose_version"
    debugImplementation "androidx.compose.ui:ui-
        tooling:$compose_version"
}

If you had to make any changes, synchronize your project with its Gradle files by clicking on the Sync your project with Gradle files button in Android Studio or by pressing on the File menu option and then by selecting Sync Project with Gradle Files.

Now we're set. Let's return to the source code generated by Android Studio.

And here we are – our first Compose project has been set up! Let's check out the source code by navigating to the MainActivity.kt file. We can conclude that it consists of three main parts:

  • The MainActivity class
  • The Greeting composable function
  • The DefaultPreview composable function

The MainActivity class is where content is passed to the setContent method in the onCreate callback. As we know by now, we need to call setContent to set up a Compose UI and pass composable functions as our UI:

setContent {
   RestaurantsAppTheme {
       Surface(color = MaterialTheme.colors.background) {
           Greeting("Android")
       }
   }
}

The IDE template has already implemented a Greeting composable that is wrapped into a Surface that uses the theme's background color. But what is that RestaurantsAppTheme function that was passed as the parent composable to the setContent method?

If you press Ctrl + B or Command + B on the function name, you will be taken to the Theme.kt file, which is where our theme is generated. RestaurantsAppTheme is a composable function that was auto-generated by the IDE as it holds the app's name:

@Composable
fun RestaurantsAppTheme(
   darkTheme: Boolean = isSystemInDarkTheme(),
   content: @Composable() -> Unit
) {
   ...
   MaterialTheme(
       colors = colors,
       typography = Typography,
       shapes = Shapes,
       content = content)
}

The app's theme is a wrapper over MaterialTheme and if we pass it to the setContent call, it allows us to reuse custom styles and color schemes defined within the app's theme. For it to take effect and reuse custom styles, we must pass our composables functions to the content parameter of our theme composable – in our case, in MainActivity, the Greeting composable wrapped in the Surface composable is passed to the RestaurantsAppTheme composable.

Let's go back inside the MainActivity.kt file to have a look at the other parts generated by Android studio. We can see that the Greeting composable displays text through Text, similar to our composable functions from the previous examples.

To preview the Greeting composable, the IDE also generated a preview composable for us called DefaultPreview, which allows us to preview the content that MainActivity displays; that is, Greeting. It also makes use of the theme composable to get the consistently themed UI.

Now that we've achieved a big milestone in that we've created a Compose-based application, it's time to start working on our Restaurants App!

Building a restaurant element layout

It's time to get our hands dirty and start building the layout for a restaurant within the app:

  1. Create a new file by left-clicking the application package and selecting New and then Kotlin Class/File. Enter RestaurantsScreen for the name and select the type as File.
  2. Inside this file, let's create a RestaurantsScreen composable function for our first Compose screen:
    @Composable
    fun RestaurantsScreen() {
       RestaurantItem()
    }
  3. Next, inside the RestaurantsScreen.kt file, let's define the RestaurantItem composable, which features a Card composable with elevation and padding:
    @Composable
    fun RestaurantItem() {
        Card(elevation = 4.dp,
             modifier = Modifier.padding(8.dp)
        ) {
            Row(verticalAlignment =
                    Alignment.CenterVertically,
                modifier = Modifier.padding(8.dp)) {
                RestaurantIcon(
                    Icons.Filled.Place,
                    Modifier.weight(0.15f))
                RestaurantDetails(Modifier.weight(0.85f))
            }
        }
    }

Make sure that every import you include is part of the androidx.compose.* package. If you're unsure what imports to include, check out the source code for the RestaurantsScreen.kt file at the following URL:

https://github.com/PacktPublishing/Kickstart-Modern-Android-Development-with-Jetpack-and-Kotlin/blob/main/Chapter_01/chapter_1_restaurants_app/app/src/main/java/com/codingtroops/restaurantsapp/RestaurantsScreen.kt

Getting back to the previous code snippet, we could say that the Card composable is similar to Cardview from the old View System as it allows us to beautify the UI piece that represents a restaurant with border or elevation.

In our case, Card contains a Row composable whose children composables are centered vertically and are surrounded by some padding. We used Row since we will show some details about the restaurant in a horizontal fashion: an icon and some text details.

We passed the RestaurantIcon and RestaurantDetails composables as children of the Row composable but these functions are not defined so we have compilation errors. For now, don't worry about the weight modifiers. Let's define the RestaurantIcon composable first!

  1. Still inside the RestaurantsScreen.kt file, create another composable function entitled RestaurantIcon with the following code:
    @Composable
    private fun RestaurantIcon(icon: ImageVector, modifier: Modifier) {
       Image(imageVector = icon,
             contentDescription = "Restaurant icon",
             modifier = modifier.padding(8.dp))
    }

The RestaurantIcon composable sets an ImageVector icon to an Image composable – in our case, a predefined Material Theme icon called Icons.Filled.Place. It also sets a contentDescription value and adds padding on top of the modifier it receives.

However, the most interesting part is the fact that RestaurantIcon receives a Modifier as an argument from its parent Row. The argument it receives is Modifier.weight(0.15f), which means that our Row assigns weights to each of its horizontally positioned children. The value – in this case, 0.15f – means that this child RestaurantIcon will take 15% of the horizontal space from its parent Row.

  1. Now, still inside the RestaurantsScreen.kt file, create a RestaurantDetails function that displays the restaurant's details:
    @Composable
    private fun RestaurantDetails(modifier: Modifier) {
       Column(modifier = modifier) {
           Text(text = "Alfredo's dishes",
                style = MaterialTheme.typography.h6)
           CompositionLocalProvider(
               LocalContentAlpha provides 
                   ContentAlpha.medium) {
               Text(text = "At Alfredo's … seafood dishes.",
                    style = MaterialTheme.typography.body2)
           }
       }
    }

Similarly, RestaurantDetails receives a Modifier.weight(0.85f) modifier as an argument from Row, which will make it occupy the remaining 85% of the horizontal space.

The RestaurantDetails composable is a simple Column that arranges two Text composables vertically, with one being the title of the restaurant, and the other being its description.

But what's up with CompositionLocalProvider? To display the description that's faded out in contrast to the title, we applied a LocalContentAlpha of ContentAlpha.medium. This way, the child Text with the restaurant description will be faded or grayed out.

CompositionLocalProvider allows us to pass data down to the composable hierarchy. In this case, we want the child Text to be grayed out, so we passed a LocalContentAlpha object with a ContentAlpha.medium value using the infix provides method.

  1. For a moment, go to MainActivity.kt and remove the DefaultPreview composable function as we will define our own a @Preview composable up next.
  2. Go back inside the RestaurantsScreen.kt file, define a @Preview composable:
    @Preview(showBackground = true)
    @Composable
    fun DefaultPreview() {
       RestaurantsAppTheme {
           RestaurantsScreen()
       }
    }

If you have chosen a different name for your app, you might need to update the previous snippet with the theme composable defined in the Theme.kt file.

  1. Rebuild the project and let's inspect the RestaurantsScreen() composable by previewing the newly created DefaultPreview composable, which should display a restaurant item:

Figure 1.22 – Previewing a restaurant item

  1. Finally, go back to MainActivity.kt and remove the Greeting composable. Also, remove the Surface and Greeting function calls in the setContent method and replace them with RestaurantScreen:
    setContent {
       RestaurantsAppTheme {
           RestaurantsScreen()
       }
    }

By passing RestaurantScreen to our MainActivity's setContent method, we ensure that the application will render the desired UI when built and run.

  1. Optionally, you can now Run the app to see the restaurant directly on your device or emulator.

Now that we have built a layout for a restaurant, it's time to learn how to display more of them!

Displaying a list of restaurants with Compose

So far, we've displayed a restaurant item, so it's time to display an entire list of them:

  1. First, create a new class in the root package, next to MainActivity.kt, called Restaurant.kt. Here, we will add a data class called Restaurant and add the fields that we expect a restaurant to have:
    data class Restaurant(val id: Int,
                          val title: String,
                          val description: String)
  2. In the same Restaurant.kt file, create a dummy list of Restaurant items, preferably at least 10 to fill up the entire screen:
    data class Restaurant(val id: Int,
                          val title: String,
                          val description: String)
    val dummyRestaurants = listOf(
        Restaurant(0, "Alfredo foods", "At Alfredo's …"),
        [...],
        Restaurant(13, "Mike and Ben's food pub", "")
    )

You can find the pre-populated list in this book's GitHub repository, inside the Restaurant.kt file:

https://github.com/PacktPublishing/Kickstart-Modern-Android-Development-with-Jetpack-and-Kotlin/blob/main/Chapter_01/chapter_1_restaurants_app/app/src/main/java/com/codingtroops/restaurantsapp/Restaurant.kt.

  1. Go back inside the RestaurantsScreen.kt file and update your RestaurantItem so that it receives a Restaurant object as an argument, while also passing the restaurant's title and description to the RestaurantDetails composable as parameters:
    @Composable
    fun RestaurantItem(item: Restaurant) {
        Card(...) {
            Row(...) {
                RestaurantIcon(...)
                RestaurantDetails(
                    item.title,
                    item.description,
                    Modifier.weight(0.85f)
                )
            }
        }
    }
  2. We have passed the restaurant's title and description to the RestaurantDetails composable as parameters. Propagate these changes in the RestaurantDetails composable and pass the title into the first Text composable and the description into the second Text composable:
    @Composable
    fun RestaurantDetails(title: String, description: String, modifier: Modifier){
       Column(modifier = modifier) {
           Text(text = title, ...)
           CompositionLocalProvider( … ) {
               Text(text = description, ...)
           }
       }
    }
  3. Go back to the RestaurantsScreen composable and update it to display a vertical list of Restaurant objects. We already know that we can use a Column to achieve this. Then, iterate over each restaurant in dummyRestaurants and bind it to a RestaurantItem:
    @Composable
    fun RestaurantsScreen() {
       Column {
           dummyRestaurants.forEach { restaurant ->
               RestaurantItem(restaurant)
           }
       }
    }

This will create a beautiful vertical list that we can preview through our DefaultPreview composable.

  1. Rebuild the project to see the updated preview generated by the DefaultPreview composable:
Figure 1.23 – Previewing RestaurantsScreen with the Column composable

Figure 1.23 – Previewing RestaurantsScreen with the Column composable

Alternatively, you can Run the app to see the restaurants directly on your device or emulator.

We've finally created our first list with Compose! It looks very nice and beautiful, yet it has one huge issue – it doesn't scroll! We'll address this together in the next section.

Exploring lists with Compose

In the previous section, we built a Compose-based screen that features a list of restaurants. However, if you run the application or preview the screen in interactive mode, you will notice that the list doesn't scroll. This is a huge inconvenience that we will address in this section by adding scroll capabilities to our Column composable.

Next, we will specify why Column is suited for static content, whereas if the list is large and its size is dynamic or dictated by the server's response, we should use lazy composables. We will explore a variety of lazy composables and understand why they are better suited for large lists.

To summarize, this section will cover the following topics:

  • Adding scrolling to the Column composable
  • Introducing lazy composables
  • Using LazyColumn to display restaurants

Let's start by adding scrolling capabilities to our RestaurantsScreen composable.

Adding scrolling to the Column composable

Our list of restaurants is long, and it can't scroll. This is a bad user experience, so let's fix it.

Let's make the Column scrollable by passing a Modifier.verticalScroll modifier that receives a ScrollState:

@Composable
fun RestaurantsScreen() {
   Column(Modifier.verticalScroll(rememberScrollState())) {
       ...
   }
}

We want the scrolling position to be retained across recompositions. That's why, by passing rememberScrollState to the verticalScroll modifier, we ensure that every time the UI recomposes, the scroll state is remembered and retained. The rememberScrollState persistence mechanism is similar to the remember { } block, which we used previously to retain the TextField's state across recompositions.

Now, you can Run the app or preview it in Interactive mode and check out the scrolling effect.

However, we have one final issue with our Column that is related to how Column lays and composes its elements. Let's dive into that now and try to find a better alternative.

Introducing lazy composables

Let's take a short break from our restaurant app and try to think of a better way of handling large lists. Using Row or Column for displaying long lists of items, or maybe a list of unknown size, can prove detrimental to your UI and impact your app's performance. This happens because Row and Column render or lay all their children out, whether they are visible or not. They are good for displaying static content, yet passing a large list can cause your UI to become laggy or even unusable.

Two lazy composables called LazyColumn and LazyRow come to your rescue since they only compose or output those items that are currently visible on the screen, hence the term lazy. So, as you can see, they are somehow similar to the old RecyclerView.

As the only difference between Row and Column was the way children were laid out on the screen – horizontally or vertically – the same thing applies with LazyRow and LazyColumn. These lazy composables lay their children out horizontally or vertically and provide scrolling capabilities out of the box. As they only render the visible items, lazy composables are a much better fit for large lists.

Yet, lazy composables are different than the regular composables that we've used so far. That's mainly because instead of accepting @Composable content, they expose a domain-specific language (DSL) defined by a LazyListScope block:

@Composable
fun LazyColumn(
   ...
   content: LazyListScope.() -> Unit
) { … }

The LazyListScope DSL allows us to describe the item contents that we want to be displayed as part of the list. The most commonly used ones are item() and items(). Such example usage of LazyColumn that makes use of DSL is as follows:

LazyColumn {
   item() {
       Text(text = "Custom header item")
   }
   items(myLongList) { myItem ->
       MyComposable(myItem)
   }
   item(2) {
       Text(text = "Custom footer item")
   }
}

item() adds a single composable element to the list, while items() can receive not only a standalone list of content such as myLongList but also an Int, which will add the same item multiple times.

The code that we featured previously should render a vertical list that contains the following:

  • A header Text composable
  • A list of MyComposable composables that are the same size as myLongList
  • Two Text footer composables

Returning from the DSL world, a noteworthy argument for the lazy composables is contentPadding, which allows you to define horizontal/vertical padding surrounding your list. This argument expects a PaddingValues object – we will use it soon; don't worry!

Now, we will soon receive the restaurants from a remote server, which means we don't know the size of the list, so it's time to implement such a lazy composable in our Restaurants application as well.

Using LazyColumn to display restaurants

We are currently using Column to display our dummyRestaurants list. We know why that's not the best practice, so to optimize our UI for dynamic content, we will replace it with LazyColumn so that we can continue displaying the restaurants vertically.

Go back to the RestaurantsScreen.kt file and, inside of the RestaurantScreen composable, replace the Column composable with LazyColumn:

@Composable
fun RestaurantsScreen() {
   LazyColumn(
       contentPadding = PaddingValues(
           vertical = 8.dp,
           horizontal = 8.dp)) {
       items(dummyRestaurants) { restaurant ->
           RestaurantItem(restaurant)
       }
   }
}

We've used its DSL and specified the items properties that should populate our LazyColumn by passing the dummyRestaurants list. We obtained access to each item as a restaurant of type Restaurant and rendered it through a RestaurantItem composable.

We also added additional padding through the contentPadding argument to our LazyColumn by passing a PaddingValues object where we configured the vertical and horizontal padding.

You can now Run the app and check out the scrolling effect. In our case, the output is the same, yet if we were to test the app with a very long list of restaurants, we would have a much smoother scroll effect and a better UI experience with LazyColumn than with Column.

We've done it! We've built our first Compose-based app from scratch while exploring tons of composable functions. We've added a list that scrolls beautifully, and we can now be proud of the result!

Summary

In this chapter, we learned how to build modern UIs on Android directly in Kotlin by using the Jetpack Compose toolkit. You learned how, in Compose, everything is a composable function and how this new declarative way of defining UIs improves and makes the way we build UIs much easier and less prone to bugs.

We learned that Compose accelerates and greatly simplifies UI development with the help of concise Kotlin APIs and without the need for XML or other additional languages. We then covered the basic concepts behind Compose and the core components that allow you to build UIs.

Finally, we saw how easy it is to build UI with Compose by creating a Compose-based screen that displays a list of restaurants.

In Chapter 2, Handling UI State with Jetpack ViewModel, we will use the fundamentals we've learned in this chapter to revisit the concept of state in Compose and learn how it is represented, as well as how we can correctly manage it with the help of another Jetpack component: ViewModel.

First, we will understand what ViewModel is and why such a component is needed. Then, by continuing working on the Restaurants application that we started in this chapter, we will learn how to define and lift the UI's state in our own ViewModel class.

Further reading

Exploring a library with the magnitude of Compose is nearly impossible in a single chapter. That's why you should also explore other topics that are of great importance when building your UI with Compose:

This article also covers the internals of Compose, so if you are curious about the execution model of Compose or what the compiler plugin does behind the scenes, make sure to check it out.

  • Building UIs with Compose is simple, yet Compose is a very powerful framework that enables you to write highly reusable UIs. To take advantage of that, every Composable should receive a Modifier object that defines how it is arranged inside its caller parent. See what this means by checking out this great article, and then try to practice a bit: https://chris.banes.dev/always-provide-a-modifier/.
  • Your layout should be adaptive and flexible for devices with different screen sizes or forms. You can learn more about this and try experimenting a bit by looking at the official documentation: https://developer.android.com/jetpack/compose/layouts/adaptive.
Left arrow icon Right arrow icon
Download code icon Download Code

Key benefits

  • Explore Jetpack libraries and other modern technologies for Android development
  • Improve the architectural design of your Android apps
  • Enhance the quality of your Android projects’ code bases and applications using the latest libraries

Description

With Jetpack libraries, you can build and design high-quality, robust Android apps that have an improved architecture and work consistently across different versions and devices. This book will help you understand how Jetpack allows developers to follow best practices and architectural patterns when building Android apps while also eliminating boilerplate code. Developers working with Android and Kotlin will be able to put their knowledge to work with this condensed practical guide to building apps with the most popular Jetpack libraries, including Jetpack Compose, ViewModel, Hilt, Room, Paging, Lifecycle, and Navigation. You'll get to grips with relevant libraries and architectural patterns, including popular libraries in the Android ecosystem such as Retrofit, Coroutines, and Flow while building modern applications with real-world data. By the end of this Android app development book, you'll have learned how to leverage Jetpack libraries and your knowledge of architectural concepts for building, designing, and testing robust Android applications for various use cases.

Who is this book for?

This book is for junior and intermediate-level Android developers looking to level up their Android development skills to develop high-quality apps using Jetpack libraries and other cutting-edge technologies. Beginners with knowledge of Android development fundamentals will also find this book useful. Familiarity with Kotlin is assumed.

What you will learn

  • Integrate popular Jetpack libraries such as Compose, ViewModel, Hilt, and Navigation into real Android apps with Kotlin
  • Apply modern app architecture concepts such as MVVM, dependency injection, and clean architecture
  • Explore Android libraries such as Retrofit, Coroutines, and Flow
  • Integrate Compose with the rest of the Jetpack libraries or other popular Android libraries
  • Work with other Jetpack libraries such as Paging and Room while integrating a real REST API that supports pagination
  • Test Compose UI and the application logic through unit tests

Product Details

Country selected
Publication date, Length, Edition, Language, ISBN-13
Publication date : May 24, 2022
Length: 472 pages
Edition : 1st
Language : English
ISBN-13 : 9781801818216
Vendor :
Google
Category :
Languages :
Tools :

What do you get with eBook?

Product feature icon Instant access to your Digital eBook purchase
Product feature icon Download this book in EPUB and PDF formats
Product feature icon Access this title in our online reader with advanced features
Product feature icon DRM FREE - Read whenever, wherever and however you want
Product feature icon AI Assistant (beta) to help accelerate your learning

Product Details

Publication date : May 24, 2022
Length: 472 pages
Edition : 1st
Language : English
ISBN-13 : 9781801818216
Vendor :
Google
Category :
Languages :
Tools :

Packt Subscriptions

See our plans and pricing
Modal Close icon
€18.99 billed monthly
Feature tick icon Unlimited access to Packt's library of 7,000+ practical books and videos
Feature tick icon Constantly refreshed with 50+ new titles a month
Feature tick icon Exclusive Early access to books as they're written
Feature tick icon Solve problems while you work with advanced search and reference features
Feature tick icon Offline reading on the mobile app
Feature tick icon Simple pricing, no contract
€189.99 billed annually
Feature tick icon Unlimited access to Packt's library of 7,000+ practical books and videos
Feature tick icon Constantly refreshed with 50+ new titles a month
Feature tick icon Exclusive Early access to books as they're written
Feature tick icon Solve problems while you work with advanced search and reference features
Feature tick icon Offline reading on the mobile app
Feature tick icon Choose a DRM-free eBook or Video every month to keep
Feature tick icon PLUS own as many other DRM-free eBooks or Videos as you like for just €5 each
Feature tick icon Exclusive print discounts
€264.99 billed in 18 months
Feature tick icon Unlimited access to Packt's library of 7,000+ practical books and videos
Feature tick icon Constantly refreshed with 50+ new titles a month
Feature tick icon Exclusive Early access to books as they're written
Feature tick icon Solve problems while you work with advanced search and reference features
Feature tick icon Offline reading on the mobile app
Feature tick icon Choose a DRM-free eBook or Video every month to keep
Feature tick icon PLUS own as many other DRM-free eBooks or Videos as you like for just €5 each
Feature tick icon Exclusive print discounts

Frequently bought together


Stars icon
Total 107.97
Kickstart Modern Android Development with Jetpack and Kotlin
€35.99
Android UI Development with Jetpack Compose
€37.99
Kotlin Design Patterns and Best Practices
€33.99
Total 107.97 Stars icon

Table of Contents

16 Chapters
Part 1: Exploring the Core Jetpack Suite and Other Libraries Chevron down icon Chevron up icon
Chapter 1: Creating a Modern UI with Jetpack Compose Chevron down icon Chevron up icon
Chapter 2: Handling UI State with Jetpack ViewModel Chevron down icon Chevron up icon
Chapter 3: Displaying Data from REST APIs with Retrofit Chevron down icon Chevron up icon
Chapter 4: Handling Async Operations with Coroutines Chevron down icon Chevron up icon
Chapter 5: Adding Navigation in Compose With Jetpack Navigation Chevron down icon Chevron up icon
Part 2: A Guide to Clean Application Architecture with Jetpack Libraries Chevron down icon Chevron up icon
Chapter 6: Adding Offline Capabilities with Jetpack Room Chevron down icon Chevron up icon
Chapter 7: Introducing Presentation Patterns in Android Chevron down icon Chevron up icon
Chapter 8: Getting Started with Clean Architecture in Android Chevron down icon Chevron up icon
Chapter 9: Implementing Dependency Injection with Jetpack Hilt Chevron down icon Chevron up icon
Chapter 10: Test Your App with UI and Unit Tests Chevron down icon Chevron up icon
Part 3: Diving into Other Jetpack Libraries Chevron down icon Chevron up icon
Chapter 11: Creating Infinite Lists with Jetpack Paging and Kotlin Flow Chevron down icon Chevron up icon
Chapter 12: Exploring the Jetpack Lifecycle Components Chevron down icon Chevron up icon
Other Books You May Enjoy Chevron down icon Chevron up icon

Customer reviews

Most Recent
Rating distribution
Full star icon Full star icon Full star icon Full star icon Half star icon 4.9
(10 Ratings)
5 star 90%
4 star 10%
3 star 0%
2 star 0%
1 star 0%
Filter icon Filter
Most Recent

Filter reviews by




Alex D Aug 12, 2024
Full star icon Full star icon Full star icon Full star icon Full star icon 5
TL;DR: Recommended for Android devs looking to bring their knowledge up to date with current best practices without reading 100s of pages of documentation. If you learn best by example, this is for you. Just beware some refactor will be needed to build with the latest tooling.As an Android developer with 12 years experience, this has proved a valuable resource in bringing my knowledge of Jetpack and the latest best practices (mostly) up to date."Best practice" in the Android world has come to mean whatever Google decides it does this week. The book's focus on Clean Architecture is refreshing to see.Having built and maintained many apps using the "old ways", I've seen plenty of best practice changes and done many a refactor to bring legacy code up to date. But adoption of MVVM and declarative UI has been the single biggest change, the latter being something I was resistant to until I was confident the tooling had matured to the point that there wasn't going to be the frequent rug-pulling, breaking changes necessitating extensive refactor, that we have seen in the past.As far as Compose is concerned, that time seems to be now.The book is well-written and concise, I would say approachable even for those with limited Android development experience. Devs already familiar with MVVM, coroutines and libraries like Retrofit will find find themsleves skimming and skipping ahead in some sections.The only caution I would add is that some of the information contained within is now out of date (through no fault of the author, things have just moved on in the last 2+ years) and libraries now deprecated. I found myself reading the Compose docs and refactoring the code to Material 3 as I proceeded through the examples, as I wanted to build with a current AS/Gradle/AGP and target API level 34. If you're not prepared to do that, I'd strongly recommend sticking with the tooling recommended by the author. Once you're familiar with the APIs, you can then undertake the refactor necessary to bring things up to date.
Amazon Verified review Amazon
N/A Feb 16, 2024
Full star icon Full star icon Full star icon Full star icon Full star icon 5
Highly recommended, not only for juniors but also for intermediate level Android developers looking to update their skills. Teaching method is amazing and concepts are explained in a very easy to understand way.
Feefo Verified review Feefo
Archangel Dec 26, 2022
Full star icon Full star icon Full star icon Full star icon Full star icon 5
This book told me exactly what needed to do to parse a JSON object to get the png image file. This will help with my project for Udacity Thank you!!
Amazon Verified review Amazon
Daniel Wilson Sep 27, 2022
Full star icon Full star icon Full star icon Full star icon Full star icon 5
The Android development eco-system has brought out major changes almost every 6 months for the last 5 years.If you have not kept up or only semi-use the following - Jetpack Compose, Hilt, Flow, MVVM, ViewModels, Room, Navigation, Lifecycle control etc. then this is a perfect book for you. It's very quick to read, a great reference and enables you to easily dive in to the topics explored further when you need to actually use them.
Amazon Verified review Amazon
Samuel Stegall Sep 18, 2022
Full star icon Full star icon Full star icon Full star icon Full star icon 5
Kickstart Modern Android Development with Jetpack and Kotlin is a book that I picked up to improve my skills as an Android developer. I'm a fairly new Android developer, having picked up Android in December 2021 and starting working on professional projects for the platform in February 2022. My Android development journey started with Android Programming - The Big Nerd Ranch Guide, which did a great job at teaching most of the basic things you see when developing Android apps. Working in a professional capacity with the platform, however, I quickly learned about the existence of a lot of nuance to working with the Android platform. In particular, things like dependency injection, navigation, testing, and presentation patterns play a large part in creating a enterprise-level Android application. Luckily, Kickstart Modern Android Development with Jetpack and Kotlin addresses all of these points and more, which can help take you from an Android novice or intermediate, and help improve you into an Android professional.One of the biggest things that comes up when creating an application, be it on desktop, mobile or web, is the idea of presentation patterns. These presentation patterns include fairly common ones like Model-View-Controller (MVC), Model-View-Presenter (MVP), and Model-View-ViewModel (MVVM). This book does a good job at describing the differences between these three common presentation patterns, and teaching you quite a bit about Jetpack ViewModel, which is a library I work with regularly. Going into a deeper dive on these presentation patterns and learning more about Jetpack ViewModel has been very helpful to me, and I'm sure my Android development skills in my own projects and professionally will improve from leveraging the presentation patterns properly.This book also starts by teaching Jetpack Compose, which is the modern UI system for creating Android apps. Jetpack Compose augments the traditional way to develop UI in Android, and helps remove a lot of the guesswork and error-prone development practices that developers have run into in the past. Jetpack Compose is also fairly young so getting resources on the topic has been a bit of a challenge for me. One of the major reasons why I picked up this book to begin with was to learn more about Jetpack Compose, in an effort to improve my personal Android projects. I think that this book does a good job of covering Jetpack Compose, and I feel like I can leverage this in my personal projects in the future.Kickstart Modern Android Development with Jetpack and Kotlin also does a good job of explaining how to consume REST APIs using Retrofit. One of the very, very common tasks you have to do when developing any application is consume an API, so learning more about how to do this properly in Android has been good to learn. I have yet to use an API in my personal applications, but we use APIs quite a lot at work. I don't believe that we use Retrofit as our API client in my work projects, but I feel that knowing more libraries is better than the alternative. I may use this in my personal projects as I start developing more personal apps.One aspect that this book covers is Kotlin Coroutines, which is a somewhat tricky concept, but it's important to get right. Android has a rule that you can't issue API requests from the main application thread, because it would lock up the UI. As a result, you must leverage coroutines to make API calls, which makes coroutines important to learn. This book does a good job of explaining coroutines, but I have already learned coroutines, so I mostly just reviewed through this section.One major point of confusion for me in my work has been working with the navigation componets versus the other ways that you can spawn activities and different UI flows in Android apps. There seems to be a couple differernt ways that you can initialize a class and start up a UI flow. Jetpack Navigation is a pretty commonly-used library, particularly in complex apps with various UI flows, in order to manage the navigation of an application. Going through this section has been a good way to review and learn more about UI navigation. I work with this quite a bit in my work, so this section is one that I appreciate quite a bit.Another major thing that I work with is dependency injection in Android apps. This is a fairly complex topic, so it was very good to have a detailed write-up about dependency injection. I am also familiar with Dagger for dependency injection from my job, but I have been very confused when working with it in the past. I have not seen a lot of good write-ups about Dagger and Hilt for dependency injection in Android, so this is another section that I greatly appreciate and I see myself using moving forward.There's also a good section in the book about testing, particularly unit testing ViewModels. This comes up a lot at my work, so it was nice to go through testing in this book to see another lense on how to write these tests effectively. This book also includes a section on UI testing for the Compose UIs, which is not something I have worked with before. Learning this means that when I write Compose UIs for my apps, I will be able to test them, independent of other UI testing like you might find with Appium or other automated testing solutions.Overall, I think that this book has been a great tool to allow me to learn Android at a deeper level. I have read several different books on the subject this year in order to get comfortable with the platform more quickly, but this book is by far one of the best ones for people hoping to go from novice to advanced with the Android platform fairly quickly.
Amazon Verified review Amazon
Get free access to Packt library with over 7500+ books and video courses for 7 days!
Start Free Trial

FAQs

How do I buy and download an eBook? Chevron down icon Chevron up icon

Where there is an eBook version of a title available, you can buy it from the book details for that title. Add either the standalone eBook or the eBook and print book bundle to your shopping cart. Your eBook will show in your cart as a product on its own. After completing checkout and payment in the normal way, you will receive your receipt on the screen containing a link to a personalised PDF download file. This link will remain active for 30 days. You can download backup copies of the file by logging in to your account at any time.

If you already have Adobe reader installed, then clicking on the link will download and open the PDF file directly. If you don't, then save the PDF file on your machine and download the Reader to view it.

Please Note: Packt eBooks are non-returnable and non-refundable.

Packt eBook and Licensing When you buy an eBook from Packt Publishing, completing your purchase means you accept the terms of our licence agreement. Please read the full text of the agreement. In it we have tried to balance the need for the ebook to be usable for you the reader with our needs to protect the rights of us as Publishers and of our authors. In summary, the agreement says:

  • You may make copies of your eBook for your own use onto any machine
  • You may not pass copies of the eBook on to anyone else
How can I make a purchase on your website? Chevron down icon Chevron up icon

If you want to purchase a video course, eBook or Bundle (Print+eBook) please follow below steps:

  1. Register on our website using your email address and the password.
  2. Search for the title by name or ISBN using the search option.
  3. Select the title you want to purchase.
  4. Choose the format you wish to purchase the title in; if you order the Print Book, you get a free eBook copy of the same title. 
  5. Proceed with the checkout process (payment to be made using Credit Card, Debit Cart, or PayPal)
Where can I access support around an eBook? Chevron down icon Chevron up icon
  • If you experience a problem with using or installing Adobe Reader, the contact Adobe directly.
  • To view the errata for the book, see www.packtpub.com/support and view the pages for the title you have.
  • To view your account details or to download a new copy of the book go to www.packtpub.com/account
  • To contact us directly if a problem is not resolved, use www.packtpub.com/contact-us
What eBook formats do Packt support? Chevron down icon Chevron up icon

Our eBooks are currently available in a variety of formats such as PDF and ePubs. In the future, this may well change with trends and development in technology, but please note that our PDFs are not Adobe eBook Reader format, which has greater restrictions on security.

You will need to use Adobe Reader v9 or later in order to read Packt's PDF eBooks.

What are the benefits of eBooks? Chevron down icon Chevron up icon
  • You can get the information you need immediately
  • You can easily take them with you on a laptop
  • You can download them an unlimited number of times
  • You can print them out
  • They are copy-paste enabled
  • They are searchable
  • There is no password protection
  • They are lower price than print
  • They save resources and space
What is an eBook? Chevron down icon Chevron up icon

Packt eBooks are a complete electronic version of the print edition, available in PDF and ePub formats. Every piece of content down to the page numbering is the same. Because we save the costs of printing and shipping the book to you, we are able to offer eBooks at a lower cost than print editions.

When you have purchased an eBook, simply login to your account and click on the link in Your Download Area. We recommend you saving the file to your hard drive before opening it.

For optimal viewing of our eBooks, we recommend you download and install the free Adobe Reader version 9.