|
|
|
|
移动端

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

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

作者:佚名来源:安卓巴士|2018-05-30 15:22

技术沙龙 | 6月30日与多位专家探讨技术高速发展下如何应对运维新挑战!


前言

从初学 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.     ) 

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

没错,上面的代码就是用来构建这个主界面的 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 的一大杀器

【编辑推荐】

  1. 查看文档的8款最佳Android PDF阅读软件
  2. Google I/O宣布设计更新!你的Android App可能会变成这样……
  3. 初探Android逆向:通过游戏APP破解引发的安全思考
  4. 2018最新大厂Android面试真题
  5. 用 Kotlin 开发 Android 项目是一种什么样的感受(二)
【责任编辑:未丽燕 TEL:(010)68476606】

点赞 0
分享:
大家都在看
猜你喜欢

读 书 +更多

非常网管——网络服务

本书使用通俗易懂的语言,通过大量的实例,从实际应用的角度出发,全面系统地介绍了网络服务操作系统平台、电子邮件系统、Web站点和FTP站点...

订阅51CTO邮刊

点击这里查看样刊

订阅51CTO邮刊