preface

As you know, Kotlin is the android development language that Google is pushing to replace Java. Kotlin is easy to use and has a lot of syntactic sugar. This article focuses on some practical Kotlin techniques

Custom rounded rectangle

In projects, we often have to define the background of a rounded rectangle, usually with a custom drawable, but the background and the corners of a rounded rectangle often change slightly, and once that happens we have to create a new drawable file which can cause the file to explode

We can use Kotlin’s extension function to achieve a simple and convenient rounded rectangle background

fun View.setRoundRectBg(color: Int = Color.WHITE, cornerRadius: Int = 15.dp) {
    background = GradientDrawable().apply {
        setColor(color)
        setCornerRadius(cornerRadius.toFloat())
    }
}
Copy the code

For a View that needs a custom background, call setRoundRectBg directly, simple and convenient

Reified use

Reified, the generic instantiation keyword in Kotlin, makes something abstract more concrete or real. Let’s give two examples of how to use reified

StartActivity example

So this is what we usually write for startActivity

startActivity(context, NewActivity::class.java)  
Copy the code

We define an extension function with reified

// Function
inline fun <reified T : Activity> Activity.startActivity(context: Context) {
    startActivity(Intent(context, T::class.java))
}

// Caller
startActivity<NewActivity>(context)
Copy the code

With reified, you pass a simplified generic parameter by adding the type so that you don’t have to pass the type manually

Gson parsing examples

Let’s first take a look at how we normally parse JSON using GSON. In a Java serialization library like GSON, when you want to deserialize the JSON string, you eventually have to pass a Class object as an argument so that Gson knows what type you want.

User user = new Gson().fromJson(getJson(), User.class)
Copy the code

Now, let’s demonstrate the magic of the reified parameter by creating a very lightweight extension function to wrap the Gson method:

inline fun <reified T> Gson.fromJson(json: String) = 
        fromJson(json, T::class.java)
Copy the code

Now, in our Kotlin code, we can deserialize JSON strings without even passing type information at all!

val user: User = Gson().fromJson(json)
Copy the code

Kotlin inferred the type from its usage – since we assigned it to a variable of type User, Kotlin used it as a type parameter for fromJson ()

The KOtin interface supports SAM conversion

What is SAM conversion? Some students may not understand, here first popular science:

SAM Conversions, or Single Abstract Method Conversions, are Conversions for interfaces that have only one non-default Abstract Method — for interfaces that meet this condition (called SAM Type), In Kotlin, it is possible to use the term Lambda directly — provided, of course, that the type of function represented by the Lambda matches the interface’s notation.

Before Kotlin1.4, Kotlin does not support SAM conversion for Kotlin. Only Java SAM conversion is supported. The official explanation is that Kotlin already has function types and higher order functions, so there is no need to do SAM conversion. This explanation doesn’t go down well with developers, if you’ve used Java Lambda and Fuction Interface. When you switch to Kotlin, you get confused. It looks like Kotlin is either aware of this, or seeing feedback from the developers, and is finally supportive.

Before 1.4, only one object could be passed, and Kotlin SAM was not supported. After 1.4, Kotlin SAM is supported, but with a slight change in usage. Interfaces need to be declared with the fun keyword. Once you’ve tagged an interface with the fun keyword, you can pass a lambda as an argument as long as you take such an interface as an argument.

// Declare with fun keyword
fun interface Action {
    fun run(a)
}

fun runAction(a: Action) = a.run()

fun main(a){
	// Before 1.4, only objects could be used
    runAction(object : Action{
        override fun run(a) {
            println("run action")}})// 1.4-m1 supports SAM,OK
    runAction {
        println("Hello, Kotlin 1.4!")}}Copy the code

entrust

Sometimes, the way to get things done is to delegate them to someone else. I’m not suggesting that you delegate your work to a friend. I’m suggesting that you delegate the work of one object to another.

Of course, delegation is nothing new in the software industry. Delegation is a design pattern in which an object delegates a helper object, called a delegate, to process a request. The proxy is responsible for processing requests on behalf of the original object and making the results available to the original object.

Commissioned by class

For example, let’s say we want to implement an enhanced version of ArrayList that supports restoring the last deleted item

One way to implement this use case is to inherit from the ArrayList class. Because the new class inherits from the concrete ArrayList class rather than implementing the MutableList interface, it is highly coupled to the implementation of ArrayList. How nice it would be if all you needed to do was override the remove() function to keep references to the deleted items and delegate the remaining empty implementations of MutableList to other objects. To achieve this, Kotlin provides a way to delegate most of the work to an internal ArrayList instance and customize its behavior, introducing a new keyword: by.

<! -- Copyright2019 Google LLC.
SPDX-License-Identifier: Apache-2.0 -->
class ListWithTrash <T>(private val innerList: MutableList<T> = ArrayList<T>()) : MutableCollection<T> by innerList {
	var deletedItem : T? = null
	override fun remove(element: T): Boolean {
	       deletedItem = element
			return innerList.remove(element)
	}
	fun recover(a): T? {
		return deletedItem
	}
}
Copy the code

The by keyword tells Kotlin to delegate the functionality of the MutableList interface to an internal ArrayList called innerList. ListWithTrash still supports all the functions in the MutableList interface by bridging the methods of the internal ArrayList object. In the meantime, now you can add your own behavior.

Attribute to entrust

In addition to class proxying, you can also use the BY keyword for property proxying. By using the property proxy, the proxy class handles the calls to the corresponding property get and set functions. This feature is useful when you need to reuse getter/setter logic across other objects, as well as allowing you to easily extend simple field support

For example, using the delegate attribute to encapsulate SharedPreference, delegating data store operations to a proxy class has several benefits. 2. Decouple with SP. If you want to replace the repository later, you only need to modify the proxy class

The call is as follows:

object Pref: PreferenceHolder() {
    var isFirstInstall: Boolean by bindToPreferenceField(false)
    var time: Long? by bindToPreferenceFieldNullable()
}
Copy the code

Concrete implementation visible :SharedPreferences with Kotlin should be written like this

LiveData with status

At present, we are using more and more MVVM mode and ViewModel in the development process. We also often use LiveData to identify the network request state. We need to define request start, request success, request failure, three LiveData

This is also very redundant code, so we can do some encapsulation, encapsulation of a state of LiveData

The definition is as follows:

typealias StatefulLiveData<T> = LiveData<RequestState<T>>
typealias StatefulMutableLiveData<T> = MutableLiveData<RequestState<T>>

@MainThread
inline fun <T> StatefulLiveData<T>.observeState(
    owner: LifecycleOwner.init: ResultBuilder<T>. () - >Unit
) {
    val result = ResultBuilder<T>().apply(init)

    observe(owner) { state ->
        when (state) {
            is RequestState.Loading -> result.onLading.invoke()
            is RequestState.Success -> result.onSuccess(state.data)
            is RequestState.Error -> result.onError(state.error)
        }
    }
}
Copy the code

Use the following

val data = StatefulMutableLiveData<String>()
viewModel.data.observeState(viewLifecycleOwner) {
            onLading = {
                //loading
            }
            onSuccess = { data ->
                //success
            }
            onError = { exception ->
                //error}}Copy the code

Through the above encapsulation, the loading,success and error states of network requests can be elegantly and succinct. The code is simplified and the structure is clear

DSL

Domain Specific Language (DSL) : a computer language that specializes in solving a particular problem, such as the familiar SQL and regular expressions. However, if you create a separate language to solve a domain-specific problem, the development and learning costs are high, hence the concept of an internal DSL. Internal DSLS are built using a general-purpose programming language. For example, the Kotlin DSL mentioned in this article, let’s make a simple definition for the Kotlin DSL:

“Domain-specific apis with unique code structures developed in Kotlin.”

For example, if we want to add listeners to a TabLayout, we need to implement the following three methods

override fun onTabReselected(tab: TabLayout.Tab?).{}override fun onTabUnselected(tab: TabLayout.Tab?).{}override fun onTabSelected(tab: TabLayout.Tab?).{}Copy the code

In fact, we usually only use the onTabSelected method, and the other two are usually empty implementations. We use a DSL to wrap the OnTabSelectedListener so that we don’t have to write unnecessary empty implementations

The specific implementation is as follows:

private typealias OnTabCallback = (tab: TabLayout.Tab?) -> Unit

class OnTabSelectedListenerBuilder : TabLayout.OnTabSelectedListener {

    private var onTabReselectedCallback: OnTabCallback? = null
    private var onTabUnselectedCallback: OnTabCallback? = null
    private var onTabSelectedCallback: OnTabCallback? = null

    override fun onTabReselected(tab: TabLayout.Tab?).= onTabReselectedCallback? .invoke(tab) ? :Unit

    override fun onTabUnselected(tab: TabLayout.Tab?).= onTabUnselectedCallback? .invoke(tab) ? :Unit

    override fun onTabSelected(tab: TabLayout.Tab?).= onTabSelectedCallback? .invoke(tab) ? :Unit

    fun onTabReselected(callback: OnTabCallback) {
        onTabReselectedCallback = callback
    }

    fun onTabUnselected(callback: OnTabCallback) {
        onTabUnselectedCallback = callback
    }

    fun onTabSelected(callback: OnTabCallback) {
        onTabSelectedCallback = callback
    }

}

fun registerOnTabSelectedListener(function: OnTabSelectedListenerBuilder. () - >Unit) =
        OnTabSelectedListenerBuilder().also(function)

Copy the code

The general steps for defining a DSL:

  • 1. Define a class to implement the callback interface and implement its callback methods.
  • 2. Observe the parameters of the callback method, extract them into a function type, and use the type alias to give the function type a nickname as required, and use the private modifier.
  • 3. Declare some null var private member variables in the class, and implement the invoke function with the corresponding variables in the callback function, passing in the corresponding parameters.
  • 4. Define functions with the same name as the callback interface, but the arguments are of the corresponding function type, and assign the function type to the corresponding member variable of the current class.
  • 5. Define a member function that takes a Lambda expression that takes the receiver object of our specified class and returns Unit. Create the corresponding object in the function and pass the Lambda expression in using the also function.

The call is as follows:

tabLayout.addOnTabSelectedListener(registerOnTabSelectedListener { onTabSelected { vpOrder.currentItem = it? .position ? :0}})Copy the code

As mentioned above, you can avoid writing unnecessary empty implementation code