Search icon CANCEL
Arrow left icon
Explore Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Conferences
Free Learning
Arrow right icon
Arrow up icon
GO TO TOP
Kickstart Modern Android Development with Jetpack and Kotlin

You're reading from   Kickstart Modern Android Development with Jetpack and Kotlin Enhance your applications by integrating Jetpack and applying modern app architectural concepts

Arrow left icon
Product type Paperback
Published in May 2022
Publisher Packt
ISBN-13 9781801811071
Length 472 pages
Edition 1st Edition
Languages
Tools
Arrow right icon
Author (1):
Arrow left icon
Catalin Ghita Catalin Ghita
Author Profile Icon Catalin Ghita
Catalin Ghita
Arrow right icon
View More author details
Toc

Table of Contents (17) Chapters Close

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

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.

You have been reading a chapter from
Kickstart Modern Android Development with Jetpack and Kotlin
Published in: May 2022
Publisher: Packt
ISBN-13: 9781801811071
Register for a free Packt account to unlock a world of extra content!
A free Packt account unlocks extra newsletters, articles, discounted offers, and much more. Start advancing your knowledge today.
Unlock this book and the full library FREE for 7 days
Get unlimited access to 7000+ expert-authored eBooks and videos courses covering every tech area you can think of
Renews at €18.99/month. Cancel anytime