flutter_for_openharmony爱淘淘助手app实战+首页实现
本文以Flutter默认计数器应用为例,详细解析了首页的实现逻辑。主要内容包括: 应用启动入口从main()开始,通过runApp()构建应用根节点 MaterialApp作为应用配置容器,集中管理主题、路由等全局设置 首页使用StatefulWidget管理内部状态,通过setState触发UI更新 Scaffold作为页面骨架,组织AppBar、内容区和浮动按钮 遵循状态就近原则,将仅属于首页

目标
这一篇我们先把“爱淘淘助手 app”的**首页(当前工程的默认入口页)**讲清楚:
你会看到首页的启动路径(从 main() 到 home)、为什么这里需要 StatefulWidget、一个最小可维护页面骨架的组成(Scaffold / AppBar / body / floatingActionButton),以及交互如何通过 setState 驱动状态变化。
说明一下:这篇的“首页”是你当前工程里真实存在的默认入口页(计数器示例)。它是一个很好的起点——结构足够标准,改造空间也足够大。
全文只引用你项目里已经存在、可直接运行的真实代码:
flutter_application_2/lib/main.dart
为了便于你后续把“计数器 demo 首页”改造成真正业务首页,我会在讲解过程中把一些可扩展点标出来,但不会为了“看起来高级”去引入额外框架。
阅读前的小约定(很重要,但不复杂)
为了让首页后续好维护,我在文章里默认遵守几个约定,你也可以把它当成写页面时的“自检清单”:
- 页面骨架要干净:
build()里只组装 UI,不放初始化、不放请求、不放复杂计算。 - 状态就近放置:只影响首页的状态,优先放在
_MyHomePageState;需要跨页面再上移。 - 能复用的样式从主题拿:颜色、字号尽量来自
Theme.of(context),避免硬编码。
这些不算“规范强迫症”,更多是为了让你两周后回来看代码时,仍然能一眼读懂。
关键实现点
首页实现这件事,在 Flutter 里通常会被拆成两层:
- 应用壳层:
MaterialApp负责主题、路由、页面栈等全局能力。 - 首页页面层:
MyHomePage负责 UI 结构、交互逻辑、状态更新。
你现在的工程就采用了这种经典拆法,优点是:
首页逻辑不会污染到应用配置;同时页面内部的小状态可以就近维护,后续要替换成网络数据也更容易找到改动位置。
下面我们从入口一步步走到首页。
1) 从 main() 进入:首页的“第一跳”在哪里
先看 main.dart 的入口部分:
void main() {
runApp(const MyApp());
}
这里不要只把它当“启动程序”。这段代码实际上定义了一个非常重要的边界:
runApp(...) 之后,一切都交给 Flutter 的渲染与事件系统;而 MyApp 会成为整个 widget 树的根节点,后续的路由、主题、字体、页面栈都从这里延伸。
我自己在项目里判断“首页从哪来”的习惯是:先找 main(),再顺着 runApp 的入参找到根 widget,然后再看 MaterialApp 的 home 或路由表。这个路径很稳定,基本不会迷路。
如果你后面要做初始化(例如提前加载配置、初始化日志等),也建议仍然围绕 main() 来组织,但最终都要回到 runApp(...)。
小提醒:如果你后面确实要加异步初始化,尽量把“初始化完成后再 runApp”这条线写清楚。入口写得清晰,后面排查启动期问题会省很多时间。
2) MyApp:把“应用级”配置收口到 MaterialApp
接着往下看 MyApp,它是个 StatelessWidget:
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
这段代码里,最值得你保留的不是默认的标题文本,而是结构:
MaterialApp 是“应用壳”(以后加多页面、路由、主题切换,入口都在这里);theme: ThemeData(...) 把主题集中配置,避免页面里到处硬编码颜色/字体;home: ... 则明确指定首页 widget。
这里我建议你把 MyApp 当成“配置容器”,别急着往里面塞业务逻辑。越是把首页做大,越能体会到:根节点简单一点,排查问题会容易很多。
一个很实用的习惯是:把 MyApp 尽量保持无状态。真正的页面状态放到页面里(或者更上层的状态管理里)。这样改动页面的时候,应用壳不会跟着一起变复杂。
关于主题:这句不是装饰,是“统一出口”
ThemeData 这一段看起来平平无奇,但它的意义是把视觉风格集中到一个地方:
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
这里你至少能得到两个好处:
- 颜色体系有来源:后面你改色调,不需要到处搜
Colors.xxx。 - 页面更克制:组件里尽量通过
Theme.of(context)取颜色/文字样式,UI 就不容易“各写各的”。
我更推荐的做法是:先把主题定下来,再去写页面的细节。反过来会很容易出现“页面写完才发现颜色系统不统一”的返工。
3) 首页入口:home 指向 MyHomePage
MaterialApp 里这一句就是“首页是谁”的最终答案:
home: const MyHomePage(title: 'Flutter Demo Home Page'),
这里有两个小细节:
第一,const 表示这个 widget 的配置参数在当前写法下是不可变的,能减少不必要的重建开销;第二,title 从 MyApp 传入 MyHomePage,意味着首页标题是“外部输入”,而不是在首页内部硬编码。
为什么我强调“外部输入”:以后你做多语言、做不同环境配置、或者首页标题要跟着登录态/Tab 变化时,这个边界会让你少改很多地方。
当你后续把 demo 文案换成业务文案时,仍然建议保留“从外部传入配置”的思路,这样页面更容易复用。
4) 为什么首页用 StatefulWidget:状态要有归属
继续看 MyHomePage 的定义:
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
State<MyHomePage> createState() => _MyHomePageState();
}
这里“用 StatefulWidget”并不等于“我想用 setState”。更准确的理由是:
首页内部存在会变化的状态(当前示例是 _counter),因此需要一个清晰的生命周期容器来管理这些状态。
你可以把
StatefulWidget当成一个“带生命周期的盒子”。状态放在盒子里,盒子销毁时状态也跟着销毁——这对页面这种局部状态来说,通常是你想要的行为。
同时你这里把 title 写成了 final,这是一个很好的状态边界:
title 是输入参数,不应该被页面内部随意修改;页面内部会变化的状态则放到 _MyHomePageState 里。
这种“输入不可变、内部状态单独维护”的习惯,后面你把首页变成“商品列表 / 推荐流 / 分类入口”等复杂 UI 时,会明显减少混乱。
5) 首页状态:把状态字段放在 State 里
看 _MyHomePageState 的开头:
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
这里用私有字段 _counter 有两个直观收益:
它不会被类外部直接访问,状态更安全;同时状态的读写路径也更清晰,只能通过这个 State 内部的逻辑去修改。
我在写首页时经常会把状态分三类(你现在只用到了第一类):
- 展示状态:比如计数器值、tab 索引、是否展开。
- 过程状态:比如
loading、refreshing、submitting。 - 异常状态:比如
errorMessage或者错误码。
把状态先按类别想清楚,后面加“加载/失败/空态”会顺很多。
在实际业务首页中,这个位置通常会放:
例如当前 tab 索引、列表数据与分页信息、是否正在加载/是否出错等。
但无论状态是什么,只要它“只属于首页”,就应该优先就近放在首页的 State 中(而不是一上来就上全局单例)。
6) 首页交互:setState 的最小正确用法
接下来是交互方法 _incrementCounter():
void _incrementCounter() {
setState(() {
_counter++;
});
}
这段代码的关键点是:
setState 包裹状态变更后,Flutter 才会安排本次 UI 重建;并且这里只更新 _counter,不做额外副作用,逻辑更可预期。
这里我再补一个很实用的判断:
当你发现某段逻辑写进
build()之后,会因为重建被反复执行,那它大概率就不该在build()里。
像 _incrementCounter() 这种“动作”单独抽出来,build() 只负责引用它,页面结构会更稳定。
后面你如果把点击事件换成“拉取接口数据”,也建议保留这种分层:
点击事件只负责触发动作,状态变化集中在一个方法里处理。
这样 build() 才不会被堆满逻辑。
7) 首页 UI:从 build() 返回一个 Scaffold
现在进入真正的首页 UI 结构。build 的返回值是 Scaffold:
Widget build(BuildContext context) {
return Scaffold(
Scaffold 是一级页面最常见的骨架组件,它不是“为了好看”,而是为了把常用结构统一收口:
它把顶部栏(appBar)、内容区(body)、悬浮按钮(floatingActionButton)这些常用结构收口在一起。
如果你后续把首页做成“多模块拼装”的形式,我建议仍然先把 Scaffold 的四个坑位定好:
appBar放什么body的主结构是什么(列表/网格/滚动区)- 有没有
floatingActionButton或者用别的交互入口 - 是否需要
drawer或bottomNavigationBar
先把坑位占住,再慢慢往里填内容,比一开始就堆一坨 widget 好维护。
当你未来做首页导航(比如底部导航栏)时,仍然会沿着这个骨架扩展,所以用 Scaffold 作为首页根容器是非常稳妥的选择。
8) 顶部栏:AppBar 的标题来自 widget.title
看 appBar 部分:
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: Text(widget.title),
),
这里有两个点特别适合“业务首页”沿用:
-
颜色从主题取值:
Theme.of(context).colorScheme...是统一风格的关键。- 后续换主题,不需要满项目找颜色。
-
标题来自
widget.title:widget代表MyHomePage这个 widget 的输入配置。- 这也再次体现了“外部输入 vs 内部状态”的边界。
如果你未来要做“动态标题”(例如根据 Tab 改标题),建议不要直接修改 widget.title,而是在 State 里加一个可变字段来控制显示文本。
经验:只要你还在用
StatefulWidget,就尽量让“可变的东西”留在State里;widget里的字段更多当成“配置”。这样你自己写着也更顺手。
9) 内容区:先用 Center 确定布局意图
继续看 body:
body: Center(
child: Column(
Center 的作用是把“内容整体居中”这个意图写得非常明确。
在业务首页里,很多布局会变复杂,但我仍然建议你保留这种“先写清楚布局意图”的做法:
能用一个 widget 表达清楚的,就不要靠多层 padding/align 叠出来;结构清晰,后面拆组件也更容易。
如果你发现页面慢慢变乱,通常不是因为 Flutter 难,而是因为“布局意图没写出来”。比如明明想要“头部固定、列表滚动”,却用了多个嵌套的 Column,最后自己也绕进去。写得直白一点,往往更稳。
10) 垂直布局:Column 负责排列而不负责业务
Column 的主轴对齐方式:
mainAxisAlignment: MainAxisAlignment.center,
它表达的是:子组件在纵向居中排列。
这里我建议你把 Column 当成“纯布局组件”,不要在 children 里塞太多复杂逻辑。等首页内容变多后,可以把 children 里的每一块抽成一个私有方法或独立 widget(比如 _buildHeader() / _buildBody()),这样可读性会明显提升。
我个人很喜欢的一个标准是:
build()往下滚两屏还看不完,就该拆组件了。拆完之后,后面加功能会更快。
11) 静态文案:能 const 就 const
children 里的第一段文本是 const Text:
const Text(
'You have pushed the button this many times:',
),
这类纯静态 widget 用 const 的好处是:
widget 可以被复用,不必每次重建都重新实例化;页面重建时的开销也更可控。
这里你可以把它理解成:const 不是为了“优化到极致”,而是为了让 widget 的身份更稳定。尤其在首页这种可能频繁重建的页面里,这个习惯会让你写起来更安心。
在真实首页里,像“模块标题”“固定提示语”等都可以尽量 const 化。
12) 动态文案:直接读取 _counter
第二段文本展示了一个最典型的“状态驱动 UI”:
Text(
'$_counter',
style: Theme.of(context).textTheme.headlineMedium,
),
这里的阅读方式非常直接:
UI 需要一个数字,数字来自 _counter;当 _counter 变化并触发 setState 时,build 重跑,UI 自然读到新值。
同时你这里的字体样式也不是硬编码,而是来自主题:
Theme.of(context).textTheme.headlineMedium
这在后续做“统一字号体系”时很省心,不会出现同一个层级的标题到处大小不一致。
13) 首页关键交互入口:FloatingActionButton
看页面末尾的悬浮按钮:
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: const Icon(Icons.add),
),
这段代码是“交互闭环”的另一半:
onPressed 绑定 _incrementCounter,在 _incrementCounter 里用 setState 更新 _counter,随后 Text('$_counter') 会自动展示新值。
这里我建议你保留“UI 绑定动作,动作里改状态”的结构。哪怕后面你换成异步请求,也可以按同样的套路写:
- 点击只负责触发
- 逻辑方法里控制
loading/error - UI 只根据状态渲染
也就是说,首页实现的最小闭环是:
用户操作(点击按钮)触发 逻辑调用(执行 _incrementCounter),完成 状态更新(_counter++),最终通过 重建刷新(build() 重建)反映到 UI。
当你把“计数器”替换成“首页数据加载”时,这个闭环仍然成立,只是状态字段会变成:
bool _loading、String? _error、List<Item> _items。
而触发逻辑会变成:
Future<void> _loadHomeData()。
但这些属于下一篇内容,这一篇先把页面骨架和状态更新的路径讲透。
代码位置
本篇涉及的代码全部来自你的工程文件:
flutter_application_2/lib/main.dart
首页相关类主要是:
MyApp、MyHomePage、_MyHomePageState。
如果你接下来要把首页拆成多个 widget,我建议仍然以 main.dart 为入口,但把首页页面本体独立到 lib/pages/home/... 之类的目录(按你团队习惯来)。拆分时只要保证 MaterialApp(home: ...) 的指向正确即可。
小结
到这里,你的“首页实现”已经具备一个非常稳的工程起点:
入口明确(main() -> runApp(MyApp)),应用壳清晰(MaterialApp 负责主题和首页挂载),页面骨架统一(Scaffold 承载 AppBar/body/FAB),状态闭环完整(点击触发 -> setState 更新 -> UI 读取新状态)。
如果你希望下一篇继续写“把首页改造成真实业务首页”,我建议优先从这两个方向选一个:
你可以从两个方向里选一个继续推进:方向 A 是首页拆组件(头部、入口区、列表区),让 build() 保持干净;方向 B 是首页引入一次异步数据加载(用最少依赖把 loading/错误/空态写完整)。
你回复我选 A 还是 B,我就按你项目现有代码风格继续往下写。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐
所有评论(0)