初學Kotlin——在自定義View裡的應用

shaDowZwy發表於2019-03-04

什麼是Kotlin

Kotlin,它是JetBrains開發的基於JVM的物件導向的語言。2017年的時候被Google推薦Android的官方語言,同時Android studio 3.0正式支援這門語言,在這個編譯器上建立一個Kotlin專案,非常方便,甚至可以Java轉為Kotlin

我主要是在通過實現自定義View過程中,說一下KotlinJava的異同,其實兩者非常相似

Kotlin語法不是太瞭解的,可以先去看看它的官方翻譯文件

以Barchart-Kotlin開始說起

Barchart-Kotlin是我用Kotlin寫的一個簡易靈活的柱狀相簿,喜歡的可以點個star!

初學Kotlin——在自定義View裡的應用

1.類的屬性

在一個類裡面我們需要定義一些屬性來儲存資料和狀態

我們先來看看Java程式碼,在BarChartView定義了一些屬性

    private SpeedLinearLayoutManger mLayoutManager;
    private BarChartAdapter mAdapter;
    private ItemOnClickListener mClickListener;
    private int mDefaultWidth = 150;
複製程式碼

然後我們再看看Kotlin是怎麼定義這些屬性的,下面的是Kotlin程式碼

    private lateinit var mLayoutManager: SpeedLinearLayoutManger
    private lateinit var mAdapter: BarChartAdapter
    private var mClickListener: ItemOnClickListener? = null
    private val mDefaultWidth = 150
複製程式碼

你會發現不一樣的宣告方式,但重要的是varval這兩個關鍵字

var代表的是可變的變數,相當於現在Java宣告變數的方式

val代表的是不可變的變數,初始化後不能再修改,相當於加了final關鍵字的變數

而且在Kotlin中屬性是需要初始化的,沒有值的時候你可以賦值null,不然編譯會報錯。加上?的意思是你不確定是否是這個型別,或者說是否為null。如果覺得實在是不方便你的使用邏輯,你可以使用這兩種方式延遲初始化。

懶初始化 by lazy

lazy是指推遲一個變數的初始化時機,只有在使用的時候才會去例項化它。適用於一個變數直到使用時才需要被初始化。在我這個專案裡面沒有使用by lazy,它大致的用法是這樣的

val data: Data by lazy {
    Data(number,string)
   }

複製程式碼
延遲初始化 lateinit

lateinit是指你保證接下來在使用之前或者使用的時候會例項化它,不然你就會crash掉,這不就跟我們使用Java屬性的方式一樣麼。。。它適用於一些view和必須用到資料結構的初始化,我覺得還是謹慎使用比較好。

2.空安全

Kotlin可以說是分了兩個大型別,可空型別和不可空型別,這樣做的原因是它希望在編譯階段就把空指標這問題顯式的檢測出來,把問題留在了編譯階段,讓程式更加健壯。它通過?來表達可為空。

mClickListener?.invoke1(position)
mClickListener?.invoke2(position)
mClickListener?.invoke3(position)
複製程式碼

如果mClickListenernull的話,後面的語句是不會執行的。而且Kotlin提供了更加簡潔的操作符let

mListener?.let {  
it.invoke1(position)
it.invoke2(position)
it.invoke3(position)  
}

複製程式碼

只有在非空的情況下才執行let裡面的操作,非常簡潔。

3.構造器

通過簡單瞭解之後,我們開始寫一個自定義View,我們需要繼承View,Java的實現方式是這樣的

public class BarChart extends View {

    public BarChart(Context context) {
        super(context);
    }

    public BarChart(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    public BarChart(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

複製程式碼

Kotlin你可以實現的更簡潔

class BarChart @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0)
    : View(context, attrs, defStyleAttr) {
    
    private val mContext: Context = context

    init { }
複製程式碼

你可以在init程式碼塊裡面獲得建構函式的傳參,當然你也可以直接在宣告屬性的時候獲得,@JvmOverloads 如果你沒有加上這個註解,它只能過載相匹配的的建構函式,而不是全部。

而且可能你也發現了,你可以在傳參裡面初始化,這相對於Java來說,靈活太多

fun shadow(width:Int=100,height:Int = 180){ }
//你可以這麼使用
shadow()
shadow(140)
shadow(140,200)
複製程式碼

4.UI佈局

一般用我們創造view的佈局是xml,Kotlin也是支援的,但是它更推薦你使用Anko的佈局方式。

那是什麼是Anko呢,AnkoJetBrains開發的一個強大的庫,它主要的目的是用來替代以前xml的方式來使用程式碼生成UI佈局的,它包含了很多的非常有幫助的函式和屬性來避免讓你寫很多的模版程式碼。

有興趣的你可以去看看它的原始碼與更多使用方式 — Anko

首先你要在Gradle裡新增Anko的引用

// Anko Layouts
compile "org.jetbrains.anko:anko-recyclerview-v7:$anko_version"
compile "org.jetbrains.anko:anko-recyclerview-v7-coroutines:$anko_version"
compile "org.jetbrains.anko:anko-sdk25:$anko_version"
compile "org.jetbrains.anko:anko-appcompat-v7:$anko_version"
// Coroutine listeners for Anko Layouts
compile "org.jetbrains.anko:anko-sdk25-coroutines:$anko_version"
compile "org.jetbrains.anko:anko-appcompat-v7-coroutines:$anko_version"

複製程式碼

然後你可以在程式碼裡寫UI佈局的程式碼了,就是用Kotlin程式碼替代xml

 private fun createView(attrs: AttributeSet? = null, defStyleAttr: Int = 0) {

   var height = dip(150)
   attrs?.let {
            val typeArray = mContext.obtainStyledAttributes(it, R.styleable.BarChartView, defStyleAttr, 0)
            height = typeArray.getDimension(R.styleable.BarChartView_chart_height, dip(150).toFloat()).toInt()
            typeArray.recycle()
        }

    verticalLayout {
            lparams(width = matchParent, height = matchParent)

            frameLayout {
                lparams(width = matchParent, height = wrapContent)

                mLineView = view {
                    backgroundColor = R.color.gray_light
                }.lparams(width = matchParent, height = dip(0.5f)) {
                    gravity = Gravity.BOTTOM
                    bottomMargin = dip(9)
                }

                mBarView = recyclerView {
                    lparams(width = matchParent, height = height)
                }
            }

            mDateView = relativeLayout {
                lparams(width = matchParent, height = wrapContent) {
                    leftPadding = dip(10)
                    rightPadding = dip(10)
                }

                mLeftTv = textView {
                    textSize = 15f
                }

                mRightTv = textView {
                    textSize = 15f
                }.lparams(width = wrapContent, height = wrapContent) {
                    alignParentRight()
                }
            }
        }
    }

複製程式碼

可以從程式碼裡看見verticalLayout(其實就是LinearLayoutvertical模式)包裹了frameLayoutrelativeLayout,裡面又有各自的子view,而且你會發現xml有的屬性這裡也有,呼叫起來非常的簡潔明瞭,熟練xml的來寫這個,我覺得上手應該會很快,它的缺點就是沒有預覽效果,以及實現複雜的view結構的時候會比較繁瑣,考驗盲寫的功力了。。。。

5.擴充套件函式

擴充套件函式是Kotlin非常方便實用的一個功能,它可以讓我們隨意的擴充套件SDK的庫,你如果覺得SDK的api不夠用,這個時候你可以用擴充套件函式完全去自定義。

例如你需要這樣來獲取顏色,每次你都需要一個上下文context

mColor = ContextCompat.getColor(mContext, R.color.primary)
複製程式碼

那你可以通過擴充套件Context這個SDK的類來實現更方便的使用

fun Context.color(colorRes: Int) = ContextCompat.getColor(this, colorRes)
fun View.color(colorRes: Int) = context.color(colorRes)
複製程式碼

而且為了更方便在View裡面使用,又擴充套件了View。在第一個方法裡面可以發現getColor所需要的this上下文,就是Context。同樣,View裡面的contextgetContext()所得到,也就是View裡面本身具有的公有方法。

這其實是很驚豔的功能

那我們可以想一想為啥可以這樣做,我們知道的是KotlinJava都是在Jvm上執行的,既然都是編譯成class位元組碼,那我們是不是可以通過位元組碼來了解一些事情。

通過Android studio 3.0上的Tools的工具Show Kotlin Bytecode,可以將剛才的擴充套件函式的程式碼編譯成位元組碼


  // access flags 0x19
  public final static color(Landroid/content/Context;I)I
    @Lorg/jetbrains/annotations/NotNull;() // invisible, parameter 0
   L0
    ALOAD 0
    LDC "$receiver"
    INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkParameterIsNotNull (Ljava/lang/Object;Ljava/lang/String;)V
   L1
    LINENUMBER 12 L1
    ALOAD 0
    ILOAD 1
    INVOKESTATIC android/support/v4/content/ContextCompat.getColor (Landroid/content/Context;I)I
    IRETURN
   L2
    LOCALVARIABLE $receiver Landroid/content/Context; L0 L2 0
    LOCALVARIABLE colorRes I L0 L2 1
    MAXSTACK = 2
    MAXLOCALS = 2

  // access flags 0x19
  public final static color(Landroid/view/View;I)I
    @Lorg/jetbrains/annotations/NotNull;() // invisible, parameter 0
   L0
    ALOAD 0
    LDC "$receiver"
    INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkParameterIsNotNull (Ljava/lang/Object;Ljava/lang/String;)V
   L1
    LINENUMBER 14 L1
    ALOAD 0
    INVOKEVIRTUAL android/view/View.getContext ()Landroid/content/Context;
    DUP
    LDC "context"
    INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkExpressionValueIsNotNull (Ljava/lang/Object;Ljava/lang/String;)V
    ILOAD 1
    INVOKESTATIC shadow/barchart/ExtensionsKt.color (Landroid/content/Context;I)I
    IRETURN
   L2
    LOCALVARIABLE $receiver Landroid/view/View; L0 L2 0
    LOCALVARIABLE colorRes I L0 L2 1
    MAXSTACK = 3
    MAXLOCALS = 2

複製程式碼

這就是編譯後的位元組碼,看不懂是吧,我也看不懂。。但是我們可以反編譯啊,生成Java程式碼

 public static final int color(@NotNull Context $receiver, int colorRes) {
      Intrinsics.checkParameterIsNotNull($receiver, "$receiver");
      return ContextCompat.getColor($receiver, colorRes);
   }

   public static final int color(@NotNull View $receiver, int colorRes) {
      Intrinsics.checkParameterIsNotNull($receiver, "$receiver");
      Context var10000 = $receiver.getContext();
      Intrinsics.checkExpressionValueIsNotNull(var10000, "context");
      return color(var10000, colorRes);
   }

複製程式碼

通過反編譯我們可以知道這是個靜態函式,Intrinsics.checkParameterIsNotNull($receiver, "$receiver");這個函式只起到了判空的作用,真正的程式碼是return ContextCompat.getColor($receiver, colorRes);這個不就是我們剛剛用的Java程式碼嘛。

重點是$receiver接收的物件,接收的是Context例項,這樣的話就可以呼叫這個類的所有公有方法和公有屬性,而且它是靜態函式,它可以通過類直接呼叫。所以擴充套件函式的實現只不過是加了一個需要當前物件的靜態方法,呼叫的時候傳入一個當前物件而已。

我們剛剛用到了反編譯,因為我們知道KotlinJava的生成位元組碼是一樣的,那我們可以瞭解一下Kotlin的編譯過程,它跟Java的區別是什麼。可以看一下這篇文章Kotlin編譯過程分析

通過這篇文章你可以瞭解到Kotlin在編譯過程中,與Java是大致相同的,只是在最後生成目的碼的時候做了很多類似於封裝的事情,生成相同的語法結構,Kotlin將我們本來在程式碼層做的一些封裝工作轉移到了編譯後端階段。那我們可不可以在學習Kotlin的時候去這樣理解,其實Kotlin是一種封裝了Java的強大的語法糖,Java做不到的事情,Kotlin其實也做不到,例如物件只能訪問公有屬性。

6.資料類

Kotlin中你要實現資料類是非常簡單的,並不需要手動加上get/set方法

data class BarItem(
        private val barData: BarData,
        var select: Boolean = false) {
    fun getData(): Double {
        return barData.getData()
    }

    fun getTag(): String {
        return barData.getTag()
    }
}
複製程式碼

在這個類裡面你會發現,我還宣告瞭兩個方法,我需要的是BarData裡的資料,但又不僅僅只需要這個資料,所以我宣告瞭一個類來封裝它,其實這個相當於裝飾者模式了。Kotlin有更好的方式實現這個模式

data class BarItem(
        private val barData: BarData,
        var select: Boolean = false) : BarData by barData
複製程式碼

7.when

BarChartView裡用到一個與switch語法類似的語句

 mSelectPosition = when (mStyle) {
            ScrollStyle.DEFAULT -> mDataList.size - 1
            ScrollStyle.START -> 0
            ScrollStyle.NONE -> -1
            ScrollStyle.CUSTOM -> mSelectPosition
            else -> { }
        }
複製程式碼

它是起到了跟switch一樣的作用,並且更強大,因為它是表示式,所以是有返回值的,在Kotlin中控制流大都是表示式,都是可以有返回值的。

8.集合

Kotlin是區分可變集合和不可變集合的,它給你提供這兩種選擇。

 //不可變
 Set<out T>
 Map<K, out V>
 List<out T>
 //可變
 MutableSet<T>
 MutableMap<K, V>
 MutableList<T> 
複製程式碼

不可變的集合提供只讀屬性,例如size,get等,Kotlin不提供專門的語法結構建立list或者set,是用標準庫獲取的,我們可以看一下它的原始碼是怎樣實現。

/**
 * Returns an immutable list containing only the specified object [element].
 * The returned list is serializable.
 * @sample samples.collections.Collections.Lists.singletonReadOnlyList
 */
@JvmVersion
public fun <T> listOf(element: T): List<T> = java.util.Collections.singletonList(element)

/**
 * Returns an empty new [MutableList].
 * @sample samples.collections.Collections.Lists.emptyMutableList
 */
@SinceKotlin("1.1")
@kotlin.internal.InlineOnly
public inline fun <T> mutableListOf(): MutableList<T> = ArrayList()

複製程式碼

從原始碼可以看見這是Javajava.util.Collections.singletonListArrayList,這就可以理解為啥不可變和可變的了。。。

總結

Kotlin相對於Java,更像是封裝了Java的強大語法糖,使用了更簡潔的語法提高了生產力。

相關文章