Building a Dialer/Call App with Android Jetpack: Chapter 1
As an Android developer, I was always fascinated by the tremendous progress the development ecosystem has gone through in recent years. From the Android Support libraries, to the warm adoption of Kotlin as the go-to language for native Android, and the introduction of Android Jetpack, Google constantly aims to make developing Android apps easier, faster and more scalable.
With the recent introduction of Jetpack Compose, I began to wonder what it would be like to build an Android application from scratch, using modern libraries, best practices and architectural patterns. While maintaining old Android code is one thing, starting with a clean slate is something completely different. Looking at potential apps to make, I came across an idea to make an automatic call recorder. This unfortunately turned out impossible due to Google’s restrictions on call recording in new Android versions. The next closest thing would be a call manger/dialer app. I decided to take this as a personal project, and share my development process here ✨. This is the first chapter of so-and-so in a series of articles, where I will work on a full-fledged application. My goal is to maintain a positive learning experience while creating something usable and nice-looking.
Ok, enough chatter. Let’s dive in 🔽
Chapter 1: Introduction to Jetpack Compose
For this first two-part chapter, we will focus in preliminaries, setting up our project, and designing a very basic user interface (UI) for our app. This article roughly covers the following topics:
- Jetpack Compose for Android
- Compose UI: Composable elements, Modifiers, Theming
I also assume basic knowledge of Kotlin, including extension methods, delegation and coroutines.
Part I: Environment Setup
First, let’s make sure we can run an Android app with Compose support. To do so, make sure you have an updated version of Android Studio Arctic Fox (Currently, it’s version 2020.3.1 Patch 1) or a newer distribution. This will ensure that you benefit from smart editor features and the compose preview.
Creating our Project
Let’s create a new project with Compose support. Enter the New Project window, and select “Empty Compose Activity”
Fill in the standard fields as you would (application name, package name) and set the minimum SDK to API 21 (Lollipop), and click Finish. I’m going to name my project Recordio, because it sounded cool in my head and has the word “record” in it 🙂.
Setting the Java Version
Since new projects are created with Gradle 7 as the build system, you should also make sure that you have at least Java 11 installed. To ensure that your project uses this version, you can roughly follow this checklist:
- In your application-level
build.gradle
(Located atapp/build.gradle
) make sure thatsourceCompatibility
andtargetCompatibility
are set toJavaVersion.VERSION_11
(They are located underandroid -> compileOptions
). If you have a different compatible version installed, you can use it instead (For example,JavaVersion.VERSION_12
will also work).
- Set a compatible Gradle JDK under Gradle Settings. You can access them from the Project Structure window, under SDK Location (Control+Alt+Shift+S on Windows/Linux, ⌘; on macOS)
When Android Studio finishes making the project, we get our first glance at MainActivity.kt
, the main entry point for our application.
Introducing Jetpack Compose
Jetpack Compose was recently released under a stable 1.0 version, which means it is no longer under beta, and is considered production-ready. Let’s look at how Google describes it:
Jetpack Compose is a modern toolkit for building native Android UI. Jetpack Compose simplifies and accelerates UI development on Android with less code, powerful tools, and intuitive Kotlin APIs. [Source]
First, it is important to note that Jetpack Compose it Kotlin Only. The main reason being that Compose makes heavy use of Kotlin’s coroutines (If you are not yet familiar with the concept, the official guide is a good place to start).
Let’s take a look at the main activity that Studio generated for us:
Let’s briefly analyze what’s going on here.
- Our
MainActivity
inherits from aComponentActivity
, instead of the traditionalAppCompatActivity
. This is done to enable use of the newsetContent
extension method, which accepts a@Composable
block - We are also given two top-level functions which are
@Composable
s:Greeting(name)
andDefaultPreview
.Greeting
composes a simpleText
composable, which is the Compose equivalent for the traditionalTextView
. - Our activity’s
onCreate
method calls the new ComposesetContent
extension method, which is in charge of building the Compose UI. In it, we compose two blocks: aRecordioTheme
(The name is derived from how we named our application, Recordio), which composes aGreeting
- Looking at
DefaultPreview
, it is built of the same composition of elements as thesetContent
block, and it is not used anywhere. The only difference is thatDefaultPreview
is annotated with@Preview
, which allows it to be previewed from Android Studio, much like the layout preview in XML layouts.
Note: @Preview
annotations can only be applied to functions annotated with @Composable
, which receive no arguments. This means that the previewed composable must provide default values for any arguments it accepts. More on that later.
If you set up everything successfully up to this point, you should be able to run your application on a physical device or emulator, to see the resulting screen, which should look something like the screenshot on the left.
Part II: A Basic UI for the Main Screen
Let’s begin by building a basic UI for our project. Before diving into code, it’s important to note some key points:
- Traditionally, Android uses activities and fragments to separate screens and independent flows. In Compose, applications are designed to run with a single-activity architecture. This makes it easy to introduce complex navigation flows with Compose Navigation, and all navigation logic is handled by the Compose runtime.
- This also means that the standard activity and fragment lifecycles do not apply here. Instead, Compose defines different side-effects that can act as lifecycle callbacks. More information here.
Data Model: In a later chapter, we will use the Room persistence library to save and retrieve call recordings, but for now we will define a simple data model to interact with the UI:
We will also provide a simple method to generate dummy data, for display purposes only:
You can find the full RegisteredCall.kt
file here.
The main screen
Our main screen is going to be very simple, with just a toolbar containing a title, and a scrollable list of call recordings. Clicking a specific recording will open the details screen. Pretty simple stuff!
Ok, preliminaries first, let’s add a few dependencies to our app-level build.gradle
:
coreLibraryDesugaringEnabled
and desugar_jdk_libs
are needed in order to enable Java 8+ APIs without a minimum Android API level (by desugaring). The other dependencies are for Jetpack Compose support, Kotlin extensions (core-ktx
) and support libraries.
Lastly, add the compose_version
variable to the top-level build.gradle
file:
It’s finally time to write our first Composable! Remember that in Compose, UI elements are functions annotated with @Composable
, and manage their own local state. Let’s look at how our recordings list would look like:
As before, we’ll go through each of the components here in details.
CallL
receives the list ofRegisteredCall
s and anonClick
listener, which will handle click events for the list items.- We use a
LazyColumn
composable, the equivalent ofRecyclerView
in the Android view system. TheLazyColumn
is similar to theColumn
element, with the main difference being that the first loads elements in the list only as they appear on the screen. - Since we load elements to the screen lazily,
LazyColumn
does not accept a composable block. Instead, it defines a LazyListScope DSL which allows us to describe how the elements are created. In our case, we use theitemsIndexed
method, which receives our list of items, and emits composables which are aware of their index in the list. If you are unfamiliar with DSLs in Kotlin, read more here. - Lastly, our list also needs to remember it’s scroll state. Remember that a composable can always be rebuilt, so without the list knowing what was its last scroll state, each redraw of the composable would result in the UI “jumping” to the top of the list. For that reason, we use
rememberLazyListState()
, which creates a state object that we can pass to the lazy list. Hopefully we will discuss state objects in more details at a later chapter 🙂.
Now, let’s define how a single recording item in the list looks, our CallItem
composable:
Ok, once again let’s look at everything that’s going on here in detail:
- Modifiers: Modifiers are a very convenient way to add extra behavior to Compose UI elements. What’s extra behavior? Well, it can be the size, shape, semantics, boundaries (padding) and motion of the elements. It can also be used to attach different user interactions such as pressing, focusing, and more. Read about it and view the full list of available modifiers here.
It is also worth noting that the Modifier interface is fluent. If you are unfamiliar with the concept, don’t worry, all it means is that the interface supports method chaining, because every method returns the instance on which it was invoked (this
).
For example, consider the modifier we give to the top-levelCard
element:Modifier.fillMaxWidth().clickable(onClick = onClick)
. This modifier means “Make this element fill the maximum width of the screen, and also make it clickable withonClick
as the click listener”. - Elements: Here, we use a
Card
element from the Compose Material library, which gives us a Material Design card. We also use the self-explanatoryRow
element to horizontally lay out aBox
andColumn
. TheBox
element contains an icon indicating the type of call which was recorded, and theColumn
lays out some textual information (The phone number/contact name associated with the call, and the duration of the recording). We also define some extension methods to make the code a bit more readable and verbose. You can find them here. - Theming: The
style
attribute passed to theText
element allows us to control the look & feel of the text.MaterialTheme
stores the application-wide theme, and gives us access to styling for layouts, typography and color. More information here. For now, we won’t get too much into theming, but we will dive deep into theming in the coming chapter, when we give our application a fresh look!
Previewing the User Interface: Now that we are mostly done with the main UI parts, we would like to preview how our components look so far. Luckily for us, Compose gives us this option out-of-the-box with the @Preview
annotation!
When defining a preview component, we define an @Composable
function with no parameters. As such, all of the initial parameters must be known in advance. We can preview our CallItem
element by passing in a dummy recording, using the functions we created earlier:
Building the project and looking at the design screen in Android Studio, we should get something like this:
Looking good! Let’s bind everything together by rewriting the main activity:
We’ve added a TopBar
element that only displays a title, and we’ve initialized our CallList
with 100 dummy calls. When a recording item is pressed, we show a sanity check snack bar. Our layout is encapsulated with a Surface
, which is in charge of applying background color to the app, a Scaffold
, which handles the structured look & feel of a Material Design page, and a RecordioTheme
, which is a MaterialTheme
generated by AndroidStudio for our application. We’ll dive into it in the coming chapters.
Finally, we run the app and get a first look at our creation:
Jetpack Compose for Android is a powerful and versatile toolkit for modern UI design, and we managed to create a simple, yet very nice-looking interface with just a few hundred lines of code in Kotlin.
In the next chapters, we will complete work on the user interface by introducing the details screen, and we will navigate between the two using Compose Navigation. We will implement our recording persistence and business logic using Room, ViewModel, LiveData. We will combine dependency injection using Dagger+Hilt, and of course, we will implement automated call management with services and broadcast receivers. Feel free to follow me if you want to stay updated when more chapters are released 😆.
Read chapter 2 here.