Mastering Kotlin Functions for Clean Android Code

A Beginner’s Guide to Elegant, Reusable Solutions

Dhananjay Trivedi
5 min readNov 8, 2024
Photo by Sebastian Bednarek on Unsplash

1. Basic Kotlin Function

A simple function just performs a task and returns a value (or doesn’t return anything at all). You call the function, it runs the code inside, and that’s it — no special handling.

Example

fun add(a: Int, b: Int): Int {
return a + b
}
  • Purpose: To perform a direct operation and return a result immediately.
  • Behavior: Runs the code inside and returns the result (a + b).

Example: Formatting Text for Display

fun formatUserName(firstName: String, lastName: String): String {
return "$firstName $lastName"
}

Usage in Activity or Fragment

val fullName = formatUserName("John", "Doe")
textView.text = fullName // Setting formatted text to a TextView

2. Function with No Return Value (Unit)

A function that does a task but doesn’t return any value. In Kotlin, this type is called Unit (similar to void in other languages).

Example

fun printMessage(message: String) {
println(message)
}

Example: Showing a Toast Message

fun showToast(message: String) {
Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
}

Usage in Activity or Fragment

showToast("User registered successfully")

3. Callback Functions

This is where things get a bit more advanced.

A callback is just a function you pass to another function to call later. Callbacks are useful for asynchronous tasks (e.g., network requests) where the task will take some time, and you want to let the caller know when it’s done.

fun fetchData(onComplete: () -> Unit) {
println("Fetching data...")
onComplete() // Calls the callback after the task is done
}
  • Purpose: To allow the calling function to decide what should happen when an action is complete (e.g., network request finished).
  • Behavior: Calls onComplete() (the lambda passed in as a callback) after the task is finished.
fun fetchDataFromNetwork(onDataFetched: (String) -> Unit) {
// Simulate network call
val data = "User data loaded"
onDataFetched(data) // Calling the callback with fetched data
}

// Usage in Activity or Fragment
fetchDataFromNetwork { data ->
println("Data received: $data")
textView.text = data // Display data in a TextView
}

4. Passing Functions as Parameters — Lambda Expressions

This is where things get a bit more advanced.

Here, a function accepts another function as a parameter (known as a lambda or function literal.

Why?

Sometimes, you want a function to perform a task that can change depending on the situation. Passing a lambda allows the function to be flexible and do different things based on the lambda provided.

Example 1

fun performAction(action: () -> Unit) {
action() // Calls the lambda passed in as action
}

Example 2 — Setting Up a Click Listener

fun setupButtonClickListener(action: () -> Unit) {
button.setOnClickListener {
action() // Calling the lambda function passed as `action`
// You can see this as an additional wrapper over the onClick
// I have explained below the use case in which it is useful
}
}

// Usage in Activity or Fragment
setupButtonClickListener {
// Custom action to perform on button click
println("Button clicked!")
// Or navigate to another screen
}

The below is straightforward and works perfectly for most cases. You don’t need to wrap this in another function.

button.setOnClickListener {
println("Button clicked!")
// Perform actions directly here
}

Why did we wrap the simple onClickListener with a function?

Creating a wrapper function setupButtonClickListener might seem complex for just one button. But here’s when it can make sense:

Usecase 1 — When You Have Multiple Buttons: Suppose you have several buttons, each needing a similar action. A helper function can save code duplication by setting up the listener consistently.

Suppose you have three buttons, and you want them all to show a Toast message but with different text.

fun setupButtonClickListener(button: Button, message: String) {
button.setOnClickListener {
Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
}
}

// Usage in Activity or Fragment
setupButtonClickListener(button1, "Button 1 clicked!")
setupButtonClickListener(button2, "Button 2 clicked!")
setupButtonClickListener(button3, "Button 3 clicked!")

Usecase 2: Reusable Dialog Creator

Suppose you frequently need to show confirmation dialogs throughout your app. You can create a higher-order function to handle dialog creation, making your code cleaner and more maintainable.

// Define a typealias for clarity
// () - Unit will be replaced by 'DialogAction' in the code below
typealias DialogAction = () -> Unit

// Higher-order function to show a confirmation dialog
fun showConfirmationDialog(
title: String,
message: String,
onConfirm: DialogAction,
onCancel: DialogAction
) {
AlertDialog.Builder(this)
.setTitle(title)
.setMessage(message)
.setPositiveButton("Confirm") { _, _ -> onConfirm() }
.setNegativeButton("Cancel") { _, _ -> onCancel() }
.show()
}

// Usage in Activity or Fragment
// Can be used across multiple times instead of doing AlertDialog.Builder()
showConfirmationDialog(
title = "Delete Item",
message = "Are you sure you want to delete this item?",
onConfirm = {
// Code to delete the item
println("Item deleted")
},
onCancel = {
// Code to handle cancellation
println("Deletion canceled")
}
)

Benefits:

  • Reusability: You can use showConfirmationDialog wherever you need a confirmation dialog without rewriting the dialog setup code.
  • Clean Code: Keeps your Activity or Fragment code concise and focused on what should happen, not how the dialog is created.

Example 3 — Handling API Responses

When dealing with network requests, higher-order functions can help manage responses and errors efficiently.

// Function to fetch data from an API
fun <T> fetchData(
url: String,
onSuccess: (T) -> Unit, // Callback for success, takes a parameter of type T
onError: (Throwable) -> Unit // Callback for error, takes an Exception (Throwable)
) {
// Simulate network call
try {
// Assume we fetched data successfully
val data: T = /* fetched data */
onSuccess(data) // Call the success callback with the data
} catch (e: Exception) {
onError(e) // Call the error callback if there's an issue
}
}

// Example Usage in an Activity or ViewModel
fetchData<User>(
url = "https://api.example.com/user",
onSuccess = { user -> // Lambda for handling success
// Update UI with user data
println("User fetched: ${user.name}")
},
onError = { error -> // Lambda for handling error
// Show error message
println("Failed to fetch user data: ${error.message}")
}
)

5. Using typealias for Callback Function in Android

Let’s say we have multiple places where we need callbacks for error handling or showing confirmation messages. Using typealias makes this easier by simplifying the callback signature.

// Example 1
typealias IntCallback = (Int) -> Unit

fun doSomethingWithNumber(callback: IntCallback) {
callback(42) // Calls the callback with 42 as the argument
}

// Example 2
typealias CallbackWithTwoParams = (Int, Boolean) -> Unit

fun performOperation(callback: CallbackWithTwoParams) {
callback(42, true) // Example of calling the callback
}

Example: Callback Type Alias for a Network Operation — Define a callback type for a network operation:

typealias OnNetworkResponse = (Boolean, String) -> Unit

OnNetworkResponse is another name for a function that takes two parameters:Boolean (to indicate success/failure) and String (the message) and returns nothing (Unit).

// Instead of '(Boolean, String) -> Unit' we do 'OnNetworkResponse'
fun performNetworkRequest(callback: OnNetworkResponse) {
// Simulating network request
val success = true
val message = "Request completed"

// Call the callback with success and message
callback(success, message)
}

Summary of Example Use Cases

  • Basic Function: Formatting a string, calculating values.
  • No Return Value: Showing messages (Toast), logging actions.
  • Lambda as Parameter: Handling button clicks or other events that require customizable actions.
  • Callback: Managing asynchronous tasks (e.g., network requests) where completion needs handling.
  • typealias for Callback: Simplifying callback types, especially when the same callback structure is used in multiple places.

--

--

Dhananjay Trivedi
Dhananjay Trivedi

Written by Dhananjay Trivedi

Android | Flutter | App Developer | Product Management | Prompt Engineer who loves writing and sharing knowledge

No responses yet