<-- Home |--jetpack

Jetpack Compose for Desktop-005 程序猿初试Composable

组合的概念

在Jetpack Compose中,提出了一个概念就是可组合的声明式界面开发。

这个Compose就是可组合中“组合”的意思。也就是我们描述界面,有一系列可以组合使用的元素,这些元素嵌套组合,构成复杂的界面。这种方式和传统的XML方式有点类似。

比如,描述一列标签构成的界面,我们可以这样写:

1@Composable
2fun Greeting(name: String) {
3    Text(text = "Hello $name!")
4}

这个例子很烦人,每个教程的开头都是这个例子,但是这个例子很好地展示了Compose的特点。我们通过一个函数来描述一个标签,这个标签的内容是一个字符串,这个字符串是一个模板,我们可以通过模板来生成不同的标签。这个函数就是一个可组合的函数,我们可以在其他地方调用这个函数,来生成一个标签。

乃至,我们在描述一个按钮的时候,也是类似的:

1@Composable
2fun MyButton(text: String, onClick: () -> Unit) {
3    Button(onClick = onClick) {
4        Text(text = text)
5    }
6}

可以看到,这里按钮中的文字,居然是一个标签,

1JButton button = new JButton("Click Me");
2
3button.addActionListener(new ActionListener() {
4    @Override
5    public void actionPerformed(ActionEvent e) {
6        System.out.println("Button Clicked!");
7    }
8});

这跟传统Java Swing的方式不同,Java Swing的按钮是一个对象,这个对象有一个方法叫做 setText,我们可以通过这个方法来设置按钮的文字。而在Compose中,按钮的文字是一个标签,这个标签是一个函数,我们可以通过这个函数来设置按钮的文字。听起来也没啥不同的。

但是这里实际上涉及到一个功能分离和复用的概念。按钮的核心是点击事件;文本控件关心的是显示一串字符。当已经有了文本控件,按钮只需要内涵(组合)一个文本控件,而不需要自己在额外处理文本、字符串的显示。这样的设计,使得我们可以更加灵活地组合界面元素,而不需要关心界面元素的具体实现。

这也是可组合的核心思想,不同的功能,组合起来使用,而不是形成复杂的对象继承关系,难以追溯某个具体的功能是在哪个类中实现的。

例如,Compose中,我们的布局,也是通过可组合的函数来实现的:

 1@Composable
 2inline fun Column(
 3    modifier: Modifier = Modifier,
 4    verticalArrangement: Arrangement.Vertical = Arrangement.Top,
 5    horizontalAlignment: Alignment.Horizontal = Alignment.Start,
 6    content: @Composable ColumnScope.() -> Unit
 7) {
 8    val measurePolicy = columnMeasurePolicy(verticalArrangement, horizontalAlignment)
 9    Layout(
10        content = { ColumnScopeInstance.content() },
11        measurePolicy = measurePolicy,
12        modifier = modifier
13    )
14}

这个可组合函数实现了一个竖向配列其内容的布局。可以看看这个函数的几个参数,来猜一猜这个函数本身的功能。

  • modifier:布局的修饰器,用来修饰布局的样式。
  • verticalArrangement:垂直排列的方式,可以是顶部对齐、居中对齐、底部对齐。
  • horizontalAlignment:水平对齐的方式,可以是左对齐、居中对齐、右对齐。

最后一个参数,正好是接受者函数,这个函数的功能是描述布局的内容。这个函数的接受者是一个 ColumnScope,这个 ColumnScope 是一个接口,用来描述布局的内容。这个接口中包含了一系列的函数,用来描述布局的内容。

我们就能在这个函数中,描述布局的内容,比如:

 1Column {
 2    Text("Hello, World!")
 3    Text("Hello, Compose!")
 4    Greeting("Android")
 5    Row {
 6        Text("Hello, World!")
 7        Text("Hello, Compose!")
 8        Greeting("Android")
 9    }
10}

看看,嵌套一个自定义的Compose函数,这就是可组合的思想。我们可以通过这种方式,组合各种各样的界面元素,来构建复杂的界面。

核心的概念:

  • 一个可组合函数描述一项能力(功能)
  • 自由组合无负担
  • 实现机制:函数接受者
  • 定义方法:@Composable注解

对照实际的界面开发

tic-tac-toe

卑微的起点

这个井字棋的界面,我们在需求分析的阶段可能提出如下的描述:

  • 3x3的格子,可以放置棋子
  • 棋子有两种状态,X和O
  • 点击格子,可以放置棋子
  • 每轮切换棋子状态
  • 提供一个展示当前玩家的标签、同时显示胜负

我们首先可以自己定义一个Grid函数,试试看:

 1@Composable
 2fun Grid() {
 3    Column {
 4        Row {
 5            Cell()
 6            Cell()
 7            Cell()
 8        }
 9        Row {
10            Cell()
11            Cell()
12            Cell()
13        }
14        Row {
15            Cell()
16            Cell()
17            Cell()
18        }
19    }
20}

如果这个Cell函数是一个可组合函数,描述一个格子的话,那么这个Grid函数就是描述一个3x3的格子的布局。这样,我们就完成了一个简单的界面的描述。

对于整个界面,我们可以这样描述:

1@Composable
2fun TicTacToe() {
3    Column {
4        Grid()
5            Text("Restart")
6        }
7        Text("Player X's turn")
8    }
9}

这样,我们就完成了一个简单的界面的描述。

稍微好一点点的改进

看看我们前面的Grid函数,正常情况下,我们就会试图通过循环来生成这个3x3的格子。这样,我们就可以通过一个循环来生成这个3x3的格子,而不是一个一个手动写。

 1@Composable
 2fun Grid() {
 3    Column {
 4        for (i in 0..2) {
 5            Row {
 6                for (j in 0..2) {
 7                    Cell()
 8                }
 9            }
10        }
11    }
12}

这样是不是就显得高级多了,也让可组合函数的内涵更加丰富,组合函数,甚至可以让程序来组合。

游戏中的最终界面

最终我们的选择跟上面类似,就是增加了亿点点细节。

首先是主函数:

 1fun main() = application {
 2    useResource("config.json") {
 3        config = loadConfig(it)
 4    }
 5
 6    Window(
 7        onCloseRequest = ::exitApplication,
 8        title = "Tic Tac Toe",
 9        state = rememberWindowState(
10            position = WindowPosition.Aligned(Alignment.Center),
11            size = DpSize(320.dp, 400.dp)
12        )
13    ) {
14        Box(
15            modifier = Modifier.background(Color.White).fillMaxSize(),
16            contentAlignment = Alignment.Center
17        ) {
18            TicToc()
19        }
20
21    }
22}

我们忽略前面的配置加载,直接看Window函数,这个函数是一个窗口的描述,我们可以看到,这个窗口的内容是一个Box,这个Box是一个盒子布局,这个盒子布局的内容是一个TicToc函数。

这里,我们就把Window理解为一个窗口,这个窗口的参数大概如下:

 1@Composable
 2fun Window(
 3    onCloseRequest: () -> Unit,
 4    state: WindowState = rememberWindowState(),
 5    visible: Boolean = true,
 6    title: String = "Untitled",
 7    icon: Painter? = null,
 8    undecorated: Boolean = false,
 9    transparent: Boolean = false,
10    resizable: Boolean = true,
11    enabled: Boolean = true,
12    focusable: Boolean = true,
13    alwaysOnTop: Boolean = false,
14    onPreviewKeyEvent: (KeyEvent) -> Boolean = { false },
15    onKeyEvent: (KeyEvent) -> Boolean = { false },
16    content: @Composable FrameWindowScope.() -> Unit
17) {
18//    ...
19}

在实际的调用中,我们可以什么参数都不用设置,只需要利用最后那个content: @Composable FrameWindowScope.() -> Unit参数,来描述窗口的内容。

当然,我们这里增加了三个参数,onCloseRequest是窗口关闭的回调函数,state是窗口的状态,title是窗口的标题。可以看到,如何设定窗口的位置和大小,都是通过state参数来设置的。这个参数的玄机,还得下一次再讲。

现在看Window组合的唯一一个对象, Box,这个Box是一个盒子布局,其内容是一个TicToc函数。

1Box(
2    modifier = Modifier.background(Color.White).fillMaxSize(),
3    contentAlignment = Alignment.Center
4) {
5    TicToc()
6}

这里Box的第一个参数是一个典型的组合对象修饰方法,大部分(几乎所有)的组合对象都有这个修饰方法,用来修饰这个组合对象的样式。这里,我们设置了背景颜色为白色,然后设置了这个盒子布局的大小为最大。然后这个Modifier最好的特点就是可以链式调用,这样我们可以一次性设置多个修饰条件。第二个函数式参数是一个对齐方式,这个对齐方式是用来设置这个盒子布局的内容的对齐方式。

TicToc函数中,我们可以描述整个井字棋的界面:

 1@Composable
 2@Preview
 3fun TicToc() {
 4    // ...
 5
 6    Column {
 7        for (i in 0..2) {
 8            Row {
 9                for (j in 0..2) {
10                    TicTocTile(i, j, ticTacToe)
11                }
12            }
13        }
14        Text(
15            modifier = Modifier.padding(10.dp).align(Alignment.CenterHorizontally),
16            text = ticTacToe.gameText(),
17            fontSize = 30.sp,
18            color = ticTacToe.color(),
19            textAlign = TextAlign.Center
20        )
21
22    }
23
24}

好吧,这跟前面我们设计的完全相同,不过也是增加了亿点点细节,修饰符啊,记录的信息啊。

值得注意的是,这里的标签同样增加了修饰符,Modifier对象。具有很好的一致性。

那么在这个TicTocTile函数中,我们可以描述一个格子的内容:

 1@Composable
 2fun TicTocTile(
 3    x: Int,
 4    y: Int,
 5    ticTacToe: TicTacToe
 6) {
 7
 8    Box(
 9        modifier = Modifier
10            .size(100.dp)
11            .background(Color.LightGray),
12        contentAlignment = Alignment.Center
13    ) {
14
15        Button(modifier = Modifier
16            .padding(5.dp)
17            .fillMaxSize(),
18            colors = ButtonDefaults.buttonColors(backgroundColor = ticTacToe[x, y].color()),
19            onClick = {
20                if (ticTacToe.isGameOver()) {
21                    ticTacToe.startNewGame()
22                    return@Button
23                }
24
25                ticTacToe.nextMove(x, y)
26
27            }) {
28            Text(
29                text = ticTacToe.textAt(x, y),
30                fontSize = 36.sp,
31                color = Color.Black
32            )
33        }
34    }
35}

更多的细节,毫无必要的复杂性……我们同样是一个Box,里面组合一个Button,这个Button里面是一个Text

无穷无尽的组合,无穷无尽的修饰符,这就是Compose的世界。

这里也就是设置了一些颜色啊、边界啊、间隔啊、大小啊。最后,通过一个onClick事件,来处理点击事件。这个当然也要下面再详细说明。

从前面的概念和具体的例子,我们已经对Compose的可组合的声明式界面开发有了一个初步的感受了。

这里的核心思想就是:

  • 一个可组合函数描述一项能力(功能)
  • 自由组合
  • 实现机制:函数接受者
  • 增加修饰符,逐步完善

Jetpack Compose提供的常用布局和控件

作为一个不那么成熟的桌面开发工具,Jetpack Compose Desktop提供的布局和控件还是比较有限的。

标准布局

  • Column:竖向布局
  • Row:横向布局
  • Box:盒子布局

basic-layout

通过嵌套这三种布局,设置各种不同的修饰条件,我们可以实现各种复杂的布局。

常用控件

  • Text:文本控件
  • Button:按钮控件
  • TextField:文本输入框
  • Checkbox:复选框
  • RadioButton:单选按钮
  • Slider:滑动条
  • ProgressBar:进度条

总之,Jetpack Compose Desktop提供了一系列的基确布局和控件,可以满足基本的界面开发需求。

总结

设计了UI,布局了界面,接下来就是处理事件和交互:

  • 输入信息
  • 显示信息

这两个核心的界面开发内容,下一次我们再来讨论。


文章标签

|-->jetpack


GitHub