Mastering Kotlin Functions for Clean Android Code
A Beginner’s Guide to Elegant, Reusable Solutions
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.