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

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

前言

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

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

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

正文

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

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

 

  1. fun demo() { 
  2.     val string1: String = "string1" 
  3.     val string2: String? = null 
  4.     val string3: String? = "string3" 
  5.      
  6.     println(string1.length) 
  7.     println(string2?.length) 
  8.     println(string3?.length) 

输出结果为:

 

  1. null 

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

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

 

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

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

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

我写过这样子的 JAVA 代码

 

  1. if(view instanceof TextView) { 
  2.     TextView textView = (TextView) view
  3.     textView.setText("text"); 

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

 

  1. if(view is TextView) { 
  2.     TextView textView = view as TextView 
  3.     textView.setText("text"

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

 

  1. JAVA 
  2.  
  3. if(view instanceof TextView) { 
  4.     ((TextView) view).setText("text"); 
  5.  
  6. Kotlin 
  7.  
  8. if(view is TextView) { 
  9.     (view as TextView).setText("text"

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

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

 

  1. if(view is TextView) { 
  2.     view.setText("text"

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

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

 

  1. val a: A? = A() 
  2. if (a != null) { 
  3.     println(a?.b) 

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

 

  1. val a: A? = A() 
  2. if (a != null) { 
  3.     println(a.b) 

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

 

  1. fun testWhen(obj: Any) { 
  2.     when(obj) { 
  3.         is Int -> { 
  4.             println("obj is a int"
  5.             println(obj + 1) 
  6.         } 
  7.  
  8.         is String -> { 
  9.             println("obj is a string"
  10.             println(obj.length) 
  11.         } 
  12.  
  13.         else -> { 
  14.             println("obj is something i don't care"
  15.         } 
  16.     } 
  17.  
  18. fun main(args: Array<String>) { 
  19.     testWhen(98) 
  20.     testWhen("98"

输出如下:

 

  1. obj is a int 
  2. 99 
  3. obj is a string 

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

 

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

或者

 

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

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

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

3.比 switch 更强大的 when

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

例如:

 

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

输出如下:

  1. 不是质数
  2. 不是质数
  3. 是质数
  4. 是质数
  5. 不是质数
  6. 是质数
  7. 不是质数
  8. 是质数
  9. 不是质数
  10. 不是质数
  11. 太大了我懒得算

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

4.容器的操作符

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

例如:

 

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

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

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

 

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

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

5.线程切换,so easy

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

 

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

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

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

6.一个关键字实现单例

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

 

  1. object Log { 
  2.     fun i(string: String) { 
  3.         println(string) 
  4.     } 
  5.  
  6. fun main(args: Array<String>) { 
  7.     Log.i("test"

再见,单例模式

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

JAVA 中有如下类

 

  1. class Person { 
  2.     private String name
  3.  
  4.     public Person(String name) { 
  5.         this.name = name
  6.     } 
  7.  
  8.     public void setName(String name) { 
  9.         this.name = name
  10.     } 
  11.  
  12.     public void getName() { 
  13.         return name
  14.     } 
  15.  
  16. Person person = new Person("张三"); 

Person person = new Person("张三");

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

而 Kotlin 中是这样的:

 

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

还可以添加默认值:

 

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

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

 

  1. data class Column
  2.         var subId: String?, 
  3.         var subTitle: String?, 
  4.         var subImg: String?, 
  5.         var subCreatetime: String?, 
  6.         var subUpdatetime: String?, 
  7.         var subFocusnum: Int?, 
  8.         var lastId: String?, 
  9.         var lastMsg: String?, 
  10.         var lastType: String?, 
  11.         var lastMember: String?, 
  12.         var lastTIme: String?, 
  13.         var focus: String?, 
  14.         var subDesc: String?, 
  15.         var subLikenum: Int?, 
  16.         var subContentnum: Int?, 
  17.         var pushSet: String? 

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

8. DSL 式编程

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

例如:

 

  1. android { 
  2.     compileSdkVersion 23 
  3.     buildToolsVersion "23.0.2" 
  4.  
  5.     defaultConfig { 
  6.         applicationId "com.zll.demo" 
  7.         minSdkVersion 15 
  8.         targetSdkVersion 23 
  9.         versionCode 1 
  10.         versionName "1.0" 
  11.     } 
  12.     buildTypes { 
  13.         release { 
  14.             minifyEnabled false 
  15.             proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 
  16.         } 
  17.     } 

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

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

 

  1. override fun onCreate(savedInstanceState: Bundle?) { 
  2.     super.onCreate(savedInstanceState) 
  3.  
  4.     val homeFragment = HomeFragment() 
  5.     val columnFragment = ColumnFragment() 
  6.     val mineFragment = MineFragment() 
  7.  
  8.     setContentView( 
  9.             tabPages { 
  10.                 backgroundColor = R.color.white 
  11.                 dividerColor = R.color.colorPrimary 
  12.                 behavior = ByeBurgerBottomBehavior(context, null
  13.  
  14.                 tabFragment { 
  15.                     icon = R.drawable.selector_tab_home 
  16.                     body = homeFragment 
  17.                     onSelect { toast("home selected") } 
  18.                 } 
  19.  
  20.                 tabFragment { 
  21.                     icon = R.drawable.selector_tab_search 
  22.                     body = columnFragment 
  23.                 } 
  24.  
  25.                 tabImage { 
  26.                     imageResource = R.drawable.selector_tab_photo 
  27.                     onClick { showSheet() } 
  28.                 } 
  29.  
  30.                 tabFragment { 
  31.                     icon = R.drawable.selector_tab_mine 
  32.                     body = mineFragment 
  33.                 } 
  34.             } 
  35.     ) 

 

[[231055]]

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

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

 

  1. override fun onCreate(savedInstanceState: Bundle?) { 
  2.     super.onCreate(savedInstanceState) 
  3.  
  4.     verticalLayout { 
  5.         textView { 
  6.             text = "这是标题" 
  7.         }.lparams { 
  8.             width = matchParent 
  9.             height = dip(44) 
  10.         } 
  11.  
  12.         textView { 
  13.             text = "这是内容" 
  14.             gravity = Gravity.CENTER 
  15.         }.lparams { 
  16.             width = matchParent 
  17.             height = matchParent 
  18.         } 
  19.     } 

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

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

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

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

 

  1. class Preference<T>(val context: Context, val name: String?, val default: T) : ReadWriteProperty<Any?, T> { 
  2.     val prefs by lazy { 
  3.         context.getSharedPreferences("xxxx", Context.MODE_PRIVATE) 
  4.     } 
  5.  
  6.     override fun getValue(thisRef: Any?, property: KProperty<*>): T = with(prefs) { 
  7.         val res: Any = when (default) { 
  8.             is Long -> { 
  9.                 getLong(name, 0) 
  10.             } 
  11.             is String -> { 
  12.                 getString(namedefault
  13.             } 
  14.             is Float -> { 
  15.                 getFloat(namedefault
  16.             } 
  17.             is Int -> { 
  18.                 getInt(namedefault
  19.             } 
  20.             is Boolean -> { 
  21.                 getBoolean(namedefault
  22.             } 
  23.             else -> { 
  24.                 throw IllegalArgumentException("This type can't be saved into Preferences"
  25.             } 
  26.         } 
  27.         res as T 
  28.     } 
  29.  
  30.     override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) = with(prefs.edit()) { 
  31.         when (value) { 
  32.             is Long -> putLong(name, value) 
  33.             is String -> putString(name, value) 
  34.             is Float -> putFloat(name, value) 
  35.             is Int -> putInt(name, value) 
  36.             is Boolean -> putBoolean(name, value) 
  37.             else -> { 
  38.                 throw IllegalArgumentException("This type can't be saved into Preferences"
  39.             } 
  40.         }.apply() 
  41.     } 

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

 

  1. class EntranceActivity : BaseActivity() { 
  2.      
  3.     private var userId: String by Preference(this, "userId"""
  4.  
  5.     override fun onCreate(savedInstanceState: Bundle?) { 
  6.         testUserId() 
  7.     } 
  8.      
  9.     fun testUserId() { 
  10.         if (userId.isEmpty()) { 
  11.             println("userId is empty"
  12.             userId = "default userId" 
  13.         } else { 
  14.             println("userId is $userId"
  15.         } 
  16.     } 

重复启动 app 输出结果:

 

  1. userId is empty 
  2. userId is default userId 
  3. userId is default userId 
  4. ... 

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

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

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

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

 

  1. fun ImageView.displayUrl(url: String?) { 
  2.     if (url == null || url.isEmpty() || url == "url") { 
  3.         imageResource = R.mipmap.ic_launcher 
  4.     } else { 
  5.         Glide.with(context) 
  6.                 .load(ColumnServer.SERVER_URL + url) 
  7.                 .into(this) 
  8.     } 
  9. ... 
  10. val imageView = findViewById(R.id.avatarIv) as ImageView 
  11. 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 的一大杀器

责任编辑:未丽燕 来源: 安卓巴士
相关推荐

2018-05-30 15:07:37

KotlinAndroid开发

2019-07-08 17:34:29

共享办公ideaPod文印

2019-04-03 14:51:18

CPU性能工艺

2015-11-03 08:51:21

程序员怪物

2020-11-06 17:49:38

程序员技术开发

2015-09-09 09:41:28

十年代码

2017-03-10 09:09:41

C语言体验

2015-12-03 09:23:25

程序员产品经理

2015-04-08 10:40:09

2021-01-14 21:46:02

Vue.jsReact框架

2010-08-02 13:30:34

移动开发移动开发平台

2017-04-06 15:00:38

编程语言

2015-02-04 10:55:14

2020-04-07 08:05:51

程序员互联网职业

2017-08-17 13:14:01

2019-01-11 10:39:24

软件架构虚拟空间机器人

2016-08-30 21:09:33

2014-02-25 09:55:07

敏捷开发

2016-12-07 18:10:08

边缘计算

2022-06-13 23:30:27

代码词汇高质量
点赞
收藏

51CTO技术栈公众号