Kotlin: a new way to play that you haven't played before

Posted by xcoderx on Fri, 21 Jan 2022 16:29:42 +0100

Air safety

  • In Java, we don't have to force us to deal with null objects, so null pointerexception null pointers often appear. Now Kotlin restricts null objects. We must deal with whether the object is null at compile time, otherwise the compilation will fail

  • When the object cannot be empty, you can use this object directly

fun getText() : String {
    return "text"
}
val text = getText()
print(text.length)

  • When the object can be empty, you must judge whether the object is empty
fun getText() : String? {
    return null
}
val text = getText()
if (text != null) {
    print(text.length)
}

// If you don't want to judge whether the text object is null, you can do this directly. If the text object is null, a null pointer exception will be reported. Generally, this is not recommended
val text = getText()
print(text!!.length)
// There is also a better processing method. If the text object is empty, no error will be reported, but text The result of length will be equal to null
val text = getText()
print(text?.length)

Method supports adding default parameters

  • On Java methods, we may overload several times in order to extend a method
public void toast(String text) {
    toast(this, text, Toast.LENGTH_SHORT);
}

public void toast(Context context, String text) {
    toast(context, text, Toast.LENGTH_SHORT);
}

public void toast(Context context, String text, int time) {
    Toast.makeText(context, text, time).show();
}
toast("Play a toast");
toast(this, "Play a toast");
toast(this, "Play a toast", Toast.LENGTH_LONG);

  • However, in Kotlin, we do not need to overload. We can directly define the default value of parameters on the method
fun toast(context : Context = this, text : String, time : Int = Toast.LENGTH_SHORT) {
    Toast.makeText(context, text, time).show()
}

toast(text = "Play a toast")
toast(this, "Play a toast")
toast(this, "Play a toast", Toast.LENGTH_LONG)

The parameters above the method are immutable

  • For Java methods, we can modify the assignment of the parameters on the method at will, but not in Kotlin. The variables on the Kotlin method parameters are of type val (corresponding to Java final). At this time, we have two solutions:

  • First, define as like as two peas in the method, and write the following exact methods.

class XxxView : View {

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        var widthMeasureSpec: Int = widthMeasureSpec
        var heightMeasureSpec: Int = heightMeasureSpec
        
        if (MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.AT_MOST) {
            widthMeasureSpec = MeasureSpec.makeMeasureSpec(30, MeasureSpec.EXACTLY)
        }

        if (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.AT_MOST) {
            heightMeasureSpec = MeasureSpec.makeMeasureSpec(30, MeasureSpec.EXACTLY)
        }
        
        setMeasuredDimension(widthMeasureSpec, heightMeasureSpec)
    }
}
  • However, the compiler will warn us that there are duplicate variables, but we can still compile and run normally, so this writing method is not recommended

  • Second, define a variable with different names in the method, which is written as follows:

class XxxView : View {

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        var finalWidthMeasureSpec: Int = widthMeasureSpec
        var finalHeightMeasureSpec: Int = heightMeasureSpec

        if (MeasureSpec.getMode(finalWidthMeasureSpec) == MeasureSpec.AT_MOST) {
            finalWidthMeasureSpec = MeasureSpec.makeMeasureSpec(30, MeasureSpec.EXACTLY)
        }

        if (MeasureSpec.getMode(finalHeightMeasureSpec) == MeasureSpec.AT_MOST) {
            finalHeightMeasureSpec = MeasureSpec.makeMeasureSpec(30, MeasureSpec.EXACTLY)
        }
        
        setMeasuredDimension(widthMeasureSpec, heightMeasureSpec)
    }
}
  • In fact, a final prefix is added on the original basis, which not only solves the problem of compiler warning, but also solves the problem that we have to think of a new name to name variables.

  • Then someone will ask, is there any way to change it like Java? I also struggled about this problem for a while, but after consulting a lot of documents and materials, I finally found that there was no way, so I had to compromise. After all, nothing in the world is perfect.

Class method extension

  • You can extend the methods of the original class without inheritance, such as the extension methods of the String class
fun String.handle() : String {
    return this + "Android Wheel brother"
}

// It should be noted that in which class the handle method is defined, this extension can only be used in that class
print("HJQ = ".handle())
HJQ = Android Wheel brother

Function variable

  • In Kotlin syntax, functions can be passed as variables
var result = fun(number1 : Int, number2 : Int) : Int {
    return number1 + number2
}
  • Use this function variable
println(result(1, 2))

Inline function

  • Someone may ask, is the inline function shrimp? Let me take a chestnut and write the following code with Kotlin
class Demo {

    fun test() {
        showToast("666666")
    }

    /**
     * This is our protagonist today: inline function, decorated with inline keyword
     */
    private inline fun showToast(message: String) {
        ToastUtils.show(message)
    }
}
  • After decompilation, it will become the following code:
/* compiled from: Demo.kt */
public final class Demo {

    public final void test() {
        ToastUtils.show("666666");
    }
}
  • I believe you should know the usage and function of inline function. Inline function is to directly replace all the code calling inline function with the code in the method when compiling. Then you may have questions. What are the practical benefits of doing so? In fact, it improves the performance of the code, which is the same as the constants of basic data types will be optimized during compilation. However, if the inline function is called in many places and there are many implementation codes of the inline function, the amount of code will increase accordingly.

  • In addition, in the above code example, the compiler has a code warning on the inline keyword. The original words are as follows:

Expected performance impact from inlining is insignificant. Inlining works best for functions with parameters of functional types

The expected impact of inlining on performance is negligible. Inline is best for functions whose arguments are function types

  • The general meaning is that in the above code example, the performance optimization of inline functions is insignificant. It is more suitable for functions with lambda parameters. According to this prompt, modify the above code example to the following, so that no code warning will be reported:
class Demo {

    fun test() {
    
        showToast({
            println("Test output")
        }, "7777777")
    }

    private inline fun showToast(function: () -> Unit, message: String) {
        function.invoke()
        ToastUtils.show(message)
    }
}
  • Some people may be curious, so there can be a great performance improvement? What is the basis for judgment? Next, let's do a set of experiments. What's the difference between the decompiled code with and without inline? Let's see what the decompiled code looks like after adding inline
/* compiled from: Demo.kt */
public final class Demo {

    public final void test() {
        System.out.println("\u6d4b\u8bd5\u8f93\u51fa\u4e86");
        ToastUtils.show("7777777");
    }
}
  • Everything is expected, so what is the effect of decompiling without inline?
/* compiled from: Demo.kt */
public final class Demo {

    public final void test() {
        showToast(1.INSTANCE, "7777777");
    }

    private final void showToast(Function0<Unit> function, String message) {
        function.invoke();
        ToastUtils.show(message);
    }
}
/* compiled from: Demo.kt */
final class Demo$test$1 extends Lambda implements Function0<Unit> {
    public static final Demo$test$1 INSTANCE = new Demo$test$1();

    Demo$test$1() {
        super(0);
    }

    public final void invoke() {
        System.out.println("\u6d4b\u8bd5\u8f93\u51fa\u4e86");
    }
}
  • Obviously, not adding inline will lead to the generation of an additional internal class. This is the class that comes out of the lambda function, and the examples in it are still static, which will undoubtedly increase memory consumption. In addition, this has another advantage, that is, it can reduce the call of a layer of method stack.

  • In addition to the keyword inline, there is another keyword: noinline. You may be confused here. What's the use of this? If I don't write inline on the method, I won't be inline? So what is the role of this keyword? In fact, this keyword is not modified on methods, but on lambda parameters. Assuming that there are multiple lambda parameters on an inline function, I just want to inline one lambda parameter. If other lambda parameters are not inline, I can use this keyword to modify lambda parameters that do not need to be inline, The general usage is as follows:

private inline fun showToast(function1: () -> Unit, noinline function2: () -> Unit, message: String) {
    function1.invoke()
    function2.invoke()
    ToastUtils.show(message)
}

Entrustment mechanism

Class delegate
  • Let's look at a piece of code first
// Define log policy interface
interface ILogStrategy {

    fun log(message: String)
}
// Implement a default log policy class
class LogStrategyImpl : ILogStrategy {

    override fun log(message: String) {
        Log.i("Test output", message)
    }
}
// Create a log agent class
class LogStrategyProxy(strategy: ILogStrategy) : ILogStrategy by strategy

  • You may have some doubts here

    • ILogStrategy by strategy?

    • Does the class LogStrategyProxy not implement the interface method cause the compilation to fail?

  • I think the same explanation can be used for these two problems. The reason why LogStrategyProxy does not implement the log method of ILogStrategy is that by strategy is added after the ILogStrategy interface, and the strategy object is the variable in the LogStrategyProxy constructor, which means that the specific implementation of this interface can be implemented by the strategy object for me, I (LogStrategyProxy class) don't need to implement it again. Is this very similar to the static proxy in Java? However, on the Kotlin class delegation feature, the compiler helps us automatically generate the code of the interface method. You can imagine the following code:

class LogStrategyProxy(val strategy: ILogStrategy) : ILogStrategy {

    override fun log(message: String) {
        strategy.log(message)
    }
}

  • Some people will certainly ask: words have no basis. Why should I believe that you are such a code?

  • This is a good question. I'll provide the decompiled code. You can understand it by looking at it:

public final class LogStrategyProxy implements ILogStrategy {

    private final /* synthetic */ ILogStrategy $$delegate_0;
    
    public LogStrategyProxy(@NotNull ILogStrategy strategy) {
        Intrinsics.checkNotNullParameter(strategy, "strategy");
        this.$$delegate_0 = strategy;
    }

    public void log(@NotNull String message) {
        Intrinsics.checkNotNullParameter(message, "message");
        this.$$delegate_0.log(message);
    }
}
  • Did you realize it immediately? It is also very simple to call. The code is as follows:
val logStrategyImpl = LogStrategyImpl()

LogStrategyProxy(logStrategyImpl).log("666666")
  • Finally, let's look at the output log:
Test output: 666666
  • I suddenly have a bold idea to rewrite its interface method when using class delegate? For example:
class LogStrategyProxy(strategy: ILogStrategy) : ILogStrategy by strategy {

    override fun log(message: String) {
        println("Test output " + message)
    }
}
  • I have already done some practice on this issue. If there is a problem, we can rest assured and boldly engage in it.
Attribute delegate
  • After reading the above class delegates, I'm sure you have a certain understanding of delegates. What are attribute delegates? In short, class delegation is to help us reduce some implementation code, while property delegation is to help us control the operations of Get and Set of variables. I won't say much nonsense. Let's demonstrate the usage. Let's create a delegate class first
class XxxDelegate {

    // Give it a default value first
    private var currentValue: String = "666666"

    operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
        println("The test field name is ${property.name} The variable of has been accessed and the current value is $currentValue")
        return currentValue
    }

    operator fun setValue(thisRef: Any?, property: KProperty<*>, newValue: String) {
        currentValue = newValue
        println("The test field name is ${property.name} The variable of is assigned, and the current value is $currentValue" + ",New value $newValue")
    }
}
  • Examples of codes used are as follows:
var temp: String by XxxDelegate()
println("Test output " + temp)
temp = "55555"
println("Test output " + temp)
  • The specific log output is as follows:
System.out: The test field name is temp The variable of is accessed, and the current value is 666666
System.out: Test output 666666
System.out: The test field name is temp The current value is 55555 and the new value is 55555
System.out: The test field name is temp The variable of is accessed, and the current value is 55555
System.out: Test output 55555
  • See here, do you understand that there are only two methods in this XxxDelegate class, one is getValue and the other is setValue. We can roughly get its role from the method name, so we won't explain more here. var temp: String by XxxDelegate() means that the creation of this temp object will be fully delegated to the XxxDelegate class.
Lazy entrustment
  • What is lazy entrustment? You know the lazy style in singleton mode? This is similar to it, but we don't need to write static methods and locking mechanisms. We just need to write as follows:
val temp: String by lazy {
    println("The test variable is initialized")
    return@lazy "666666"
}
  • The calling code is as follows:
println("Test start")
println("Test first output " + temp)
println("Test second output " + temp)
println("End of test")
  • The output log is as follows:
System.out: Test start
System.out: The test variable is initialized
System.out: Test first output 666666
System.out: Test second output 666666
System.out: End of test
  • Is it really like lazy? However, this writing method simplifies a lot. In addition, it can be used as findViewById in daily development, which is the most suitable.
private val viewPager: ViewPager? by lazy { findViewById(R.id.vp_home_pager) }
  • In addition, lazy delegates provide several lazy loading modes for us to choose from,

    • LazyThreadSafetyMode.SYNCHRONIZED: synchronous mode, which ensures that only a single thread can initialize an instance. In this mode, initialization is thread safe. When by lazy does not specify a mode, it is the default mode.

    • LazyThreadSafetyMode.PUBLICATION: concurrent mode. Concurrent initialization is allowed under multithreading, but only the first returned value is used as an instance. This mode is thread safe, and lazythreadsafetymode The biggest difference of synchronized is that this mode has the highest initialization efficiency under multi-threaded concurrent access. In essence, it uses space for time. Whichever thread executes faster will return the result first, and the results executed by other threads will be discarded.

    • LazyThreadSafetyMode.NONE: normal mode. This mode does not use locks to restrict multi-threaded access, so it is thread unsafe, so do not use it in the case of multi-threaded concurrency.

  • The specific use method is also very simple, as follows:

val temp: String by lazy(LazyThreadSafetyMode.NONE) {
    println("The test variable is initialized")
    return@lazy "666666"
}
  • Another thing to note is that a variable that uses lazy delegates must be declared as val (immutable) because it can only be assigned once.

spread function

  • The extension function is generated by Kotlin to simplify the writing of some code, including five functions: let, with, run, apply and also
let function
  • Within a function block, you can refer to the object through it. The return value is the last line of the function block or the specified return expression

  • General writing

fun main() {
    val text = "Android Wheel brother"
    println(text.length)
    val result = 1000
    println(result)
}
  • let writing
fun main() {
    val result = "Android Wheel brother".let {
        println(it.length)
        1000
    }
    println(result)
}
  • The most common scenario is to use the let function for processing. It is necessary to uniformly judge the null of a nullable object
videoPlayer?.setVideoView(activity.course_video_view)
videoPlayer?.setControllerView(activity.course_video_controller_view)
videoPlayer?.setCurtainView(activity.course_video_curtain_view)

videoPlayer?.let {
   it.setVideoView(activity.course_video_view)
   it.setControllerView(activity.course_video_controller_view)
   it.setCurtainView(activity.course_video_curtain_view)
}
  • Or you need to specify that a variable can be used within a specific scope
with function
  • The previous functions are used in a slightly different way because they do not exist in the form of extensions. It refers to an object as a parameter of a function. this can be used to refer to the object in the function block. The return value is the last line of the function block or the specified return expression

  • Define the Person class

class Person(var name : String, var age : Int)
  • General writing
fun main() {
    var person = Person("Android Wheel brother", 100)
    println(person.name + person.age)
    var result = 1000
    println(result)
}
  • with writing
fun main() {
    var result = with(Person("Android Wheel brother", 100)) {
        println(name + age)
        1000
    }
    println(result)
}
  • When it is applicable to calling multiple methods of the same class, you can omit the duplication of class names and directly call the methods of the class. It is often used in recyclerview. Com in Android In onbinderviewholder, the attributes of the data model are mapped to the UI
override fun onBindViewHolder(holder: ViewHolder, position: Int){
    val item = getItem(position)?: return
    holder.nameView.text = "full name: ${item.name}"
    holder.ageView.text = "Age: ${item.age}"
}
override fun onBindViewHolder(holder: ViewHolder, position: Int){
    val item = getItem(position)?: return
    with(item){
        holder.nameView.text = "full name: $name"
        holder.ageView.text = "Age: $age"
    }
}
run function
  • In fact, it can be said to be a combination of let and with functions. The run function only receives a lambda function as a parameter and returns in the form of closure. The return value is the value of the last line or the expression of the specified return

  • General writing

var person = Person("Android Wheel brother", 100)
println(person.name + "+" + person.age)
var result = 1000
println(result)
  • run writing
var person = Person("Android Wheel brother", 100)
var result = person.run {
    println("$name + $age")
    1000
}
println(result)
  • It is applicable to let and with functions in any scenario. Because the run function is a combination of let and with functions, it makes up for the fact that the let function must use the IT parameter to replace the object in the function body. It can be omitted in the run function like the with function and directly access the public properties and methods of the instance. On the other hand, it makes up for the problem of null judgment of the object passed in by the with function, In the run function, empty judgment can be performed like the l et function. Here, it is simplified with the help of the onBindViewHolder case
override fun onBindViewHolder(holder: ViewHolder, position: Int){
    val item = getItem(position)?: return
    holder.nameView.text = "full name: ${item.name}"
    holder.ageView.text = "Age: ${item.age}"
}
override fun onBindViewHolder(holder: ViewHolder, position: Int){
    val item = getItem(position)?: return
    item?.run {
        holder.nameView.text = "full name: $name"
        holder.ageView.text = "Age: $age"
    }
}
apply function
  • Structurally, the apply function is very similar to the run function. The only difference is that they return different values. The run function returns the value of the last line of code in the form of closure, while the apply function returns the incoming object itself

  • General writing

val person = Person("Android Wheel brother", 100)
person.name = "HJQ"
person.age = 50
  • apply writing
val person = Person("Android Wheel brother", 100).apply {
    name = "HJQ"
    age = 50
}
  • The overall function is very similar to the run function. The only difference is that the value it returns is the object itself, while the run function returns in the form of closure, which returns the value of the last line. Based on this difference, its applicable scenario is slightly different from the run function. apply is generally used for the initialization of an object instance, and the attributes in the object need to be assigned. Or it is also necessary to bind data to the View when dynamically inflating an XML View. This scenario is very common. Especially in our development, some data models need to be used in the process of transformation and instantiation from View model
mRootView = View.inflate(activity, R.layout.example_view, null)
mRootView.tv_cancel.paint.isFakeBoldText = true
mRootView.tv_confirm.paint.isFakeBoldText = true
mRootView.seek_bar.max = 10
mRootView.seek_bar.progress = 0
  • The code after using the apply function is like this
mRootView = View.inflate(activity, R.layout.example_view, null).apply {
   tv_cancel.paint.isFakeBoldText = true
   tv_confirm.paint.isFakeBoldText = true
   seek_bar.max = 10
   seek_bar.progress = 0
}
  • Multi-level space judgment problem
if (sectionMetaData == null || sectionMetaData.questionnaire == null || sectionMetaData.section == null) {
    return;
}
if (sectionMetaData.questionnaire.userProject != null) {
    renderAnalysis();
    return;
}
if (sectionMetaData.section != null && !sectionMetaData.section.sectionArticles.isEmpty()) {
    fetchQuestionData();
    return;
}
  • Application function optimization of kotlin
sectionMetaData?.apply {

	// Operate sectionMetaData when the sectionMetaData object is not empty

}?.questionnaire?.apply {

	// Operate questionnaire when the questionnaire object is not empty

}?.section?.apply {

	// Operate the section when the section object is not empty

}?.sectionArticle?.apply {

	// When the sectionArticle object is not empty, operate the sectionArticle

}
also function
  • The structure of the also function is actually very similar to that of the let. The only difference is that the return value is different. Let returns the value of the last line in the function body in the form of closure. If the last line is empty, it returns a default value of type Unit. The also function returns the incoming object itself
fun main() {
    val result = "Android Wheel brother".let {
        println(it.length)
        1000
    }
    println(result) // Print: 1000
}

fun main() {
    val result = "Android Wheel brother".also {
        println(it.length)
    }
    println(result) // Printing: Android wheel
}
  • Applicable to any scenario of the let function, the also function is very similar to the let, except that the only difference is that the last return value of the let function is the return value of the last line, while the return value of the also function returns the current object. Generally, it can be used for chain calls of multiple extension functions

Operator overloading

  • Using operators in Kotlin will eventually call the methods corresponding to the object. We can rewrite these methods to make the object support operators. The code will not be demonstrated here
operatorCall method
+aa.unaryPlus()
-aa.unaryMinus()
!aa.not()
operatorCall method
a++a.inc()
a–a.dec()
operatorCall method
a + ba.plus(b)
a - ba.minus(b)
a * ba.times(b)
a / ba.div(b)
a % ba.rem(b), a.mod(b) (deprecated)
a...ba.rangeTo(b)
operatorCall method
a in bb.contains(a)
a !in b!b.contains(a)
operatorCall method
a[i]a.get(i)
a[i, j]a.get(i, j)
a[i_1, ..., i_n]a.get(i_1, ..., i_n)
a[i] = ba.set(i, b)
a[i, j] = ba.set(i, j, b)
a[i_1, ..., i_n] = ba.set(i_1, ..., i_n, b)
operatorCall method
a()a.invoke()
a(i)a.invoke(i)
a(i, j)a.invoke(i, j)
a(i_1, ..., i_n)a.invoke(i_1, ..., i_n)
operatorCall method
a += ba.plusAssign(b)
a -= ba.minusAssign(b)
a *= ba.timesAssign(b)
a /= ba.divAssign(b)
a %= ba.remAssign(b), a.modAssign(b) (deprecated)
operatorCall method
a == ba?.equals(b) ?: (b === null)
a != b!(a?.equals(b) ?: (b === null))
operatorCall method
a > ba.compareTo(b) > 0
a < ba.compareTo(b) < 0
a >= ba.compareTo(b) >= 0
a <= ba.compareTo(b) <= 0

Topics: Java Android kotlin