恒美微站 Logo 恒美微站
  • 首页
  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心
  • 联系我们

Jetpack Compose基础控件实战:从View迁移到声明式UI的完整指南

  • 首页
  • 资讯中心
  • /
  • Jetpack Compose基础控件实战:从View迁移到声明式UI的完整指南

相关资讯

AI辅助编程实战:从工具选型到高效落地的完整指南 2026/9/8 7:36:26
嵌入式Linux安全加固实战:裁剪、权限、日志与防火墙 2026/9/8 7:36:26
PDF在线预览实战:从原生嵌入到pdf.js方案选型与避坑指南 2026/9/8 7:31:26

最新资讯

打造准确高效的域名查询系统:WHOIS与DNS核心技术解析
从npx到SKILL.md:AI代理技能包ponytail安装与实战解析
Delphi图像控件ImageEn v5完整安装与实战避坑指南
接口测试断言完全指南:从类型解析到工具实战与落地经验
AI+潮玩建模:用WorkBuddy和Skill驯服Claude Code与Codex
Minecraft插件生存服务器技术拆解:从开荒到长期运营

今日推荐

Redis缓存与离线预计算在大数据处理中的实战应用
Android 12热启动闪屏排查:从冷热启动差异到官方SplashScreen避坑指南
加密资产价值投资:原理、方法与实战策略

本周热门

超人会飞不算本事:系统稳定依赖清晰规则与边界设计
超人VS蜘蛛侠:拆解超级IP的影响力与传播方法论
基于CNN的调制信号识别:MATLAB实现时频图分类实战

本月精选

自研推理加速器Redwood:两周内实现PyTorch模型高效部署的实战教程
V4L2摄像头采集实战:从camera_client.rar到出图全流程解析
从“谁发明了钢琴键”到知识问答智能体:RAG与记忆工程实践

Jetpack Compose基础控件实战:从View迁移到声明式UI的完整指南

发布时间:2026/9/8 7:36:26
Jetpack Compose基础控件实战:从View迁移到声明式UI的完整指南 聊到Jetpack Compose基础控件我得先坦白一件事从View体系切换过来的第一个月我对“控件”这个老概念差点失去信任。findViewById没了setText没了连代码里写一个Text(Hello)都让我怀疑自己到底干了什么。等把Compose基础控件逐个理清、在真机上跑完几个页面之后我才发现——Compose里的控件并不是“新版本的控件”而是一套完全不同的界面构建逻辑。这篇就把我梳理Compose基础控件时的过程和经验写出来给准备入门或者刚入门的同学一条参考路线。内容全部来自实际写过、跑过、在真机上看过效果的部分不会有那种“官方文档复制粘贴”的废话。1. 为什么Compose的基础控件“长”得和View完全不一样1.1 控件不再是控件而是一个函数第一次用Compose的人最容易犯的错是拿View那套思维去套它。在传统View体系里TextView是一个实实在在的Java对象你findViewById拿到它的引用然后可以随时setText、setTextColor、setVisibility。想改界面就改对象属性。Compose里完全反过来。Text(Hello)不是一个对象它是一次函数调用。这个函数名首字母大写加了Composable注解它做的事情不是“创建控件”而是向Compose运行时描述“我希望这里出现一段文本”。控件真正长什么样、画在哪里由Compose运行时根据这套描述去完成。这个区别直接决定了你看待基础控件的方式View关注的是“拿到控件后怎么改”Compose关注的是“这堆函数怎么组合出我想要的界面”。所以基础控件在Compose里有个专门的叫法——可组合项Composable只是大家习惯上还叫它控件。刚开始我不适应觉得这纯粹是概念游戏但写多了才明白这个转变是后面所有用法差异的总根源。1.2 你写的是界面描述不是操作命令再往深一层说View是命令式UIimperative UICompose是声明式UIdeclarative UI。这两个词听着抽象用做饭类比就好懂了。命令式UI像你亲自去厨房做菜拿锅、开火、倒油、下菜、翻炒、关火每一步都是明确动作。声明式UI像你给厨师下菜单你说“来一份番茄炒蛋”至于他什么时候洗番茄、什么时候打鸡蛋你不用管只要最终这道菜按你的期望端上来就行。对应到写代码上View写的是“如何一步步操作界面”的指令Compose写的是“界面应该长成什么样”的声明。比如一个居中显示的按钮Compose的写法是Composable fun DemoButton() { Box(Modifier.fillMaxSize(), contentAlignment Alignment.Center) { Button(onClick { /* do something */ }) { Text(点击我) } } }你不用关心Button是怎么被添加进布局的也不用在Activity里再写一行setContentView。你只需要描述“外层是一个铺满全屏的Box内容居中里面放一个按钮按钮里是一段文字”。描述完界面就该是这个样子。这个思路渗透到所有基础控件里。Text的状态、Button的样式、TextField的内容全部通过函数参数表达。你不再需要“两步走”先拿到控件再改属性而是一次声明到位。1.3 Composable函数的三个硬性规则搞清楚Composable函数不是对象之后有三个硬性规则必须尽早记住否则写代码会四处碰壁。第一Composable函数只能在另一个Composable函数作用域里被调用。普通函数想调用Text()是不行的这是编译期强制要求。第二函数名惯例上首字母大写虽然不写也不会报错但Compose的重组、作用域识别都对函数调用位置敏感保持这个惯例能让你少踩很多坑。第三Composable函数会因为状态变化被反复执行这叫重组所以千万不能在函数体里做耗时操作比如读数据库、执行网络请求更不要在函数体内随意创建对象不加以管理。别问我是怎么知道“把列表筛选放在组合函数里导致每次滑动都卡顿”的。这三个规则才是Compose基础控件和View最大的分水岭控件从“你可以操作的东西”变成了“会被反复执行的界面描述”。后面的所有技巧其实都是围绕这个根子展开的。2. 文本、按钮、输入框最常用的三个控件参数比你想得多2.1 Text的七个高频参数从字号到截断策略先看最简单的Text。你以为写Text(你好)就完了真做项目的时候这七个参数你会反复用到。参数作用我的使用频率text显示内容必填modifier尺寸、边距、点击等极高color文字颜色高fontSize字号单位用sp高fontWeight字重加粗等中maxLines最大显示行数高overflow超出后的截断策略高这几个参数里最容易忽略的是overflow。它单独用没什么效果必须和maxLines配合才生效Text( text 这是一段非常长的文本如果不限制行数它会把整个屏幕都占满所以实际开发中一定要记得设置行数限制, maxLines 2, overflow TextOverflow.Ellipsis )这段代码的效果是最多显示两行如果超了就显示省略号。没有overflow时超出部分会被直接裁剪不一定是你想要的效果。我见过不少同学只设了maxLines没设overflow结果文本后半截被砍掉还看不出任何提示跟系统bug似的。另外提醒一下fontSize的单位是sp不是dpCompose里如果写Text(fontSize 14.dp)编译器会直接报类型错误因为TextUnit和Dp是不同的类型。刚迁移的人容易在这里犯迷糊。2.2 Button和TextButton别再把整个布局塞进onClickCompose的Button参数排序很有意思第一个参数是onClickButton( onClick { viewModel.login(username, password) }, enabled username.isNotBlank() password.isNotBlank(), shape RoundedCornerShape(8.dp) ) { Text(登录) }onClick放第一个是因为它是Button唯一一个必选参数。基础控件的参数排序其实藏着官方对“哪些参数重要”的判断多留意能帮你更快记住API。enabled是控制按钮可用状态的传入false后按钮会变灰同时点击事件不再触发——我经常用它配合输入框的内容做表单校验登录按钮在用户没填完信息前就是灰色的体验比点了再弹提示好得多。常见的坑是把整个布局塞进onClick里。Compose的Button的content是一个RowScope作用域你可以往里面放任意Composable内容包括文字、图标甚至你自己的自定义布局。但你绝对不能把另一个Button直接塞进去——嵌套可点击控件会导致点击事件被吞掉子按钮的点击永远不触发或者随机触发。别问我为什么知道得这么清楚。如果你想做一个看起来像普通文字的按钮官方提供了几种变体TextButton纯文字、OutlinedButton带边框、FilledTonalButton填充色偏柔和。它们的参数和Button几乎一样但视觉层级不同。在实际界面里主操作我用Button次要操作用OutlinedButton需要弱化的操作用TextButton三者的搭配比你自己传一堆颜色参数去改样式要协调很多。2.3 TextField的value和onValueChange为什么必须成对出现TextField是基础控件里最让新手头疼的一个。很多人第一次写输入框会用换行测试Composable fun WrongDemo() { var name // 错误示例这样写每次重组都会重置 OutlinedTextField( value name, onValueChange { name it } ) }你觉得把输入内容存进变量了但页面一刷新输入框里还是空的。原因在于局部变量name在Composable函数每次重组时都会被重新初始化它不是“有状态”的。要让变量能跨重组保存应该用remember配合mutableStateOfComposable fun CorrectDemo() { var name by remember { mutableStateOf() } // 注意变量声明方式 OutlinedTextField( value name, onValueChange { name it }, label { Text(请输入姓名) }, singleLine true ) }这里顺便回答了为什么value和onValueChange必须成对出现Compose的输入框是受控组件它的value决定显示什么内容onValueChange告诉你用户输入了什么。你拿到输入的新内容后要自己更新状态让value变成新值界面才会刷新。从没有接触过前端概念的同学可能会觉得“这不是多此一举吗”。其实这个设计的好处是你可以轻易地在更新值之前处理输入内容。比如限制输入长度onValueChange { if (it.length 10) name it // 超过10个字符的输入直接丢弃 }或者过滤特殊字符、只允许数字输入都在onValueChange里处理。如果你只想让TextField做展示不接收输入用readOnly true它会保留正常的文本框样式和光标效果比enabled false那种灰掉的状态看起来友好得多。密码输入框的写法用visualTransformation实现var showPassword by remember { mutableStateOf(false) } OutlinedTextField( value password, onValueChange { password it }, visualTransformation if (showPassword) VisualTransformation.None else PasswordVisualTransformation(), trailingIcon { TextButton(onClick { showPassword !showPassword }) { Text(if (showPassword) 隐藏 else 显示) } }, label { Text(密码) } )这里PasswordVisualTransformation()把输入内容变成圆点切换showPassword后可以恢复明文。真正的明文切换到密码模式用户之前输入的字符会不会丢实测过不会。因为visualTransformation只是改变了视觉呈现真正存的值一直是原样的。3. Row、Column、Box三种布局控件的使用逻辑3.1 Row和Column主轴交叉轴怎么用代码表达布局控件是整个界面的骨架。Compose的三巨头是Row横向排列、Column纵向排列、Box堆叠排列。先看Row和Column它们各有两个关键参数主轴排列方式和交叉轴对齐方式。Row的主轴是水平方向对应参数horizontalArrangement交叉轴是垂直方向对应verticalAlignment。Column则相反主轴是垂直方向对应verticalArrangement交叉轴是水平方向对应horizontalAlignment。举个例子你想让一行里三个控件均匀分布Row( modifier Modifier.fillMaxWidth(), horizontalArrangement Arrangement.SpaceBetween, verticalAlignment Alignment.CenterVertically ) { Text(左) Text(中) Text(右) }Arrangement.SpaceBetween的效果是第一个元素靠左最后一个元素靠右中间的间距自动均分。类似的还有Arrangement.SpaceEvenly所有间距包括两端都一致和Arrangement.SpaceAround每个元素两侧间距相等。这几个排列方式的视觉效果差别挺细的建议在预览面板里一个一个试比死记定义快得多。Alignment.CenterVertically是Row垂直居中的写法对应Column里水平居中则是Alignment.CenterHorizontally。这两个参数反着用也能编译过但效果就不对了。我一开始老搞混后来记了一个土办法凡是“在主轴上”的排列选项前面带Arrangement凡是“在交叉轴上”的对齐选项前面带Alignment。3.2 Box堆叠布局和居中的快捷方式Box解决的是控件叠加问题。它不像线性布局那样一个接一个排列而是所有子元素叠在同一个坐标系里后写的在上面。最常见的用法是给一个区域居中的子内容Box( modifier Modifier.fillMaxSize(), contentAlignment Alignment.BottomEnd ) { Text(右下角的角标) }contentAlignment控制所有子元素的默认对齐位置。如果你只想让某一个子元素单独对齐不给整个Box设统一对齐可以在子元素上挂Modifier.align(Alignment.TopCenter)注意这个align是BoxScope里的扩展函数只能在Box内部使用。实际项目里Box最常见的场景是“图片上叠加文字”或者“头像上叠加在线状态点”Box { Image(bitmap avatarBitmap, contentDescription 头像) Box( modifier Modifier .align(Alignment.BottomEnd) .size(12.dp) .background(Color.Green, CircleShape) ) // 右下角的在线小绿点 }理解了Box的层级逻辑很多需要“层叠”效果的界面都能很自然地写出来。3.3 Modifier.weight和Spacer空间分配的正确姿势在Row和Column内部子元素还有两个特殊能力Modifier.weight和Spacer。Modifier.weight只在RowScope或ColumnScope作用域里可用它控制元素在主轴方向占据剩余空间的比例Row(modifier Modifier.fillMaxWidth()) { Text(标题, modifier Modifier.weight(1f)) Text(操作) }这样写“标题”文字会占据剩余的全部宽度“操作”文字靠右显示。如果放三个元素分别给weight(1f)、weight(1f)、weight(2f)那它们占据的宽度比例就是1:1:2。注意weight是作用域函数离开了Row或Column的包裹体直接调用会编译报错。Spacer是一个不可见的占位控件专门用来撑开间距Column { Text(上面的文字) Spacer(modifier Modifier.height(16.dp)) Text(下面的文字) }有同学问“直接给上面的Text加Modifier.padding(bottom 16.dp)不行吗”行但效果有区别。Spacer占用的是布局空间后续元素会被推开padding改的是当前元素的边距。在需求是“固定留白”的场景里Spacer更直观而且它的尺寸独立成行别人看代码时一眼就能明白这里留了多大空隙。3.4 顺带说清楚Modifier顺序为什么这么重要布局控件的灵魂其实是Modifier它的写法顺序直接影响最终渲染结果。同样两个修饰符排在前面的先执行Modifier .padding(16.dp) .clickable { doSomething() }和Modifier .clickable { doSomething() } .padding(16.dp)第一个写法的点击区域是“padding之后的内容区域”意味着点击内边距范围不会触发点击第二个写法的点击区域是“整个包含padding的范围”点空白边距也能触发。这两个效果看着差不多但实际交互差别很大。我习惯把clickable放在最外层最后写这样用户点击整个区域都能响应误触率更低。还有个高频顺序坑先background再padding颜色会画在padding外侧先padding再background颜色只覆盖内容区域。想清楚了再排序能少改好几版样式。4. LazyColumn列表控件的标准答案和高频坑4.1 为什么列表要单独设计一个LazyColumn传统View里列表有RecyclerViewCompose里对应的是LazyColumn纵向列表和LazyRow横向列表。用普通Column加for循环能不能写列表能但性能有隐患。// 这种写法数据量少还行上百条就会卡 Column { (0..1000).forEach { index - Text(第 $index 条) } } // 几百条数据还行上千条就开始掉帧原因在于Column会把所有子项全部组合compose出来不管在不在屏幕范围内。而LazyColumn是懒加载的只组合当前屏幕可见的那几个item滚出屏幕的会自动回收。这个机制和RecyclerView的ViewHolder复用本质上是一回事只是Compose替你管理了大部分细节。4.2 item与items三种常见写法LazyColumn的用法和其他基础控件不太一样它的内容块是在LazyListScope作用域里和普通布局的参数不通用。最基本的三种写法LazyColumn { // 第一种单独一个静态项 item { Text(我是第一个item) } // 第二种根据数量展开 items(50) { index - Text(编号 $index) } // 第三种根据数据列表展开 val users listOf(张三, 李四, 王五) items(users) { name - // 这里items接收ListTit就是具体元素 Text(name) } }第三种写法在业务代码里最常用因为数据往往来自网络接口的列表。如果你同时需要索引和数据用itemsIndexed它提供index和item两个参数。4.3 提升列表体验的三个常用参数LazyColumn的高级参数里我几乎每个列表都会用到这三个。第一个是contentPadding它给列表内容设置内边距配合Arrangement.spacedBy可以实现item之间的间距LazyColumn( contentPadding PaddingValues(16.dp), verticalArrangement Arrangement.spacedBy(12.dp) ) { items(users) { user - UserCard(user) } }这样写列表顶部和底部天然留了16dp边距item之间相隔12dp不用每个item自己在内部加padding。第二个是key参数。当列表数据需要被增删或移动时给item设置一个稳定id能防止渲染错乱items(users, key { it.id }) { user - UserCard(user) }没设key的列表在数据变化时Compose靠位置去匹配item一旦插入了新数据或者删除了某一条很容易出现复用错位、状态残留的bug。加了key后Compose就能根据id准确判断哪个item是新增、哪个被删除。第三个是LazyListState用于读取或者控制滚动位置val listState rememberLazyListState() LazyColumn(state listState) { items(100) { Text(item $it) } } // 滚动到第20个 scope.launch { listState.scrollToItem(20) }“返回顶部”这种功能就靠它实现监听listState.firstVisibleItemIndex一旦大于某个值就显示悬浮按钮点击后scrollToItem(0)平滑滚动回顶部。4.4 LazyColumn里的两个高频坑第一个坑在LazyColumn的item里使用Modifier.weight会报错或者行为诡异。weight只在RowScope或ColumnScope里可用而LazyItemScope并不提供这个扩展函数。想让item内部有“占满剩余宽度”的元素改用Modifier.fillMaxWidth()或者自己布局。第二个坑LazyColumn嵌套在Column里如果不给LazyColumn限定高度它会把所有item都尝试组合懒加载效果就消失了表现为滑动卡顿或者内容异常。正确做法是给LazyColumn设置Modifier.weight(1f)让它占据剩余高度或者用固定高度。5. 状态与重组基础控件不会自动刷新的根因和正确写法5.1 mutableStateOf remember让控件感知变化前面写TextField的时候提到过remember和mutableStateOf。这是Compose状态驱动的基石值得展开讲透。普通局部变量不触发重组Compose控件的界面不会自动刷新。要让控件感知变化数据必须是“可观察的”。mutableStateOf创建的就是一个可观察对象读它的值时控件会注册依赖改它的值时控件会标记为“需要重组”。Composable fun Counter() { var count by remember { mutableStateOf(0) } Column { Text(点击次数$count) Button(onClick { count }) { Text(加一) } } }点击按钮count变化依赖了count的Text自动更新。这就是Compose控件“活”起来的原理。remember在这里的作用是让mutableStateOf(0)在重组之后仍然保持同一个实例而不是重新创建。如果你把remember去掉count每次重组都重置成0按钮点了没反应。rememberSaveable比remember更进一步它还能在Activity被系统回收或者屏幕旋转之后通过Bundle把状态保存并恢复。表单页面建议直接用rememberSaveablevar username by rememberSaveable { mutableStateOf() }5.2 状态读取的位置决定了重组范围状态驱动虽然方便但有个性能细节容易被忽略读取状态的位置决定了重组的最小范围。举个例子Composable fun Parent() { var count by remember { mutableStateOf(0) } ChildOne(count) ChildTwo() // 不读取count }这里的count变化时Parent整体会重组ChildOne因为接收新值也需要重组但ChildTwo如果未读取任何状态通常可以跳过重组。Compose的编译器会做一定程度的智能优化所以写代码时尽量把状态读取控制在最小作用域内不要为了省事把所有变量都提到最外层。这点和remember的另一个坑连在一起如果你用remember保存一个自定义对象对象内部属性变了但引用没变Compose可能不会感知到变化。正确做法是用mutableStateOf包装可变的字段或者用mutableStateListOf管理列表。5.3 一个完整的小表单控件之间的联动把前面的知识点串起来一个真实的登录表单是这个样子Composable fun LoginScreen() { var username by rememberSaveable { mutableStateOf() } var password by rememberSaveable { mutableStateOf() } var showPassword by rememberSaveable { mutableStateOf(false) } val canSubmit username.isNotBlank() password.length 6 Column( modifier Modifier .fillMaxSize() .padding(24.dp), verticalArrangement Arrangement.spacedBy(16.dp) ) { OutlinedTextField( value username, onValueChange { username it }, label { Text(用户名) }, singleLine true, modifier Modifier.fillMaxWidth() ) OutlinedTextField( value password, onValueChange { password it }, label { Text(密码) }, singleLine true, visualTransformation if (showPassword) VisualTransformation.None else PasswordVisualTransformation(), trailingIcon { TextButton(onClick { showPassword !showPassword }) { Text(if (showPassword) 隐藏 else 显示) } }, modifier Modifier.fillMaxWidth() ) Button( onClick { /* 这里触发登录逻辑 */ }, enabled canSubmit, modifier Modifier.fillMaxWidth() ) { Text(登录) } } }注意几个联动点密码输入框的keyboardOptions可以设置成密码键盘类型用户名框可以设置成普通文本键盘登录按钮的enabled由两个输入框的状态共同决定showPassword是独立状态只影响密码框的可视模式不影响实际数据。这段代码里没有任何一个控件在“被操作”它们只是根据当前状态“自我描述”。界面的变化全部源于状态的变化这就是Compose基础控件的核心使用逻辑——状态是唯一真相。6. 把基础控件拼成真实页面组件拆分与我的三个教训6.1 从一个页面需求开始学了单个控件不会拼装是另一个痛点。拿个人中心页举例常见的结构是顶部用户信息区、中间一排统计数字、下面几行功能菜单。用Compose写基本结构是这样Composable fun ProfileScreen(userName: String, avatarUrl: String) { Column( modifier Modifier .fillMaxSize() .verticalScroll(rememberScrollState()) ) { ProfileHeader(userName userName, avatarUrl avatarUrl) Spacer(modifier Modifier.height(16.dp)) StatsRow(followCount 128, fansCount 309, likeCount 521) Spacer(modifier Modifier.height(16.dp)) MenuList() } }这里用verticalScroll让整页可滚动因为当前页面不是高频列表所有元素一次性组合出来问题不大。每个区块——顶部头部的ProfileHeader、统计区的StatsRow、菜单区的MenuList——各自抽成一个Composable函数页面结构一眼能看懂。6.2 组件拆分和参数设计的经验组件拆分看起来是代码组织问题但直接影响你使用基础控件的方式。我踩过几次坑后总结出三条经验。一是“一个函数干一件事”。ProfileHeader只管头像、用户名和简介StatsRow只管三个统计数字。如果一开始图省事全写在一个大函数里后面改样式、加状态、做预览都会很痛苦。二是“参数要收敛”。ProfileHeader接收userName和avatarUrl两个参数就够了不要传一个复杂的User对象进去。参数太多组件复用性和可读性都会下降。组件内部的状态尽量自己管外部状态通过参数传进来。三是“善用Preview”。每次拆出来的组件都写一个Preview函数传入假数据直接看渲染效果。开发体验比View时代好太多。我有一个习惯先写预览函数再写组件实现等于用预览驱动开发。6.3 我在实际项目中踩过的基础控件坑先说Modifier顺序坑。一个卡片组件我最初写的是Modifier .background(Color.White) .clickable { onClick() } .padding(16.dp)结果点击区域特别奇怪背景色也只覆盖了内边距内部。后来改成Modifier .clickable { onClick() } .padding(16.dp) .background(Color.White)顺序不同视觉和交互完全不同。这里的经验是clickable尽量放在最外侧也就是先执行让整个区域可点击background的位置决定颜色覆盖的范围想覆盖整个区域就放在padding后面。第二个坑是关于LazyColumn里的remember。在列表item内部如果用了remember { mutableStateOf(...) }列表 item 被回收再复用后状态可能“串台”。解决办法有两个一是给items提供稳定的key二是把需要保留的状态提升到item之外或者用rememberSaveable持久化。第三个坑是文本显示异常。中文环境下Text的默认行高偏紧凑换行时字符容易被裁掉一部分。我当时排查了很久最后给文本设置了lineHeight 22.sp才正常。这个参数在只显示一行时看不出区别一旦文本长到需要换行或者设置了maxLines行高不足会导致字母或汉字下边缘被遮挡。6.4 从写出界面到写出好界面的进阶建议基础控件学完进阶方向大概有三个一是深入学习可组合项的生命周期和重组机制搞清楚“什么变了才会触发重组”能帮你写出更高效的界面二是熟悉Material 3的规范Compose默认了Material设计体系控件样式很容易做统一三是利用好官方提供的现有组件库和开源项目别什么都自己用基础组件从零拼。我自己的建议是找个实际页面比如你正在做的某个App的静态界面用Compose把它完整重写一遍。过程中一定会遇到很多文档里没写到的问题逐个解决后基础控件的理解会比看十遍教程都深。最后分享一个实在的小技巧Android Studio里给Compose函数加一个Preview注解然后分屏边写边看布局效果比每次跑到模拟器上强多了。项目里如果开了多个预览主题还能直接切换深色模式检查对比度非常省事。基础控件这个东西知道得再多不如亲手拖几个进页面里试试特别是Row、Column、Box的嵌套关系看文字讲解容易真组合起来错综复杂多写几个页面自然会有手感。

关于恒美微站

恒美微站专注于为个体商户、工作室提供极简自助建站服务,让每个人都能轻松拥有专业网站。

快速链接

  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心

服务项目

  • 可视化建站
  • 拖拽编辑
  • 主题定制
  • SEO 优化
  • 网站托管

联系方式

  • 📍 地址:北京市朝阳区建国路 88 号
  • 📞 电话:400-888-8888
  • ✉️ 邮箱:info@hmyw.cn
  • 🕐 时间:周一至周日 9:00-18:00

© 2024 恒美微站 hmyw.cn 版权所有 | 京 ICP 备 12345678 号