写出优雅的Kotlin代码:聊聊我认为的 “Kotlinic“

“Kotlinic” 一词属于捏造的,参考的是著名的"Pythonic",后者可以译为“很Python”,意思是写的代码一看就很有Python味。照这个意思,"Kotlinic"就是“很Kotlin”,很有Kotlin味。

Kotlin程序员们不少是从Java转过来的,包括我;大部分时候,大家也都把它当大号的Java语法糖在用。但Kotlin总归是一门新语言,而且,在我眼里还是门挺优雅的语言。所以,或许我们可以把Kotlin写得更Kotlin些。我想简单粗浅的聊聊。

本文希望:聊聊一些好用的、简洁的但又不失语义的Kotlin代码

本文不希望:鼓励无脑追求高超技巧,完全放弃了可读性、可维护性,全篇奇技淫巧的操作

受限于本人水平,可能有错误或不严谨之处。如有此类问题,欢迎指出。也欢迎在评论区探讨交流~

善用with、apply、also、let

with和apply

with和apply,除了能帮忙少打一些代码外,重要的是能让代码区分更明确。比如

val textView = TextView(context)
textView.text = "fish"
textView.setTextColor(Color.BLUE)
textView.setOnClickListener {  }
val imageView = ImageView(context)
// ...

这就是典型的Java写法,自然,没什么问题。但要是类似的代码多起来,总感觉不知道哪里是哪里。如果换用apply呢?

val textView = TextView(context).apply {
 text = "fish"
    setTextColor(Color.BLUE)
    setOnClickListener {  }
}
val imageView = ImageView(context).apply {

} 

apply的大括号轻松划清了边界:我这里的代码和TextView相关。看着更整齐。

如果后面不需要这个变量,赋值还能省了

 // 设置某个view下的各个控件
with(view) {
findViewById<TextView>(R.id.some_id).apply {
 text = "fish"
        setTextColor(Color.BLUE)
        setOnClickListener {  }
}

findViewById<ImageView>(R.id.some_id).apply {

}
} 

apply的另一个常见场景是用于那些返回自己的函数,比如常见的Builder类的方法

fun setName(name: String): Builder{
    this.name = name
    return this
}

改成apply就简洁得多

fun setName(name: String) = apply{ this.name = name }

also

also的常见场景有很多,它的语义就是干完上一件事后附带干点什么事。 举个例子,给个函数

fun someFunc() : Model{
    // ...
    return Model(name = "model", value = "value")
}

如果我们突然想加个Log,打印一下返回值,按Java的写法,要这么干:

fun someFunc(): Model{
    // ...
    val tempModel = Model(name = "model", value = "value")
    print(tempModel)
    return tempModel
}

改的不少。但是按Kotlin的写法呢?

fun someFunc() : Model{
    return Model(name = "model", value = "value").also {
 print(it)
    }
}

不需要额外整个变量出来。

类似的,比如上面apply的例子,在没有声明变量的情况下,也可以这样用这个值

findViewById<ImageView>(R.id.some_id).apply {
 // ...
}.also{ println(it) } 

整在一起

这几个函数结合起来,在针对一些比较复杂的场景时,对提高代码的可读性还是挺有帮助的。

假设需求如下:“缩放 textView 的同时平移 button ,然后拉长 imageView,动画结束后 toast 提示”。

“Java”式写法

PropertyValuesHolder scaleX = PropertyValuesHolder.ofFloat("scaleX", 1.0f, 1.3f);
PropertyValuesHolder scaleY = PropertyValuesHolder.ofFloat("scaleY", 1.0f, 1.3f);
ObjectAnimator tvAnimator = ObjectAnimator.ofPropertyValuesHolder(textView, scaleX, scaleY);
tvAnimator.setDuration(300);
tvAnimator.setInterpolator(new LinearInterpolator());

PropertyValuesHolder translationX = PropertyValuesHolder.ofFloat("translationX", 0f, 100f);
ObjectAnimator btnAnimator = ObjectAnimator.ofPropertyValuesHolder(button, translationX);
btnAnimator.setDuration(300);
btnAnimator.setInterpolator(new LinearInterpolator());

ValueAnimator rightAnimator = ValueAnimator.ofInt(ivRight, screenWidth);
rightAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
    @Override
    public void onAnimationUpdate(ValueAnimator animation) {
        int right = ((int) animation.getAnimatedValue());
        imageView.setRight(right);
    }
});
rightAnimator.setDuration(400);
rightAnimator.setInterpolator(new LinearInterpolator());

AnimatorSet animatorSet = new AnimatorSet();
animatorSet.play(tvAnimator).with(btnAnimator);
animatorSet.play(tvAnimator).before(rightAnimator);
animatorSet.addListener(new Animator.AnimatorListener() {
    @Override
    public void onAnimationStart(Animator animation) {}
    @Override
    public void onAnimationEnd(Animator animation) {
        Toast.makeText(activity,"animation end" ,Toast.LENGTH_SHORT).show();
    }
    @Override
    public void onAnimationCancel(Animator animation) {}
    @Override
    public void onAnimationRepeat(Animator animation) {}
});
animatorSet.start();

乱糟糟的。改成“Kotlin式”写法呢?

AnimatorSet().apply {
    ObjectAnimator.ofPropertyValuesHolder(
            textView,
            PropertyValuesHolder.ofFloat("scaleX", 1.0f, 1.3f),
            PropertyValuesHolder.ofFloat("scaleY", 1.0f, 1.3f)
    ).apply {
        duration = 300L
        interpolator = LinearInterpolator()
    }.let {
        play(it).with(
                ObjectAnimator.ofPropertyValuesHolder(
                        button,
                        PropertyValuesHolder.ofFloat("translationX", 0f, 100f)
                ).apply {
                    duration = 300L
                    interpolator = LinearInterpolator()
                }
        )
        play(it).before(
                ValueAnimator.ofInt(ivRight,screenWidth).apply { 
                    addUpdateListener { animation -> imageView.right= animation.animatedValue as Int }
                    duration = 400L
                    interpolator = LinearInterpolator()
                }
        )
    }
    addListener(object : Animator.AnimatorListener {
        override fun onAnimationRepeat(animation: Animator?) {}
        override fun onAnimationEnd(animation: Animator?) {
            Toast.makeText(activity,"animation end",Toast.LENGTH_SHORT).show()
        }
        override fun onAnimationCancel(animation: Animator?) {}
        override fun onAnimationStart(animation: Animator?) {}
    })
    start() 
}

从上往下读,层次分明。读起来可以感觉到:

构建动画集,它包含{
    动画1
    将动画1和动画2一起播放
    将动画3在动画1之后播放
    。。。
}

(上面的代码均来自所引文章)

用好拓展函数

继续上面动画的例子接着说,可以看到,最后的Listener实际上我们只用了onAnimationEnd这一部分,但却写出了一大堆。这时候,拓展函数就起作用了。

幸运的是,Google官方的androidx.core:core-ktx已经有了对应的拓展函数:

public inline fun Animator.doOnEnd(
    crossinline action: (animator: Animator) -> Unit
): Animator.AnimatorListener =
    addListener(onEnd = action)

public inline fun Animator.addListener(
    crossinline onEnd: (animator: Animator) -> Unit = {} ,
    crossinline onStart: (animator: Animator) -> Unit = {} ,
    crossinline onCancel: (animator: Animator) -> Unit = {} ,
    crossinline onRepeat: (animator: Animator) -> Unit = {}
): Animator.AnimatorListener {
    val listener = object : Animator.AnimatorListener {
        override fun onAnimationRepeat(animator: Animator) = onRepeat(animator)
        override fun onAnimationEnd(animator: Animator) = onEnd(animator)
        override fun onAnimationCancel(animator: Animator) = onCancel(animator)
        override fun onAnimationStart(animator: Animator) = onStart(animator)
    }
    addListener(listener)
    return listener
}

所以上面的最后几行addListener可以改成

doOnEnd { Toast.makeText(activity,"animation end", Toast.LENGTH_SHORT).show() } 

是不是简单得多?

当然,弹出Toast似乎也很常用,所以再搞个拓展函数

inline fun Activity.toast(text: String, duration: Int = Toast.LENGTH_SHORT) 
    = Toast.makeText(this, text, duration).show()

上面的代码又可以改成这样

 (animation.) doOnEnd  { activity.toast("animation end") } 

再比较下原来的

 (animation.) addListener(object : Animator.AnimatorListener {
        override fun onAnimationRepeat(animation: Animator?) {}
        override fun onAnimationEnd(animation: Animator?) {
            Toast.makeText(activity,"animation end",Toast.LENGTH_SHORT).show()
        }
        override fun onAnimationCancel(animation: Animator?) {}
        override fun onAnimationStart(animation: Animator?) {}
})

是不是简洁得多?

上面提到androidx.core:core-ktx,其实它包含了大量有用的拓展函数。如果花点时间了解了解,或许能优化不少地方。最近掘金上也有不少类似的文章,可以参考参考

juejin.cn/post/711504…

juejin.cn/post/711692…

juejin.cn/post/712171…

用好运算符重载

Kotlin的运算符重载其实很有用,举个栗子

给List添加值

我见过这种代码

val list = listOf(1)
val newList = listOf(1, 2, 3)

val mutableList = list.toMutableList() // 转成可变的
mutableList.addAll(newList) // 添加新的
return mutableList.toList() // 返回,改成不可变的

但是换成运算符重载呢?

val list = listOf(1)
val newList = listOf(1, 2, 3)
return list + newList

一个"+"号,简明扼要。

又比如,想判断

某个View是否在ViewGroup中

最简单的看看索引呗

val group = LinearLayout(this)
val isContain = group.indexOfChild(view) != -1

不过,借助core-ktx提供的运算符,我们可以写出这样的代码

val group = LinearLayout(this)
val isContain = view in group

语义上更直接

想添加(删除)一个View?除了addView(removeView),也可以直接"+="(-=)

val group = LinearLayout(activity)
group += view // 添加子View

group -= view // 移除子View

想遍历?重载下iterator()运算符(core-ktx也写好了),就可以直接for了

val group = LinearLayout(this)
for (child in group) {
    //执行操作
}

(这几个View的例子基本也来自上面的文章)

此外,良好设计的拓展属性和拓展函数也能帮助写出更符合语意的代码,形如

// 设置view的大小
view.setSize(width = 50.dp, height = 100.dp) 
// 设置文字大小
textView.setFontSize(18.sp)

// 获取三天后的时间
val dueTime = today + 3.days

// 获取文本的md5编码
val md5 = "FunnySaltyFish".md5

上面的代码很容易能看出是要干嘛,而且也非常容易实现,此处就不再赘述了。

DSL

关于DSL,大家可能都知道有这么个东西,但可能用的都不多。但DSL若用得好,确实能达到化繁为简的功效。关于DSL的基本原理和实现,fundroid大佬在Kotlin DSL 实战:像 Compose 一样写代码 - 掘金中已经写得非常清晰了,本人就不再画蛇添足,接下来仅谈谈可能的使用吧。

构建UI

DSL的一个广泛应用应该就是构建UI了。

Anko(已过时)

较早的时候,一个比较广泛的应用可能就是之前的anko库了。JetBrains推出的这个库允许我们能够不用xml写布局。放一个来自博客Kotlin之小试Anko(Anko库的导入及使用) - SoClear - 博客园的例子

private fun showCustomerLayout() {
    verticalLayout {
        padding = dip(30)
        editText {
            hint = "Name"
            textSize = 24f
        }.textChangedListener {
            onTextChanged { str, _, _, _ ->
                println(str)
            }
        }
        editText {
            hint = "Password"
            textSize = 24f
        }.textChangedListener {
            onTextChanged { str, _, _, _ ->
                println(str)
            }
        }
        button("跳转到其它界面") {
            textSize = 26f
            id = BTN_ID
            onClick {
                // 界面跳转并携带参数
                startActivity<IntentActivity>("name" to "小明", "age" to 12)
            }
        }

        button("显示对话框") {
            onClick {
                makeAndShowDialog()
            }
        }
        button("列表selector") {
            onClick {
                makeAndShowListSelector()
            }
        }
    }
}

private fun makeAndShowListSelector() {
    val countries = listOf("Russia", "USA", "England", "Australia")
    selector("Where are you from", countries) { ds, i ->
        toast("So you're living in ${countries[i]},right?")
    }
}

private fun makeAndShowDialog() {
    alert("this is the msg") {
        customTitle {
            verticalLayout {
                imageView(R.mipmap.ic_launcher)
                editText {
                    hint = "hint_title"
                }
            }
        }

        okButton {
            toast("button-ok")
            // 会自行关闭不需要我们手动调用
        }
        cancelButton {
            toast("button-cancel")
        }
    }.show()
}

简洁优雅,而且由于是Kotlin代码生成的,还省去了解析xml的消耗。不过,由于“现在有更好的选择”,Anko官方已经停止维护此库;而被推荐的、用于取而代之的两个库分别是:Views DSLJetpack Compose

Views DSL

关于这个库,Anko官方在推荐时说,它是“An extensible View DSL which resembles Anko.”。二者也确实很相像,但Views DSL在Anko之上提供了更高的拓展性、对AppCompat的支持、对Material的支持,甚至提供了直接预览kt布局的能力!

基本的使用可以看看上图,额外的感兴趣的大家可以去官网查看,此处就不多赘述。

Jetpack Compose

作为一个用Compose超过一年的萌新,我自己是十分喜欢这个框架的。但同时,目前(2022-07-25)Compose的基建确实还尚不完善,所以对企业项目来说还,是应该充分评估后再考虑。但我仍然推荐你尝试一下,因为它简单、易用。即使是在现有的View项目中,也能无缝嵌入部分Compose代码;反之亦然。

Talk is cheap, show me your code. 比如要实现一个列表,View项目(使用RecyclerView)需要xml+Adapter+ViewHolder。而Compose就简洁得多:

LazyColumn(Modifier.fillMaxSize()) {
items(10) { i ->
Text(text = "Item $i", modifier = Modifier
            .fillMaxWidth()
            .clickable {
context.toast("点击事件")
            }
.padding(8.dp), style = MaterialTheme.typography.h4)
    }
} 

上面的代码创造了一个全屏的列表,并且添加了10个子项。每个item是一个文本,并且简单设置了其样式和点击事件。即使是完全不懂Compose,阅读代码也不难猜到各项的含义。运行起来,效果如下:

构建复杂的“字符串”

拼接字符串是一项常见的工作,不过,当它复杂起来但又有一定结构时,简单的"+"或者模板字符串看起来就有些杂乱了。这时,DSL就能很优雅的解决这个任务。

举几个常见的例子吧:

Html

使用DSL,能够写出类似这样的代码

val htmlText = buildHtml{
    html{
        body{
            div("id" to "wrapper"){
                p{ +"这是一个段落" }
                repeat(3){ i ->
                    li{ +"Item ${i+1}" }
                }
                img("src" to "https://www.xxx.xxx/", "width" to "100px")
            }
        }
    }
}

上述代码会生成类似这样的html

<!DOCTYPE html>
<html lang="zh-CN">
<body>
    <div id="wrapper">
        <p>这是一个段落</p>
        <ul>Item 1</ul>
        <ul>Item 2</ul>
        <ul>Item 3</ul>
        <img src="https://www.xxx.xxx/" width="100px">
    </div>
</body>
</html>

简洁直接,而且不容易出错。

你可能比较疑惑上面的+"xxx"是个啥,其实这是用了运算符重载把String转成了纯文本Tag。代码可能类似于

open class Tag()
open class TextTag(val value: String) : Tag()
operator fun String.unaryPlus() = TextTag(this)

Markdown

类似的,也可以用这种方式生成markdown。代码可能类似于

val markDownText = buildMarkdown {
    text("我是")
    link("FunnyFaltyFish", "https://github.com/FunnySaltyFish")
    newline()
    bold("很高兴见到你~")
}

生成的文本类似于

我是 [FunnySaltyFish](https://github.com/FunnySaltyFish)  
** 很高兴见到你~ **

SpannableString

对Android开发者来说,这个东西估计更常见。但传统的构造方式可以说够复杂的,所以DSL也能用。好的是,Google已经在core-ktx里写好了更简便的方法

使用例子如下:

val build = buildSpannedString {
        backgroundColor(Color.YELLOW) {
            append("我叫")
            bold {
                append("FunnySaltyFish")
            }
            append(",是一名学生")
        }
    }

渲染出的效果如下

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-31h31bXF-1659079663555)(https://upload-images.jianshu.io/upload_images/27208505-ebed289c6342f916.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)]

待续

本文应该还没有完,不过貌似写着写着也不短了,所以就先发了吧(主要是再晚些就赶不上征稿了 (笑))。后面我还想聊聊kotlin的代理、协程、Collection……争取下次见!

作者:FunnySaltyFish
链接:https://juejin.cn/post/7124676793801392136
最新**《Kotlin入门教程指南,全家桶学习资料》及更多更多资讯请扫描下方二维码**咨询!!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 要用 C 语言写出一个微信群聊机器人,您需要先了解微信群聊的相关信息和 C 语言的编程知识。 首先,微信群聊是通过微信服务器进行通信的,因此您需要了解如何通过网络协议与微信服务器进行通信。微信群聊使用的是基于 HTTP 的协议,因此您可以使用 C 语言中的网络编程库,如 socket、curl 等来实现与微信服务器的通信。 其次,您需要了解微信群聊机器人的功能。常见的功能包括: - 自动回复:当收到群聊中的消息时,机器人可以自动回复一条消息。 - 消息转发:机器人可以将收到的消息转发给其他用户。 - 消息过滤:机器人可以根据关键词或正则表达式对收到的消息进行过滤,并做出相应的处理。 为了实现这些功能,您需要了解微信群聊消息的数据结构和消息的发送方式。微信群聊消息的数据结构一般使用 JSON 格式,您可以使用 C 语言中的 JSON 解析库来解析消息数据。消 ### 回答2: 要用C语言编写一个微信群聊机器人,需要使用微信开放平台的API来实现与微信服务器的通信。以下是实现这个机器人的一般步骤: 1. 注册开发者账号:在微信开放平台上注册一个开发者账号,并创建一个应用。 2. 获取开发者凭证:在注册完成后,获取开发者凭证(AppID和AppSecret),这些凭证将用于访问微信API。 3. 配置服务器:在开发者平台的应用设置中,配置服务器地址。在本地搭建一个服务器,并确保能够接收和处理微信服务器的请求。 4. 接收和验证消息:编写C代码,实现服务器接收微信服务器发送的消息,并对消息进行验证,确保消息是合法的。 5. 解析消息内容:对接收到的消息进行解析,获取发送者的ID、消息类型、消息内容等信息。 6. 处理消息:根据不同的消息类型和内容,编写相应的处理逻辑。例如,如果接收到文本消息,机器人可以根据关键词回复相应的内容。 7. 构造回复消息:根据处理结果,构造回复消息的内容和格式。 8. 发送回复消息:使用微信API,将构造的回复消息发送给指定的微信群聊。 9. 循环监听:设置程序的循环监听,确保能够持续接收和处理微信服务器发送的消息。 需要注意的是,C语言并不是常用的编写微信机器人的语言,因为微信开放平台使用的是基于HTTP协议的接口,而C语言并不擅长处理网络通信。通常使用更适合网络编程的语言,如Python、Java等来编写微信机器人。以上仅是一般的步骤,具体的实现细节还需要参考微信开放平台的文档和示例代码。 ### 回答3: 要用C语言写出一个微信群聊机器人,首先需要了解微信公众平台的接入与消息交互流程。以下是一个简单的实现思路: 1. 注册微信公众号:首先需要注册一个微信公众号,并获取到开发者ID和AppSecret等必要信息。 2. 配置微信公众号平台:将开发者ID和AppSecret等信息配置到微信公众号平台,以便接收和发送消息。 3. 接入微信服务器:使用C语言实现一个HTTP Server,用于接收微信服务器发送过来的消息和事件。可以使用C语言的socket库或者第三方库实现HTTP Server。 4. 处理消息:根据微信服务器发送的消息类型,编写C语言代码解析消息内容。可使用JSON库解析JSON格式的消息,并根据消息类型进行相应的处理逻辑。 5. 发送回复消息:根据消息类型和内容,编写C语言代码生成对应的回复消息。可以使用XML库构建XML格式的回复消息,并使用HTTP协议将回复消息发送回微信服务器。 6. 部署与测试:将编写的C语言代码部署到服务器上,并注册该服务器的公网IP地址到微信公众号平台。进行测试并调试,确保机器人可以正常接收和回复微信群聊消息。 总结起来,实现一个用C语言编写的微信群聊机器人,需要了解微信公众平台的接入流程、掌握C语言的网络编程知识、使用相应的库进行消息解析和封装、以及进行服务器的部署与测试。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值