原文地址:ANDROID KTX – ANDROID DEVELOPMENT WITH KOTLIN
[正在翻译中]
介绍
Android KTX is an open source library or set of functionalities designed to make the Android development with Kotlin even more pleasant. You can find its website here. The abbreviation KTX stands for Kotlin Extensions, so this library is basically a set of extension functions, extension properties and other top-level functions. In this article, we take a look at what’s inside this library and how we can take advantage of it. This library’s goal is not to add new features to the existing Android APIs, but rather make those APIs easier to use by leveraging the features of the Kotlin language.
Android KTX 是一个开源的库,或者说是一个开源的方法集。它的设计目的主要是为了让开发者在开发Android应用的时候更愉快。这是它的官方网站。“KTX”是“Kotlin Extensions",因此这个库本质上是一个扩展方法集,属性集和顶级函数集。在这篇文章里,我将带领大家看一下在这个库里究竟有什么,我们将如何利用这库来提高我们的开发效率。这个库的目标不是为了为已有的Andorid API增加新的特性,而是为了帮助我们通过使用kotlin语言来使我们在开发Android应用的时候更简洁。
Structure of Android KTX A very important thing to note at the beginning is that Android KTX provides functionalities which would be added to many individual projects by the developers most of the time anyway. Arguably, there are also many things that will get included by adding the dependency, which are not going to be used. Thanks to the ProGuard, all the unused code will get stripped out, so there should be no worry regarding the library footprint. Now, let’s take a look at some of the very important and crucial concepts of the Kotlin language which are used for building the Android KTX.
Android KTX 结构
首先我们需要注意一个非常重要的事情:Android KTX 提供了一系列的功能,这些功能会被开发者完整的添加到各个不同的项目中去。显然,有的项目只用到了Android KTX中的一部分功能,但却不得不把整个库添加进来。好在有ProGuard,它可以把所有没用到的代码全部清除掉。所以我们没必要为这个事担心。现在,让我们一起看下几个Kotlin 语言中非常重要的概念,正是由于Kotlin语言的这些特性,才构建出了Android KTX的库。
Extension functions An extension function is one of the Kotlin features that allows us to add a functionality to an existing class without modifying it directly. By adding extension functions to a class, they are represented as simple static methods on bytecode level. Then, we can call those functions on the objects of that class type, just as they were part of the class’s API initially. Here is an example to give a little better insight into the way it’s done. Say, we want to add an extension function to the String class to print itself. Here is how we can do it:
扩展函数
扩展函数是Kotlin的一个特性,他可以让我们不改变现有类的代码的情况,给现有类添加新的方法。当我们给一个现有类添加方法的时候,该方法在字节码层级是以静态方法的形式存在的。所以我们可以直接在这个类new出来的对象上调用这个方法,就像这个类本省就有这个方法一样。通过下面这个例子,我们来看下它具体是怎么实现的。例如,我们想要给String类添加一个方法,让String可以把自己打印出来。我们可以这样做:
fun String.printSelf() {
println(this)
}
复制代码
Inside the extension function, we can use this to refer to the current object on which the function is being executed. Then, this function becomes available to be called on any object of String type. So we can do:
在这个扩展方法内部,我们可以直接通过this引用调用这个方法的对象。这样,我们就可以在任何一个String类的对象上调用这个方法。如下:
fun usage() {
"Kotlin Rocks".printSelf()
}
复制代码
Extension properties Similarly to extension function, Kotlin supports extension properties. Also, the way we define them is quite the same:
扩展属性
和扩展方法类似,Kotlin支持扩展属性。我们可以相同的方式来定义扩展属性:
val String.isLongEnough: Boolean
get() = this.length > 5
复制代码
Then, we can use this extension property on any object of String type:
然后我们可以在任何String类对象上使用这个扩展属性:
"Kotlin".isLongEnough
复制代码
The behavior of the extension property can only be defined by explicitly providing getter (plus setter for vars). Initializing those properties the ordinary way doesn’t work. The reason behind this is that an extension property does not insert members into the type, so there is no efficient way for it to have a backing field.
扩展属性这个特性只能通过显示的提供getter的方式来实现(变量还需要提供setter)。我们不能通过指定初始值的方式对扩展属性进行初始化。因为扩展属性并没有真的为对应的类添加一个成员变量,所有没一个成员变量来存储初始化的值。
Top level functions In Kotlin, a function is a first-class citizen. We are allowed to define functions in Kotlin files (.kt) which could afterward be accessed and used from other Kotlin files. This is a very powerful concept. If we define a file inside a package com.example and define functions in it, they can be used simply by importing them into the usage side. Here is an example:
顶级函数
在Kotlin中,函数是第一公民。我们可以在Kotlin文件(.kt)中定义方法,然后在其他的Kotlin文件中访问或调用它。这个一个非常强大的特性。如果我们在包com.example创建一个.kt文件,然后在这个文件中定义一个方法,我们就可以通过简单的导包的方式来使用它。例如:
package com.example
fun sum(x: Int, y: Int): Int = x + y
复制代码
现在我没就可以通过简单的导入在任何.kt文件中使用它:
import com.example.sum
fun test() {
println(sum(1, 2))
}
复制代码
An important note here is the access modifier. In the example above, the function sum does not have defined an access modifier, and in Kotlin it’s public by default. Being public, it makes the sum function accessible from any other Kotlin file. Kotlin also has an internal access modifier keyword, which would make this function accessible only in the module where this file exists (a module could contain many different packages). Ultimately, the function could also be private which will make it accessible only from inside the file where it is defined.
这里,有很重要的一点你需要注意,那就是访问修饰符。在上面那个例子中,sum方法在定义的时候并没有指定访问修饰符,在Kotlin中,它默认就是public的。作为一个public的方法,sum可以在任何.kt文件中访问。kotlin还有一个访问修饰符"internal",被它修饰的方法只能在同一module中的其他.kt文件访问(一个module可能包含多个package)。最后,还有一个修饰符"private"。被"private"修饰的方法只能被同一个.kt文件下的其他地方被访问。
Default arguments Most probably, you are familiar with the overloading concept like in Java already. Overloading is a concept that allows us to define constructors or methods with the same signature, only differing in their parameter list (both types and number of parameters could be different). In Kotlin, this concept is taken a step further, so we could achieve the same result by defining a single function, by specifying default values to some or all of the arguments. Eventually, it boils down to the approach used in Java again. Let’s take a look at the following example:
fun greet(firstName: String, lastName: String = "") {
println("Hello $firstName $lastName")
}
复制代码
In this example, the function takes two arguments, but the lastName is optional because by default its value is going to be an empty String. So, when calling this function we are only required to supply a firstName, and we can call it this way:
greet("John")
复制代码
Kotlin also supports named arguments, so we could call a function supplying the arguments by their names:
greet("John", lastName = "Doe")
复制代码
This is particularly useful when we have a function with multiple optional arguments and we want to call it with supplying only specific ones, or in a different order.
You can read more about Kotlin’s awesome features in this article.
Deep Dive into Android KTX Now, as we know what the Android KTX library is based on, let’s dig and observe some of the most common extension functions.
Converting URL to URI To begin with, there is a very simple extension function on the Uri class. Many times in Android, we need to convert a String URL into a Uri object, for instance when creating an Intent with data etc. The way we are usually doing this is by calling Uri.parse("string_url"). Android KTX defines an extension function that does the job. Here is how it looks like:
inline fun String.toUri(): Uri = Uri.parse(this)
复制代码
so we can use it by calling toUri() on any string, like this:
"any_sting_url".toUri()
复制代码
Editing shared preferences Next, let’s take a look at an extension function defined on the SharedPreferences interface. The usual way of putting values into the SharedPreferences in Android is to call edit() in order to obtain the SharedPreferences.Editor instance. Then, we can insert the values by calling editor.putType("key", typeValue). After that, it is very important to call apply() or commit() on the editor, in order for the values to be stored in the SharedPreferences. Many times we forget doing so, and we waste some time debugging until we notice what is happening. A usual example of storing values in the SharedPreferences looks like this:
val editor = sharedPreferences.edit()
editor.putString("key", value)
editor.apply()
复制代码
By using the Android KTX extension on the SharedPreferences the code shortens and simplifies quite a lot, and it becomes:
sharedPreferences.edit {
putString("key", value)
}
复制代码
The relevant extension function looks like this:
inline fun SharedPreferences.edit(
commit: Boolean = false,
action: SharedPreferences.Editor.() -> Unit
) {
val editor = edit()
action(editor)
if (commit) {
editor.commit()
} else {
editor.apply()
}
}
复制代码
The first parameter to this function is a Boolean value that controls the call to the editor, whether it would use the commit() or the apply() call. Clearly, by default this value is set to false which means by default the function will call apply() on the editor. A more interesting parameter is the second one. It’s a function literal with a receiver, and the receiver is of type SharedPreferences.Editor. It means that when calling this function, we can use lambda over the receiver, so we can directly call functions that are exposed by the receiver type without additional qualifiers. This is shown with the putString call.
Operating on view before drawing Most of the apps we are using every day are having some sort of lists where some images are being loaded. Often, those images are of a different size, and the image sizes are usually provided in the response. Since the images are normally loaded in the background, we want to allocate the space required for the image to be displayed, and once it’s loaded we already have the space allocated, so we would avoid UI expanding when the image is being displayed, which prevents the UI flickering effect. This is usually done by using a ViewTreeObserver which provides an OnPreDrawListener. This listener has a callback that is being called before the view drawing. For our images example, usually we set the view sizes that are provided in the response inside this callback, and we apply some default background (for example gray). That is one of the many use cases of the ViewTreeObserver observer and its OnPreDrawListener. Here is a snippet that shows the way we normally approach it:
view.viewTreeObserver.addOnPreDrawListener(
object : ViewTreeObserver.OnPreDrawListener {
override fun onPreDraw(): Boolean {
viewTreeObserver.removeOnPreDrawListener(this)
performSomethingOverTheView()
return true
}
})
复制代码
Android KTX has defined an extension function on the View type named doOnPreDraw() that simplifies the above snippet, so it would become:
view.doOnPreDraw {
performSomethingOverTheView()
}
复制代码
There are also some other nice extensions defined on the View type which are working with the view visibility, updating view padding or layout params etc.
Working with bundle Bundles are a very common thing in Android, but working with bundles is often quite a boilerplate. The way we normally compose a bundle looks like this:
val bundle = Bundle()
bundle.putString("key", "value")
bundle.putString("keyBoolean", true)
bundle.putString("keyInt", 1)
复制代码
Android KTX defines a top-level function named bundleOf(), and by using it, composing bundle becomes a lot nicer:
val bundle = bundleOf("key" to "value", "keyBoolean" to true, "keyInt" to 1)
复制代码
There is also a persistableBundleOf() function for creating a PersistableBundle but it’s available for API version 21 and up. Similarly, there is a contentValuesOf() function that could be used in the same way as the functions for creating bundle above, and it returns a ContentValues object.
Iteration over view group Working with ViewGroup in Android is quite a common thing. The ViewGroup is a kind of container that could contain other views called children. Many times we need to loop through its children, but the traditional way to do so could be quite a mess. Android KTX has defined an extension property that exposes its children, and here is how it looks like:
val ViewGroup.children: Sequence<View>
get() = object : Sequence<View> {
override fun iterator() = this@children.iterator()
}
复制代码
As we can see, the children property is returning a sequence of child views, and it allows us to write very concise loops over any ViewGroup type:
viewGroup.children.forEach {
doSomething(it)
}
复制代码
Displaying toast One of the most common ways of displaying some sort of short info to the user is a Toast. When displaying a toast by using the standard API, we have to pass the Context, the actual message that we want to be displayed, whether it is a String or a resource reference to load value from the strings.xml, and the toast duration. Since we cannot display a toast without having a Context, and since most of the time the toast duration is its Toast.LENGTH_SHORT constant, it would be great to have an option to display a toast by simply calling a function and passing the actual message argument. A common way to achieve this is by defining some sort of base class that defines such method, and then it would be called from the subclasses. However, Android KTX defines an extension functions to the Context type for displaying toasts. It takes 2 arguments, the message and the duration, while the duration is being set to Toast.LENGTH_SHORT by default. So we can call to display toast wherever we have a Context by simply passing the message argument, whether it is a String type or a reference to the string resources:
toast("Hello")
toast(R.string.hello)
复制代码
Wrap Up Android KTX is a very nice part of the Android JetPack project, and as we’ve seen it contains quite some nice ways to improve the Android development we are used to. As told before, it doesn’t provide any new functionalities, but rather simplifies the APIs which are already provided by the Android SDK. Here is a nice talk from the Google I/O 2018 by Jake Wharton, where he elaborates more on the Android KTX. This article only scratches the surface, and there are many more fun things to be revealed inside the Android KTX, related to animation, database, location, graphics, text and so on. Also, the team is welcoming new contributions, which is great, so if you have an idea that is not there yet, feel free to submit it.