初学Kotlin——在自定义View里的应用

什么是Kotlin

Kotlin,它是JetBrains开发的基于JVM的面向对象的语言。2017年的时候被Google推荐Android的官方语言,同时Android studio 3.0正式支持这门语言,在这个编译器上创建一个Kotlin项目,非常方便,甚至可以Java转为Kotlin

我主要是在通过实现自定义View过程中,说一下KotlinJava的异同,其实两者非常相似

Kotlin语法不是太了解的,可以先去看看它的官方翻译文档

以Barchart-Kotlin开始说起

Barchart-Kotlin是我用Kotlin写的一个简易灵活的柱状图库,喜欢的可以点个star!

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的强大语法糖,使用了更简洁的语法提高了生产力。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 151,511评论 1 330
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 64,495评论 1 273
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 101,595评论 0 225
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 42,558评论 0 190
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 50,715评论 3 270
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 39,672评论 1 192
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,112评论 2 291
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,837评论 0 181
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,417评论 0 228
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,928评论 2 232
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,316评论 1 242
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,773评论 2 234
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,253评论 3 220
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,827评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,440评论 0 180
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 34,523评论 2 249
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 34,583评论 2 249

推荐阅读更多精彩内容