用 Kotlin 开发 Android 项目是一种什么样的感受?

作者: neverwoods | 来源:发表于2017-03-31 16:49 被阅读7493次

    前言

    从初学 Kotlin,到尝试性的写一点体验代码,再到实验性的做一些封装工作,到最后摸爬滚打着写了一个项目。不得不说过程中还是遇上了不少的问题,尽管有不少坑是源于我自己的选择,比如使用了 anko 布局放弃了 xml,但是总体来说,这门语言带给我的惊喜是完全足以让我忽略路上的坎坷。

    这篇文章仅仅是想整理一下这一路走过来的一些感想和惊喜,随着我对 Kotlin 的学习和使用,会长期修改。

    正文

    1.有了空安全,再也不怕服务端返回空对象了

    简单一点的例子,那就是 String 和 String?是两种不同的类型。String 已经确定是不会为空,一定有值;而 String?则是未知的,也许有值,也许是空。在使用对象的属性和方法的时候,String 类型的对象可以毫无顾忌的直接使用,而 String?类型需要你先做非空判断。

    fun demo() {
        val string1: String = "string1"
        val string2: String? = null
        val string3: String? = "string3"
        
        println(string1.length)
        println(string2?.length)
        println(string3?.length)
    }
    
    输出结果为:
    7
    null
    7
    

    尽管 string2 是一个空对象,也并没有因为我调用了它的属性/方法就报空指针。而你所需要做的,仅仅是加一个"?"。

    如果说这样还体现不出空安全的好处,那么看下面的例子:

    val a: A? = A()
    println(a?.b?.c)
    

    试想一下当每一级的属性皆有可能为空的时候,JAVA 中我们需要怎么处理?

    2.转型与智能转换,省力又省心

    我写过这样子的 JAVA 代码

    if(view instanceof TextView) {
        TextView textView = (TextView) view;
        textView.setText("text");
    }
    

    而在 Kotlin 中的写法则有所不同

    if(view is TextView) {
        TextView textView = view as TextView
        textView.setText("text")
    }
    

    缩减代码之后对比更加明显

    JAVA
    
    if(view instanceof TextView) {
        ((TextView) view).setText("text");
    }
    
    Kotlin
    
    if(view is TextView) {
        (view as TextView).setText("text")
    }
    
    

    相比于 JAVA 在对象前加 (Class) 这样子的写法,Kotlin 是在对象之后添加 as Class 来实现转型。至少我个人而言,在习惯了 as Class 顺畅的写法之后,是再难以忍受 JAVA 中前置的写法,哪怕有 cast 快捷键的存在,仍然很容易打断我写代码的顺序和思路

    事实上,Kotlin 此处可以更简单:

    if(view is TextView) {
        view.setText("text")
    }
    

    因为当前上下文已经判明 view 就是 TextView,所以在当前代码块中 view 不再是 View 类,而是 TextView 类。这就是 Kotlin 的<b>智能转换</b>。

    接着上面的空安全来举个例子,常规思路下,既然 String 和 String? 是不同的类型,是不是我有可能会写出这样的代码?

    val a: A? = A()
    if (a != null) {
        println(a?.b)
    }
    

    这样子写,Kotlin 反而会给你显示一个高亮的警告,说这是一个不必要的 safe call。至于为什么,因为你前面已经写了 a != null 了啊,于是 a 在这个代码块里不再是 A? 类型, 而是 A 类型。

    val a: A? = A()
    if (a != null) {
        println(a.b)
    }
    

    智能转换还有一个经常出现的场景,那就是 switch case 语句中。在 Kotlin 中,则是 when 语法。

    fun testWhen(obj: Any) {
        when(obj) {
            is Int -> {
                println("obj is a int")
                println(obj + 1)
            }
    
            is String -> {
                println("obj is a string")
                println(obj.length)
            }
    
            else -> {
                println("obj is something i don't care")
            }
        }
    }
    
    fun main(args: Array<String>) {
        testWhen(98)
        testWhen("98")
    }
    
    输出如下:
    obj is a int
    99
    obj is a string
    2
    

    可以看出在已经判断出是 String 的条件下,原本是一个 Any 类的 obj 对象,我可以直接使用属于 String 类的 .length 属性。而在 JAVA 中,我们需要这样做:

    System.out.println("obj is a string")
    String string = (String) obj;
    System.out.println(string.length)
    

    或者

    System.out.println("obj is a string")
    System.out.println(((String) obj).length)
    

    前者打断了编写和阅读的连贯性,后者嘛。。

    Kotlin 的智能程度远不止如此,即便是现在,在编写代码的时候还会偶尔蹦一个高亮警告出来,这时候我才知道原来我的写法是多余的,Kotlin 已经帮我处理了好了。此处不再一一赘述。

    3.比 switch 更强大的 when

    通过上面智能转化的例子,已经展示了一部分 when 的功能。但相对于 JAVA 的 switch,Kotlin 的 when 带给我的惊喜远远不止这么一点。

    例如:

    fun testWhen(int: Int) {
        when(int) {
            in 10 .. Int.MAX_VALUE -> println("${int} 太大了我懒得算")
            2, 3, 5, 7 -> println("${int} 是质数")
            else -> println("${int} 不是质数")
        }
    }
    
    fun main(args: Array<String>) {
        (0..10).forEach { testWhen(it) }
    }
    
    输出如下:
    0 不是质数
    1 不是质数
    2 是质数
    3 是质数
    4 不是质数
    5 是质数
    6 不是质数
    7 是质数
    8 不是质数
    9 不是质数
    10 太大了我懒得算
    

    和 JAVA 中死板的 switch-case 语句不同,在 when 中,我既可以用参数去匹配 10 到 Int.MAX_VALUE 的区间,也可以去匹配 2, 3, 5, 7 这一组值,当然我这里没有列举所有特性。when 的灵活、简洁,使得我在使用它的时候变得相当开心(和 JAVA 的 switch 对比的话)

    4.容器的操作符

    自从迷上 RxJava 之后,我实在很难再回到从前,这其中就有 RxJava 中许多方便的操作符。而 Kotlin 中,容器自身带有一系列的操作符,可以非常简洁的去实现一些逻辑。

    例如:

    (0 until container.childCount)
            .map { container.getChildAt(it) }
            .filter { it.visibility == View.GONE }
            .forEach { it.visibility = View.VISIBLE }
    

    上述代码首先创建了一个 0 到 container.childCount - 1 的区间;再用 map 操作符配合取出 child 的代码将这个 Int 的集合转化为了 childView 的集合;然后在用 filter 操作符对集合做筛选,选出 childView 中所有可见性为 GONE 的作为一个新的集合;最终 forEach 遍历把所有的 childView 都设置为 VISIBLE。

    这里再贴上 JAVA 的代码作为对比。

    for(int i = 0; i < container.childCount - 1;  i++) {
        View childView = container.getChildAt(i);
        if(childView.getVisibility() == View.GONE) {
            childView.setVisibility(View.VISIBLE);
        }
    }
    

    这里就不详细的去描述这种链式的写法有什么优点了。

    5.线程切换,so easy

    既然上面提到了 RxJava,不得不想起 RxJava 的另一个优点——线程调度。Kotlin 中有一个专为 Android 开发量身打造的库,名为 anko,其中包含了许多可以简化开发的代码,其中就对线程进行了简化。

    async {
        val response = URL("https://www.baidu.com").readText()
        uiThread {
            textView.text = response
        }
    }
    

    上面的代码很简单,通过 async 方法将代码实现在一个异步的线程中,在读取到 http 请求的响应了之后,再通过 uiThread 方法切换回 ui 线程将 response 显示在 textView 上。

    抛开内部的实现,你再也不需要为了一个简简单单的异步任务去写一大堆的无效代码。按照惯例,这里似乎应该贴上 JAVA 的代码做对比,但请原谅我不想刷屏(啊哈哈)

    6.一个关键字实现单例

    没错,就是一个关键字就可以实现单例:

    object Log {
        fun i(string: String) {
            println(string)
        }
    }
    
    fun main(args: Array<String>) {
        Log.i("test")
    }
    

    再见,单例模式

    7.自动 getter、setter 及 class 简洁声明

    JAVA 中有如下类

    class Person {
        private String name;
    
        public Person(String name) {
            this.name = name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        public void getName() {
            return name;
        }
    }
    
    Person person = new Person("张三");
    

    可以看出,标准写法下,一个属性对应了 get 和 set 两个方法,需要手动写的代码量相当大。当然有快捷键帮助我们生成这些代码,但是考虑到各种复杂情形总归不完美。

    而 Kotlin 中是这样的:

    class Person(var name: String)
    val person = Person("张三");
    

    还可以添加默认值:

    class Person(var name: String = "张三")
    val person = Person()
    

    再附上我项目中一个比较复杂的数据类:

    data class Column(
            var subId: String?,
            var subTitle: String?,
            var subImg: String?,
            var subCreatetime: String?,
            var subUpdatetime: String?,
            var subFocusnum: Int?,
            var lastId: String?,
            var lastMsg: String?,
            var lastType: String?,
            var lastMember: String?,
            var lastTIme: String?,
            var focus: String?,
            var subDesc: String?,
            var subLikenum: Int?,
            var subContentnum: Int?,
            var pushSet: String?
    )
    

    一眼望去,没有多余代码。这是为什么我认为 Kotlin 代码比 JAVA 代码要更容易写得干净的原因之一。

    8. DSL 式编程

    说起 dsl ,Android 开发者接触的最多的或许就是 gradle 了

    例如:

    android {
        compileSdkVersion 23
        buildToolsVersion "23.0.2"
    
        defaultConfig {
            applicationId "com.zll.demo"
            minSdkVersion 15
            targetSdkVersion 23
            versionCode 1
            versionName "1.0"
        }
        buildTypes {
            release {
                minifyEnabled false
                proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
            }
        }
    }
    

    这就是一段 Groovy 的 DSL,用来声明编译配置

    那么在 Android 项目的代码中使用 DSL 是一种什么样的感觉呢?

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
    
        val homeFragment = HomeFragment()
        val columnFragment = ColumnFragment()
        val mineFragment = MineFragment()
    
        setContentView(
                tabPages {
                    backgroundColor = R.color.white
                    dividerColor = R.color.colorPrimary
                    behavior = ByeBurgerBottomBehavior(context, null)
    
                    tabFragment {
                        icon = R.drawable.selector_tab_home
                        body = homeFragment
                        onSelect { toast("home selected") }
                    }
    
                    tabFragment {
                        icon = R.drawable.selector_tab_search
                        body = columnFragment
                    }
    
                    tabImage {
                        imageResource = R.drawable.selector_tab_photo
                        onClick { showSheet() }
                    }
    
                    tabFragment {
                        icon = R.drawable.selector_tab_mine
                        body = mineFragment
                    }
                }
        )
    }
    
    效果图

    没错,上面的代码就是用来构建这个主界面的 viewPager + fragments + tabBar 的。以 tabPages 作为开始,设置背景色,分割线等属性;再用 tabFrament 添加 fragment + tabButton,tabImage 方法则只添加 tabButton。所见的代码都是在做配置,而具体的实现则被封装了起来。

    前面提到过 anko 这个库,其实也可以用来替代 xml 做布局用:

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
    
        verticalLayout {
            textView {
                text = "这是标题"
            }.lparams {
                width = matchParent
                height = dip(44)
            }
    
            textView {
                text = "这是内容"
                gravity = Gravity.CENTER
            }.lparams {
                width = matchParent
                height = matchParent
            }
        }
    }
    

    相比于用 JAVA 代码做布局,这种 DSL 的方式也是在做配置,把布局的实现代码封装在了背后,和 xml 布局很接近。

    关于 DSL 和 anko 布局,以后会有专门的文章做介绍,这里就此打住。

    9.委托/代理,SharedPreference 不再麻烦

    通过 Kotlin 中的委托功能,我们能轻易的写出一个 SharedPreference 的代理类

    class Preference<T>(val context: Context, val name: String?, val default: T) : ReadWriteProperty<Any?, T> {
        val prefs by lazy {
            context.getSharedPreferences("xxxx", Context.MODE_PRIVATE)
        }
    
        override fun getValue(thisRef: Any?, property: KProperty<*>): T = with(prefs) {
            val res: Any = when (default) {
                is Long -> {
                    getLong(name, 0)
                }
                is String -> {
                    getString(name, default)
                }
                is Float -> {
                    getFloat(name, default)
                }
                is Int -> {
                    getInt(name, default)
                }
                is Boolean -> {
                    getBoolean(name, default)
                }
                else -> {
                    throw IllegalArgumentException("This type can't be saved into Preferences")
                }
            }
            res as T
        }
    
        override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) = with(prefs.edit()) {
            when (value) {
                is Long -> putLong(name, value)
                is String -> putString(name, value)
                is Float -> putFloat(name, value)
                is Int -> putInt(name, value)
                is Boolean -> putBoolean(name, value)
                else -> {
                    throw IllegalArgumentException("This type can't be saved into Preferences")
                }
            }.apply()
        }
    }
    

    暂且跳过原理,我们去看怎么使用

    class EntranceActivity : BaseActivity() {
        
        private var userId: String by Preference(this, "userId", "")
    
        override fun onCreate(savedInstanceState: Bundle?) {
            testUserId()
        }
        
        fun testUserId() {
            if (userId.isEmpty()) {
                println("userId is empty")
                userId = "default userId"
            } else {
                println("userId is $userId")
            }
        }
    }
    
    重复启动 app 输出结果:
    userId is empty
    userId is default userId
    userId is default userId
    ...
    

    第一次启动 app 的时候从 SharedPreference 中取出来的 userId 是空的,可是后面却不为空。由此可见,userId = "default userId" 这句代码成功的将 SharedPreference 中的值修改成功了。

    也就是说,在这个 Preference 代理的帮助下,SharedPreference 存取操作变得和普通的对象调用、赋值一样的简单。

    10.扩展,和工具类说拜拜

    很久很久以前,有人和我说过,工具类本身就是一种违反面向对象思想的东西。可是当时我就想了,你不让我用工具类,那有些代码我该怎么写呢?直到我知道了扩展这个概念,我才豁然开朗。

    fun ImageView.displayUrl(url: String?) {
        if (url == null || url.isEmpty() || url == "url") {
            imageResource = R.mipmap.ic_launcher
        } else {
            Glide.with(context)
                    .load(ColumnServer.SERVER_URL + url)
                    .into(this)
        }
    }
    ...
    val imageView = findViewById(R.id.avatarIv) as ImageView
    imageView.displayUrl(url)
    

    上述代码可理解为:

    1.我给 ImageView 这个类扩展了一个名为 displayUrl 的方法,这个方法接收一个名为 url 的 String?类对象。如不出意外,会通过 Glide 加载这个 url 的图片,显示在当前的 imageView 上;

    2.我在另一个地方通过 findViewById 拿到了一个 ImageView 类的实例,然后调用这个 imageView 的displayUrl 方法,试图加载我传入的 url

    通过扩展来为 ImageView 添加方法,相比于通过继承 ImageView 来写一个 CustomImageView,再添加方法而言,侵入性更低,不需要在代码中全写 CustomImageView,也不需要在 xml 布局中将包名写死,造成移植的麻烦。

    这事用工具类当然也可以做,比如做成 ImageUtil.displayUrl(imageView, url),但是工具类阅读起来并没有扩展出来的方法读起来更自然更流畅。

    扩展是 Kotlin 相比于 JAVA 的一大杀器

    目前先写到这里,后续还会有更新~~

    相关文章

      网友评论

      • h2coder:好赞:+1:
      • d8184ca3c970:其它地方看不到类扩展了什么功能,感觉还是工具类好用啊
        ygzbrsnm:扩展方法要写在文件里.不要放在类里面.这样所以地方都能用了
      • 月光和我:非常给力
      • 皮特天:但是用anko写xml真为不够快,不方便哪,而且还不能预览
        neverwoods:目前 anko 确实不够成熟,部分功能甚至不支持。但是就目前已有的内容来说,熟练之后真不比 xml 慢,甚至可能更快。至于预览,这真是一个问题,官方的预览插件从 AS 2.3 开始就不能用了,也是 AS 本身的预览模块大改造成的吧
      • this_is_for_u:早看见你这篇文章能早点着手Kotlin
      • N丶aMe丨宇宇:在使用对象的属性和方法的时候,String 类型的对象可以毫无顾忌的直接使用,而 String?类型需要你先做非空判断。 这一段语句是不是写错了?加问号的不需要做判断的吧
        N丶aMe丨宇宇:@N丶aMe丨宇宇 kotlin的空指针安全不是可以不用非空判断吗。。
        N丶aMe丨宇宇:@neverwoods 可能是我理解错了
        neverwoods:@N丶aMe丨宇宇 大兄弟,加问号的是可为空类型,可能为空的对象你不做判断的?
      • huhu2008:程序员真苦逼 学不完
        年才下:这么感叹你本身就不适合做程序员
        17ae460d5c1b:看了一天的kotlin语法,现在就可以重写我以前的项目了,这个入手不要太简单,学习才会快乐
        neverwoods:@huhu2008 这有什么好苦逼的,个人认为要喜欢新鲜的东西才能在这个行业长久的干下去
      • code点点DD:JetBrain出品,必属精品
      • dongjunkun:相比于Java,确实简洁很多
      • RetroX:看了没几条就又心动了😅😅😅
      • 八阿哥_:给大哥顶一顶,写的很好支持

      本文标题:用 Kotlin 开发 Android 项目是一种什么样的感受?

      本文链接:https://www.haomeiwen.com/subject/mwpmottx.html