Replace the callback interface with Higher-order function in Kotlin

Photo by Pavan Trikutam on Unsplash

At Google IO 2019 Google announced that the Kotlin programming language is now the preferred language for Android app developers.

Android development will become increasingly Kotlin-first.

That means everything they are going to build for Android will be developed in Kotlin first as their first-class language. If anyone has not started with Kotlin yet then now it’s a really good time to switch to Kotlin.

Motivation

It’s pretty simple and straightforward to start with Kotlin if we are building an app from scratch. But the real challenge is when we already have existing codebase in Java. So the question is: how do we do it with existing codebase?
The answer is: Do it in small steps. Don’t try to do it all at once. Convert small Java classes into Kotlin one by one, feature by feature or story by story.

Jetbrains did a fantastic job for this conversion. If we want to convert Java file into Kotlin, we just need to go to that Java file and press Cmd+Opt+Shift+K for Mac and Ctrl+Alt+Shift+K in Windows and see the magic.

But this blog is not about how to convert Java files into Kotlin. It’s more about what we can optimize after the conversion and how we can leverage a particular language feature: replacing callbacks with higher-order functions.

A higher-order function is a function that takes functions as parameters, or returns a function.

For more details follow this link

So in this blog, we are going to refactor the callback interface used in Java with higher-order functions in Kotlin. Let’s start with an example.

Note: We’ve used Android as an example here. But the idea can be adapted to any framework.

Getting Started

If we want to refactor, the essential precondition is having solid tests.

Refactoring- Martin Fowler

Before we start any refactoring we need to follow the first rule of refactoring i.e to have tests in first. If we already have tests then it’s great and can move ahead. If not, then we need to add tests first.

This is our normal RecycleView adapter class which has onClick callback on user selection.

public class UserAdapter extends RecyclerView.Adapter<UserViewHolder> {
    
    private UserSelection userSelection;
    public UserAdapter(UserSelection userSelection) {
        this.userSelection = userSelection;
    }
    // On our UserViewHolder  
   itemView.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            userSelection.onClick(users.get(getLayoutPosition()));
        }
   });
}

Since Activity is our Client here, we register and listen to that callback in the UserActivity.

public class UserActivity extends AppCompatActivity {
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
       //...
        UserAdapter userAdapter = new UserAdapter(new UserSelection() {
            @Override
            public void onClick(User user) {
                // Do something with the selected user
            }
        });
    }
}

So after having tests, the first step we need to do is to convert this Java file into Kotlin using IntelliJ shortcut and after conversion, it looks like this.

class UserAdapter(private val userSelection: UserSelection) :
                       RecyclerView.Adapter<UserViewHolder>() {
  //...
}
interface UserSelection {
    fun onClick(user: User)
}
class UserActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        //..
        val userAdapter = UserAdapter(object : UserSelection {
            override fun onClick(user: User) {
               // Do something
            }
        })
    }
}

Note: This will work fine in Java class which are small. In the case of large and complex Java file, we need to do some changes manually and be careful about it.

So now when we have converted in Kotlin we can use higher-order functions now.

Mechanics

1. Figure it out the signature of the higher-order functions. In the above example, in the interface, we need to pass a User to the callback and want nothing in return. So this how our signature will look like callback: (User)-> Unit where Unit is void and returns nothing.

class UserAdapter(private val callback: (User) -> Unit)
    : RecyclerView.Adapter<UserViewHolder>()

2. Replace the signature in UserAdapter constructor with the above signature and make it private property val so that it can be accessible to only it’s class.

3. To trigger the callback we need to have a User object to pass in a function. So we pass User by calling invoke function on callback like this callback.invoke(user).

itemView.setOnClickListener {
    val user = users[layoutPosition]
    callback.invoke(user)
}

4. If the interface has only one usage then we can directly delete it but if that not a case then we need to find all the usage and replace it one by one and until no usage found. After that, we can safely delete the interface.

Note: If we have written tests for this class then it’s recommended that whenever we do a small change or move something we need to compile and run tests on each step.

5. The client i.e UserActivity will now have a lambda function annotated by {} braces. Since we have only one parameter in the callback we can directly access user object using it parameter in Kotlin.

val userAdapter = UserAdapter {
val fullName = "${it.firstName} {${it.lastName}"
}

Extension Function in Higher-order function

This is an add-on feature. Extension functions provide the ability to extend a class with new functionality without having to inherit from the class. Suppose we have callback: (User)-> Unit we can convert this into extension funtions using .() notation. So this will become callback: User.()-> Unit what we are doing is here is creating extension function on User class which is accessible within this UserAdapter class scope. That means this extension function is available only in the scope of UserAdapter, we won’t be able to use this function outside this class.

Now we can directly call a method on User object within the UserAdapter class like this.

itemView.setOnClickListener {
val user = users[layoutPosition]
user.callback()
}

From Client i.e UserActivity.

val userAdapter = UserAdapter {
val fullName = "$firstName $lastName"
}

Note: The naming of function callback is created on the basis of the higher-order function parameter name.

Under the Hood

If we convert this lambda in Java then you will see that there is no difference between an interface and a higher-order function. They both create an anonymous class under the hood. So there is no performance impact on using the high-order function. So you would ask then why to use a higher-order function instead of the interface? The answer, for now, is the readability and language feature leverages. Less code will have fewer errors. This how it looks in Java.

private UserAdapter userAdapter = new UserAdapter(new Function1<User, Unit>() {
    @Override
    public Unit invoke(User user) {
        return null;
    }
});

Gotchas !!

1. Java interoperability: Higher-order function works very well when all our codebase in Kotlin. But when you are migrating piece by piece from Java to Kotlin. There would be still some Java classes in the codebase which need to call the Kotlin code. The good thing in Kotlin is that it’s nicely interop with Java. But when it comes to higher-order functions, it becomes a little messy in Java.

private UserAdapter userAdapter = new UserAdapter(new Function1<User, Unit>() {
@Override
public Unit invoke(User user) {
return null;
}
});

In the above code, we can see that the callback function in Java looks a little messy and hard to read. So while refactoring we need to consider this issue. Anyway if we are planning to move into full Kotlin than someday that java class will be converted in Kotlin which will fix this issue.

2. Inline functions: This looks good when we have a simple callback. But what if that callback is added in a loop? If that’s the case then it will create N number of Function1 objects which is not good for the app performance. So the solution for this is to mark the functions as inline using inline keyword on the function like this.

class UserAdapter(inline val callback: User.() -> Unit)

For more details on Inline function, I highly recommend checking these resources.

Conclusion

Kotlin is a great language. We should take all possible leverage of the feature it provides. Hope this article will help you for this Java to Kotlin migration.

If you like the article please share to other developers who are in the process of migrating from Java to Kotlin.

In case of any feedback, please do share in the comment section. The best way to reach me is Twitter.

Photo by : Pavan Trikutam on Unsplash

Thank you. Keep reading. Keep sharing 🙂

Site Footer