Lambda’s in Kotlin

Once you understand them, they seem so simple and you wonder how you ever had a problem with them. Early in my career, I definitely spent countless nights studying and trying to wrap my head around lambda’s in Swift; terrified to tell anyone that I struggled to understand them.

Years later, I’ve come to find that it’s a very common place for even seasoned engineers to get stuck on, especially if their early experiences were with a non-functional language. I’m here to hopefully dispel some of the confusion surrounding them.

In short, they’re just functions that you can pass around. They can be used for configuration, dependency injection, and when using Jetpack Compose, they’re highly recommended for use in State Hoisting.

For Java Engineers

Perhaps most recognizable for Java engineers are callbacks implemented as SAM interfaces. A few recognizable examples standout as still being widely used within Kotlin today:

  • Runnable: void run()
  • Callable: V call()
  • Comparator: int compare(T o1, T o2)
  • Consumer: void accept(T t)
  • Predicate: boolean test(T t)
  • Function: R apply(T t)
  • Supplier: T get()

Simply put, they’re interfaces annotated with @FunctionalInterface. They have a single function and look like this:

@FunctionalInterface
public interface ConfigurableAction {
    void execute(View view, String data);
}

A custom MaterialButton in Java using the interface would be written this way:

public class ConfigurableButton extends MaterialButton {

    private ConfigurableAction action;
    private String data = "";

    public ConfigurableButton(Context context) {
        this(context, null);
    }

    public ConfigurableButton(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public ConfigurableButton(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                if (action != null) {
                    action.execute(ConfigurableButton.this, data);
                }
            }
        });
    }

    public void configure(String data, ConfigurableAction action) {
        this.data = data;
        this.action = action;
    }
}

An early Java implementation would have you configuring the button as an anonymous class; using the new ConfigurableAction() and override approach:

button.configure("user_123", new ConfigurableAction() {
    @Override
    public void execute(View view, String data) {
        Toast.makeText(this, "Data: " + data, Toast.LENGTH_SHORT).show();
    }
});

If you’re using Java 8+, the IDE would likely suggest that you implement it as a lambda; which really just means you remove new ConfigurableAction() , the function override, and bring the parameters from the execute function up to the where new ConfigurableAction() was previously:

button.configure("user_123", (view, data) -> {
    Toast.makeText(this, "Data: " + data, Toast.LENGTH_SHORT).show();
});

In Kotlin, with the interface being the last parameter of the function, we can make it look a little nicer:

button.configure("user_123") { view, data ->
    Toast.makeText(this, "Data: $data", Toast.LENGTH_SHORT).show()
}

The previous three snippets are equivalent. The second snippet is nearly perfectly valid Kotlin code:

button.configure("user_123", { view, data ->
    Toast.makeText(context, "Data: " + data, Toast.LENGTH_SHORT).show()
})

The third snippet is the result of taking the IDE’s suggestion for moving the lambda argument of the most recent example out of the parenthesis.

For Jetpack Compose Engineers

Life gets a whole lot easier in a lot of ways with Kotlin. Jetpack Compose is just the chef’s kiss. The same interface and button from the Java example implemented in Kotlin and as a Composable is simply this:

fun interface KConfigurableAction {
    fun execute(data: String)
}

@Composable
fun ConfigurableButton(
    modifier: Modifier = Modifier,
    data: String,
    action: KConfigurableAction
) {
    Button(
        modifier = modifier,
        onClick = { action.execute(data) }
    ) {
        Text("Click me!")
    }
}

A view implementing the above button would do it this way:

@Composable
fun ButtonView() {
    val context = LocalContext.current
    ConfigurableButton(
        data = "Foo"
    ) { data ->
        Toast.makeText(context, "Data: " + data, Toast.LENGTH_SHORT).show()
    }
}

But wait. We don’t actually need the fun interface (Kotlin’s way of dropping the need for an annotation for a common use case) at all and it’s probably just going to be confusing and seem like a waste of keystrokes (it is) for the majority of engineers who’ve had the pleasure of working with Kotlin for a decent amount of time. So we would simply implement ConfigurableButton with a lambda in the signature and never consider an interface for this scenario:

@Composable
fun ConfigurableButton(
    modifier: Modifier = Modifier,
    data: String,
    action: (data: String) -> Unit
) {
    Button(
        modifier = modifier,
        onClick = { action(data) }
    ) {
        Text("Click me!")
    }
}

With the above implementation, ButtonView doesn’t change at all; it perfectly understands that it has a matching signature.
Since the base Button‘s onClick parameter in Compose doesn’t expect an argument and you may not always need one, another sample button could be created like:

@Composable
fun ConfigurableButtonToo(
    modifier: Modifier = Modifier,
    action: () -> Unit
) {
    Button(
        modifier = modifier,
        onClick = action
    ) {
        Text("Click me!")
    }
}

Notice how action is just passed directly to the onClick parameter without any curly braces at all. This would still be perfectly valid though: onClick = { action() }, it’s just another waste of keystrokes.

To close out this section, we can simply say that if two lambda’s have the same signature you can just pass one into the other without using curly braces or parentheses at all. If they have different signatures, you’ll need to expand the receiver with curly braces and populate the parameters.

Advanced Usages

One thing that really strikes you as odd the first time you see it is that you can directly reference a function, perhaps from a ViewModel, and use it as a value for the lambda. Consider the following example where the ViewModel has a function with a signature matching Button’s onClick parameter:

class ButtonViewModel : ViewModel() {
    var data: String by mutableStateOf("")
        private set

    fun onSetData() {
        data = "Primary button clicked!"
    }
}

@Composable
fun ButtonView() {
    val viewModel: ButtonViewModel = viewModel()
    Button(
        onClick = viewModel::onSetData
    ) {
        Text("Primary button")
    }
}

Notice the double colon’s which say “go to this class and use this function as the lambda.” Not being aware of this capability, you would be inclined to just open curly braces and call the ViewModel’s function “appropriately”, which is still perfectly valid and the IDE won’t make a suggestion at all (currently, at least):

@Composable
fun ButtonView() {
    val viewModel: ButtonViewModel = viewModel()
    Button(
        onClick = { viewModel.onSetData() }
    ) {
        Text("Primary button")
    }
}

But once again, why use so many keystrokes and reach for those symbols while the colon is resting under your pinkie finger? Unless you’ve moved away from QWERTY (as you should) and your layout has the colon elsewhere… which I’m sure is still more convenient than curly braces and parentheses.

In Conclusion

I hope you enjoyed reading and learned a thing or two. If you have questions, comments, or a request for understanding a more complex use case, please feel free to let me know!

Total
0
Shares
Leave a Reply

Your email address will not be published. Required fields are marked *

Previous Post
loop-marketing-examples-from-companies-we-love

Loop marketing examples from companies we love

Next Post

Unlocking Peak Performance on Qualcomm NPU with LiteRT

Related Posts