Developing Native Android Applications in Kotlin — Intuitively and Exhaustively Explained
A comprehensive guide to making native Android apps

There are three popular approaches to developing an Android application; you can use some cross platform framework like React Native, or you can develop a truly native application in either Java or Kotlin. In this article, we’ll explore why developing native applications in Kotlin is a great choice.
We’ll start by breaking down the difference of developing an application natively or with a framework, then we’ll explore why native development in Android is moving towards Kotlin and away from Java.
After we get an idea of why Kotlin is the go-to choice for android development, we’ll install Android Studio and develop two Android applications in Kotlin to see how they tick. A “hello world” style app where we’ll unpack the basics using a view approach, and a highly interactive app that will allow us to go in-depth with complex UIs and user interaction using the more modern Jetpack Compose approach.
Who is this useful for? Anyone who wants to understand app development on the world’s most popular consumer operating system.
Android has been the most popular consumer operating system for nearly a decade. Source
Most of my articles are geared towards data science. However, as the industry evolves the practical application of AI in real products becomes an increasingly useful skill. I think this article is relevant to virtually anyone interested in making a substantive impact on the world through software development, regardless of your specific discipline.
How advanced is this post? This article is designed to be a first exploration into Android app development.
Prerequisites: None, though prior programming experience would be very helpful, especially if you’ve spent some time in some explicitly typed programming language (like Java), in doing web development, or preferably both.
Native vs Framework
The choice of developing an app with native tools or using a framework is a tricky first decision. It’s tricky because there’s not a correct answer, you can do virtually anything with either approach, and regardless of the approach you’ll likely find some inconveniences at some point based on your selection.
Basically, In app development there’s two schools of thought:
I can make an app once that runs everywhere
I can make an app that’s optimized for the device I’m developing for
For many applications, the first option is incredibly compelling. If you want to make an app that runs on both Android and iOS, it’d be nice if you could develop (and maintain) a single app that works on both types of devices. Frameworks like React Native and Flutter allow you to do just that.
The basic idea of frameworks is that they function as a layer of abstraction. You might develop your application in JavaScript for React Native, or perhaps in Dart if you’re using Flutter. The framework then compiles these into components which can be understood by Android and iOS.
Native vs Framework based development. When using a framework you develop code for that framework, then the framework handles running that code on different device types. In native development, you develop separate applications for separate operating systems.
As you can see, there’s a layer of abstraction between the app you define and the app that actually runs on the device. This is both the blessing and the curse of frameworks.
If you’re building a simple web application, then it makes a lot of sense to use a framework. They’re as performant as native apps most of the time, require less effort to port to multiple device types, and might use approaches and programming languages that you’re familiar with outside of app development.
The abstraction of frameworks becomes less compelling if you need to interface closely with the device, however. Modern devices are complicated, and have incredibly complex APIs that are ever growing and evolving. Android and iOS devices have completely different operating systems that have different rules and constraints that a developer might need to account for.
If you have some functionality in your application that might require you to interact with a low level API on your device, perhaps to run some background task while the app is closed, interface with another device via bluetooth, do video encoding, or make some fancy widget on the OS’ main screen, you may be hard pressed to find functionality that implements that within the framework.
Cross platform frameworks do, generally, allow you to implement native modules, but these are native modules, meaning you have to implement them in the native language then bridge them with the framework. This is fine if 90% of the code can be within the framework. However, if you have an application that has a ton of OS specific functionality, the added complexity of a framework can be a big headache.
In the end there’s no one right answer. If you have a simple application that uses very little native functionality, then a cross platform framework might be a compelling choice. If your app requires heightened performance or OS specific functionality, native development may be a better choice. If you’re really serious about app development, it’s good to know both. In this article, we’re focusing on native Android development.
Java vs Kotlin
This one, based on my research, is not so nuanced. Native Android supports two languages; Java and Kotlin. If you can, use Kotlin. Kotlin is derivative of, similar to, and interoperable with Java, while being much less verbose and generally safer.
If you’ve ever programmed in Java, you might be familiar with its incredible verbosity. Here’s a data class in Java, for instance, which defines a User as a name and age.
public class User {
private String name;
private int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public int getAge() { return age; }
public void setAge(int age) { this.age = age; }
@Override
public String toString() {
return “User{name=’” + name + “’, age=” + age + “}”;
}
}And here’s the same data class in Kotlin
data class User(val name: String, val age: Int)Kotlin takes care of a lot of boilerplate stuff, like defining the constructor, getters, etc.
Java also has some safety concerns that some people care about. I’ll be honest, I’m a solo Python developer and cowboy coder; safety at the language level has never been something I’ve been highly resonant with. If you’re working on critical infrastructure in a large codebase, though, it’s nice for a language to help you avoid common pitfalls.
This Java code, for instance, doesn’t throw an error at compile time but does throw an error at runtime.
String name = null;
int length = name.length(); // ❌ Throws NullPointerExceptionIn Kotlin, however, you’re forced to acknowledge that a variable can be null with ? , then do something if the value is null with ?:
var name: String? = null
val length = name?.length ?: 0Kotlin has a few such implementation details which make the language more robust to common pitfalls. (Also, fun fact, ?: is known as an “Elvis” operator because, when viewed sideways, it resembles the top of Elvis Presley’s head)
Generally speaking I’ve seen consistent sentiment that Kotlin is superior to Java in many respects, and the sentiment that Kotlin should be adopted rather than Java in new projects seems consistent. For this reason (plus some spicy legal battles), Android has transitioned to a Kotlin first approach.
So, if you’re developing on Android, the only reason you should choose Java is if you’re working in a company that uses an overwhelming amount of legacy Java code. If not, you should probably use Kotlin.
Installing Android Studio
Before we do anything, we’re going to need Android Studio installed. You can download Android studio here.
Downloading android studio, source.
Once you download Android Studio, you should have access to an install wizard.
I just did the standard installation.
Then, once you agree to the licenses (you may need to aggrege to more than one)
It’ll install everything.
Once it’s all said and done, and you open Android Studio, you should see something like this.
And, tadah, we can start playing around with Android app development! Before we do, though, let’s explore Kotlin by itself to get a feel for the language.
A Brief Exploration of Kotlin
This won’t be an exhaustive exploration, but if you already have some programming experience this should teach you enough to have an idea of what’s going on in Kotlin. First we’ll setup a Kotlin project in Android Studio, then we’ll make a few basic Kotlin scripts.
Kotlin 1) Setup
Before we explore building an Android application, let’s get a handle of Kotlin in general. We can make a new project so we can start playing around.
Click “New Project”
We’ll be prompted to select a template “activity”. An activity, essentially, is a single screen that allows for some type of functionality in the app. This is an app development specific concept which we’ll explore later. For now, we can select “No Activity”.
Now we can specify a Name for the Project and set the Language to Kotlin.
This will create an empty project that we can play around in.
We’ll explore the folder structure of android projects in-depth. For now, though, we just need to look at the kotlin+java folder.
This folder contains:
com.example.kotlinexploration: The source code of an actual Android applicationcom.example.kotlinexploration (androidTest): Testing code, that can run on the Android devicecom.example.kotlinexploration (test): Testing code, that can run on your computer
We can play around with Kotlin in any of these, but for now we’ll use the first one. We can right click on com.example.kotlinexploration and create a new Kotlin file.
We’ll then make a new Kotlin file called helloWorld
This will then open an empty Kotlin file that looks something like this
package com.example.kotlinexploration
We can then get our application to print out hello world via the following:
package com.example.kotlinexploration
fun main(){
println(”Hello World!”)
}We can run that code by right clicking and pressing the Run button.
An output terminal should pop up, with the following content
“/Applications/Android Studio.app/Contents/jbr/Contents/Home/bin/java” ...
“Hello World!”
Process finished with exit code 0Congrats, you just ran Kotlin code!
Kotlin 2) Variables, Arithmetic, and String Formatting
The most important thing in any programming language is to make variables. The second most important thing is to do things with those variables. The third most important thing is to see the result of what’s been done.
We can construct a variable using the var keyword, and then format that variable within a string by using $ followed by the name of the variable
fun main(){
var name = “Daniel”
println(”My name is $name!”)
}My name is Daniel!In Kotlin, variables are statically typed and inferred, meaning kotlin inferrs the type of the variable upon creation and doesn’t allow you to change it later. Here, name is a string, based on how it’s originally defined, so I can’t go and set it to an integer later.
fun main(){
var name = “Daniel”
name = 0
}Line 3: Assignment type mismatch: actual type is ‘Int’, but ‘String’ was expected.Kotlin has many of the usual suspects in terms of types. Strings, ints, floats, booleans, lists, etc. We’ll explore a few of them in this article.
As well as defining a variable with var, you can define a “value” with val. This is an immutable value that can’t be edited after it’s creation.
fun main(){
val name = “Daniel”
}If we do try to edit it, we’ll get an error.
fun main(){
val name = “Daniel”
name = “this throws an error”
}Line 3: ‘val’ cannot be reassigned.Naturally, Kotlin allows us to do all the basic arithmetic one might expect from a modern programming language.
fun main() {
val num1 = 3.1415
val num2 = 69
val res1 = num1 + num2
println(”Result 1: $res1”)
val res2 = num1 * num2
println(”Result 2: $res2”)
val res3 = num1 / num2
println(”Result 3: $res3”)
}Result 1: 72.1415
Result 2: 216.76350000000002
Result 3: 0.04552898550724638Kotlin 3) Functions and Data Classes
Functions are defined in Kotlin in the following way
fun add(a: Int, b: Int) = a+b
fun main(){
println(add(1,2))
}3add is a single line function, allowing one to construct a function as being equal to some simple one-line expression.
main, as you might know from other programing languages, is itself a function. It accepts no inputs, and is used as an entry point in Kotlin to run your code.
We can define our own, more complex multi-line functions like so
data class ArithmaticResult(val sum: Double, val product: Double)
fun calculate(a: Double, b: Double): ArithmaticResult{
val sum = a+b
val product = a*b
return ArithmaticResult(sum, product)
}
fun main() {
val (s, p) = calculate(5.0, 10.0)
println(”Sum: $s, Product: $p”)
}Sum: 15.0, Product: 50.0Here, the function calculate takes in two inputs, a and b, and outputs the result of adding and multiplying them together.
Because Kotlin is a statically typed language, we need to specify the types of outputs we will be providing. We do that by defining the “data class” ArithmaticResult. This class contains a sum which is a Double, and a product which is also a Double.
when we call val (s, p) = calculate(5.0, 10.0), we’re unpacking the attributes of the resulting ArithmaticResult in order to the values s and p. If we wanted to be a bit more explicit, we could code the same thing like this.
data class ArithmaticResult(val sum: Double, val product: Double)
fun calculate(a: Double, b: Double): ArithmaticResult{
val sum = a+b
val product = a*b
return ArithmaticResult(sum, product)
}
fun main() {
val result = calculate(5.0, 10.0)
val s = result.sum
val p = result.product
println(”Sum: $s, Product: $p”)
}Here, we’re explicitly extracting the sum and saving it to s, and the product and saving it to p.
Kotlin 4) Classes and Inheritance
data classes, which we previously discussed, are a particular flavor of “class”, a class being that famous object-oriented paradigm of marrying data with some functionality.
Here’s a simple implementation of a box:
class Box(val width: Double, val height: Double) {
fun area(): Double = width * height
fun perimeter(): Double = 2 * (width + height)
fun isSquare(): Boolean = width == height
fun scale(factor: Double): Box = Box(width * factor, height * factor)
override fun toString(): String =
“Box(width=$width, height=$height, area=${area()}, perimeter=${perimeter()})”
}As you might observe, a Box is defined as a width and a height , and has certain functions defined within it. You can calculate the area, perimeter, evaluate if it’s a square or not, get a new box that’s the same as if this box was scaled by some scaling factor, and turn the box into a string.
You might notice, the toString function has an override. This is required because every new class in Kotlin inherits from the Any class. This is an arbitrary class that defines some basic functionality that any class has. Turning the class into a String is one such piece of functionality. If we want to implement a custom toString function, we need to override the original defined in the Any class.
We can go ahead and do stuff with this class
fun main() {
val box = Box(5.0, 3.0)
println(”Attributes:”)
println(”Width: ${box.width}, Height: ${box.height}\n”)
println(”toString result:”)
println(”$box\n”)
println(”Function result:”)
println(”Is square? ${box.isSquare()}\n”)
println(”Function result:”)
val biggerBox = box.scale(2.0)
println(”$biggerBox\n”)
}Attributes:
Width: 5.0, Height: 3.0
toString result:
Box(width=5.0, height=3.0, area=15.0, perimeter=16.0)
Function result:
Is square? false
Function result:
Box(width=10.0, height=6.0, area=60.0, perimeter=32.0)Here, you can see that because we implemented a toString function, each box instance is nicely printed out. We are computing things like area and perimiter each time those values are queried, though. If we wanted to pre-compute those values, we could do it at initialization like so:
class Box(val width: Double, val height: Double) {
val area: Double
val perimeter: Double
val isSquare: Boolean
init {
area = width * height
perimeter = 2 * (width + height)
isSquare = width == height
}
fun scale(factor: Double): Box = Box(width * factor, height * factor)
override fun toString(): String =
“Box(width=$width, height=$height, area=$area, perimeter=$perimeter)”
}Here, area, perimeter, and isSquare are defined as immutable attributes. We declare that they exist at the top of the class definition, then actually define their value within the init block.
Just like how Box inherits functionality from the default Any class, we can make a new class called Square that inherits from the Box class
class Square(side: Double) : Box(side, side) {
val sideLength = side
override fun toString(): String =
“Square(side=$sideLength, area=$area, perimeter=$perimeter)”
}Here, we’re saying that Square inherits from Box with the : symbol in the class definition, and that a Square is a Box where both width and height are equal to side.
Square, because it inherits from Box, Inherits all the same initialization logic and attributes like area, perimeter, and isSquare, while adding a new attribute called sideLength and overriding the toString function with one that’s more appropriate for a square.
Classes, and logic around classes, is a massive topic with a lot of subtlety. We’ll refine our understanding of this topic as necessary as we progress.
Kotlin 5) If and When
Similarly to other languages, if statements can be complex multi-line expressions
fun main(){
val score = 85
if (score > 90) {
println(”Excellent!”)
} else if (score > 70) {
println(”Good job!”)
} else {
println(”Needs improvement.”)
}
}Good job!or simple in-line expressions used to, for instance, assign a value to a variable.
String hotString = if (temperature > 30) “Hot” else “Not Hot”Kotlin also has some pretty fancy functionality for multi-line if statements used in assignment. Notice how the value of the assignment is in the last line of each block, but other code is able to execute in the interim.
fun main(){
val weatherDescription = if (temperature > 40) {
println(”It’s extremely hot!”)
“Scorching”
} else if (temperature > 25) {
println(”Warm day detected”)
“Warm”
} else {
println(”Cool weather”)
“Cool”
}
println(”Weather: $weatherDescription”)
}Warm day detected
Weather: WarmKotlin has a very unique approach to scoping, which we’ll explore throughout the article. It can be counterintuitive if you’re not familiar with it, but very useful when developing complex apps.
When statements (which are similar to switch statements in some other languages) are a convenient way to chain numerous logical cases together.
val day = 3
when (day) {
1 -> println(”Monday”)
2 -> println(”Tuesday”)
3 -> println(”Wednesday”)
4 -> println(”Thursday”)
5 -> println(”Friday”)
6, 7 -> println(”Weekend!”)
else -> println(”Invalid day”)
}Also, like if statements, you can use when statements to do assignment.
val day = 3
val message = when (day) {
1 -> “Monday”
2 -> “Tuesday”
3 -> “Wednesday”
4 -> “Thursday”
5 -> “Friday”
6, 7 -> “Weekend!”
else -> “Invalid day”
}
println(message)Like other languages, When statements are functionally similar to If statements, but are a matter of convenience when many cases need to be covered.
Kotlin 6) Lists, Iteration, Ranges, and Lambda Expressions
Kotlin allows you to define either an immutable list, which can’t be changed after creation.
val fruits = listOf(”apple”, “banana”, “cherry”)
println(fruits[0])
println(fruits.size)
println(”banana” in fruits)apple
3
trueor a mutable list, which can be modified after creation.
val numbers = mutableListOf(1, 2, 3)
numbers.add(4)
numbers.remove(2)
println(numbers)[1, 3, 4]You might notice that we’re modifying numbers despite numbers being defined as a val . This might seem strange to some; as you might recall, we use val to set immutable values, while we use var to specify mutable variables.
If you’re familiar with lower level languages like C, you might know that the value numbers is actually a pointer to the list in memory, not the actual list itself. thus, even when we modify the values within the list, we’re not modifying the pointer to the list, which is what the actual value numbers represents. Thus, even though a list is mutable, we can assign its reference to an immutable val. This happens a lot in strongly typed languages, You might have an immutable reference to a mutable object. In other words, you can’t change which object a val references, but you might be able to change the data contained in the thing the val references.
We can iterate over lists element by element
for (fruit in fruits) {
println(”I like $fruit”)
}I like apple
I like banana
I like cherryWe can also add indexes into the iteration by employing the withIndex method.
for ((index, fruit) in fruits.withIndex()) {
println(”${index + 1}. $fruit”)
}1. apple
2. banana
3. cherryKotlin also has while loops, which one might expect.
var count = 3
while (count > 0) {
println(”Countdown: $count”)
count--
}Countdown: 3
Countdown: 2
Countdown: 1If we want to do an iteration some number of times, or over some range of values, we can use ranges:
for (i in 1..5) print(”$i “)1 2 3 4 5 .. is the “range” operator, and allows you to specify a range of numbers between two numbers inclusively.
The range operator assumes an ascending order, but we can flip the order by using the downTo operator
for (i in 5 downTo 1) print(”$i “)5 4 3 2 1 If we don’t want the range operator to be inclusive (for instance, when iterating over indexes in a list based on the lists size). we can use the until operator.
for (i in 0 until 5) print(”$i “)0 1 2 3 4 You can also dictate the step size of a range.
for (i in 1..10 step 2) print(”$i “)1 3 5 7 9 A common application of iteration is to apply some function to each element in a list. This might be done for performing actions, filtering the list, or modifying the elements in that list. Each of these can be done with lambda functions, which are simply custom functions which one defines for the purposes of a single iteration.
val fruits = listOf(”apple”, “banana”, “cherry”)
fruits.forEach { println(it.uppercase()) }APPLE
BANANA
CHERRYthe forEach function tells Kotlin we want to apply some function to each element in a list, and it (meaning “iterator”) is automatically assigned to hold the value of each element in the list throughout iteration. the function println(it.uppercase()), specified between the two curly brackets, is then applied to each element in the list.
If you weren’t happy with it, you can assign a variable name like so.
fruits.forEach { fruit -> println(fruit.uppercase()) }You can also use functions like forEachIndexed to do a similar thing, but specifying the name of both the index and iterable
fruits.forEachIndexed { index, fruit -> println(”${index + 1}. $fruit”)}If the llambda function returns a boolean function, you can use filter rather than forEach. This returns a new list, consisting of each element that evaluated as true.
val fruits = listOf(”apple”, “banana”, “cherry”)
val longNames = fruits.filter { it.length > 5 }
longNames.forEach { println(it) }banana
cherryYou can also use map to return a new list that’s the result of applying the llambda function to each element.
val fruits = listOf(”apple”, “banana”, “cherry”)
val shout = fruits.map { it.uppercase() }
shout.forEach { println(it) }APPLE
BANANA
CHERRYKotlin 7) Null Values, Safe Calling, Elvis Operators, and Smart Casting
As we discussed previously, explicitly handling null values is one of the things that makes Kotlin attractively robust.
By default, variables cannot hold null values
var name: String = “Daniel”
// name = null // ❌ Compile-time errorTo be able to hold null values, ? needs to be added explicitly.
var nickname: String? = “Dan”
nickname = null // ✅ AllowedIn most languages, if a String was defined as null, trying to get its length would result in an error at runtime. However, ?. can be used in Kotlin to safely evaluate potentially null variables. If the variable exists, then the length will be retrieved. If the variable is null, the ?.length returns null.
var nickname: String? = null
println(nickname?.length)nullIf you’re trying to assign a variable that can be null, to a variable that can’t be null, you can use the Elvis (?:) operator to assign a default value for null
val length = nickname?.length ?: 0Typically, you must use these safe evaluators for varaibles that can be null, with a major exception being smart casting.
Take this code, for example. It doesn’t work because I can’t assign a String? to a String.
var nickname: String? = “Dan”
var notNullNickname: String = nicknameInitializer type mismatch: expected ‘String’, actual ‘String?’.However, if I create an if statement which checks if nickname is not null, then I can assign nickname as a string.
var nickname: String? = “Dan”
var notNullNickname: String
if (nickname != null) {
notNullNickname = nickname
} else {
notNullNickname = “was null”
}
println(notNullNickname)DanThis happens because Kotlin can smart cast null enabled variables intelligently based on surrounding logical components. Because I’m checking if nickname != null, it’s impossible for nickname to be null when I assign notNullNickname = nickname. Thus I don’t have to do any special null handling. This is called “smart casting”.
Fundamental Concepts in Android
Now that we have a working understanding of Kotlin, we can start applying it to Android app development. We’ll be building a few simple apps throughout this article, but before that let’s take a moment to cover some fundamental Android concepts.
For those of you who know a thing or two about Android app development, we’re going to start our exploration by way of views, then move onto Jetpack Compose later in the article.
Fundamental 1) Activities
Possibly the most fundamental idea in Android is that of “Activities”. Loosely speaking, a single screen within your application. You might have several “activities” in your application, for instance:
a login activity
a content browser activity
a chatroom activity
Each Activity is a self-contained unit responsible for drawing its UI and handling user input.
Fundamental 2) Views and View Groups
A view is an atomic component within your app. Buttons, text fields, images, switches, progress bars; These are all “views”.
What characterizes a view is that it:
occupies some bounding box on the screen
knows how to draw itself
knows how to react to user input
It’s often useful to group these views together. For instance, you might imagine a chat app with a text field and a send button. For this type of functionality, “view groups” exist. A ViewGroup is a special kind of View that can contain other Views.
Fundamental 3) Fragments
A fragment, is, essentially a reusable mini activity that can live within an actual activity. In other words, they’re like a window within your screen which can be re-used throughout the application.
In this particular example, a scrollable content browser is implemented as a fragment within an application. This is useful because it allows app developers to implement this major piece of functionality in a way that can be copied throughout the application. Source.
Fundamental 4) Services
A service is, essentially, code that you can register to the device that isn’t directly tied to your application. It’s used for tasks that need to keep running even when the user isn’t actively using your app, like:
Playing music while the user opens another app
Downloading files in the background
Syncing data periodically
Tracking location continuously
Fundamental 5) Lifecycle
Activities, views, fragments, and services follow a “lifecycle”, meaning a they have a predefined set of events that can happen to them.
An example of callbacks which can be used based on the various states of both fragments and views. These represent the possible “lifecycle” of fragments and views. source.
It’s common to add listeners which execute functions at certain points within the lifecycle of a particular element.
Fundamental 6) Intents
Intents are like messengers within Android, allowing different pieces of an app, or even different apps, to communicate with one another. Generally speaking, intents are divided into two categories:
explicit intents: When you want to do something fully defined, like navigating from one activity to another activity within the same app
implicit intents: When you want to do something, like open a website in a web browser, but you’re not sure what the users default browser is.
There’s a lot more to Android development than what was just covered here, but I wanted to cover the lions share of the core ideas as simply as possible.
The best way to begin is by beginning. Let’s make a simple app! In doing so, we’ll explore some other core Android app development ideas as we go.
Our First Android App: Hello/World App
Let’s start with something simple, but not so simple that we can’t learn a thing or two. This app will consist of two screens; a “hello” screen and a “world” screen. Each screen will have the definition of its respective word, as well as a button to switch to the other word.

We’ll start by hopping into Android studio and making a new project. We’ll start off with a blank template (No Activity) for this one.
I called this project Hello or World in plain english. This defines the actual name of the app as it would appear on the app store. The package name and save location are automatically updated based on the name I specified. Otherwise I’m just using defaults.
We covered the folder structure of an Android project briefly, but now that we’re making an actual app let’s go a bit more in-depth.
App 1.1) An In-Depth exploration of the project structure
There are two folders in our project, the app folder and the Gradle Scripts folder.
The app folder contains all of the code and assets used to define the app. The Gradle Scripts “folder” isn’t actually a folder at all, but a convenience of Android Studio. Gradle is the build system Android Studio uses to actually turn your code into a functioning app, and the Gradle Scripts “folder” contains the files one might need to access to configure how the app gets built. First, let’s unpack the app folder, then we’ll go back to exploring Gradle.
The app folder contains the following:
The manifests folder currently contains a single file, called AndroidManifest.xml
A “manifest” file contains key information used to register your application with the device. We can open up AndroidManifest.xml and see the following
<?xml version=”1.0” encoding=”utf-8”?>
<manifest xmlns:android=”http://schemas.android.com/apk/res/android”
xmlns:tools=”http://schemas.android.com/tools”>
<application
android:allowBackup=”true”
android:dataExtractionRules=”@xml/data_extraction_rules”
android:fullBackupContent=”@xml/backup_rules”
android:icon=”@mipmap/ic_launcher”
android:label=”@string/app_name”
android:roundIcon=”@mipmap/ic_launcher_round”
android:supportsRtl=”true”
android:theme=”@style/Theme.HelloOrWorld” />
</manifest>The first line is probably the most complicated.
<manifest xmlns:android=”http://schemas.android.com/apk/res/android”
xmlns:tools=”http://schemas.android.com/tools”>This is boilerplate, and for the vast majority of apps will never change. This defines two namespaces android and tools. android exposes key android attributes like android:label , android:theme, and android:permission which is used later in the manifest. tools exposes fancy developer tooling that can be used in Android Studio.
Then there’s this, which is also provided by default.
<application
android:allowBackup=”true”
android:dataExtractionRules=”@xml/data_extraction_rules”
android:fullBackupContent=”@xml/backup_rules”
android:icon=”@mipmap/ic_launcher”
android:label=”@string/app_name”
android:roundIcon=”@mipmap/ic_launcher_round”
android:supportsRtl=”true”
android:theme=”@style/Theme.HelloOrWorld” />This defines the app-wide settings the application can use.
android:allowBackup - Lets the system back up your app’s data to the user’s Google account.
android:dataExtractionRules - Points to an XML file defining what data can be transferred or restored.
android:fullBackupContent - Specifies which files are included or excluded from full backups.
android:icon - Sets the main launcher icon shown on the home screen and app list.
android:label - Defines the app’s display name visible to users.
android:roundIcon - Provides a round version of the app icon for compatible launchers.
android:supportsRtl - Enables layout mirroring for right-to-left languages.
android:theme - Sets the global visual theme (colors, typography, style) for the app.There’s a bunch of other things that could be added here, it’s worth giving them a skim if you’re interested. Besides application, there are several other tags that can be added to the manifest:
<uses-permission> - Declares what permissions your app needs (e.g., internet, camera, location).
<uses-feature> - Lists hardware or software features your app depends on (e.g., camera, GPS).
<queries> - Specifies which external apps or intents your app can look up or interact with.
<permission> / <permission-group> - Defines custom permissions that other apps must request to interact with yours.
...That’s just a subset. There’s a lot. There’s too much to go through in depth, nor do I think it’s necessary. As long as you know what the manifest file is about, you can search for how “feature x” or “permission y” might be configured based on your specific needs. For now, it’s enough to understand that the AndroidManifest file is used to configure how your app interacts with the phone, which we’ll explore more later.
As previously mentioned, the kotlin+java folder is where our actual app code exists.
immediately, we have some strangely named folders. The name of these folders comes from a Java convention, where folders have a “reversed domain name structure”, for some esoteric reason I have no interest in understanding. Basically these are like a domain name but backwards.
helloorworld.example.com.com on the internet was defined from the word commercial, and is a web naming convention. However, in Java, this doesn’t have to be a valid url, it’s just for naming. The idea is that you might tie this to a url you have access to. For instance, I own danielwarfield.dev, so I might use
dev.danielwarfield.helloorworldIf I was google, and I was developing a series of android example apps, I might use something like
com.google.androidexamples.exampleapp1
com.google.androidexamples.exampleapp2
com.google.androidexamples.exampleapp3The important thing is that, across all developers on the google play store, these are uniquely defined. You can name your app whatever you want when you’re developing locally, but when you publish to the app store there can never be more than one dev.danielwarfield.helloorworld. Google does enforce this upon uploading to the app store, so if I published an app like com.facebook.totalylegitapp facebook could then submit a complaint to Google and they’d take down the app, likely marking me as fraudulent in the process.
I can go ahead and change this to something like dev.danielwarfield.heloorworld by right clicking on the root folder appand clicking Open Module Settings
I can then navigate to the Default Config tab and change the Application Id
I renamed this to dev.danielwarfield.helloworld and… The folders didn’t change. Instead, this applicationId down in build.gradle.kts changed.
That’s because there’s actually two things that define the name of our app, the Package Name and Application ID. These used to be the same thing, but are now decoupled. Previously I was talking about how this reversed URL convention is important for ensuring that an app is unique on the app store. That’s relevant to the Application ID, which can’t be changed once an app is published. If you change the Application ID , you’re publishing a completely new app.
The Package Name, on the other hand, is for organizing your code. For instance, you might have different flavors of your app, like for free, paid, and debug. You could create different Package Names, but within the same Application ID
dev.danielwarfield.helloworld.free
dev.danielwarfield.helloworld.paid
dev.danielwarfield.helloworld.debugDespite the Package Name having no real bearing, it’s probably a good idea for us to rename it so it matches with the Application ID we previously defined. To rename our package, first click these three dots up at the top right of the file browser
Then, deselect Compact Middle Packages
This will allow us to see the entire folder structure without intermediary folders being collapsed.
We can then right click on the com folder, select Refactor, then select Rename...
we can then click “All Directories”,
Then I’ll rename com to dev and press “Refactor”
I’ll then do the same thing with example, renaming it to danielwarfield. I can then re-enable the Compact Middle Packages setting, and voila; our Package Name is renamed to be inline with our Application ID.
We don’t have any code, but if we did, using refactor would automatically update our code. It doesn’t automatically update our build script (which we’ll be exploring in a bit) so hop into build.gradle.kts (module :app) and update the namespace
I’ve noticed, when refactoring the app name in Android Studio, it’s pretty common for the change not to propagate to everywhere it’s supposed to. I recommend settling on a name ASAP so you don’t have to go around chasing this in a fully developed app.
Otherwise, there’s the resources folder. We don’t have to go through each of these one by one. Basically, the resources folder is where you put everything that isn’t code.
Some common subfolders are
layout/ — UI layout XML files (screens, fragments, dialogs)
drawable/ — Images, vector icons, and shape graphics
mipmap/ — App launcher icons (for different screen densities)
values/ — Shared definitions: strings, colors, styles, dimensions
menu/ — Menu XMLs for app bars or context menus
anim/ — Animation definitions (tween or frame animations)
xml/ — Misc configuration files (backup, data rules, security, etc.)
raw/ — Arbitrary raw assets (audio, text, JSON, etc.)
font/ — Custom font files
color/ — Centralized color resourcesIn terms of Gradle Scripts, it contains the following:
You’ll rarely if ever touch most of these files. The most common exceptions are:
build.gradle.kts (Module :app) - we already modified this one. This is used to Add dependencies, tweak SDK/versions
libs.versions.toml - used to manage library versionsI know that was a lot, but hopefully when you look at the project folder structure you have a much better idea of what is there and why it exists.
App 1.2) Creating the Default Activity
If we press the play button at the top of the screen
we’ll be prompted to configure our run. We can then set Hello_or_world.app as our target Module.
If we press “ok”, android will then build our app, spool up an android emulator, and attempt to deploy our app to that emulator. However, we’ll see an error saying that there’s not a default activity.
This is because we chose a completely empty project without any activities (screens) defined. We can create a simple default activity by right-clicking on our app folder (in my case, dev.danielwarfield.helloorworld) and creating a new Kotlin Class/File
We then want to create a new class called MainActivity .
That will give us the following file
package dev.danielwarfield.helloorworld
class MainActivity {
}We can change this code to the following (we’ll explore it line-by-line in just a second)
package dev.danielwarfield.helloorworld
import android.os.Bundle
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Create a TextView directly in code
val textView = TextView(this).apply {
text = “Hello World”
textSize = 24f
textAlignment = TextView.TEXT_ALIGNMENT_CENTER
}
setContentView(textView)
}
}and then hop over to our AndroidManifest.xml file and set this as the default activity.
<?xml version=”1.0” encoding=”utf-8”?>
<manifest xmlns:android=”http://schemas.android.com/apk/res/android”
xmlns:tools=”http://schemas.android.com/tools”>
<application
android:allowBackup=”true”
android:dataExtractionRules=”@xml/data_extraction_rules”
android:fullBackupContent=”@xml/backup_rules”
android:icon=”@mipmap/ic_launcher”
android:label=”@string/app_name”
android:roundIcon=”@mipmap/ic_launcher_round”
android:supportsRtl=”true”
android:theme=”@style/Theme.HelloOrWorld”>
<!-- Adding the main activity to the application -->
<activity android:name=”.MainActivity” android:exported=”true”>
<intent-filter>
<action android:name=”android.intent.action.MAIN” />
<category android:name=”android.intent.category.LAUNCHER” />
</intent-filter>
</activity>
</application>
</manifest>This is the same as our original AndroidManifest.xml file, but with the addition of
<activity android:name=”.MainActivity” android:exported=”true”>
<intent-filter>
<action android:name=”android.intent.action.MAIN” />
<category android:name=”android.intent.category.LAUNCHER” />
</intent-filter>
</activity>The first line
<activity android:name=”.MainActivity” android:exported=”true”>declares an activity (screen) in your app. Each activity must be declared, but usually it’s a one-liner. This one’s an exception because it’s the main activity. We define it as the main activity by giving it an intent-filter, which defines when and how this activity can be started.
We have two intent filters.
<action android:name=”android.intent.action.MAIN” />
<category android:name=”android.intent.category.LAUNCHER” />the first line says “This is the main activity, start it when the app starts on its own”. The second says “allow this to be launched by clicking the icon on the home screen”. When you click an app icon on your home screen, Android registers an intent that looks like this:
action = android.intent.action.MAIN
category = android.intent.category.LAUNCHERSo, by setting our intent filters similarly, we thus make this the main activity that starts the app. You might be able to imagine other entry points, like clicking a notification, which might have other intents and thus could launch our app into other activities (aka other screens).
If we click run now, our app will launch.
Take a moment to appreciate this. I know it’s been a lot to get to this point, but you just built an Android app! Also, you know a thing or two about how it got built! That’s no mean feat!
Along with the Hello World we want this bar to render, we’re getting Hello or World , which is the name of the app. This is happening because our default style, defined in AndroidManifest.xml has an app bar built in. We can change this line in the manifest file
android:theme=”@style/Theme.HelloOrWorld”>to this
android:theme=”@style/Theme.Material3.DayNight.NoActionBar”>Rebuild, and voila. No bar.
Alright, let’s take a look at our MainActivity and see how it ticks.
package dev.danielwarfield.helloorworld
import android.os.Bundle
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Create a TextView directly in code
val textView = TextView(this).apply {
text = “Hello World”
textSize = 24f
textAlignment = TextView.TEXT_ALIGNMENT_CENTER
}
setContentView(textView)
}
}First of all, we need to declare the package that this activity belongs to.
package dev.danielwarfield.helloorworldThen, we import a few necessities.
import android.os.Bundle
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivityBundle is a lightweight key-value store built into Android. It’s critical because, at pretty much any time, Android can kill your app. For instance, when you switch apps or rotate your phone. Bundle allows you to save key values so that they can be recalled. It’s used all over the place in Android, including in the constructor for AppCompatActivity
AppCompatActivity is a modern and backwards compatible class for defining Activities, and it’s generally recommended over things like Activity. We’re using it to define the actual activity we’re creating.
Finally, TextView is a view (recall that a view is like an individual component in Android, like a button, image, slider, etc) that allows us to render some text onto the screen.
To actually create our MainActivity , we create a class that inherits from AppCompatActivity and then we override the onCreate function.
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {When Android creates an activity, it calls the onCreate function and passes a Bundle. So we need to define it this way, weather we use the Bundle or not.
We then call the default constructor for AppCompatActivity
super.onCreate(savedInstanceState)and create our text view
// Create a TextView directly in code
val textView = TextView(this).apply {
text = “Hello World”
textSize = 24f
textAlignment = TextView.TEXT_ALIGNMENT_CENTER
}Here, we’re creating our TextView as a view that’s bound to this ,this being our MainActivity. When creating any view, a “context” is required, which gives the view access to key resources so it can function within the application. The MainActivity serves as this context, though there are other ways one can provide context to a view.
Once the TextView is created we can set key attributes. The less Kotlin-y way to do this might look like
val textView = TextView(this)
textView.text = “Hello World”
textView.textSize = 24f
textView.textAlignment = TextView.TEXT_ALIGNMENT_CENTERHowever, Kotlin has this funky apply {...} syntax that allows you to apply several attributes without repeating the name of the thing. This is, essentially, the same code as the code block above. apply is an example of a “scoped function”. I don’t want to get caught in the weeds right now, feel free to read up on scoped functions if you’re curious. We might bump into more of them later.
Finally,
setContentView(textView)tells Android to display the text view as the content of the activity’s screen. This is all fine and dandy, but it’s not exactly how most developers would define a screen like this.
App 1.3) Defining Layout with XML
Typically, we don’t define the layout of views within the code of an activity. Rather, we create an xml file that defines the layout for the activity, and reference it in the code.
It’s customary to do that in a folder called layout within the res directory. First, right click on the res folder and make a new Android Resource Directory .
A dialogue box will appear. If we set the Resource type to layout
our directory name will be defined automatically.
Pressing “ok” then results in a new folder called layout, which we can define our layout files in.
I’ll then right click the layout file, and create a new Layout Resource File .
We’ll then define the new resource file as activity_main.
It’s customary for classes to be in upper camel case.
MainActivity
and for the layout file to be in reversed snake case.
activity_main
Why? I have no idea. When you press “ok”, you’ll see a lot of stuff happen.
Android studio has a bunch of tools to help you work on styling. For me, these tools automatically opened in the design view
We’ll start by switching it over to split view.
That will allow us to see the xml code and the resulting layout.
If we replace that code with this code (which has a textview, like in the simple activity we just defined)
<?xml version=”1.0” encoding=”utf-8”?>
<TextView xmlns:android=”http://schemas.android.com/apk/res/android”
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:text=”Hello World”
android:textSize=”24sp” />We’ll see a preview pop-up on the right.
We’ll talk about the various approaches to defining layouts later.
We can now replace our MainActivity code with this
package dev.danielwarfield.helloorworld
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
}Run it, and get “hello world”, but defined by an xml defined layout.
here, R is an index of resources that’s automatically generated by Android upon building the application. because we defined our layout in res/layout/activity_main.xml, we can reference it via R.layout.activity_main. It’s common in front-end development to decouple functionality, layout, and styling. This is how that’s done in native Android.
App 1.3) Creating Hello Or World
Alright, let’s make our app actually do something. We can adjust our template to have a text field and a button
<?xml version=”1.0” encoding=”utf-8”?>
<LinearLayout xmlns:android=”http://schemas.android.com/apk/res/android”
android:orientation=”vertical”
android:gravity=”center”
android:layout_width=”match_parent”
android:layout_height=”match_parent”
android:padding=”24dp”>
<TextView
android:id=”@+id/definitionText”
android:textSize=”20sp”
android:textAlignment=”center”
android:layout_width=”wrap_content”
android:layout_height=”wrap_content” />
<Button
android:id=”@+id/switchButton”
android:layout_marginTop=”20dp”
android:text=”Switch”
android:layout_width=”wrap_content”
android:layout_height=”wrap_content” />
</LinearLayout>and we can implement the functionality in our MainActivity to have our button swap between a screen that says “hello” and another screen that says “world”
package dev.danielwarfield.helloorworld
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import android.widget.Button
import android.widget.TextView
class MainActivity : AppCompatActivity() {
private var showingHello = true
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val textView = findViewById<TextView>(R.id.definitionText)
val button = findViewById<Button>(R.id.switchButton)
fun updateScreen() {
if (showingHello) {
textView.text = “Hello\n/həˈlō/\nA greeting or expression of goodwill.”
button.text = “Show World”
} else {
textView.text = “World\n/wərld/\nthe earth, together with all of its countries, peoples, and natural features.”
button.text = “Show Hello”
}
}
button.setOnClickListener {
showingHello = !showingHello
updateScreen()
}
updateScreen()
}
}If we run it, it should look something like this:
Let’s unpack what’s going on here line-by-line. We’ll start with the layout, which itself starts with a LinearLayout
<LinearLayout xmlns:android=”http://schemas.android.com/apk/res/android”
android:orientation=”vertical”
android:gravity=”center”
android:layout_width=”match_parent”
android:layout_height=”match_parent”
android:padding=”24dp”>
...a LinearLayout is a “view group”, which is to say it’s a view (component) that can hold other views (components). Here, we’re saying:
android:orientation=”vertical”tells the content of the linear layout to stack verticallyandroid:gravity=”center”makes it so that the content of the linear layout centers in the middle of the screenandroid:layout_width=”match_parent”every view needs a width. This sets the width to inherit from the parent, which is the screen.android:layout_height=”match_parent”same aslayout_widthandroid:padding=”24dp”adds padding which the content of the linear layout must exist within.
There are plenty of other view groups if you want to check them out. This “LinearLayout” then has two views within it
<LinearLayout xmlns:android=”http://schemas.android.com/apk/res/android”
android:orientation=”vertical”
android:gravity=”center”
android:layout_width=”match_parent”
android:layout_height=”match_parent”
android:padding=”24dp”>
<TextView
android:id=”@+id/definitionText”
android:textSize=”20sp”
android:textAlignment=”center”
android:layout_width=”wrap_content”
android:layout_height=”wrap_content” />
<Button
android:id=”@+id/switchButton”
android:layout_marginTop=”20dp”
android:text=”Switch”
android:layout_width=”wrap_content”
android:layout_height=”wrap_content” />
</LinearLayout>These are more-or-less self-explanatory, except perhaps a few details.
an
idis assigned to each view. In Android, components in a layout can be referenced byid, both within the code and within the samexmlfile. the syntax@+id/means we’re creating a new id. We could reference already created ids within the same xml file with the@idkeyword. We won’t be referencing views in the xml for this example, but we will be referencing these in the code for the activity.wrap_contentmeans the width and height of the view will be big enough to contain the content, and no biggeryou might notice that the margin size for the button is defiled as
20dp, and the text size of the text is defined by20sp.dpstands for “density-independent pixels”. It’s customary to define images on a screen based on pixels, but depending on the resolution of a screen, 20 pixels can represent wildly different things.dpaccounts for this by representing1dpas one pixel on a160dpiscreen, or around0.006inches. If you have a high resolution screen,20dpcorresponds with more pixels, if you have a lower resolution, it corresponds to less. This keeps app size physically consistent on different screen densities.sp, on the other hand, stands for “scale-independent pixels”. Likedp, it assumes a160dpiscreen so, by default, a singlespis equivalent to0.006inches. However, it has the addition of a scaling factor for font size, which users can set based on their preferences in the accessibility settings of Android.
The formula for number of pixels based on dp is this:
px = dp × (dpi / 160)The formula for number of pixels based on sp is this:
px = sp × (dpi / 160) × fontScaleThat sets the layout of our code. To understand how it actually does stuff, we look at the code
package dev.danielwarfield.helloorworld
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import android.widget.Button
import android.widget.TextView
class MainActivity : AppCompatActivity() {
private var showingHello = true
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val textView = findViewById<TextView>(R.id.definitionText)
val button = findViewById<Button>(R.id.switchButton)
fun updateScreen() {
if (showingHello) {
textView.text = “Hello\n/həˈlō/\nA greeting or expression of goodwill.”
button.text = “Show World”
} else {
textView.text = “World\n/wərld/\nthe earth, together with all of its countries, peoples, and natural features.”
button.text = “Show Hello”
}
}
button.setOnClickListener {
showingHello = !showingHello
updateScreen()
}
updateScreen()
}
}First, we set a variable called showingHello and tie it to the activity itself. This allows the activity to keep track of if the word “hello” or the word “world” is being rendered.
We then override the existing AppCompatActivity‘s onCreate function to define some new logic when the activity is created. This requires us to run
super.onCreate(savedInstanceState)first, so that the AppCompatActivity can do any of its required startup stuff, then we set the content of the Activity based on our xml file
setContentView(R.layout.activity_main)Previously, the elements we made were defined statically, but now we need to update the look of our app based on user interaction. To do that, we need to get references to our views out of our xml. We do that with the following code.
val textView = findViewById<TextView>(R.id.definitionText)
val button = findViewById<Button>(R.id.switchButton)Here, we’re calling the findViewById function on the id defined in our auto-populated resources within R. The id’s, as specified in activity_main.xml are global, meaning all our ids across all our screens get squashed together into a single global namespace. On more complex apps you might want to adopt a more rigorous naming convention to account for many different screens.
After that, we’re defining a function that can swap between our different screens. In actuality, they’re the same screen, it’s just when showingHello is true, we use certain text on the text view and button. Otherwise, we show other text.
fun updateScreen() {
if (showingHello) {
textView.text = “Hello\n/həˈlō/\nA greeting or expression of goodwill.”
button.text = “Show World”
} else {
textView.text = “World\n/wərld/\nthe earth, together with all of its countries, peoples, and natural features.”
button.text = “Show Hello”
}
}We can then set a listener to a function that swaps the boolean, and then updates the scene. That way, every time we click the button our screen will change.
button.setOnClickListener {
showingHello = !showingHello
updateScreen()
}This simple block of code gets more complicated the longer you look at it. We’re calling a function setOnClickListener, but we’re calling the function with { , rather than (. The reason for this is because of a quality of life feature built into Kotlin called a “Single Abstract Method (SAM) interface”.
If you’re in straight-up java, you might do something like this to replace a listener.
// how this would work in normal java
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
showingHello = !showingHello
updateScreen()
}
});So, we’re creating a new OnClickListener anonymous class, which consists of the interface
public interface OnClickListener {
void onClick(View v);
}(An interface defines what a class can do, but not how it gets done. An anonymous class is a one-off implementation of an interface without giving it a name), and then passing that to the setOnClickListener of the button. This is a lot of boilerplate, which is problematic when you’re building an app and need to define about a billion buttons.
A SAM interface can be employed when the interface has exactly one method within it. If it does have one method (in this case, onClickListener has only one method, onClick) there’s no ambiguity around the function you’re trying to implement, so you can simply define the function. Kotlin takes care of the anonymous class stuff for you.
button.setOnClickListener {
showingHello = !showingHello
updateScreen()
}Depending on your stylistic preferences, you might prefer the simple and explicit style of Java, or the more implicit style of Kotlin. I find Java to be longer, and also not exactly intuitive anyway. At least Kotlin is shorter.
Anywho, at the end of the onCreate of our activity, we call updateScreen to actually update the text when the app launches
updateScreen()and, with all that, our App works!
Styling Our App
Virtually every tutorial on Android app development concludes with a functional yet incredibly ugly application, which is a shame. Visual appeal is a significant part of front end development. We just made our cool little “Hello or World” app, let’s make it look halfway decent.

There’s five core things working together to make this version of the app nicer on the eyes:
a better layout
some styling on the text
a fun translucent button
a background with an animated gradient
animated transitions between words
Let’s go through each one.
1) Better Layout
First, let’s adjust the layout to make the button occupy the bottom portion of the screen, and the definition occupy the top portion of the screen
Often, doing layout work is an iterative process, consisting of making adjustments and seeing how they impact the end result. For the first approach, let’s try thinking of our layout as two views which take up the center of the top and bottom half of the screen.
To do that, we might change this code, from our original layout
<LinearLayout xmlns:android=”http://schemas.android.com/apk/res/android”
android:orientation=”vertical”
android:gravity=”center”
android:layout_width=”match_parent”
android:layout_height=”match_parent”
android:padding=”24dp”>
<TextView
android:id=”@+id/definitionText”
android:textSize=”20sp”
android:textAlignment=”center”
android:layout_width=”wrap_content”
android:layout_height=”wrap_content” />
<Button
android:id=”@+id/switchButton”
android:layout_marginTop=”20dp”
android:text=”Switch”
android:layout_width=”wrap_content”
android:layout_height=”wrap_content” />
</LinearLayout>to this layout code
<?xml version=”1.0” encoding=”utf-8”?>
<RelativeLayout xmlns:android=”http://schemas.android.com/apk/res/android”
android:layout_width=”match_parent”
android:layout_height=”match_parent”
android:padding=”32dp”
android:gravity=”center”>
<TextView
android:id=”@+id/definitionText”
android:layout_centerInParent=”true”
android:textSize=”20sp”
android:textAlignment=”center”
android:layout_width=”wrap_content”
android:layout_height=”wrap_content” />
<Button
android:id=”@+id/switchButton”
android:layout_centerHorizontal=”true”
android:layout_alignParentBottom=”true”
android:layout_marginTop=”20dp”
android:text=”Switch”
android:layout_width=”wrap_content”
android:layout_height=”wrap_content” />
</RelativeLayout>First, the LinearLayout was replaced with a RelativeLayout . Linear layouts are designed to simply render things in a list, like a checklist, while relative layouts allow you to place elements relative to each other and to the parent. Just using a relative layout, we can see that our elements start to position themselves based on ratios of the screen, rather than as a pure list.
When we add layout_centerInParent=”true” that centers the element both horizontally and vertically within its parent. If we set both the button and the text to be centerInParent=”true”, they would stack in the center of the screen.
However, if we set one of them to layout center, the centered element will be placed in the center of the remaining space relative to the other element.
Either approach will work. For instance, with the first approach, where the button was on top and shifted to the left, we can improve its position by centering it horizontally and aligning it to the bottom of the parent (the RelativeLayout, which has a vertical height of match_parent, so the entire screen).
android:layout_centerHorizontal=”true”
android:layout_alignParentBottom=”true”This layout is better than it was, but it has an annoying tendency to have the button jiggle up and down when the text changes.
After a bit of playing around with different approaches, I concluded that it’s a fundamental issue with having the position of the button tied to the text box, which itself changes in size. I’m sure there’s some fancy way I could adjust the layout to get rid of the jiggling issue while following this general approach, but instead I decided to change how I thought about the layout itself.
Instead of defining the elements relative to each other, let’s try laying out the elements in the center horizontally, and relative to the top and bottom of the screen vertically.
notice how the location of the views don’t depend on one another, but only in terms of their location within the view group.
The following code does that:
<?xml version=”1.0” encoding=”utf-8”?>
<RelativeLayout xmlns:android=”http://schemas.android.com/apk/res/android”
android:layout_width=”match_parent”
android:layout_height=”match_parent”
android:padding=”32dp”>
<TextView
android:id=”@+id/definitionText”
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:layout_alignParentTop=”true”
android:layout_centerHorizontal=”true”
android:layout_marginTop=”160dp”
android:textAlignment=”center”
android:text=”word”
android:textSize=”20sp” />
<Button
android:id=”@+id/switchButton”
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:layout_alignParentBottom=”true”
android:layout_centerHorizontal=”true”
android:layout_marginBottom=”160dp”
android:text=”Switch” />
</RelativeLayout>Here, both views are set to android:layout_centerHorizontal=”true”, meaning they’re centered horizontally, but the text view is aligned to the top (alignParentTop=”true”) and the button is aligned to the bottom (alignParentBottom=”true” ). We set a margin with respect to either the bottom or the top of the screen, and voila, our button doesn’t move when we click it.
Which is great, except now our layout is much less responsive.
At the top right of our screen, when exploring a layout xml file, there’s an option to enable something called the “split view”.
If we click that, we can see a dynamic preview of the layout we’re currently working on.
You might have noticed, I snuck in android:text=”word” into the TextView, that’s so it’s easier to see the element in the layout preview.
This looks great, but if we click and drag these diagonal lines at the bottom of the screen
We’ll find that some screen sizes result in a bad result, like the elements overlapping.
Depending on which devices you support, this may or may not be an issue. Generally speaking, I think it’s a good policy to make layouts robust to arbitrary screen sizes, but if you’re only developing for a Pixel 8 Pro, for instance
The “Device for Preview” button, named “custom” if you set a custom aspect ratio, lets you choose a specific device to preview with.
You can confirm that your layout works on that device.
Well… At least if it’s in portrait mode.
switching the phone to landscape mode reveals that the layout no-longer works.
Again, it’s a good idea to make your layouts generally robust. Let’s cut to something that actually works.
We’re going to use something called a ConstrainedLayout, which is a view group similar to the LinearLayout and RelativeLayout we discussed previously, except it has more robust features around setting constraints relative to the position of a view within the screen. Specifically, it has the ability to define a views position as a percentage of the size of the parent container.
When the screen size doesn’t change, these elements will be locked in place. However, when the screen size does change (for different devices, or by rotating the orientation of the device) the locations will adjust in proportion to the screen size.
Here’s the code that makes that happen.
<?xml version=”1.0” encoding=”utf-8”?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android=”http://schemas.android.com/apk/res/android”
xmlns:app=”http://schemas.android.com/apk/res-auto”
android:layout_width=”match_parent”
android:layout_height=”match_parent”
android:padding=”32dp”>
<TextView
android:id=”@+id/definitionText”
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:text=”word”
android:textSize=”20sp”
android:textAlignment=”center”
app:layout_constraintStart_toStartOf=”parent”
app:layout_constraintEnd_toEndOf=”parent”
app:layout_constraintTop_toTopOf=”parent”
app:layout_constraintBottom_toBottomOf=”parent”
app:layout_constraintVertical_bias=”0.25” />
<Button
android:id=”@+id/switchButton”
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:text=”Switch”
app:layout_constraintStart_toStartOf=”parent”
app:layout_constraintEnd_toEndOf=”parent”
app:layout_constraintTop_toTopOf=”parent”
app:layout_constraintBottom_toBottomOf=”parent”
app:layout_constraintVertical_bias=”0.85” />
</androidx.constraintlayout.widget.ConstraintLayout>first of all, we swapped out our RelativeLayout with an androidx.constraintlayout.widget.ConstraintLayout , allowing us to use the constrained layout to set our layout based on percentages. Then, each view was constrained with the following:
app:layout_constraintStart_toStartOf=”parent”Android is designed to work both with languages that read left-to-right and languages which read right-to-left. Thus, sometimes in android, “left” and “right” are abstracted into “start” and “end”, where start could be either the left or right, depending on the language, allowing the UI flow of the app to follow the flow of the native language. This line says the “start” of this element should be constrained to the “start” of the parent. So, if you speak english, left side to left side.app:layout_constraintEnd_toEndOf=”parent”Same as the previous, but with the “end” of the view being constrained to the “end” of the parent.app:layout_constraintTop_toTopOf=”parent”There’s not really languages that read bottom up, so the top of a view is simply the top of the view. This constrains the top of the view with the top of the parent.app:layout_constraintBottom_toBottomOf=”parent”Same as previous, but with the bottom of the view being constrained to the bottom of the parent.app:layout_constraintVertical_bias= <some value>This value specifies how biased the view is to the top or bottom constraint. If0, the view is biased to the top. If1, the view is biased towards the bottom.
If we look at the layout of both elements, we can see that both elements are constrained to all four sides of the view group via squiggly lines.
In Android, it’s common to think of constraints as springs, which pull each element into equilibrium. Because the horizontal springs are “pulling” with the same force, each element is centered horizontally. When we introduce a Vertical_bias, it biases the force of the spring, allowing the element to reach equilibrium either higher or lower down in the screen.
As you can see, this works well with a bunch of aspect ratios.
We could stop here, but let’s do one more adjustment to make this even better.
Constrained layouts do their constraining based on the center of a view. This locks our button in place, but it means our text shifts up and down based on the text length.

This is purely stylistic, but I would like for the text to appear fixed at the top of the text block. I think that would aid in readability, and make the application look cleaner and more professional.
To do that, I’m going to employ “Guidelines”. These are lines that you can place within a view group, which other views can be placed in respect to
So, we can set these guides at some vertical location on the screen, then align our elements to those guidelines. That way we get the percentage based spacing of the constrained layout, with more consistent text alignment regardless of updating text size.
Here’s the code.
<?xml version=”1.0” encoding=”utf-8”?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android=”http://schemas.android.com/apk/res/android”
xmlns:app=”http://schemas.android.com/apk/res-auto”
android:layout_width=”match_parent”
android:layout_height=”match_parent”
android:padding=”32dp”>
<!-- 25% from top for the TextView -->
<androidx.constraintlayout.widget.Guideline
android:id=”@+id/topGuide”
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:orientation=”horizontal”
app:layout_constraintGuide_percent=”0.25” />
<!-- 15% from bottom for the Button (i.e., 85% from top) -->
<androidx.constraintlayout.widget.Guideline
android:id=”@+id/bottomGuide”
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:orientation=”horizontal”
app:layout_constraintGuide_percent=”0.85” />
<TextView
android:id=”@+id/definitionText”
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:text=”word”
android:textSize=”20sp”
android:textAlignment=”center”
app:layout_constraintTop_toTopOf=”@id/topGuide”
app:layout_constraintStart_toStartOf=”parent”
app:layout_constraintEnd_toEndOf=”parent” />
<Button
android:id=”@+id/switchButton”
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:text=”Switch”
app:layout_constraintBottom_toBottomOf=”@id/bottomGuide”
app:layout_constraintStart_toStartOf=”parent”
app:layout_constraintEnd_toEndOf=”parent” />
</androidx.constraintlayout.widget.ConstraintLayout>Here, we set up two horizontal guide lines, specify their height within the screen with app:layout_constraintGuide_percent= <some value> , assign them an id, like android:id=”@+id/topGuide” then constrain our views to those guides via app:layout_constraintTop_toTopOf=”@id/topGuide”.
You might notice how some of these attributes start with android: and some of them start with app:. This is a bit of a rabbit whole, which we’ll explore more when we implement our own custom views. We can briefly touch on it now, though.
In essence, there’s a build tool called AAPT2 (Android Asset Packaging Tool 2). This is responsible for turning your high level resources (like layout files, images, etc.) into a binary format that’s actually usable by an android phone. To deal with different libraries having potentially similar naming conventions, Android uses “namespaces” in the xml file to contextualize attributes via some unique identifier. xmlns:android=”http://schemas.android.com/apk/res/android" , for instance. This isn’t a URL, but a URI (universal resource indicator) that references the core SDK for Android, and assigns it to the namespace android.
xmlns:app=”http://schemas.android.com/apk/res-auto" is a special “auto resolving” URI, that auto-resolves based on where the attribute is defined, in this case androidx.constraintlayout.widget.ConstraintLayout. xmlns:app="...res-auto" binds the prefix app: to a special namespace that lets AAPT2 automatically resolve attributes to the correct library resource package. Unlike android:, which points to the Android framework, res-auto tells the build tools: “Find the attribute definition in whatever library provides it,” which is why custom attributes from ConstraintLayout (like layout_constraintTop_toTopOf) appear under the app: namespace.
Android Jetpack is an official Android library which greatly improves Android development with modern design principles. Jetpack exists under the library named AndroidX. AndroidX defines custom XML attributes that are not part of the core Android framework. When we set xmlns:app="http://schemas.android.com/apk/res-auto", we’re telling AAPT2 to automatically resolve any app: attribute to whichever library defines it. In the case of ConstraintLayout, its custom attributes like layout_constraintTop_toTopOf come from the AndroidX ConstraintLayout library, so AAPT2 resolves the app: attributes to that library’s resources.
I’m not stating these things so that you understand it, but that you’re aware that, in modern Android app development, it’s common to use several different namespaces simultaneously, and you may need to account for that when building views.
Back to the task at hand, though, we used a constrained layout, defined two guidelines, and set our text and button view positions relative to those guidelines. Thus, we have a layout that serves for our purposes.
2) Text Styling
Now that we have a layout we’re happy with, let’s make the text look good. Recall that, on our improved app, we have a variety of fonts that we use for the main word, the phonetic pronunciation, the definition, and the font for the text.
Where we want to end up, in terms of styling.
There are various ways we can achieve this effect. We’ll make a few adjustments in our layout file, and also make some adjustments in our activity code. Let’s start with layout adjustments.
Our original XML defines our text style as the following
<TextView
android:id=”@+id/definitionText”
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:text=”word”
android:textSize=”20sp”
android:textAlignment=”center”
app:layout_constraintTop_toTopOf=”@id/topGuide”
app:layout_constraintStart_toStartOf=”parent”
app:layout_constraintEnd_toEndOf=”parent” />Which we’ll change to
<TextView
android:id=”@+id/definitionText”
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:text=”word”
android:textSize=”24sp”
android:lineSpacingExtra=”8dp”
android:textColor=”#FFFFFF”
android:fontFamily=”serif”
android:textAlignment=”center”
app:layout_constraintTop_toTopOf=”@id/topGuide”
app:layout_constraintStart_toStartOf=”parent”
app:layout_constraintEnd_toEndOf=”parent” />We changed textSize to 24sp (24 scale-independent pixels, which is commonly used for fonts), to make it a bit bigger. We also set lineSpacingExtra to 8dp (density-independent pixels which is commonly used in layout to accommodate different screen sizes). This adds a small amount of space between each line which remains consistent across screens with different pixel densities. We also set textColor to “#FFFFFF” , which is hexadecimal for pure white, and we set the fontFamily to serif .
That will result in pretty text, which is difficult to see because our app currently has a white background.
We can set our constrained layout to have a background of blue, allowing us to actually see the text.
<androidx.constraintlayout.widget.ConstraintLayout
...
android:background=”#ADD8E6”>Already looking way better than before. Let’s give our button a bit of font treatment as well. We’ll keep the font family the same, but make the text bigger and set the font color to pure white.
<Button
android:id=”@+id/switchButton”
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:text=”Switch”
android:textSize=”18sp”
android:textColor=”#FFFFFF”
app:layout_constraintBottom_toBottomOf=”@id/bottomGuide”
app:layout_constraintStart_toStartOf=”parent”
app:layout_constraintEnd_toEndOf=”parent” />This doesn’t look quite as spiffy as our end goal, largely because we’re still missing our cool translucent button. We’ll get around to that later. Another minor difference is that the text doesn’t have italicization on the phonetic pronunciation, and bold for the word.
Recall where we want to end up, in terms of styling. Notice how the word is bold, and the phonetic pronunciation is italicized.
We can achieve that by using HTML rendering within our activity. For those not familiar with HTML, it’s a language used throughout front end development to structure content on websites and applications.
<p data-testid=”editorParagraphText” name=”0aa4” class=”graf graf--p graf-after--figure”>
We can achieve that by using HTML rendering within our activity. For those not familiar with HTML, it’s a language used throughout front end development to structure content on websites and applications. The website you’re looking at to read this article uses HTML to build its own layout.
</p>One of the most fundamental aspects of HTML is the “tag”, which is used to either encapsulate a section of text to have some styling, or inject some special component.
For instance, in HTML the <b> tag means to “bold” some text. We denote when a section of text auto to start being bold with <b>, then close out the tag with </b>.
<b>This text is bold!</b> This text is not!We can do something similar with italicization. html uses the tag <em>, which stands for “emphasis” to control italicization.
<em>This text is italicized!</em> This text is not!Some tags don’t require an open and a close. For instance, if we want to inject a newline in our sentence, we don’t need to specify anything “within” the newline, we just want to add a newline in a certain spot. To create a newline we can use the <br> tag, which stands for line break, and we can make is “self closing” by adding a slash at the end, a la <br/>
This text is on line 1<br/>this text is on line 2Using bold, italics, and line breaks, we can turn our text into an HTML equivalent within our activity. We can then use Androids HtmlCompat (a backwards compatible HTML processor) to process the html.
package dev.danielwarfield.helloorworld
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import android.widget.Button
import android.widget.TextView
import androidx.core.text.HtmlCompat
class MainActivity : AppCompatActivity() {
private var showingHello = true
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val textView = findViewById<TextView>(R.id.definitionText)
val button = findViewById<Button>(R.id.switchButton)
fun updateScreen() {
val helloHtml = “<b>Hello</b><br/><i>/həˈlō/</i><br/>A greeting or expression of goodwill.”
val worldHtml = “<b>World</b><br/><i>/wərld/</i><br/>The earth, together with all of its countries, peoples, and natural features.”
if (showingHello) {
textView.text = HtmlCompat.fromHtml(helloHtml, HtmlCompat.FROM_HTML_MODE_LEGACY)
button.text = “Show World”
} else {
textView.text = HtmlCompat.fromHtml(worldHtml, HtmlCompat.FROM_HTML_MODE_LEGACY)
button.text = “Show Hello”
}
}
button.setOnClickListener {
showingHello = !showingHello
updateScreen()
}
updateScreen()
}
}Along with our current layout
<?xml version=”1.0” encoding=”utf-8”?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android=”http://schemas.android.com/apk/res/android”
xmlns:app=”http://schemas.android.com/apk/res-auto”
android:layout_width=”match_parent”
android:layout_height=”match_parent”
android:padding=”32dp”
android:background=”#ADD8E6”>
<!-- 25% from top for the TextView -->
<androidx.constraintlayout.widget.Guideline
android:id=”@+id/topGuide”
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:orientation=”horizontal”
app:layout_constraintGuide_percent=”0.25” />
<!-- 15% from bottom for the Button (i.e., 85% from top) -->
<androidx.constraintlayout.widget.Guideline
android:id=”@+id/bottomGuide”
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:orientation=”horizontal”
app:layout_constraintGuide_percent=”0.85” />
<TextView
android:id=”@+id/definitionText”
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:text=”word”
android:textSize=”24sp”
android:lineSpacingExtra=”8dp”
android:textColor=”#FFFFFF”
android:fontFamily=”serif”
android:textAlignment=”center”
app:layout_constraintTop_toTopOf=”@id/topGuide”
app:layout_constraintStart_toStartOf=”parent”
app:layout_constraintEnd_toEndOf=”parent” />
<Button
android:id=”@+id/switchButton”
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:text=”Switch”
android:textSize=”18sp”
android:textColor=”#FFFFFF”
app:layout_constraintBottom_toBottomOf=”@id/bottomGuide”
app:layout_constraintStart_toStartOf=”parent”
app:layout_constraintEnd_toEndOf=”parent” />
</androidx.constraintlayout.widget.ConstraintLayout>This results in the following
Which is getting awfully close to our desired output. Next, let’s make our button look a bit more fancy.
3) A Fun Translucent Button
Recall where we want to end up in terms of styling. The button is larger, and has a transluscent quality which allows the background color to show through.
Views, like buttons, do a lot of things under the hood. They handle user interaction, logic, and how they render themselves on the screen. The actual visual part is a drawable, which you can think of as the logic within a view that is only relevant to how that view visually looks. In our resources folder, we can right-click drawable , create a new Drawable Resource File and call it translucent_button
In the resulting translucent_button.xml , we’ll use this code
<ripple xmlns:android=”http://schemas.android.com/apk/res/android”
android:color=”#40FFFFFF”>
<item>
<shape android:shape=”rectangle”>
<solid android:color=”#33FFFFFF”/>
<corners android:radius=”24dp”/>
</shape>
</item>
</ripple>The core drawable we’ll be building off of is called a ripple, which is a
Drawable that shows a ripple effect in response to state changes — Android docs
Basically, when the user clicks our button, it’ll make a cool subtle ripple effect.
You can think of the ripple as an effect that wraps around an item, where that item is a shape that’s a rectangle. The rectangle has a small radius on the corners, as specified by <corners android:radius=”24dp”/>, and a color that’s specified by <solid android:color=”#33FFFFFF”/>. This color is what defines the buttons translucency. I don’t want to go into hexadecimal color encoding in-depth, there are plenty of color pickers and converters you can use, but basically the first two characters say the button will be 20% opaque (80% transparent). The F’s say the button will be white. So, the button will be white and mostly transparent.
We can enable our button view to use this drawable as a background by simply referencing it with the @ symbol within our layout, and specifying the path based on the resources directory.
<Button
...
android:background=”@drawable/translucent_button”
...All put together, this code
res/drawable/translucent_button.xml
<ripple xmlns:android=”http://schemas.android.com/apk/res/android”
android:color=”#40FFFFFF”>
<item>
<shape android:shape=”rectangle”>
<solid android:color=”#33FFFFFF”/>
<corners android:radius=”24dp”/>
</shape>
</item>
</ripple>res/layout/activity_main.xml
<?xml version=”1.0” encoding=”utf-8”?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android=”http://schemas.android.com/apk/res/android”
xmlns:app=”http://schemas.android.com/apk/res-auto”
android:layout_width=”match_parent”
android:layout_height=”match_parent”
android:padding=”32dp”
android:background=”#ADD8E6”>
<!-- 25% from top for the TextView -->
<androidx.constraintlayout.widget.Guideline
android:id=”@+id/topGuide”
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:orientation=”horizontal”
app:layout_constraintGuide_percent=”0.25” />
<!-- 15% from bottom for the Button (i.e., 85% from top) -->
<androidx.constraintlayout.widget.Guideline
android:id=”@+id/bottomGuide”
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:orientation=”horizontal”
app:layout_constraintGuide_percent=”0.85” />
<TextView
android:id=”@+id/definitionText”
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:text=”word”
android:textSize=”24sp”
android:lineSpacingExtra=”8dp”
android:textColor=”#FFFFFF”
android:fontFamily=”serif”
android:textAlignment=”center”
app:layout_constraintTop_toTopOf=”@id/topGuide”
app:layout_constraintStart_toStartOf=”parent”
app:layout_constraintEnd_toEndOf=”parent” />
<Button
android:id=”@+id/switchButton”
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:text=”Switch”
android:textSize=”18sp”
android:textColor=”#FFFFFF”
android:background=”@drawable/translucent_button”
app:layout_constraintBottom_toBottomOf=”@id/bottomGuide”
app:layout_constraintStart_toStartOf=”parent”
app:layout_constraintEnd_toEndOf=”parent” />
</androidx.constraintlayout.widget.ConstraintLayout>MainActivity.kt
package dev.danielwarfield.helloorworld
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import android.widget.Button
import android.widget.TextView
import androidx.core.text.HtmlCompat
class MainActivity : AppCompatActivity() {
private var showingHello = true
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val textView = findViewById<TextView>(R.id.definitionText)
val button = findViewById<Button>(R.id.switchButton)
fun updateScreen() {
val helloHtml = “<b>Hello</b><br/><i>/həˈlō/</i><br/>A greeting or expression of goodwill.”
val worldHtml = “<b>World</b><br/><i>/wərld/</i><br/>The earth, together with all of its countries, peoples, and natural features.”
if (showingHello) {
textView.text = HtmlCompat.fromHtml(helloHtml, HtmlCompat.FROM_HTML_MODE_LEGACY)
button.text = “Show World”
} else {
textView.text = HtmlCompat.fromHtml(worldHtml, HtmlCompat.FROM_HTML_MODE_LEGACY)
button.text = “Show Hello”
}
}
button.setOnClickListener {
showingHello = !showingHello
updateScreen()
}
updateScreen()
}
}Yields this:
when we click the button, we get a fun little effect as a result of using ripple
That’s already looking pretty great, but we have even more improvements to make.
4) Animated Gradient Background
Another improvement we’ll make is drawing a subtle gradient of color that slowly changes over time.
To do this, we’ll implement another drawable, similarly to what we did for the button, but we’ll set it as the background of the entire activity. We’ll also need to implement animation so the color can change over time.
we’ll start by creating the drawable in a new xml, res/drawable/gradient_bg.xml
<shape xmlns:android=”http://schemas.android.com/apk/res/android”
android:shape=”rectangle”>
<gradient
android:id=”@+id/animatedGradient”
android:type=”linear”
android:angle=”45”
android:startColor=”#FF5F6D”
android:endColor=”#FFC371” />
</shape>This defines a shape , that’s a rectangle , with a gradient inside. the gradient is a linear gradient (meaning there’s a steady shift from one color to the other along some line) with an angle of 45 (meaning the gradient is not a blend from side-to-side, which would be 0, or a blend from top to bottom 90, but a blend from the top right corner to the bottom left corner). It blends from the color #FF5F6D (a redish-pinkish color) to the color #FFC371 (a yellow-orange ish color).
We can get an idea of what that looks like by setting our constrained layout’s background to this drawable
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android=”http://schemas.android.com/apk/res/android”
xmlns:app=”http://schemas.android.com/apk/res-auto”
android:layout_width=”match_parent”
android:layout_height=”match_parent”
android:padding=”32dp”
android:background=”@drawable/gradient_bg”>\
...To animate the background, we’ll need to reference this drawable via the main activity code, and implement animations that schedule when these colors should change.
To do that, first, we need to assign an id to our constraindLayout for our activity_main.xml so we can reference it’s background in the code.
<androidx.constraintlayout.widget.ConstraintLayout
...
android:id=”@+id/rootLayout”
...
Then, in our main activity, we’ll access our layout by the id. We’ll also extract the background and make sure it’s correctly set to a GradientDrawable type, allowing us to edit the gradients. While we’re in here, we’ll also define a few different gradients as an array of integer arrays.
...
import androidx.constraintlayout.widget.ConstraintLayout
...
class MainActivity : AppCompatActivity() {
...
override fun onCreate(savedInstanceState: Bundle?) {
...
val root = findViewById<RelativeLayout>(R.id.rootLayout)
val gradient = root.background as GradientDrawable
val colors = arrayOf(
intArrayOf(0xFFFF5F6D.toInt(), 0xFFFFC371.toInt()), // pink → orange
intArrayOf(0xFF2193B0.toInt(), 0xFF6DD5ED.toInt()), // blue → cyan
intArrayOf(0xFFCC2B5E.toInt(), 0xFF753A88.toInt()), // magenta → purple
intArrayOf(0xFFEE9CA7.toInt(), 0xFFFFDDE1.toInt()) // rose → light pink
)
...
}
...
}Here, root is the root of the view, which is the ConstraintLayout where the entire screen exists within. We’re getting its background and making sure that gets cast as a GradientDrawable. Basically, at compile time, Kotlin can only infer that the background is a Drawable, not a GradientDrawable, and thus the compiler will throw an error when it analyzes our code and sees we’re messing with variables that don’t exist in the Drawable class. We need to cast it explicitly as a GradientDrawable, which inherits from Drawable, so we don’t get this issue.
Now we can set up and register our animation which modifies our gradient.
import android.animation.ArgbEvaluator
import android.animation.ValueAnimator
...
class MainActivity : AppCompatActivity() {
...
override fun onCreate(savedInstanceState: Bundle?) {
...
val gradient = root.background as GradientDrawable
val colors = arrayOf(
intArrayOf(0xFFFF5F6D.toInt(), 0xFFFFC371.toInt()), // pink → orange
intArrayOf(0xFF2193B0.toInt(), 0xFF6DD5ED.toInt()), // blue → cyan
intArrayOf(0xFFCC2B5E.toInt(), 0xFF753A88.toInt()), // magenta → purple
intArrayOf(0xFFEE9CA7.toInt(), 0xFFFFDDE1.toInt()) // rose → light pink
)
fun animateGradient(startIdx: Int = 0) {
val (startA, startB) = colors[startIdx]
val (endA, endB) = colors[(startIdx + 1) % colors.size]
val animator = ValueAnimator.ofFloat(0f, 1f).apply {
duration = 8000L
addUpdateListener { valueAnimator ->
val fraction = valueAnimator.animatedFraction
val newStart = ArgbEvaluator().evaluate(fraction, startA, endA) as Int
val newEnd = ArgbEvaluator().evaluate(fraction, startB, endB) as Int
gradient.colors = intArrayOf(newStart, newEnd)
}
addListener(object : android.animation.Animator.AnimatorListener {
override fun onAnimationEnd(animation: android.animation.Animator) {
animateGradient((startIdx + 1) % colors.size)
}
override fun onAnimationStart(animation: android.animation.Animator) {}
override fun onAnimationCancel(animation: android.animation.Animator) {}
override fun onAnimationRepeat(animation: android.animation.Animator) {}
})
}
animator.start()
}
animateGradient()
...Alright, there’s a lot going on. At its heart, we’re using something called a ValueAnimator which blends between two colors in our list of gradients. When the ValueAnimator is done we create a new ValueAnimator that blends between the next pair of colors. We do that, indefinitely, while the current activity is running. When we call start on a ValueAnimator, Android registers it, keeps track of it, and and uses it to calculate what the screen should look like each time it draws a frame.
The animateGradient is a recursive function. The first time it’s called, it registers an animator that blends between the gradient specified at index 0 and the gradient specified at index 1 in our list of colors. Within the animator that gets created, we define a listener onAnimationEnd that creates a new animation for the next batch of colors once this animation ends.
val colors = arrayOf(
intArrayOf(0xFFFF5F6D.toInt(), 0xFFFFC371.toInt()), // pink → orange
intArrayOf(0xFF2193B0.toInt(), 0xFF6DD5ED.toInt()), // blue → cyan
intArrayOf(0xFFCC2B5E.toInt(), 0xFF753A88.toInt()), // magenta → purple
intArrayOf(0xFFEE9CA7.toInt(), 0xFFFFDDE1.toInt()) // rose → light pink
)
fun animateGradient(startIdx: Int = 0) {
...
fun animateGradient(startIdx: Int = 0) {
val (startA, startB) = colors[startIdx]
val (endA, endB) = colors[(startIdx + 1) % colors.size]
val animator = ValueAnimator.ofFloat(0f, 1f).apply {
...
addListener(object : android.animation.Animator.AnimatorListener {
override fun onAnimationEnd(animation: android.animation.Animator) {
animateGradient((startIdx + 1) % colors.size)
}
...
})
}
animator.start()
}
animateGradient()
...Thus, we only need to create the first animation, then that animation will create the next, that animation will create the next, so on and so forth. Thus, virtually all of the functionality of our animated background functions within the animateGradient function, which then calls itself recursively.
Looking at that function, in its entirety:
fun animateGradient(startIdx: Int = 0) {
val (startA, startB) = colors[startIdx]
val (endA, endB) = colors[(startIdx + 1) % colors.size]
val animator = ValueAnimator.ofFloat(0f, 1f).apply {
duration = 8000L
addUpdateListener { valueAnimator ->
val fraction = valueAnimator.animatedFraction
val newStart = ArgbEvaluator().evaluate(fraction, startA, endA) as Int
val newEnd = ArgbEvaluator().evaluate(fraction, startB, endB) as Int
gradient.colors = intArrayOf(newStart, newEnd)
}
addListener(object : android.animation.Animator.AnimatorListener {
override fun onAnimationEnd(animation: android.animation.Animator) {
animateGradient((startIdx + 1) % colors.size)
}
override fun onAnimationStart(animation: android.animation.Animator) {}
override fun onAnimationCancel(animation: android.animation.Animator) {}
override fun onAnimationRepeat(animation: android.animation.Animator) {}
})
}
animator.start()We get whatever colors we want to start and end with, and define them as (startA, startB) and (endA, endB). Then we define our animator, which is a ValueAnimator. At its core, a ValueAnimator interpolates between two values over time. The updating of this value is what ends up governing how other things should react. .ofFloat(0f, 1f) means we’re interpolating a float (decimal number) between 0 and 1. Then, we’re doing one of those fancy Kotlin “scope functions” with .apply { ; ValueAnimator.ofFloat(0f, 1f) creates a ValueAnimator , then calling .apply { on that ValueAnimator says that we’re going to work on that value animator by setting the valueAnimator to this.
Then, this code is using another Kotlin convenience. Within the scope function, Kotlin assumes that, if you specify duration = <some number> , you’re actually doing this.duration = <some number> if this does have a duration attribute. Same with addUpdateListener and addListener. It’s assumed that these are calling functions in this , where this is the ValueAnimator we just created.
If you’re anything like me, you might be skeptical of all this abstraction. Why do all this when you can just create animator = ValueAnimator then just set stuff like animator.duration = <some number> ? You certainly can do that. However, if you do wrap your head around these scoped functions, you might find that the strict scoping and encapsulation is practically convenient when managing complex apps with large numbers of things being defined and manipulated. I don’t know about you, but I’ve often had a sneaky bug where I copy-pasted some code that thought I was modifying button A, but I was actually modifying button B. Scoping can help clean up a lot of those issues, hence why Kotlin apps tend to be more reliable.
Anyway, so this weird syntax is really just setting the duration of animator , and calling its addUpdateListener and addListener methods. We’re defining the update listener via a Kotlin lambda with an implicit Single Abstract Method (SAM) conversion.
addUpdateListener { valueAnimator ->
val fraction = valueAnimator.animatedFraction
val newStart = ArgbEvaluator().evaluate(fraction, startA, endA) as Int
val newEnd = ArgbEvaluator().evaluate(fraction, startB, endB) as Int
gradient.colors = intArrayOf(newStart, newEnd)
}Again, more weird Kotlin abstraction which is useful once you understand it. The addUpdateListener expects a single-method interface that looks something like this
public interface AnimatorUpdateListener {
void onAnimationUpdate(ValueAnimator animator);
}This allows us to define a lambda function which takes in the input valueAnimator, and pass that lambda function into addUpdateListener. Again, this might seem annoyingly abstract until you think about how many animations, buttons, toggles, switches, and other things need to have functions assigned to them. In a language like Java, that would require 6–8 lines of boilerplate just to set up, every single time. In Kotlin, there’s basically no boilerplate code.
Calling addListener has a similar story, except because it expects an interface that has more than one method, we need to implement it slightly differently. The interface which addListener expects, AnimatorListener, has four functions. We can create an anonymous class which satisfies that interface and then implement the functionality we want, which is to start a new animation when the animation ends.
addListener(object : android.animation.Animator.AnimatorListener {
override fun onAnimationEnd(animation: android.animation.Animator) {
animateGradient((startIdx + 1) % colors.size)
}
override fun onAnimationStart(animation: android.animation.Animator) {}
override fun onAnimationCancel(animation: android.animation.Animator) {}
override fun onAnimationRepeat(animation: android.animation.Animator) {}
})So, we have our complete main activity
package dev.danielwarfield.helloorworld
import android.animation.ArgbEvaluator
import android.animation.ValueAnimator
import android.graphics.drawable.GradientDrawable
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import android.widget.Button
import androidx.constraintlayout.widget.ConstraintLayout
import android.widget.TextView
import androidx.core.text.HtmlCompat
import android.util.Log
class MainActivity : AppCompatActivity() {
private var showingHello = true
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val root = findViewById<ConstraintLayout>(R.id.rootLayout)
val textView = findViewById<TextView>(R.id.definitionText)
val button = findViewById<Button>(R.id.switchButton)
Log.d(”BG”, “Background type = ${root.background::class.java.name}”)
val gradient = root.background as GradientDrawable
val colors = arrayOf(
intArrayOf(0xFFFF5F6D.toInt(), 0xFFFFC371.toInt()), // pink → orange
intArrayOf(0xFF2193B0.toInt(), 0xFF6DD5ED.toInt()), // blue → cyan
intArrayOf(0xFFCC2B5E.toInt(), 0xFF753A88.toInt()), // magenta → purple
intArrayOf(0xFFEE9CA7.toInt(), 0xFFFFDDE1.toInt()) // rose → light pink
)
fun animateGradient(startIdx: Int = 0) {
val (startA, startB) = colors[startIdx]
val (endA, endB) = colors[(startIdx + 1) % colors.size]
val animator = ValueAnimator.ofFloat(0f, 1f).apply {
duration = 8000L
addUpdateListener { valueAnimator ->
val fraction = valueAnimator.animatedFraction
val newStart = ArgbEvaluator().evaluate(fraction, startA, endA) as Int
val newEnd = ArgbEvaluator().evaluate(fraction, startB, endB) as Int
gradient.colors = intArrayOf(newStart, newEnd)
}
addListener(object : android.animation.Animator.AnimatorListener {
override fun onAnimationEnd(animation: android.animation.Animator) {
animateGradient((startIdx + 1) % colors.size)
}
override fun onAnimationStart(animation: android.animation.Animator) {}
override fun onAnimationCancel(animation: android.animation.Animator) {}
override fun onAnimationRepeat(animation: android.animation.Animator) {}
})
}
animator.start()
}
animateGradient()
fun updateScreen() {
val helloHtml = “<b>Hello</b><br/><i>/həˈlō/</i><br/>A greeting or expression of goodwill.”
val worldHtml = “<b>World</b><br/><i>/wərld/</i><br/>The earth, together with all of its countries, peoples, and natural features.”
if (showingHello) {
textView.text = HtmlCompat.fromHtml(helloHtml, HtmlCompat.FROM_HTML_MODE_LEGACY)
button.text = “Show World”
} else {
textView.text = HtmlCompat.fromHtml(worldHtml, HtmlCompat.FROM_HTML_MODE_LEGACY)
button.text = “Show Hello”
}
}
button.setOnClickListener {
showingHello = !showingHello
updateScreen()
}
updateScreen()
}
}Our layout file for our main activity
<?xml version=”1.0” encoding=”utf-8”?>
<androidx.constraintlayout.widget.ConstraintLayout
android:id=”@+id/rootLayout”
xmlns:android=”http://schemas.android.com/apk/res/android”
xmlns:app=”http://schemas.android.com/apk/res-auto”
android:layout_width=”match_parent”
android:layout_height=”match_parent”
android:padding=”32dp”
android:background=”@drawable/gradient_bg”>
<!-- 25% from top for the TextView -->
<androidx.constraintlayout.widget.Guideline
android:id=”@+id/topGuide”
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:orientation=”horizontal”
app:layout_constraintGuide_percent=”0.25” />
<!-- 15% from bottom for the Button (i.e., 85% from top) -->
<androidx.constraintlayout.widget.Guideline
android:id=”@+id/bottomGuide”
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:orientation=”horizontal”
app:layout_constraintGuide_percent=”0.85” />
<TextView
android:id=”@+id/definitionText”
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:text=”word”
android:textSize=”24sp”
android:lineSpacingExtra=”8dp”
android:textColor=”#FFFFFF”
android:fontFamily=”serif”
android:textAlignment=”center”
app:layout_constraintTop_toTopOf=”@id/topGuide”
app:layout_constraintStart_toStartOf=”parent”
app:layout_constraintEnd_toEndOf=”parent” />
<Button
android:id=”@+id/switchButton”
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:text=”Switch”
android:textSize=”18sp”
android:textColor=”#FFFFFF”
android:background=”@drawable/translucent_button”
app:layout_constraintBottom_toBottomOf=”@id/bottomGuide”
app:layout_constraintStart_toStartOf=”parent”
app:layout_constraintEnd_toEndOf=”parent” />
</androidx.constraintlayout.widget.ConstraintLayout>and our drawables for our button and background, respectively
<ripple xmlns:android=”http://schemas.android.com/apk/res/android”
android:color=”#40FFFFFF”>
<item>
<shape android:shape=”rectangle”>
<solid android:color=”#33FFFFFF”/>
<corners android:radius=”24dp”/>
</shape>
</item>
</ripple><shape xmlns:android=”http://schemas.android.com/apk/res/android”
android:shape=”rectangle”>
<gradient
android:id=”@+id/animatedGradient”
android:type=”linear”
android:angle=”45”
android:startColor=”#FF5F6D”
android:endColor=”#FFC371” />
</shape>And that gets our animated background done and dusted.
We have one more aesthetic adjustment I want to implement on this app.
5) Animating Transitions Between Words
Next, we’ll implement a nice blending animation that plays when switching between words.

Our current button has a simple listener attatched to it that flips a boolean and calls updateScreen, which in turn updates the text based on that boolean.
button.setOnClickListener {
showingHello = !showingHello
updateScreen()
}We can implement fading by simply assigning an animation to our text when we click the button.
button.setOnClickListener {
showingHello = !showingHello
val fadeOut = android.view.animation.AlphaAnimation(1f, 0f).apply {
duration = 200
}
val fadeIn = android.view.animation.AlphaAnimation(0f, 1f).apply {
duration = 300
startOffset = 200
}
fadeOut.setAnimationListener(object : android.view.animation.Animation.AnimationListener {
override fun onAnimationStart(animation: android.view.animation.Animation?) {}
override fun onAnimationRepeat(animation: android.view.animation.Animation?) {}
override fun onAnimationEnd(animation: android.view.animation.Animation?) {
updateScreen()
textView.startAnimation(fadeIn)
}
})
textView.startAnimation(fadeOut)
}Here we’re defining two animations, which are both AlphaAnimation. These animate the transparency of a view from fully opaque (1) to fully transparent (0). fadeOut makes the text transparent, and fadeIn makes the text opaque.
Similarly to what we did with the background, we’re controlling these sequential animations by calling setAnimationListener on our fadeOut animation. We’re using that to update the screen (swap the text) while the text is completely transparent, then call the fadeIn animation. So the text fades out, swaps without the user seeing, then fades in as the new text.
the fade out animation takes 200 milliseconds, then the text stays transparent for 200 more milliseconds (on account of the startOffset before fade in) before fading in over the course of 300 milliseconds.
And, with that, our fancy Hello or World app is done!
Now that we have a grasp of the basics, we can start building some pretty sophisticated stuff. Let’s get into it!
Our Second App: Flag Crank App
The idea of this app is to experiment with more complex user interaction, more complex UI design, and integrate more thoroughly into the phones resources. We’ll do that by building a simple game where you need to spin a crank to raise a flag. If the flag goes all the way up, you win. If it goes all the way to the bottom, the phone vibrates.
App 2, Part 1 — Setup
Just like the last app, I kicked this one off as a new blank project, this time called “Flag Crank”. I didn’t bother changing the whole com.example thing, so the package name for this project is com.example.flagcrank.
ignore that my “Save Location” is “…FlagCrank2”, I already had a FlagCrank directory on my computer. This shouldn’t change anything, as all of the resources within the project are relative to each other.
Then, in com.example.flagcrank, right click and make a new Kotlin Class, which we’ll call MainActivity
If we swing up to our AndroidManifest.xml file in the manifests folder, we’ll see the default manifest.
<?xml version=”1.0” encoding=”utf-8”?>
<manifest xmlns:android=”http://schemas.android.com/apk/res/android”
xmlns:tools=”http://schemas.android.com/tools”>
<application
android:allowBackup=”true”
android:dataExtractionRules=”@xml/data_extraction_rules”
android:fullBackupContent=”@xml/backup_rules”
android:icon=”@mipmap/ic_launcher”
android:label=”@string/app_name”
android:roundIcon=”@mipmap/ic_launcher_round”
android:supportsRtl=”true”
android:theme=”@style/Theme.FlagCrank” />
</manifest>We need our new activity to be within the application, and we need to set it’s intent to be compatible with opening the application from the home screen, which (as previously discussed) we define via the following:
<?xml version=”1.0” encoding=”utf-8”?>
<manifest xmlns:android=”http://schemas.android.com/apk/res/android”
xmlns:tools=”http://schemas.android.com/tools”>
<application
android:allowBackup=”true”
android:dataExtractionRules=”@xml/data_extraction_rules”
android:fullBackupContent=”@xml/backup_rules”
android:icon=”@mipmap/ic_launcher”
android:label=”@string/app_name”
android:roundIcon=”@mipmap/ic_launcher_round”
android:supportsRtl=”true”
android:theme=”@style/Theme.Material3.DayNight.NoActionBar”>
<activity
android:name=”.MainActivity”
android:exported=”true”>
<intent-filter>
<action android:name=”android.intent.action.MAIN”/>
<category android:name=”android.intent.category.LAUNCHER”/>
</intent-filter>
</activity>
</application>
</manifest>While I was in there, I also snuck in android:theme=”@style/Theme.Material3.DayNight.NoActionBar”>. This makes it so we don’t have a bar on the top of our screen with the app name.
It’s a good policy, when doing this type of configuration work, to try running the app often just to make sure everything’s set up right. We can define a super simple MainActivity, which just renders the text “Hello World” on the top left of the screen
package com.example.flagcrank
import android.os.Bundle
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val tv = TextView(this).apply {
text = “Hello World”
textSize = 24f
setPadding(50, 50, 50, 50)
}
setContentView(tv)
}
}Run it, and make sure everything works
Alright, let’s start building this FlagCrank app
App 2, Part 2 — Planning The App
unlike the previous app we made, we’re going to jump right to a functional solution, and we won’t spend a lot of time on UI prettification. On this app, we’re focusing on app logic and defining custom UI elements.
There are going to be two major elements, the flagpole and the crank. These will exist within the screen, which will manage how the two components communicate with one another. We’ll define these components so that, when the crank moves, the flag updates.
You might think the crank should be a view, the flag should be a different view, and we should implement a view group which contains both views. That’s one way to approach building an app like this, but we won’t be doing it. Instead, we’ll be using something called “Jetpack Compose”; a different and more modern approach to Android app development which replaces the concept of views.
App 2, Part 3 — Exploring Jetpack Compose vs Traditional Views
The traditional approach to Android development is to use views, which we discussed in the previous application; you define a layout in XML, and communicate with those layouts via Kotlin code. Each view has some state, can accept user input, and knows how to render itself to the user. Developing an app, then, is essentially defining these views and how they connect with one another.
The traditional approach to android app development. Each element is a “view” which has it’s own state information. App development, then, is a game of getting the states of these views to play nicely with one another.
One of the biggest issues with this approach is synchronization. There’s a lot of different sources of truth here, with different components reacting to user inputs and updating their own state independently. The user might press multiple buttons, each of which might impact multiple views, which in turn might impact multiple states. Generally the approach is to move state upward, so that container views hold the state that is relevant to the views it contains. Even when perfectly designed though, there’s a ton of edge cases a developer needs to account for.
Jetpack Compose is a modern framework for building applications in Android, which employs a different model than the view model. Instead of thinking of a component as having state that needs to be shared amongst one another, instead, components structure state in a strictly top-down manner. Instead of components needing to negotiate on state, state is passed from top to bottom, component to component, and that change in state is used to update the UI (a process called “recomposition”).
Using Jetpack Compose, state is shared between app components in a hierarchical manner.
Because different components don’t have to “agree” on the state, and instead modify and react to state based on a rigid top-down structure, handling complex state changes in a compose powered application is significantly more straightforward. This is made possible by parents passing callback functions to child elements. Children can call these callbacks, modifying state in the parent.
A conceptual diagram of state and callbacks in Jetpack Compose. State information flows in a top-down hierarchy, while callbacks allow granular events like user interaction to move up the chain.
It’s worth noting that you can achieve this in a view style app. In fact, a top down state management is encouraged, JetpackCompose just makes it more rigid and enforced based on the way the system works.
While some components need to keep track of state, most compose components don’t need to hold a state which changes over time, but instead react to a state change. When working with compose you define a function that defines the UI based on the state. When the state changes, the function is re-run, modifying the UI.
In Jetpack Compose, components are updated as functions which responed to changes in a shared app state.
There’s two conflicting ideas that start to make more sense when you get practical: hierarchical state management, and not managing state at all and treating the UI like a function. These are hard to understand without getting into the specifics, so let’s get composed set up in our build tools, then we can use it to begin implementing our app.
App 2, Part 4 — Configuring Dependencies
In order to use jetpack in our project, we need to set it up with Gradle (the build tool we’ve been using to turn our code into an actual running app). These types of configurations have a tendency to shift over time, but for me all I had to do is navigate to app/build.gradle.kts and add
buildFeatures {
compose = true
}to it, like so:
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.compose.compiler)
}
android {
namespace = “com.example.flagcrank”
compileSdk {
version = release(36)
}
buildFeatures {
compose = true
}
defaultConfig {
...We can then replace our MainActivity.kt with the following code to confirm that we’re properly using Jetpack Compose
package com.example.flagcrank
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MaterialTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = Color(0xFF101010)
) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text(
text = “Hello, Compose!”,
style = MaterialTheme.typography.headlineMedium,
color = Color.White
)
}
}
}
}
}
}This is our first time seeing any Compose code, so I thought it might be productive to explore it in depth.
The first three lines of code are similar to what we’ve seen previously, but now we’re using import androidx.activity.compose.setContent. This is an integration detail, allowing our traditional activities to use the more modern Compose approach.
Then we have a bunch of imports to androidx.compose, which is where the core Jetpack Compose library lives. Within androidx.compose there are various modules, like:
ui: Contains foundational types and systems, for things like colors and layout measurement, which other modules use.
foundation: Un-styled and pre-built primatives, like boxes, rows, columns, as well as tools for understanding user interaction. These are primitive UI components that can be styled and modified to construct an app.
material3: This is built on top of foundation, and contains style information like color schemes, typography, and stuff like that.
There are other modules in jetpack compose as well, like animation and runtime, that can be used for more advanced functionality.
The component itself looks a lot like Android code we’ve explored previously. We have MainActivity that inherits from ComponentActivity(), and we have an onCreate function. However, instead of using setContentView, like we did previously, we’re using setContent. This is the bridge that connects Jetpack Compose to the outside application.
When you define something with JetpackCompose, the generall pattern looks something like this:
SomeComposable(
parameter = value,
anotherParameter = value
) {
// children go here
} Which allows you to nest composable elements inside one another. It uses a weird kotlin syntax called “trailing lambda syntax”. If the last argument to a function is another function, like so:
function(arg, arg, function_def)You can create the function as a lambda function, using curly brackets after the function, and it will automatically resolve
function(arg, arg) {function_def}If the function only takes in another function, you can skip the parantheses entirely
function{funciton_def}Because Compose uses functions to define UI components, there’s a lot of nesting of those components. This kind of quirky syntax is super useful in doing that without having a lot of boilerplate.
So, taking another look at our code, the UI is a bunch of nested funcitons that feed into eachother. When the state of the app changes, jetpack compose calls these various functions to draw the app.
setContent {
MaterialTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = Color(0xFF101010)
) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text(
text = “Hello, Compose!”,
style = MaterialTheme.typography.headlineMedium,
color = Color.White
)
}
}
}
}setContent exists at the top to act as a bridge, then the MaterialTheme contains a Surface that contains a Box that contains Text.
The point of materials, is to create and then delineate design themes across child elements. It exists at the top, then all children can use it. This example doesn’t have a lot of details in that regard, I don’t want to get bogged down on styling, but the following example shows how material information might be propegated through a slightly more complex activity.
setContent {
MaterialTheme(
colorScheme = darkColorScheme(
primary = Color(0xFF6650A4),
background = Color(0xFF101010),
onBackground = Color.White
),
typography = Typography(
headlineMedium = TextStyle(fontSize = 28.sp, fontWeight = FontWeight.Bold),
bodyMedium = TextStyle(fontSize = 16.sp),
labelSmall = TextStyle(fontSize = 12.sp, color = Color.Gray)
)
) {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = “Hello, Compose!”,
style = MaterialTheme.typography.headlineMedium,
color = MaterialTheme.colorScheme.onBackground
)
Text(
text = “This is body text.”,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onBackground
)
Text(
text = “small label”,
style = MaterialTheme.typography.labelSmall
)
}
}
}
}Inside the material is a Surface, which is pretty mugh just the background. Then there’s a Box, which helps us lign stuff up correctly, then Text, which renders the text on the screen. Give it a run, and we have a Jetpack Compose powered app.
I could describe all of the layout and styling ideas we’ve discussed previously, but many of them are directly applicable to Jetpack Compose with a modest amount of googling/LLMing.
App 2, Part 5— Implementing the Flag in Jetpack Compose
Recall, we’re trying to make this flag crank app
Let’s build it. First; right click, new, Kotlin Class/File
Then create a new file called FlagPole
and then define it like so
package com.example.flagcrank
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.dp
@Composable
fun FlagPole(modifier: Modifier = Modifier) {
val poleHeight = 300.dp
val flagWidth = 90.dp
val flagHeight = 60.dp
var progress by remember { mutableStateOf(0.5f) }
val density = LocalDensity.current
val flagW = with(density) { flagWidth.toPx() }
val flagH = with(density) { flagHeight.toPx() }
val offsetX = with(density) { 14.dp.toPx() }
Box(
modifier
.fillMaxWidth()
.height(poleHeight)
) {
Canvas(Modifier.fillMaxSize()) {
val poleX = size.width / 2f
drawLine(
Color(0xFFCCCCCC),
Offset(poleX, size.height),
Offset(poleX, 0f),
strokeWidth = 10f
)
val topY = 0f
val bottomMargin = size.height * 0.10f
val bottomY = size.height - flagH - bottomMargin
val clampedBottomY = bottomY.coerceAtLeast(topY)
val y = topY + (clampedBottomY - topY) * (1f - progress)
drawRect(
Color(0xFFFF5555),
topLeft = Offset(poleX + offsetX, y),
size = androidx.compose.ui.geometry.Size(flagW, flagH)
)
}
}
}and then use it in our main activity
package com.example.flagcrank
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MaterialTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = Color(0xFF101010)
) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
FlagPole()
}
}
}
}
}
}yielding
Let’s unpack the FlagPole composable. First of all, there’s a new piece of syntax @Composable, then a funciton definition. The FlagPole is the composable itself, a function that renders UI changes based on state. We put @Composable above it to tell the build system it’s a composable function. We didn’t need this before because we didn’t define a composable on its own, but just built a “hello world” one on-the-fly in our main activity.
@Composable
fun FlagPole(modifier: Modifier = Modifier) {The modifier is a convention in Compose, allowing a composable to pass layout information to its children. It allows you to do things like making a child component half the width of the parent, for instance. It has a lot of uses, like:
Modifier.fillMaxSize()— make the component fill all available spaceModifier.fillMaxWidth()— fill available width onlyModifier.size(100.dp)— fixed sizeModifier.padding(16.dp)— add space around the componentModifier.background(Color.Red)— set a background colorModifier.clickable { … }— add a click handlerModifier.align(…)— position within a parent like Box
In FlagPole.kt we then define a few parameters defining the size of the flag pole, then call this:
var progress by remember { mutableStateOf(0.5f) }This is a mix of kotlin specific syntax and Compose functionality, that’s honestly more complex than it is enlightening to understand. by is a delegation keyword in kotlin that lets you skip some boilerplate, yadah yadah. Basically, though, the code block above allows you to remember a state, and interface with it via the variable progress.
val current = progress // read
progress = 0.7f // write This variable will persist across function calls, which is critical because compose uses functions to define UI components. This is how Compose allows you to use stateless things (functions) to modify a UI that requires state.
Anyway, we’ll explore state management in a bit.
Next, we’re drawing the actual flag itself. This takes place in a Canvas that is inside a Box. One quirk of a Canvas is that it draws in raw pixels. As we mentioned previously, different devices can hav drastically different pixel densities, which will change the size of the flag. Thus, before drawing, we want to scale our sizes based on the pixel density of our screen so that the size of the flag is consistent across devices. That’s where this code block comes in
val density = LocalDensity.current
val flagW = with(density) { flagWidth.toPx() }
val flagH = with(density) { flagHeight.toPx() }
val offsetX = with(density) { 14.dp.toPx() }There’s some Kotlin syntax stuff mixed with Jetpack Compose stuff in this codeblock. To be honest, this article is getting so long that I’m experiencing a delay in my browser when writing it, so I’ll save understanding that as an exercise for the reader.
We then render the flag itself, which is a Box with a Canvas in it. The flag itself is simply a Rect and a Line within that Canvas.
One detail the astute might notice is that this function isn’t actually returning anything. If I create two Boxs, each with their own drawing, I’ll get two flag poles. In this example the second one is weird because I tweaked some numbers around to show both of them. Otherwise they’d draw on top of one another.
Box(
modifier
.fillMaxWidth()
.height(poleHeight)
) {
Canvas(Modifier.fillMaxSize()) {
val poleX = size.width / 2f
drawLine(
Color(0xFFCCCCCC),
Offset(poleX, size.height),
Offset(poleX, 0f),
strokeWidth = 10f
)
val topY = 0f
val bottomMargin = size.height * 0.10f
val bottomY = size.height - flagH - bottomMargin
val clampedBottomY = bottomY.coerceAtLeast(topY)
val y = topY + (clampedBottomY - topY) * (1f - progress)
drawRect(
Color(0xFFFF5555),
topLeft = Offset(poleX + offsetX, y),
size = androidx.compose.ui.geometry.Size(flagW, flagH)
)
}
}
Box(
modifier
.fillMaxWidth()
.height(poleHeight/2)
) {
Canvas(Modifier.fillMaxSize()) {
val poleX = size.width / 2f+100
drawLine(
Color(0xFFCCCCCC),
Offset(poleX, size.height),
Offset(poleX, 0f),
strokeWidth = 10f
)
val topY = 0f
val bottomMargin = size.height * 0.10f
val bottomY = size.height - flagH - bottomMargin
val clampedBottomY = bottomY.coerceAtLeast(topY)
val y = topY + (clampedBottomY - topY) * (1f - progress)
drawRect(
Color(0xFFFF5555),
topLeft = Offset(poleX + offsetX, y),
size = androidx.compose.ui.geometry.Size(flagW, flagH)
)
}
}Composables, don’t “return” drawings, they “emit” them. When you draw a box, it doesn’t trickle up to your top-level entity, it gets drawn right there in the compose, and you can make as many of them as you want.
Practically, what you end up doing is drawing whatever you want, and have that thing exist within the thing that drew it by using the modifier to properly position it visually. State exists at the top, and drawing goes down the component tree until it hits things that actually draw stuff.
The point of the flag crank app is to have a crank raise and lower the flag. To do that, we’re going to need to implement the crank, but also have state information from the crank update the state that affects the flag. We’ll achieve that by creating a third component that contains both the crank and the flag.
Before we do anything, let’s define what “state” even is. There’s two things, the crank and the flag. For the flag, we care about how high up the flag is, called “progress” and for the crank we care about if the cranks rotation. Let’s create a new file called Models.kt that we can use to explicitly define these state variables.
package com.example.flagcrank
data class FlagState(
val progress: Float = 0f
)
data class CrankState(
val rotation: Float = 0f
)Next, let’s modify our FlagPole. Before, it was in control of its own state, the progress of the flag. Now we want it to receive its state from its parent. We do that by simply changing the FlagPole to accept FlagState, and then use it to draw stuff.
package com.example.flagcrank
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.dp
@Composable
fun FlagPole(
flagState: FlagState,
modifier: Modifier = Modifier
) {
val poleHeight = 300.dp
val flagWidth = 90.dp
val flagHeight = 60.dp
val density = LocalDensity.current
val flagW = with(density) { flagWidth.toPx() }
val flagH = with(density) { flagHeight.toPx() }
val offsetX = with(density) { 14.dp.toPx() }
Box(
modifier
.fillMaxWidth()
.height(poleHeight)
) {
Canvas(Modifier.fillMaxSize()) {
val poleX = size.width / 2f
drawLine(
Color(0xFFCCCCCC),
Offset(poleX, size.height),
Offset(poleX, 0f),
strokeWidth = 10f
)
val topY = 0f
val bottomMargin = size.height * 0.10f
val bottomY = size.height - flagH - bottomMargin
val clampedBottomY = bottomY.coerceAtLeast(topY)
val y = topY + (clampedBottomY - topY) * (1f - flagState.progress)
drawRect(
Color(0xFFFF5555),
topLeft = Offset(poleX + offsetX, y),
size = androidx.compose.ui.geometry.Size(flagW, flagH)
)
}
}
}We also need to define a crank, which will exist in the file Crank.kt
package com.example.flagcrank
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.gestures.detectDragGestures
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.drawscope.rotate
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.dp
import kotlin.math.PI
import kotlin.math.sin
import kotlin.math.cos
@Composable
fun Crank(
crankState: CrankState,
onDrag: (Float) -> Unit,
onDraggingChanged: (Boolean) -> Unit,
modifier: Modifier = Modifier
) {
val crankSize = 200.dp
val density = LocalDensity.current
val sizePx = with(density) { crankSize.toPx() }
val radius = sizePx / 2.3f
var theta by remember { mutableStateOf(0f) }
Box(
modifier
.size(crankSize)
.pointerInput(Unit) {
detectDragGestures(
onDragStart = { onDraggingChanged(true) },
onDragEnd = { onDraggingChanged(false) },
onDragCancel = { onDraggingChanged(false) }
) { _, dragAmount ->
val dx = dragAmount.x
val dy = dragAmount.y
val angRad = theta * PI.toFloat() / 180f
val tx = -sin(angRad)
val ty = cos(angRad)
val dot = dx * tx + dy * ty
val delta = dot * 0.4f
theta += delta
onDrag(delta)
}
}
) {
Canvas(Modifier.fillMaxSize()) {
rotate(crankState.rotation) {
drawCircle(
Color(0xFF666666),
radius = radius,
style = Stroke(20f, cap = StrokeCap.Round)
)
drawLine(
Color(0xFFEEEEEE),
start = center,
end = Offset(center.x + radius, center.y),
strokeWidth = 14f
)
drawCircle(
Color(0xFFFF4444),
radius = 26f,
center = Offset(center.x + radius, center.y)
)
}
}
}
}In a lot of ways it’s similar to the Flag. It accepts a modifier to help with positioning, receives a state, and draws stuff. There are some big differences though.
First of all, the Crank accepts two listeners, along with state and layout information.
@Composable
fun Crank(
crankState: CrankState,
onDrag: (Float) -> Unit,
onDraggingChanged: (Boolean) -> Unit,
modifier: Modifier = Modifier
) {
...onDrag is a function that will receive a number representing the user dragging their hand on the phone then send information back to the parent, and onDraggingChanged is a function that receives if the user started or stopped dragging and send it back to the parent. These are functions that will be passed by the parent, allowing the Crank to tell the parent what’s happening.
The Crank has kind of a complicated job. It needs to keep track of rotation in a drag event, so that it can update its parent, but doesn’t really “own” that data, because we need that state information to exist such that it can impact the Flag. To make this easier, the Crank keeps track of its own internal angle measurement of rotation by a user during a “Drag Gesture”.
var theta by remember { mutableStateOf(0f) }User interaction can be complicated, and it can be useful to decouple state for interaction vs state for drawing and logic. In this simple application, you could probably also just have one state variable and use it for both.
Regardless, we’re using androidx.compose.foundation.gestures.detectDragGestures to detect drag gestures by the user,
detectDragGestures(
onDragStart = { onDraggingChanged(true) },
onDragEnd = { onDraggingChanged(false) },
onDragCancel = { onDraggingChanged(false) }
) { _, dragAmount ->
val dx = dragAmount.x
val dy = dragAmount.y
val angRad = theta * PI.toFloat() / 180f
val tx = -sin(angRad)
val ty = cos(angRad)
val dot = dx * tx + dy * ty
val delta = dot * 0.4f
theta += delta
onDrag(delta)
}It has a few key callbacks, like a function that triggers when the drag starts, ends, or is cancelled. We can create a lambda function that calls onDraggingChanged for each of these events. Recall that onDraggingChanged is a listener passed by the parent of this component, so we can send those messages up the chain and the parent can update state based on that information. This is necessary in making the flag automatically fall when the user isn’t spinning the crank.
There’s also some trigonometry used to calculate how much we should spin the crank. Once we calculate that, delta, a certain rotation, we pass it to the onDrag listener that then calls it in the parent.
Notice that the actual drawing of the Crank has nothing to do with theta or delta. These are used to do calculations and pass data to the parent. The Crank receives the actual rotation data from the crankState, passed by the parent, to render.
...
Canvas(Modifier.fillMaxSize()) {
rotate(crankState.rotation) {
drawCircle(
Color(0xFF666666),
radius = radius,
style = Stroke(20f, cap = StrokeCap.Round)
)
drawLine(
Color(0xFFEEEEEE),
start = center,
end = Offset(center.x + radius, center.y),
strokeWidth = 14f
)
drawCircle(
Color(0xFFFF4444),
radius = 26f,
center = Offset(center.x + radius, center.y)
)
}
}
}So, in a nutshell, the crank has some internal state to manage calculating information about user interaction across different UI refreshes, but that purpose is ephemeral and thus can be contained in the crank. That’s used to update the actual app state in the crankState, which is then fed back into the Crank to draw it correctly.
In a real app you might experiment with the costs and benefits of different state logic, but this approach results in functionality that works nicely.
So, the Flag and Crank both are designed to be children under something. This is that something, CrankWithFlag.kt
package com.example.flagcrank
import android.os.Build
import android.os.VibrationEffect
import android.os.VibratorManager
import android.widget.Toast
import androidx.compose.foundation.layout.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.delay
@Composable
fun CrankWithFlag() {
val context = LocalContext.current
var flagState by remember { mutableStateOf(FlagState()) }
var crankState by remember { mutableStateOf(CrankState()) }
var isDragging by remember { mutableStateOf(false) }
var hasNotifiedTop by remember { mutableStateOf(false) }
val vibrator = remember {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
context.getSystemService(VibratorManager::class.java).defaultVibrator
} else {
@Suppress(”DEPRECATION”)
context.getSystemService(android.content.Context.VIBRATOR_SERVICE) as android.os.Vibrator
}
}
LaunchedEffect(Unit) {
while (true) {
if (!isDragging) {
flagState = flagState.copy(
progress = (flagState.progress - 0.0025f).coerceIn(0f, 1f)
)
}
if (flagState.progress <= 0.02f) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
vibrator.vibrate(VibrationEffect.createOneShot(80, 180))
} else {
@Suppress(”DEPRECATION”)
vibrator.vibrate(80)
}
}
if (flagState.progress >= 1f && !hasNotifiedTop) {
Toast.makeText(context, “Flag reached the top!”, Toast.LENGTH_SHORT).show()
hasNotifiedTop = true
}
if (flagState.progress < 0.85f && hasNotifiedTop) {
hasNotifiedTop = false
}
delay(16)
}
}
Column(
modifier = Modifier
.fillMaxSize()
.padding(40.dp),
verticalArrangement = Arrangement.SpaceBetween
) {
FlagPole(flagState)
Spacer(modifier = Modifier.height(40.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center
) {
Crank(
crankState = crankState,
onDraggingChanged = { isDragging = it },
onDrag = { delta ->
crankState = crankState.copy(rotation = crankState.rotation + delta)
if (delta > 0f) {
flagState = flagState.copy(
progress = (flagState.progress + delta / 360f).coerceIn(0f, 1f)
)
}
}
)
}
}
}This component handles state between the two Composables. It remembers the following things
var flagState by remember { mutableStateOf(FlagState()) }
var crankState by remember { mutableStateOf(CrankState()) }
var isDragging by remember { mutableStateOf(false) }
var hasNotifiedTop by remember { mutableStateOf(false) }and also has some setup logic to allow the phone to vibrate when the flag reaches the bottom.
val context = LocalContext.current
...
val vibrator = remember {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
context.getSystemService(VibratorManager::class.java).defaultVibrator
} else {
@Suppress(”DEPRECATION”)
context.getSystemService(android.content.Context.VIBRATOR_SERVICE) as android.os.Vibrator
}
}
...
vibrator.vibrate(VibrationEffect.createOneShot(80, 180))
..
if (flagState.progress <= 0.02f) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
vibrator.vibrate(VibrationEffect.createOneShot(80, 180))
...Most of the actual logic of the application exists in LaunchedEffect, which is launched as an asynchronous coroutine in the background.
LaunchedEffect(Unit) {
while (true) {
if (!isDragging) {
flagState = flagState.copy(
progress = (flagState.progress - 0.0025f).coerceIn(0f, 1f)
)
}
if (flagState.progress <= 0.02f) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
vibrator.vibrate(VibrationEffect.createOneShot(80, 180))
} else {
@Suppress(”DEPRECATION”)
vibrator.vibrate(80)
}
}
if (flagState.progress >= 1f && !hasNotifiedTop) {
Toast.makeText(context, “Flag reached the top!”, Toast.LENGTH_SHORT).show()
hasNotifiedTop = true
}
if (flagState.progress < 0.85f && hasNotifiedTop) {
hasNotifiedTop = false
}
delay(16)
}
}You might recall, way earlier in the article, we talked about lifecycles. This is created early in the lifecycle and persists, due to the while (true) loop, during the duration of the components existence. AKA, while the app is running.
You can see that the logic is pretty straight forward. While the user isn’t cranking, the progress is updated. If the flag hits the top, a notification says the top has been reached. There’s some threshold to that so the app doesn’t spam notifications when you reach the top.
Some more of the logic exists within the callbacks supplied to the Flag and Crank
Column(
modifier = Modifier
.fillMaxSize()
.padding(40.dp),
verticalArrangement = Arrangement.SpaceBetween
) {
FlagPole(flagState)
Spacer(modifier = Modifier.height(40.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center
) {
Crank(
crankState = crankState,
onDraggingChanged = { isDragging = it },
onDrag = { delta ->
crankState = crankState.copy(rotation = crankState.rotation + delta)
if (delta > 0f) {
flagState = flagState.copy(
progress = (flagState.progress + delta / 360f).coerceIn(0f, 1f)
)
}
}
)
}
}Like before, the FlagPole and Crank exist within another composable for organization purposes, a Column for instance. The FlagPole is simple because it’s purely reactive, but the Crank has some complications due to callbacks being defined.
The Crank also has its own state. onDraggingChanged is a lambda function that has some clever and obscure Kotlin syntax in it. You could read this as
onDraggingChanged = { value -> isDragging = value } if you wanted. Basically, it’s a function that sets isDragging to whatever the input value is. Because this is passed to the Crank, it allows the Crank to define what the value of isDragging is, which is contained in the higher order CrankWithFlag composable.
The onDrag callback is a bit more complicated, but does a very similar thing.
onDrag = { delta ->
crankState = crankState.copy(rotation = crankState.rotation + delta)
if (delta > 0f) {
flagState = flagState.copy(
progress = (flagState.progress + delta / 360f).coerceIn(0f, 1f)
)
}
}When the onDrag callback is called within the Crank, it overwrites the crankState and the flagState (if the crank is spinning in the right direction). These state variables are then propegated to each component.
You might notice that we’re overwriting the state variables, rather than modifying them. To make apps more efficient, Jetpack Compose only renders components whos state changes. It compares old vs new values to do that. Thus, state variables are immutable and must be overwritten for Compose to do It’s job.
We can change our MainActivity to use our new component
package com.example.flagcrank
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MaterialTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = Color(0xFF101010)
) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
CrankWithFlag()
}
}
}
}
}
}and, with that, we have a Jetpack Compose App
The flag goes up and down when you spin the crank. There’s a notification that pops up when you get to the top. Take my word for it.
Conclusion
And now you know why I don’t often teach front end development. It’s a lot of information without a ton of core logic underneath. Necessarily, the domain is full of arbitrary rules and long lists of pre-defined tools. I’m more of a conceptual learner, so front end development has never really scratched an itch for me.
However, as I grow as a developer, manager, entrepreneur, and technical director, I find that front end presentation is incredibly important. You can make the most sophisticated thing under the sun, but if it doesn’t grab peoples attention or present them with an experience they understand, the value won’t be understood.
All developers, including data scientists, need to have some grasp of front end development. In that vein, I hope this article has proved useful. Do I really have no typos, or is this article simply too long for my proofreading system… Only time will tell.

















































































































