原文地址: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.