Building a Modern Call Recorder with Kotlin + 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. While there are ample solutions out there that millions of people use, I always felt like they were not quite there for me. So, 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 automatic call recorder app. 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 call recorder. 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

Android Studio Arctic Fox

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). 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

Android Studio Arctic Fox adds support for creating an empty Compose application.

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 at app/build.gradle) make sure that sourceCompatibility and targetCompatibility are set to JavaVersion.VERSION_11 (They are located under android -> 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)
Set the Gradle JDK to a compatible version.

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 a ComponentActivity , instead of the traditional AppCompatActivity. This is done to enable use of the new setContent extension method, which accepts a @Composable block
  • We are also given two top-level functions which are @Composables: Greeting(name) and DefaultPreview. Greeting composes a simple Text composable, which is the Compose equivalent for the traditional TextView.
  • Our activity’s onCreate method calls the new Compose setContent extension method, which is in charge of building the Compose UI. In it, we compose two blocks: a RecordioTheme (The name is derived from how we named our application, Recordio), which composes a Greeting
  • Looking at DefaultPreview, it is built of the same composition of elements as the setContent block, and it is not used anywhere. The only difference is that DefaultPreview 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 CallRecording.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.

  1. RecordingList receives the list of CallRecordings and an onClick listener, which will handle click events for the list items.
  2. We use a LazyColumn composable, the equivalent of RecyclerView in the Android view system. The LazyColumn is similar to the Column element, with the main difference being that the first loads elements in the list only as they appear on the screen.
  3. 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 the itemsIndexed 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.
  4. 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 RecordingItem composable:

Ok, once again let’s look at everything that’s going on here in detail:

  1. 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-level Card element: Modifier.fillMaxWidth().clickable(onClick = onClick) . This modifier means “Make this element fill the maximum width of the screen, and also make it clickable with onClick as the click listener”.
  2. Elements: Here, we use a Card element from the Compose Material library, which gives us a Material Design card. We also use the self-explanatory Row element to horizontally lay out a Box and Column. The Box element contains an icon indicating the type of call which was recorded, and the Column 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.
  3. Theming: The style attribute passed to the Text 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 RecordingItem 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 RecordingList with 100 dummy recordings. 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 recording with good old services and broadcast receivers. Feel free to follow me if you want to stay updated when more chapters are released 😆.

Read chapter 2 [here] (Not published yet)

I’m always excited to learn something new. My interests include Full stack development, Machine learning, Android architecture, sports and music.