本文正在参与「金石方案」

Compose推出之初,就曾引发广泛的评论,其间一个比较遍及的声响便是——“这跟Flutter也长得太像了吧?!”

这儿说的长得像,实际更多指的是UI编码的风格类似,而关于这种风格有一个专门的术语,叫做声明式UI

关于那些已经习惯了指令式UI的Android或iOS开发人员来说,刚开端的确很难了解什么是声明式UI。就像当初刚踏入编程领域的咱们,同样也很难了解面向进程编程面向对象编程的差异相同。

为了帮助这部分原生开发人员完结从指令式UI到声明式UI的思想改动,本文将结合示例代码编写、动画演示以及日子例子类比等形式,详细介绍声明式UI的概念、长处及其运用。

按例,先奉上思想导图一张,方便复习:

从Flutter到Compose,为什么都在推崇声明式UI?


指令式UI的特色

既然指令式UI与声明式UI是相对的,那就让咱们先来回忆一下,在一个常规的视图更新流程中,假如选用的是指令式UI,会是怎样的一个操作办法。

以Android为例,首要咱们都知道,Android所选用的界面布局,是基于View与ViewGroup对象、以树状结构来进行构建的视图层级。

从Flutter到Compose,为什么都在推崇声明式UI?

当咱们需求对某个节点的视图进行更新时,通常需求执行以下两个操作进程:

  1. 运用findViewById()等办法遍历树节点以找到对应的视图。
  2. 经过调用视图对象公开的setter办法更新视图的UI状况

咱们以一个最简单的计数器运用为例:

从Flutter到Compose,为什么都在推崇声明式UI?

这个运用仅有的逻辑便是“当用户点击”+”号按钮时数字加1”。在传统的Android完结办法下,代码应该是这姿态的:

class CounterActivity : AppCompatActivity() {
    var count: Int = 0
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_counter)
        val countTv = findViewById<TextView>(R.id.count_tv)
        countTv.text = count.toString()
        val plusBtn = findViewById<Button>(R.id.plus_btn)
        plusBtn.setOnClickListener {
            count += 1
            countTv.text = count.toString()
        }
    }
}

这段代码看起来没有任何难度,也没有明显的问题。可是,假设咱们鄙人一个版别中增加了更多的需求:

从Flutter到Compose,为什么都在推崇声明式UI?

  • 当用户点击”+”号按钮,数字加1的一起鄙人方容器中增加一个方块。
  • 当用户点击”-“号按钮,数字减1的一起鄙人方容器中移除一个方块。
  • 当数字为0时,下方容器的背景色变为透明。

现在,咱们的代码变成了这样:

class CounterActivity : AppCompatActivity() {
    var count: Int = 0
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_counter)
        // 数字
        val countTv = findViewById<TextView>(R.id.count_tv)
        countTv.text = count.toString()
        // 方块容器
        val blockContainer = findViewById<LinearLayout>(R.id.block_container)
        // "+"号按钮
        val plusBtn = findViewById<Button>(R.id.plus_btn)
        plusBtn.setOnClickListener {
            count += 1
            countTv.text = count.toString()
            // 方块
            val block = View(this).apply {
                setBackgroundColor(Color.WHITE)
                layoutParams = LinearLayout.LayoutParams(40.dp, 40.dp).apply {
                    bottomMargin = 20.dp
                }
            }
            blockContainer.addView(block)
            when {
                count > 0 -> {
                    blockContainer.setBackgroundColor(Color.parseColor("#FF6200EE"))
                }
                count == 0 -> {
                    blockContainer.setBackgroundColor(Color.TRANSPARENT)
                }
            }
        }
        // "-"号按钮
        val minusBtn = findViewById<Button>(R.id.minus_btn)
        minusBtn.setOnClickListener {
            if(count <= 0) return@setOnClickListener
            count -= 1
            countTv.text = count.toString()
            blockContainer.removeViewAt(0)
            when {
                count > 0 -> {
                    blockContainer.setBackgroundColor(Color.parseColor("#FF6200EE"))
                }
                count == 0 -> {
                    blockContainer.setBackgroundColor(Color.TRANSPARENT)
                }
            }
        }
    }
}

已经开端看得有点难受了吧?这正是指令式UI的特色,侧重于描绘怎么做,咱们需求像下达指令相同,手动处理每一项UI的更新,假如UI的杂乱度足够高的话,就会引发一系列问题,比如:

  • 可保护性差:需求编写很多的代码逻辑来处理UI变化,这会使代码变得臃肿、杂乱、难以保护。
  • 可复用性差:UI的规划与更新逻辑耦合在一起,导致只能在当时程序运用,难以复用。
  • 健壮性差:UI元素之间的关联度高,每个纤细的改动都或许一系列不知道的连锁反应。

声明式UI的特色

而同样的功用,假如选用的是声明式UI,则代码应该是这姿态的:

class _CounterPageState extends State<CounterPage> {
  int _count = 0;
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Column(
        children: [
          // 数字
          Text(
            _count.toString(),
            style: const TextStyle(fontSize: 48),
          ),
          Row(
            mainAxisSize: MainAxisSize.min,
            children: [
              // +"号按钮
              ElevatedButton(
                  onPressed: () {
                    setState(() {
                      _count++;
                    });
                  },
                  child: const Text("+")),
              // "-"号按钮
              ElevatedButton(
                  onPressed: () {
                    setState(() {
                      if (_count == 0) return;
                      _count--;
                    });
                  },
                  child: const Text("-"))
            ],
          ),
          Expanded(
              // 方块容器
              child: Container(
            width: 60,
            padding: const EdgeInsets.all(10),
            color: _count > 0 ? const Color(0xFF6200EE) : Colors.transparent,
            child: ListView.separated(
              itemCount: _count,
              itemBuilder: (BuildContext context, int index) {
                // 方块
                return Container(width: 40, height: 40, color: Colors.white);
              },
              separatorBuilder: (BuildContext context, int index) {
                return const Divider(color: Colors.transparent, height: 10);
              },
            ),
          ))
        ],
      ),
    );
  }
}

在这样的代码中,咱们简直看不到任何操作UI更新的代码,而这正是声明式UI的特色,它侧重于描绘做什么,而不是怎么做,开发者只需求重视UI应该怎么出现,而不需求关心UI的详细完结进程。

开发者要做的,就只是供给不同UI与不同状况之间的映射联系,而无需编写怎么在不同UI之间进行切换的代码。

所谓状况,指的是构建用户界面时所需求的数据,例如一个文本框要显示的内容,一个进展条要显示的进展等。Flutter结构答应咱们仅描绘当时状况,而转化的作业则由结构完结,当咱们改动状况时,用户界面将主动重新构建

下面咱们将依照通常情况下,用声明式UI完结一个Flutter运用所需求阅历的几个进程,来详细解析前面计数器运用的代码:

  1. 分析运用或许存在的各种状况

根据咱们前面关于“状况”的界说,咱们能够很容易地得出,在本例中,数字(_count值)本身即为计数器运用的状况,其间还包括数字为0时的一个特殊状况。

  1. 供给每个不同状况所对应要展示的UI

build办法是将状况转化为UI的办法,它能够在任何需求的时分被结构调用。咱们经过重写该办法来声明UI的结构:

关于顶部的文本,只需声明每次都运用最新回来的状况(数字)即可:

Text(
_count.toString(),
...
),

关于方块容器,只需声明当_count的值为0时,容器的背景色彩为透明色,否则为特定色彩:

Container(
    color: _count > 0 ? const Color(0xFF6200EE) : Colors.transparent,
... 
)

关于方块,只需声明回来的方块个数由_count的值决定:

ListView.separated(
  itemCount: _count,
  itemBuilder: (BuildContext context, int index) {
    // 方块
    return Container(width: 40, height: 40, color: Colors.white);
  },
  ...
),
  1. 根据用户交互或数据查询结果更改状况

当由于用户的点击数字发生变化,而咱们需求改写页面时,就能够调用setState办法。setState办法将会驱动build办法生成新的UI:

// "+"号按钮
ElevatedButton(
  onPressed: () {
    setState(() {
      _count++;
    });
  },
  child: const Text("+")),
// "-"号按钮
ElevatedButton(
  onPressed: () {
    setState(() {
      if (_count == 0) return;
      _count--;
    });
  },
  child: const Text("-"))
],

能够结合动画演示来回忆这整个进程:

最终,用一个公式来总结一下UI、状况与build办法三者的联系,那便是:

从Flutter到Compose,为什么都在推崇声明式UI?

以指令式和声明式别离点一杯奶茶

现在,你能了解指令式UI与声明式UI的差异了吗?假如仍是有些抽象,咱们能够用一个点奶茶的例子来做个比方:

当咱们用指令式UI的思想办法去点一杯奶茶,相当于咱们需求告知制造者,冲一杯奶茶必须依照煮水、冲茶、加牛奶、加糖这几个进程,一步步来完结,也即咱们需求明晰每一个进程,然后使得咱们的主意详细而可操作。

而当咱们用声明式UI的思想办法去点一杯奶茶,则相当于咱们只需求告知制造者,我需求一杯“温度适中、口感浓郁、有一点点甜味”的奶茶,而不用关心详细的制造进程和操作细节。

声明式编程的长处

综合以上内容,咱们能够得出声明式UI有以下几个长处:

  • 简化开发:开发者只需求保护状况->UI的映射联系,而不需求重视详细的完结细节,很多的UI完结逻辑被转移到了结构中。

  • 可保护性强:经过函数式编程的办法构建和组合UI组件,使代码愈加简练、明晰、易懂,便于保护。

  • 可复用性强:将UI的规划和完结分离开来,使得同样的UI组件能够在不同的运用程序中运用,提高了代码的可复用性。

总结与展望

总而言之,声明式UI是一种愈加高层次、愈加抽象的编程办法,其最大的长处在于能极大地简化现有的开发模式,因此在现代运用程序中得到广泛的运用,跟着更多结构的选用与更多开发者的加入,声明式UI必将继续发展壮大,成为今后构建用户界面的首选办法。