关于android:深入详解-Jetpack-Compose-优化-UI-构建

2次阅读

共计 6289 个字符,预计需要花费 16 分钟才能阅读完成。

人们对于 UI 开发的预期曾经不同往昔。现如今,为了满足用户的需要,咱们构建的利用必须蕴含欠缺的用户界面,其中必然包含动画 (animation) 和动效 (motion),这些诉求在 UI 工具包创立之初时并不存在。为了解决如何疾速而高效地创立欠缺的 UI 这一技术难题,咱们引入了 Jetpack Compose —— 这是一个古代的 UI 工具包,可能帮忙开发者们在新的趋势下取得成功。

在本系列的两篇文章中,咱们将论述 Compose 的劣势,并探讨它背地的工作原理。作为开篇,在本文中,我会分享 Compose 所解决的问题、一些设计决策背地的起因,以及这些决策如何帮忙开发者。此外,我还会分享 Compose 的思维模型,您应如何思考在 Compose 中编写代码,以及如何创立您本人的 API。

Compose 所解决的问题

关注点拆散 (Separation of concerns, SOC) 是一个家喻户晓的软件设计准则,这是咱们作为开发者所要学习的基础知识之一。然而,只管其广为人知,但在实践中却经常难以把握是否该当遵循该准则。面对这样的问题,从 “ 耦合 ” 和 “ 内聚 ” 的角度去思考这一准则可能会有所帮忙。

编写代码时,咱们会创立蕴含多个单元的模块。” 耦合 ” 便是不同模块中单元之间的依赖关系,它反映了一个模块中的各局部是如何影响另一个模块的各个局部的。” 内聚 ” 则示意的是一个模块中各个单元之间的关系,它批示了模块中各个单元互相组合的正当水平。

在编写可保护的软件时,咱们的指标是最大水平地 缩小耦合 减少内聚

当咱们解决 紧耦合 的模块时,对一个中央的代码改变,便象征对其余的模块作出许多其余的改变。更糟的是,耦合经常是隐式的,以至于看起来毫无关联的批改,却会造成了意料之外的谬误产生。

关注点拆散是尽可能的将相干的代码组织在一起,以便咱们能够轻松地保护它们,并不便咱们随着利用规模的增长而扩大咱们的代码。

让咱们在以后 Android 开发的上下文中进行更为理论的操作,并以视图模型 (view model) 和 XML 布局为例:

视图模型会向布局提供数据。事实证明,这里暗藏了很多依赖关系: 视图模型与布局间存在许多耦合。一个更为相熟的能够让您查看这一清单的形式是通过一些 API,例如 findViewByID。应用这些 API 须要对 XML 布局的模式和内容有肯定理解。

应用这些 API 须要理解 XML 布局是如何定义并与视图模型产生耦合的。因为利用规模会随着工夫增长,咱们还必须保障这些依赖不会过期。

大多数古代利用会动静展现 UI,并且会在执行过程中一直演变。后果导致利用不仅要验证布局 XML 是否动态地满足了这些依赖关系,而且还须要保障在利用的生命周期内满足这些依赖。如果一个元素在运行时来到了视图层级,一些依赖关系可能会被毁坏,并导致诸如 NullReferenceExceptions 一类的问题。

通常,视图模型会应用像 Kotlin 这样的编程语言进行定义,而布局则应用 XML。因为这两种语言的差别,使得它们之间存在一条强制的分隔线。然而即便存在这种状况,视图模型与布局 XML 还是能够关联得非常严密。换句话说,它们二者严密耦合。

这就引出了一个问题: 如果咱们开始用雷同的语言定义布局与 UI 构造会怎么?如果咱们选用 Kotlin 来做这件事会怎么?

因为咱们能够应用雷同的语言,一些以往隐式的依赖关系可能会变得更加显著。咱们也能够重构代码并将其挪动至那些能够使它们缩小耦合和减少内聚的地位。

当初,您可能会认为这是建议您将逻辑与 UI 混合起来。不过事实的状况是,无论您如何组织架构,您的利用中都将呈现与 UI 相关联的逻辑。框架自身并不会扭转这一点。

不过框架能够为您提供一些工具,从而帮您更加简略地实现关注点拆散: 这一工具便是 Composable 函数,长久以来您在代码的其余中央实现关注点拆散所应用的办法,您在进行这类重构以及编写简洁、牢靠、可保护的代码时所取得的技巧,都能够利用在 Composable 函数上。

Composable 函数分析

这是一个 Composable 函数的示例:

@Composable
fun App(appData: AppData) {val derivedData = compute(appData)
  Header()
  if (appData.isOwner) {EditButton()
  }
  Body {for (item in derivedData.items) {Item(item)
    }
  }
}

在示例中,函数从 AppData 类接收数据作为参数。现实状况下,这一数据是不可变数据,而且 Composable 函数也不会扭转: Composable 函数该当成为这一数据的转换函数。这样一来,咱们便能够应用任何 Kotlin 代码来获取这一数据,并利用它来形容的咱们的层级构造,例如 Header() 与 Body() 调用。

这意味着咱们调用了其余 Composable 函数,并且这些调用代表了咱们层次结构中的 UI。咱们能够应用 Kotlin 中语言级别的原语来动静执行各种操作。咱们也能够应用 if 语句与 for 循环来实现控制流,来解决更为简单的 UI 逻辑。

Composable 函数通常利用 Kotlin 的尾随 lambda 语法,所以 Body() 是一个含有 Composable lambda 参数的 Composable 函数。这种关系意味着层级或构造,所以这里 Body() 能够蕴含多个元素组成的多个元素组成的汇合。

申明式 UI

“ 申明式 ” 是一个风行词,但也是一个很重要的字眼。当咱们议论申明式编程时,咱们议论的是与命令式相同的编程形式。让咱们来看一个例子:

假如有一个带有未读音讯图标的电子邮件利用。如果没有音讯,利用会绘制一个空信封;如果有一些音讯,咱们会在信封中绘制一些纸张;而如果有 100 条音讯,咱们就把图标绘制成如同在着火的样子 ……

应用命令式接口,咱们可能会写出一个上面这样的更新数量的函数:

fun updateCount(count: Int) {if (count > 0 && !hasBadge()) {addBadge()
  } else if (count == 0 && hasBadge()) {removeBadge()
  }
  if (count > 99 && !hasFire()) {addFire()
    setBadgeText("99+")
  } else if (count <= 99 && hasFire()) {removeFire()
  }
  if (count > 0 && !hasPaper()) {addPaper()
  } else if (count == 0 && hasPaper()) {removePaper()
  }
  if (count <= 99) {setBadgeText("$count")
  }
}

在这段代码中,咱们接管新的数量并且必须搞清楚如何更新以后的 UI 来反映对应的状态。只管是一个绝对简略的示例,这里依然呈现了许多极其状况,而且这里的逻辑也不简略。

作为代替,应用申明式接口编写这一逻辑则会看起来像上面这样:

@Composable
fun BadgedEnvelope(count: Int) {Envelope(fire=count > 99, paper=count > 0) {if (count > 0) {Badge(text="$count")
    }
  }
}

这里咱们定义:

  • 当数量大于 99 时,显示火焰;
  • 当数量大于 0 时,显示纸张;
  • 当数量大于 0 时,绘制数量气泡。

这便是申明式 API 的含意。咱们编写代码来按咱们的想法形容 UI,而不是如何转换到对应的状态。这里的要害是,编写像这样的申明式代码时,您不须要关注您的 UI 在先前是什么状态,而只须要指定以后该当处于的状态。框架管制着如何从一个状态转到其余状态,所以咱们不再须要思考它。

组合 vs 继承

在软件开发畛域,Composition (组合) 指的是多个简略的代码单元如何联合到一起,从而形成更为简单的代码单元。在面向对象编程模型中,最常见的组合模式之一便是基于类的继承。在 Jetpack Compose 的世界中,因为咱们应用函数代替了类型,因而实现组合的办法颇为不同,但相比于继承也领有许多长处,让咱们来看一个例子:

假如咱们有一个视图,并且咱们想要增加一个输出。在继承模型中,咱们的代码可能会像上面这样:

class Input : View() { /* ... */}
class ValidatedInput : Input() { /* ... */}
class DateInput : ValidatedInput() { /* ... */}
class DateRangeInput : ??? {/* ... */}

View 是基类,ValidatedInput 应用了 Input 的子类。为了验证日期,DateInput 应用了 ValidatedInput 的子类。然而接下来挑战来了: 咱们要创立一个日期范畴的输出,这意味着须要验证两个日期——开始和完结日期。您能够继承 DateInput,然而您无奈执行两次,这便是继承的限度: 咱们只能继承自一个父类。

在 Compose 中,这个问题变得很简略。假如咱们从一个根底的 Input Composable 函数开始:

@Composable
fun <T> Input(value: T, onChange: (T) -> Unit) {/* ... */}

当咱们创立 ValidatedInput 时,只须要在办法体中调用 Input 即可。咱们随后能够对其进行装璜以实现验证逻辑:

@Composable
fun ValidatedInput(value: T, onChange: (T) -> Unit, isValid: Boolean) {InputDecoration(color=if(isValid) blue else red) {Input(value, onChange)
  }
}

接下来,对于 DataInput,咱们能够间接调用 ValidatedInput:

@Composable
fun DateInput(value: DateTime, onChange: (DateTime) -> Unit) { 
  ValidatedInput(
    value,
    onChange = {... onChange(...) },
    isValid = isValidDate(value)
  )
}

当初,当咱们实现日期范畴输出时,这里不再会有任何挑战:只须要调用两次即可。示例如下:

@Composable
fun DateRangeInput(value: DateRange, onChange: (DateRange) -> Unit) {DateInput(value=value.start, ...)
  DateInput(value=value.end, ...)
}

在 Compose 的组合模型中,咱们不再有单个父类的限度,这样一来便解决了咱们在继承模型中所遭逢的问题。

另一种类型的组合问题是对装璜类型的形象。为了可能阐明这一状况,请您思考接下来的继承示例:

class FancyBox : View() { /* ... */}
class Story : View() { /* ... */}
class EditForm : FormView() { /* ... */}
class FancyStory : ??? {/* ... */}
class FancyEditForm : ??? {/* ... */}

FancyBox 是一个用于装璜其余视图的视图,本例中将用来装璜 Story 和 EditForm。咱们想要编写 FancyStory 与 FancyEditForm,然而如何做到呢?咱们要继承自 FancyBox 还是 Story?又因为继承链中单个父类的限度,使这里变得非常含混。

 

相同,Compose 能够很好地解决这一问题:

@Composable
fun FancyBox(children: @Composable () -> Unit) {Box(fancy) {children() }
}
@Composable fun Story(…) {/* ... */}
@Composable fun EditForm(...) {/* ... */}
@Composable fun FancyStory(...) {FancyBox { Story(…) }
}
@Composable fun FancyEditForm(...) {FancyBox { EditForm(...) }
}

咱们将 Composable lambda 作为子级,使得咱们能够定义一些能够包裹其余函数的函数。这样一来,当咱们要创立 FancyStory 时,能够在 FancyBox 的子级中调用 Story,并且能够应用 FancyEditForm 进行同样的操作。这便是 Compose 的组合模型。

封装

Compose 做的很好的另一个方面是 “ 封装 ”。这是您在创立公共 Composable 函数 API 时须要思考的问题: 公共的 Composable API 只是一组其接管的参数而已,所以 Compose 无法控制它们。另一方面,Composable 函数能够治理和创立状态,而后将该状态及它接管到的任何数据作为参数传递给其余的 Composable 函数。

当初,因为它正治理该状态,如果您想要扭转状态,您能够启用您的子级 Composable 函数通过回调告知以后扭转已备份。

重组

“ 重组 ” 指的是任何 Composable 函数在任何时候都能够被从新调用。如果您有一个宏大的 Composable 层级构造,当您的层级中的某一部分产生扭转时,您不会心愿从新计算整个层级构造。所以 Composable 函数是可重启动 (restartable) 的,您能够利用这一个性来实现一些弱小的性能。

举个例子,这里有一个 Bind 函数,外面是一些 Android 开发的常见代码:

fun bind(liveMsgs: LiveData<MessageData>) {liveMsgs.observe(this) { msgs ->
    updateBody(msgs)
  }
}

咱们有一个 LiveData,并且心愿视图能够订阅它。为此,咱们调用 observe 办法并传入一个 LifecycleOwner,并在接下来传入 lambda。lambda 会在每次 LiveData 更新被调用,并且产生这种状况时,咱们会想要更新视图。

应用 Compose,咱们能够反转这种关系。

@Composable
fun Messages(liveMsgs: LiveData<MessageData>) {val msgs by liveMsgs.observeAsState()
 for (msg in msgs) {Message(msg)
 }
}

这里有一个类似的 Composable 函数—— Messages。它接管了 LiveData 作为参数并调用了 Compose 的 observeAsState 办法。observeAsState 办法会把 LiveData<T> 映射为 State<T>,这意味着您能够在函数体的范畴应用其值。State 实例订阅了 LiveData 实例,这意味着 State 会在 LiveData 产生扭转的任何中央更新,也意味着,无论在何处读取 State 实例,包裹它的、已被读取的 Composable 函数将会主动订阅这些扭转。后果就是,这里不再须要指定 LifecycleOwner 或者更新回调,Composable 能够隐式地实现这两者的性能。

总结

Compose 提供了一种古代的办法来定义您的 UI,这使您能够无效地实现关注点拆散。因为 Composable 函数与一般 Kotlin 函数很类似,因而您应用 Compose 编写和重构 UI 所应用的工具与您进行 Android 开发的常识储备和所应用的工具将会无缝连接。

正文完
 0