003-Kotlin界面开发之声明式编程范式

news2024/11/6 3:24:49

在这里插入图片描述

概念本源

在界面程序开发中,有两个非常典型的编程范式:命令式编程和声明式编程。命令式编程是指通过编写一系列命令来描述程序的运行逻辑,而声明式编程则是通过编写一系列声明来描述程序的状态。在命令式编程中,程序员需要关心程序的执行过程,而在声明式编程中,程序员只需要关心程序的状态。在界面开发中,声明式编程的优势尤为明显,因为界面开发的本质就是描述界面的状态。

命令式编程范式

举个简单的例子,图形界面通常需要考虑的问题是把一堆界面元素组成合理的树状结构,在命令式编程中,我们的做法看起来是下面这样的伪代码:

val root = Container()
val label = Label()
val button = Button()
val panel = Panel()

panel.add(label)
panel.add(button)
root.add(panel)

root.show()

这很合理,这个命令式编程的代码描述了我们创建一系列对象,包括容器、面板、界面元素,然后通过结构调整他们的相互关系,构成界面。在完成构造之后,我们调用 root.show() 来显示这个界面。这个过程中,我们需要关心的是对象的创建、对象的关系、对象的显示,这是一个过程性的描述。

声明式编程范式

其实最常见的声明式编程范式就是 HTML,HTML 是一种标记语言,它的本质是一种声明式的描述,我们通过 HTML 来描述界面的结构,而不是描述界面的构造过程。下面是一个简单的 HTML 代码片段:

<!DOCTYPE html>

<html>
<head>
    <title>My First HTML Page</title>
</head>
<body>
    <h1>Hello, World!</h1>
</body>
</html>

好多图形界面开发的程序,Qt或者WPF都采用XML来描述界面,这样的方式也是声明式的。在这种方式中,我们只需要关心界面的结构,而不需要关心界面的构造过程。这种方式的优势在于,我们可以更加专注于界面的结构,而不需要关心界面的构造过程。

Jetpack Compose的声明式界面开发

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

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

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


@Composable
fun MyScreenContent(names: List<String> = listOf("Android", "there")) {
    Column {
        for (name in names) {
            Greeting(name = name)
            Divider(color = Color.Black)
        }
    }
}

这里,通过@Composable注解,我们定义了一个可组合的函数Greeting,这个函数接受一个字符串参数,然后返回一个Text组件。然后我们定义了一个MyScreenContent函数,这个函数接受一个字符串列表参数,然后返回一个Column组件,这个Column组件包含了一系列的Greeting组件和Divider组件。这样,我们就完成了一个简单的界面的描述。

深入一下

这样的方式就好像是XML这样的结构化文档,但是有是能够运行的代码。非常有意思,最好玩的是还能通过循环、判断来动态生成界面,这样的方式非常灵活,而且非常容易理解。

那么,这个玩意是如何实现的呢?

在前面Kotlin旋风之旅中,我们提到了Kotlin的DSL,这个DSL就是Jetpack Compose的核心。

Jetpack Compose的核心思想就是通过实现一种专门用于描述界面的DSL,开发人员通过这套DSL来描述和生成界面。

下面我们也试着用Kotlin的DSL来实现一个简单的DSL,通过这个DSL实现过程,对Jetpack Compose的实现原理有一个祛魅的过程,神秘感不那么强,调试的过程也会更加容易。

简化家族树DSL

我们要实现的是一种单体繁殖、类人、外星生物(也称为Person)的家族树DSL,这个DSL的结构如下:

fun main() {
    Person("Alice", 80) {
        Children {
            name = "Tom"
            age = 50
            Children {
                name = "Jerry"
                age = 25
                Children("Tom", 2)
            }

            Children {
                name = "Yan"
                age = 15
            }

            // 年龄写错了,改一下
            age = 53
        }

        Children(name = "Tim", age = 40) {
            Children(name = "Jerry", age = 5)
            Children(name = "Alex", age = 15)
        }

        // 调用输出函数,打印家族树
        print()
    }
}

这个描述的家族树大概是:

    |___Name: Bob, Age: 60
        |___Name: Tom, Age: 50
            |___Name: Jerry, Age: 25
                |___Name: Tom, Age: 2
            |___Name: Yan, Age: 15
        |___Name: Tim, Age: 40
            |___Name: Jerry, Age: 5
            |___Name: Alex, Age: 15

可以看到这个代码有几个特点:

  1. 通过Person函数来描述一个人,这个函数接受一个名字和年龄,然后通过Children函数来描述这个人的孩子。
  2. 名字和年龄可以省略,也可以通过nameage参数来指定。
  3. 描述的过程中,如果需要修改,也能通过nameage参数来修改。
  4. 能够调用print函数来打印自己的家族树。

这个DSL看起来非常简单,其实非常强大。这样就能够把一个家族树描述成跟其天然结构非常接近的、合法的Kotlin代码。

实现DSL

这个东西是怎么实现的呢?现给出完整代码:

class PersonImpl(n: String = "", a: Int = 0) {
    var name: String = n
    var age: Int = a

    private fun nBlank(indent: Int) = " ".repeat(indent)

    fun print(indent: Int = 0) {
        print("${nBlank(indent)}|___")
        print("Name: $name, ")
        println("Age: $age")
        for (child in children) {
            child.print(indent + 2)
        }
    }

    private val children = mutableListOf<PersonImpl>()

    fun addChildren(name: String, age: Int, block: PersonImpl.() -> Unit = {}) {
        val child = PersonImpl()
        child.name = name
        child.age = age
        child.block()
        children.add(child)
    }

    operator fun invoke(block: PersonImpl.() -> Unit = {}): PersonImpl {
        block()
        return this
    }


}

fun Person(name: String = "", age: Int = 0, block: PersonImpl.() -> Unit = {}) = PersonImpl(name, age)(block)

fun PersonImpl.Children(name: String = "", age: Int = 0, block: PersonImpl.() -> Unit = {}) =
    addChildren(name, age, block)

代码解析

上面的调用Person的方式要能实现,就需要定义一个函数,它包括三个参数,并且最后一个参数必须是一个能够接受某个类型的函数。

所以fun Person(name: String = "", age: Int = 0, block: PersonImpl.() -> Unit = {}) = PersonImpl(name, age)(block)符合这个要求,这也是一个Kotlin的语法糖,单行函数的函数定义。

同时,这个函数申明还省略了返回值类型,这是因为Kotlin的类型推导能力很强,编译器能够根据函数体的返回值类型推导出函数的返回值类型。

写成完整的函数形式,并且把构造对象和调用函数分开来写,是这样的。

fun Person(name: String = "", age: Int = 0, block: PersonImp.() -> Unit={}) {
    val p = PersonImp(name, age)
    p(block)
}

这个函数提供了调用Person(){}的方式,在大括号里面的代码,针对一个PersonImpl实例进行操作,这种方式称为接受者函数字面值。这个功能的实现,我猜要依赖于扩展函数的特性,相当于零时定义一个对象的扩展函数,并且在函数体内部可以直接访问这个对象的属性和方法。

当然,要能够想函数一样调用这个新建的对象,就需要在PersonImpl类中定义一个invoke操作符函数,这个函数的返回值是PersonImpl,这样就能够实现Person(){}的调用方式。

接下来就是Children函数,这个函数的作用是为一个PersonImpl对象添加一个孩子,这个函数的实现也是类似的,通过addChildren函数来实现。

fun PersonImpl.Children(name: String = "", age: Int = 0, block: PersonImpl.() -> Unit = {}) =
    addChildren(name, age, block)

这个实现了一个扩展函数,这个函数因此只能在PersonImpl对象上调用,当然,前面那个接受者函数的代码block里面,所有的调用都是针对PersonImpl对象的。

其他普通的构造函数、默认参数、属性、方法等等,都是普通的Kotlin代码,没有什么特别的。

总结

在深入进行Jetpack Compose的学习之前,我们先通过一个简单的DSL实现,了解了Jetpack Compose的核心思想:通过声明式的DSL来描述界面。这样的方式非常灵活,而且非常容易理解,也非常容易调试。通过这样的方式,我们可以更加专注于界面的结构,而不需要关心界面的构造过程。

这个实现的过程中,两个语法糖要自己在大脑里反复转换,最后一个参数是匿名函数,则可以移到括号外面;接受者匿名函数相当是临时定义一个扩展函数。

有一点点绕,但是多改改代码,也能够理解。

接下来,就要开始真正的Jetpack Compose的学习之旅了。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2234017.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Ubuntu 20.04 部署向量数据库 Milvus + Attu

前言 最开始在自己的办公电脑&#xff08;无显卡的 windows 10 系统&#xff09; 上使用 Docker Desktop 部署了 Milvus 容器&#xff0c;方便的很&#xff0c; 下载 Attu 也很方便&#xff0c;直接就把这个向量数据库通过 Attu 这个图形化界面跑了起来&#xff0c;使用起来感…

Linux(inode + 软硬链接 图片+大白话)

后面也会持续更新&#xff0c;学到新东西会在其中补充。 建议按顺序食用&#xff0c;欢迎批评或者交流&#xff01; 缺什么东西欢迎评论&#xff01;我都会及时修改的&#xff01; 在这里真的很感谢这位老师的教学视频让迷茫的我找到了很好的学习视频 王晓春老师的个人空间…

CM API方式设置YARN队列资源

简述 对于CDH版本我们可以参考Fayson的文章,本次是CDP7.1.7 CM7.4.4 ,下面只演示一个设置队列容量百分比的示例,其他请参考cloudera官网。 获取cookies文件 生成cookies.txt文件 curl -i -k -v -c cookies.txt -u admin:admin http://192.168.242.100:7180/api/v44/clusters …

【Linux】简易版shell

文章目录 shell的基本框架PrintCommandLineGetCommandLineParseCommandLineExecuteCommandInitEnvCheckAndExecBuildCommand代码总览运行效果总结 shell的基本框架 要写一个命令行我们首先要写出基本框架。 打印命令行获取用户输入的命令分析命令执行命令 基本框架的代码&am…

基于MySQL的企业专利数据高效查询与统计实现

背景 在进行产业链/产业评估工作时&#xff0c;我们需要对企业的专利进行评估&#xff0c;其中一个重要指标是统计企业每一年的专利数量。本文基于MySQL数据库&#xff0c;通过公司名称查询该公司每年的专利数&#xff0c;实现了高效的专利数据统计。 流程 项目流程概述如下&…

盘点 2024 十大免费/开源 WAF

WAF 是 Web Application Firewall 的缩写&#xff0c;也被称为 Web 应用防火墙。区别于传统防火墙&#xff0c;WAF 工作在应用层&#xff0c;对基于 HTTP/HTTPS 协议的 Web 系统有着更好的防护效果&#xff0c;使其免于受到黑客的攻击。 近几年经济增速开始放缓&#xff0c;科…

鸿蒙进阶-AlphabetIndexer组件

大家好&#xff0c;这里是鸿蒙开天组&#xff0c;今天我们来学习AlphabetIndexer组件&#xff0c;喜欢就点点关注吧&#xff01; 通过 AlphabetIndexer 组件可以与容器组件结合&#xff0c;实现导航联动&#xff0c;以及快速定位的效果 核心用法 AlphabetIndexer不是容器组件…

【Unity】【游戏开发】Sprite背景闪烁怎么解决

【现象】 VR游戏中&#xff0c;给作为屏幕的3D板子加上Canvas后再加背景image&#xff0c;运行时总是发现image闪烁不定。 【分析】 两个带颜色的object在空间上完全重合时也遇到过这样的问题&#xff0c;所以推测是Canvas的image背景图与木板的面重合导致。 【解决方法】 …

sublime Text中设置编码为GBK

要在sublime Text中设置编码为GBK&#xff0c;请按照以下步骤操作 1.打开Sublime Text编辑器, 2.点击菜单栏中的“Preferences”(首选项)选项&#xff0c;找打Package Control选项。 3.点击Package Control&#xff0c;随后搜索Install Package并点击&#xff0c;如下图 4.再…

队列与栈的代码对比(Java)

目录 链表实现队列 数组实现队列 链表实现栈 数组实现栈 图片: 链表实现队列 package Queue;import java.util.Iterator;public class LinkedListQueue <E> implements Queue<E>, Iterable<E>{//单向环形哨兵链表//节点类private static class Node<…

一些常规IP核功能

一,util_vector_logic util_vector_logic 主要支持以下类型的逻辑操作: 逻辑与(AND): 当所有输入都为1时,输出为1,否则为0。逻辑或(OR): 当任意输入为1时,输出为1,否则为0。逻辑非(NOT): 当输入为1时,输出为0;输入为0时,输出为1。异或(XOR): 当输入中有奇…

Docker篇(Docker安装)

目录 一、Centos7.x 1. yum 包更新到最新 2. 安装需要的软件包 3. 设置 yum 源为阿里云 4. 安装docker 5. 安装后查看docker版本 6. 设置ustc镜像源 二、CentOS安装Docker 前言 1. 卸载&#xff08;可选&#xff09; 2. 安装docker 3. 启动docker 4. 配置镜像加速 …

【c++ gtest】使用谷歌提供的gtest和抖音豆包提供的AI大模型来对代码中的函数进行测试

【c gtest】使用谷歌提供的gtest和抖音豆包提供的AI大模型来对代码中的函数进行测试 下载谷歌提供的c测试库在VsCode中安装抖音AI大模型找到c项目文件夹&#xff0c;使用VsCode和VS进行双开生成gtest代码进行c单例测试 下载谷歌提供的c测试库 在谷歌浏览器搜索github gtest, 第…

google adsense广告费中国收款结算被银行拒解决办法

多年前搞了几个网站&#xff0c;挂了谷歌google adsense广告&#xff0c;不知道不觉到了100美金最低结算&#xff0c;谷歌给我打款&#xff0c;之前是绑定交银银行的。被银行镜内登陆谷歌不合法不合规给拒绝入账&#xff0c;把美金退回了&#xff0c;怎么办&#xff1f; googl…

蓝桥杯 区间移位--二分、枚举

题目 代码 #include <stdio.h> #include <string.h> #include <vector> #include <algorithm> #include <iostream> using namespace std; struct node{ int a,b; }; vector<node> q; bool cmp(node x,node y){ return x.b <…

书生第四期实训营基础岛——L1G1000书生大模型全链路开源体系

书生浦语大模型开源开放体系 书生浦语开源一周年历史 2023.7.6&#xff1a;InternLM-7B开源率先免费商用发布全链条开源工具体系2023.9.20&#xff1a;InternLM-20B开源&#xff0c;开源工具链全线升级2024.1.17&#xff1a;InternLM2开源&#xff0c;性能超最新同量级开源模…

单元测试(Junit)

系统—模块—子模块&#xff0c;子模块中不可分割的程序单元的测试&#xff0c;单元的粒度根据实际情况可能是 类或方法等。 面向对象编程中&#xff0c;最小单元就是方法。 单元测试目的是在集成测试和功能测试之前对系统可测试单元进行逐一检查和验证。 单元测试基本原则 …

MySQL表的增删改查(CRUD3约束)

这次我们开始先不复习嗷&#xff0c;等到把数据表的删除说完咱们统一&#xff0c;总结书写 1.数据表的删除&#xff1a; 语法&#xff1a; 1. 使用 DROP TABLE 语句删除单个表 基本语法&#xff1a;DROP TABLE [IF EXISTS] table_name; table_name是要删除的表的名称。IF EXIS…

go中Println和Printf的区别

Don’t worry , just coding! 内耗与overthinking只会削弱你的精力&#xff0c;虚度你的光阴&#xff0c;每天迈出一小步&#xff0c;回头时发现已经走了很远。 go中Println和Printf的区别 package mainimport ( "fmt" )//TIP To run your code, right-click the c…

【系统面试篇】进程和线程类(1)(笔记)——区别、通讯方式、同步、互斥、锁分类

目录 一、问题综述 1. 进程和线程的区别&#xff1f; 2. 进程的状态有哪些&#xff1f; 3. 进程之间的通信方式? &#xff08;1&#xff09;管道 &#xff08;2&#xff09;消息队列 &#xff08;3&#xff09;共享内存 &#xff08;4&#xff09;信号量 &#xff08…