Kotlin学习历程一:Kotlin开发安卓的初体验

1.告别findViewById

不同于 JAVA 中,在 Kotlin 中 findViewById 本身就简化了很多,这得益于 Kotlin 的类型推断以及转型语法后置:

val onlyTv = findViewById(R.id.onlyTv) as TextView

在官方库Anko的支持下,可以更简化

当你试图 command/ctrl + 左键点击 onlyTv 想要查看 onlyTv 的来源的时候,你会发现你跳到了 activity_main 的布局文件:

是不是很方便呢?

注意:需要在actiivty中引入下布局

import kotlinx.android.synthetic.main.activity_main.*

在 anko 的帮助下,你只需要根据布局的 id 写一句 import 代码,然后你就可以把布局中的 id 作为 view 对象的名称直接进行使用。不仅 activity 中可以这样玩,你甚至可以 viewA.viewB.viewC,所以大可不必担心 adapter 中应当怎么写。

没有 findViewById,也就减少了空指针;没有 cast,则几乎不会有类型转换异常。

 

2.自动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 代码要更容易写得干净的原因之一。

 

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

简单一点的例子,那就是 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 中我们需要怎么处理?想想是不是就头(beng)大(kui)了?

 

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

这样子的 JAVA 代码:

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

在Kotlin中可以简化成:

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

直接少了一半的代码量,是不是很省力

智能转换还有一个经常出现的场景,那就是 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 属性。

 

5.紧跟上一条来说下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 对比的话)

 

6.容器的操作符

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);
    }
}

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

 

7.非常方便快捷的线程切换方式

 anko中包含了许多可以简化开发的代码,其中就对线程进行了简化。

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

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

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

 

8.一个关键字实现单例

毫不夸张,就是一个关键字,废话不多说,直接上代码

object Log {
    fun i(string: String) {
        println(string)
    }
}

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

 

9.简单粗暴的 startActivity

原本跳转activity我们是这样做的:

Intent intent = new Intent(LoginActivity.this, MainActivity.class);
startActivity(intent);

有时候需要传递数据:

Intent intent = new Intent(LoginActivity.this, MainActivity.class);
intent.putExtra("name", "张三");
intent.putExtra("age", 27);
startActivity(intent);

在 anko 的帮助下,kotlin的startActivity 是这样子的:

startActivity<MainActivity>()
startActivity<MainActivity>("name" to "张三", "age" to 27)
startActivityForResult<MainActivity>(101, "name" to "张三", "age" to 27)

无参情况下,只需要在调用 startActivity 的时候加一个 Activity 的 Class 泛型来告知要到哪去。有参也好说,这个方法支持你传入 vararg(变长参数) params: Pair<String, Any>。有没有觉得代码写起来、读起来流畅了许多?

 

10.玲珑小巧的 toast

Java中写个Toast是这样的:

Toast.makeText(context, "this is a toast", Toast.LENGTH_SHORT).show();

做个封装处理可能会是:

ToastUtil.showShort("this is a toast");

kotlin这边呢:

context.toast("this is a toast")

如果当前已经在context上下文中(比如activity),context也可以直接省略:

toast("this is a toast")

如果你是想要一个长时间的 toast:

longToast("this is a toast")

11.代理模式,让SharedPreference 不再麻烦

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 存取操作变得和普通的对象调用、赋值一样的简单。

 

12.扩展,Kotlin相比于Java的一大杀器

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),但是工具类阅读起来并没有扩展出来的方法读起来更自然更流畅。

还有比较常用的String的判断:

fun String.isName(): Boolean {
    if (isEmpty() || length > 10 || contains(" ")) {
        return false
    }

    val reg = Regex("^[a-zA-Z0-9\u4e00-\u9fa5]+$")
    return reg.matches(this)
}

fun String.isPassword(): Boolean {
    return length in 6..12
}

fun String.isNumber(): Boolean {
    val regEx = "^-?[0-9]+$"
    val pat = Pattern.compile(regEx)
    val mat = pat.matcher(this)

    return mat.find()
}
...

println("张三".isName())
println("123abc".isPassword())
println("123456".isNumber())

 

13.用 apply 方法进行数据组合

假设有如下 A、B、C 三个 class:

class A(val b: B)

class B(val c: C)

class C(val content: String)

可以看到,A 中有 B,B 中有 C。在实际开发的时候,我们有的时候难免会遇到比这个更复杂的数据,嵌套层级很深。这种时候,用 JAVA 初始化一个 A 类数据会变成一件非常痛苦的事情。例如:

C c = new C("content");
B b = new B(c);
A a = new A(b);

这还是 A、B、C 的关系很单纯的情况下,如果有大量数据进行组合,那么我们会需要初始化大量的对象进行赋值、修改等操作。如果我描述的不够清楚的话,大家不妨想一想用 JAVA 代码布局是一种什么样的感觉?

当然,在 JAVA 中也是有解决方案的,比如 Android 中常用的 Dialog,就用了 Builder 模式来进行相应配置。(说到这里,其实用 Builder 模式基本上也可以说是 JAVA 语言的 DSL)

但是在更为复杂的情况下,即便是有设计模式的帮助,也很难保证代码的可读性。那么 Kotlin 有什么好方法,或者说小技巧来解决这个问题吗?

Kotlin 中有一个名为 apply 的方法,它的源码是这样子的:

@kotlin.internal.InlineOnly
public inline fun <T> T.apply(block: T.() -> Unit): T { block(); return this }

没有 Kotlin 基础的小伙伴看到这里一定会有点晕。我们先忽略一部分细节,把关键的信息提取出来,再改改格式看看:

public fun <T> T.apply(block: T.() -> Unit): T {
    block()
    return this
}

1.首先,我们可以看出 T 是一个泛型,而且后面没有给 T 增加约束条件,那么这里的 T 可以理解为:我这是在给所有类扩展一个名为『apply』的方法;

2.第一行最后的: T 表明,我最终是要返回一个 T 类。我们也可以看到方法内部最后的 return this 也能说明,其实最后我就是要返回调用方法的这个对象自身;

3.在 return this 之前,我执行了一句 block(),这意味着 block 本身一定是一个方法。我们可以看到,apply 方法接收的 block 参数的类型有点特殊,不是 String 也不是其他什么明确的类型,而是T.() -> Unit

4.T.() -> Unit 表示的意思是:这是一个 ①上下文在 T 对象中②返回一个 Unit 类对象的方法。由于 Unit 和 JAVA 中的 Void 一致,所以可以理解为不需要返回值。那么这里的 block 的意义就清晰起来了:一个执行在 T,即调用 apply 方法的对象自身当中,又不需要返回值的方法。

有了上面的解析,我们再来看一下这句代码:

val textView = TextView(context).apply {
    text = "这是文本内容"
    textSize = 16f
}

这句代码就是初始化了一个 TextView,并且在将它赋值给 textView 之前,将自己的文本、字体大小修改了。

或许你会觉得这和 JAVA 比起来并没有什么优势。别着急,我们慢慢来:

layout.addView(TextView(context).apply {
    text = "这是文本内容"
    textSize = 16f
})

这样又如何呢?我并不需要声明一个变量或者常量来持有这个对象才能去做修改操作。

上面的A、B、C 问题用 Kotlin 来实现是可以这么写的:

val a = A().apply {
    b = B().apply {
        c = C("content")
    }
}

我只声明了一个 a 对象,然后初始化了一个 A,在这个初始化的对象中先给 B 赋值,然后再提交给了 a。B 中的 C 也是如此。当组合变得复杂的时候,我也能保持我的可读性:

val a = A().apply {
    b = B().apply {
        c = C("content")
    }

    d = D().apply {
        b = B().apply {
            c = C("test")
        }

        e = E("test")
    }
}

上面的代码用 JAVA 实现会是如何一番场景?反正我是想一想就已经晕了。说到底,这个小技巧也就是①扩展方法 + ②高阶函数两个特性组合在一起实现的效果。

14.利用高阶函数搞事情

inline fun debug(code: () -> Unit) {
    if (BuildConfig.DEBUG) {
        code()
    }
}
...
// Application 中
debug {
    Timber.plant(Timber.DebugTree())
}

上述代码是先定义了一个全局的名为 debug 的方法,这个方法接收一个方法作为参数,命名为 code。然后在方法体内部,我先判断当前是不是 DEBUG 版本,如果是,再调用传入的 code 方法。

而后我们在 Application 中,debug 方法就成为了依据条件执行代码的关键字。仅当 DEBUG 版本的时候,我才初始化 Timber 这个日志库。

如果这还不够体现优点的话,那么可以再看看下面一段:

supportsLollipop {
    window.statusBarColor = Color.TRANSPARENT
    window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
}

当系统版本在 Lollipop 之上时才去做沉浸式状态栏。系统 api 经常会有版本的限制,相对于一个 supportsLollipop 关键字, 我想一定不是所有人都希望每次都去写:

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
    // do something
}

诸如此类的场景和可以自创的 关键字/代码块 还有很多。

inline fun handleException(code : () -> Unit) {
    try {
        code()
    } catch (e : Exception) {
        e.printStackTrace()
    }
}
...
handleException {
     println(Integer.parseInt("这明显不是数字"))
}

虽然大都可以用 if(xxxxUtil.isxxxx()) 来凑合,但是既然有了更好的方案,那还何必凑合呢?

 

15.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 布局很接近。

构建并显示 BottomSheet的对比:

Builder 版

BottomSheet.Builder(this@ShareActivity, R.style.ShareSheetStyle)
        .sheet(999, R.drawable.share_circle,  R.string.wXSceneTimeline)
        .sheet(998, R.drawable.share_freind,  R.string.wXSceneSession)
        .listener { _, id ->
            shareTo(bitmap, target = when(id) {
                999 -> SendMessageToWX.Req.WXSceneTimeline
                998 -> SendMessageToWX.Req.WXSceneSession
                else -> throw Exception("it can not happen")
            })
        }
        .build()
        .show()
DSL 版

showBottomSheet {
    style = R.style.ShareSheetStyle

    sheet {
        icon = R.drawable.share_circle
        text = R.string.wXSceneTimeline

        selected {
            shareTo(bitmap, SendMessageToWX.Req.WXSceneTimeline)
        }
    }

    sheet {
        icon = R.drawable.share_freind
        text = R.string.wXSceneSession

        selected {
            shareTo(bitmap, SendMessageToWX.Req.WXSceneTimeline)
        }
    }
}

apply 构建数据实例(微信分享)

普通版

val obj = WXImageObject(bitmap)
val thumb = ......
bitmap.recycle()

val msg = WXMediaMessage()
msg.mediaObject = obj
msg.thumbData = thumb

val req = SendMessageToWX.Req()
req.transaction = "share"
req.scene = target
req.message = msg

WxObject.api.sendReq(req)
DSL 版

WxObject.api.sendReq(
        SendMessageToWX.Req().apply {
            transaction = "share"
            scene = target
            message = WXMediaMessage().apply {
                mediaObject = WXImageObject(bitmap)
                thumbData = ......
                bitmap.recycle()
            }
        }
)

 

借鉴了下简书上的文档,此处附下链接,感兴趣的小伙伴可以移步瞅一瞅
https://www.jianshu.com/p/8a1fce6fa93a
https://www.jianshu.com/p/b444aea1b038

 

  • 5
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值