Book Image

Kickstart Modern Android Development with Jetpack and Kotlin

By : Catalin Ghita
5 (1)
Book Image

Kickstart Modern Android Development with Jetpack and Kotlin

5 (1)
By: Catalin Ghita

Overview of this book

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.
Table of Contents (17 chapters)
1
Part 1: Exploring the Core Jetpack Suite and Other Libraries
7
Part 2: A Guide to Clean Application Architecture with Jetpack Libraries
13
Part 3: Diving into Other Jetpack Libraries

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!