“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 ,其实它包含了大量有用的拓宽函数。假如花点时间了解了解,或许能优化不少地方。最近上也有不少相似的文章,可以参阅参阅

/post/711504…

/post/711692…

/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 DSL 和 Jetpack Compose

Views DSL

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

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

根本的运用可以看看上图,额定的感兴趣的咱们可以去官网查看,此处就不多赘述。

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,阅读代码也不难猜到各项的意义。运行起来,作用如下:

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

构建杂乱的“字符串”

拼接字符串是一项常见的作业,不过,当它杂乱起来但又有必定结构时,简略的”+”或许模板字符串看起来就有些杂乱了。这时,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(",是一名学生")
     }
   }

渲染出的作用如下

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

待续

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

我正在参加技术社区创作者签约方案招募活动,点击链接报名投稿