Write useful tool methods

So far, we’ve covered most of Kotlin’s systematic points. With so many Kotlin features in hand, do you know how to use them flexibly?

In fact, the rich syntactic features provided by Kotlin make it possible for us to extend the complexity of various apis and make them easier to use with special encapsulation. For example, the KTX library, which we’ve experimented with before, was specifically designed by Google to simplify the use of many apis. However, the functions covered by the KTX library are limited, so the most important thing is to cultivate the awareness of flexible use of various features of Kotlin. So in this Kotlin lecture, I’m going to take you through the simplification of a few common apis to write some useful tools.

Find the maximum and minimum values of N numbers

The ratio of two numbers to size this function, I believe every developer has encountered. If I want to get the larger of the two numbers, in addition to the basic if statement, I can use Kotlin’s built-in Max () function, as follows:

val a = 10
val b = 15
val larger  = max(a, b)
Copy the code

The code looks straightforward and easy to understand, so there doesn’t seem to be much need for optimization.

But now if we want to get the largest number out of three, how do we write it? Since the Max () function can only take two arguments, we need to compare the first two numbers, and then compare the larger number with the rest of the number, as follows:

val a = 10
val b = 15
val c = 5
val larger = max(max(a, b), c)
Copy the code

Does the code start to get complicated? To get the maximum of 3 numbers you need to use this nested Max () method, but what about 4 numbers, 5 numbers? Yes, at this point you should realize that we can simplify the use of the Max () function.

To recall, the vararg keyword we learned earlier, which allows a method to accept any number of parameters of the same type, fits our needs here. Then we can create a new max.kt file and customize a Max () function in it, as shown below:

fun max(vararg nums: Int): Int {
    var maxNum = Int.MIN_VALUE
    for (num in nums) {
        maxNum = kotlin.math.max(num, maxNum)
    }
    return maxNum
}
Copy the code

As you can see, Max () uses the vararg keyword in its argument declaration, which means it can now take any number of integer arguments. We then use a maxNum variable to record the maximum value of all numbers and initially assign it to the minimum value in the integer range. Then use a for-in loop to iterate through the nums argument list. If the number currently traversed is larger than maxNum, update the value of maxNum to that number, and finally return maxNum.

With just this layer of encapsulation, we can use the Max () function dramatically differently. For example, the same function can now be written as follows:

val a = 10
val b = 15
val c = 5
val larger = max(a, b, c)
println(larger)
Copy the code

So we get rid of the nested function calls altogether, and now we just keep passing arguments to Max (), whether we want to maximize 3 numbers or N numbers.

However, one drawback of our custom Max () function is that it can only maximize N integers. What if I want to maximize N floating-point or long integers? Of course, you can define multiple overloads of Max () to accept different types of arguments, because Kotlin’s built-in Max () function does the same thing. But this approach is too cumbersome to implement and produces a lot of repetitive code, so I’m going to use a more subtle approach here.

Java states that all types of numbers are Comparable and therefore must implement the Comparable interface, and this rule holds true in Kotlin as well. We can then modify the Max () function to accept any number of arguments that implement Comparable by using generics, as follows:

fun <T : Comparable<T>> max(vararg nums: T): T {
    if (nums.isEmpty()) {
        throw RuntimeException("Param can not be empty.")}var maxNums = nums[0]
    for (num in nums) {
        if (num > maxNums) {
            maxNums = num
        }
    }
    return maxNums
}
Copy the code

As you can see, the upper bound of the generic T is specified as Comparable, so the argument T must be a subtype of Comparable. Next, we determine if the nums argument list is empty, and if so, we actively throw an exception to remind the caller that the Max () function must pass arguments. The value of maxNum is then assigned to the value of the first argument in the nums argument list, and again the argument list is traversed, updating maxNum if a larger value is found.

This makes it easier to use the Max () function to maximize, say, three floating-point numbers:

 val a = 3.5 f
 val b = 4.8 f
 val c = 4.1 f
 val larger = max(a, b, c)
Copy the code

The Max () function now supports the maximum value of all subtypes that implement the Comparable interface, whether they are double, single, short, integer, or long.

And if you want to get a minimum of N numbers, you do it the same way, you just define a min() function, and I’ll leave that to you as an exercise.

Simplify the use of Toast

We’ve used Toast so many times in this book that you’re pretty familiar with it, but do you think it’s a bit of a chore?

To review the standard use of Toast, if you want a text prompt to pop up on the screen, you need to write:

Toast.makeText(context, "This is Toast", Toast.LENGTH_SHORT).show()
Copy the code

Is that a long piece of code? And I don’t know how many bugs people have had because they forgot to call the last show() method, causing Toast to fail to pop up.

Since Toast is such a common feature, it can be a pain to write such a long piece of code every time, and you should consider simplifying the use of Toast.

The makeText() method of Toast takes three arguments. The first argument is the context in which the Toast is displayed, which is essential. The second argument is what the Toast displays and needs to be specified by the caller, which can be either a string or a string resource ID. The third parameter is the length of the Toast display. Only totoast.LENGTH_SHORT and totoast.

We could add an extension function to the String class and an extension function to the Int class that encapsulates the logic for popping Toast. So every time you want to pop up a Toast prompt, you just need to call their extension function.

Create a new totoast. Kt file and write the following code in it:

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

fun Int.showToast(context: Context) {
    Toast.makeText(context, this, Toast.LENGTH_SHORT).show()
}
Copy the code

We add a showToast() function to the String and Int classes, respectively, and have them both take a Context argument. Then, inside the function, we still use the Toast native API usage, but change the popup to this and set the length of the Toast display to toasts.LENGTH_SHORT.

So how easy will it be to use Toast in the future with this extension? Experience it, such as the same pop-up text reminder can write:

"This is Toast".showToast(context)
Copy the code

So, do you think it’s a lot easier to write than the original Toast? If you want to pop up a string resource defined in strings.xml, it is also very simple:

R.string.app_name.showToast(context)
Copy the code

This calls the showToast() extension function we just added to the String and Int classes, respectively.

Of course, the problem with this notation is that the length of the Toast display is fixed. What if I want to use the toasts.LENGTH_LONG type now? The easiest way to solve this problem is to declare an additional display duration parameter in the showToast() function, but this adds complexity by passing an additional parameter every time the showToast() function is called.

Do you have any inspiration now? As a review, we learned about setting default values for functions in Chapter 2, when we studied basic Kotlin syntax. With this functionality, we can make it possible to dynamically specify the display duration of the showToast() function without adding complexity to its use. Modify the code in totoast. Kt to look like this:

fun String.showToast(context: Context, duration: Int = Toast.LENGTH_SHORT) {
    Toast.makeText(context, this, duration).show()
}

fun Int.showToast(context: Context, duration: Int = Toast.LENGTH_SHORT) {
    Toast.makeText(context, this, duration).show()
}
Copy the code

As you can see, we added a display duration parameter to the showToast() function, but also gave it a default parameter. The way we wrote the showToast() function before is not affected at all. The default is toast.length_short. If you want to use totoast.LENGTH_LONG, just write it like this:

"This is Toast".showToast(context, Toast.LENGTH_LONG)
Copy the code

Trust me, a Toast tool like this will make a huge difference to your productivity.

Simplify the use of Snackbar

Snackbar is similar to Toast, but a little more complicated.

To review the usual usage of Snackbar, look like this:

Snackbar.make(view, "This is Snackbar", Snackbar.LENGTH_SHORT)
    .setAction("Action") {
        // Handle the concrete logic
    }
    .show()
Copy the code

As you can see, the first argument to the make() method in Snackbar becomes View, and the first argument to the makeText() method in Toast is Context, Snackbar can also call the setAction() method to set an additional click event. Aside from these differences, the other uses of Snackbar and Toast are similar.

So how can we simplify an API for this structure? There are no fixed ways to simplify, and the one I’m going to show you is just one that I think is a good one.

Since the make() method takes a View argument, Snackbar uses this View to automatically find the outermost layout to display the Snackbar. Therefore, we can add an extension function to the View class that encapsulates the concrete logic for displaying the Snackbar. Create a new snackbar. kt file and write the following code:

fun View.showSnackbar(text: String, duration: Int = Snackbar.LENGTH_SHORT) {
    Snackbar.make(this, text, duration).show()
}

fun View.showSnackbar(resId: Int, duration: Int = Snackbar.LENGTH_SHORT) {
    Snackbar.make(this, resId, duration).show()
}
Copy the code

This code should be easy to understand, and is similar to the showToast() function. We just added the extension function to the View class and declared in the argument list what Snackbar will display and how long it will be displayed. In addition, Snackbar is similar to Toast in that the content displayed supports both the passing string and the string resource ID, so here we overload the showSnackbar() function with both parameter types.

Now to display a text prompt using Snackbar, just write this:

view.showSnackbar("This is Snackbar")
Copy the code

If Snackbar does not have a setAction() method, then this is where our simplification ends. But the setAction() method is one of Snackbar’s biggest features, and it would be pointless to write the showSnackbar() function without support.

At this point, the power of higher-order functions comes in handy again. We can have the showSnackbar() function take an additional function type argument to achieve full Snackbar functionality. Modify the code in snackbar. kt to look like this:

fun View.showSnackbar(text: String, actionText: String? = null, duration: Int = Snackbar.LENGTH_SHORT, block: (() -> Unit)? = null) {
    val snackbar = Snackbar.make(this, text, duration)
    if(actionText ! =null&& block ! =null) {
        snackbar.setAction(actionText) {
            block()
        }
    }
    snackbar.show()
}

fun View.showSnackbar(resId: Int, actionResId: Int? = null, duration: Int = Snackbar.LENGTH_SHORT, block: (() -> Unit)? = null) {
    val snackbar = Snackbar.make(this, resId, duration)
    if(actionResId ! =null&& block ! =null) {
        snackbar.setAction(actionResId) {
            block()
        }
    }
    snackbar.show()
}
Copy the code

As you can see, here we have added a function type argument to both showSnackbar() functions, and also a string or string resource ID to pass to the setAction() method. Here we need to set both of the new arguments to nullable types, set the default values to null, and then only call Snackbar’s setAction() method to set additional click events if neither argument is null. If a click event is triggered, simply call the function type argument to pass the event to an external Lambda expression.

The showSnackbar() function now has the full Snackbar functionality. For example, the example code at the beginning of this section can now be implemented as follows:

view.showSnackbar("This is Snackbar"."Action") {
    // Handle the concrete logic
}
Copy the code

So, isn’t the showSnackbar() function we wrote significantly simpler and easier to use than the Snackbar native API?

In the Kotlin class in this chapter, I took you to write a total of three tools, which respectively applied the knowledge of top-level functions, extension functions and higher-order functions, and of course used techniques like vararg, parameter default values and so on. Kotlin gives us so many great features that once you’ve learned all of them, it’s important to be able to use them flexibly. The three tools implemented in this lesson are just appetizers, and I look forward to seeing you write many of your own in the future that take full advantage of the great features That Kotlin has to offer.