DEV Community

Cover image for Lambda's in Kotlin
Jacob Jerrell
Jacob Jerrell

Posted on • Originally published at machinehead.Medium

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>: V call()
  • Comparator<T>: int compare(T o1, T o2)
  • Consumer<T>: void accept(T t)
  • Predicate<T>: boolean test(T t)
  • Function<T, R>: R apply(T t)
  • Supplier<T>: 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);
}
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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();
    }
});
Enter fullscreen mode Exit fullscreen mode

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();
});
Enter fullscreen mode Exit fullscreen mode

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()
}
Enter fullscreen mode Exit fullscreen mode

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()
})
Enter fullscreen mode Exit fullscreen mode

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!")
    }
}
Enter fullscreen mode Exit fullscreen mode

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()
    }
}
Enter fullscreen mode Exit fullscreen mode

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!")
    }
}
Enter fullscreen mode Exit fullscreen mode

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!")
    }
}
Enter fullscreen mode Exit fullscreen mode

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")
    }
}
Enter fullscreen mode Exit fullscreen mode

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")
    }
}
Enter fullscreen mode Exit fullscreen mode

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!

Top comments (0)