在这里插入图片描述

目标

这一篇我们先把“爱淘淘助手 app”的**首页(当前工程的默认入口页)**讲清楚:

你会看到首页的启动路径(从 main()home)、为什么这里需要 StatefulWidget、一个最小可维护页面骨架的组成(Scaffold / AppBar / body / floatingActionButton),以及交互如何通过 setState 驱动状态变化。

说明一下:这篇的“首页”是你当前工程里真实存在的默认入口页(计数器示例)。它是一个很好的起点——结构足够标准,改造空间也足够大。

全文只引用你项目里已经存在、可直接运行的真实代码:

flutter_application_2/lib/main.dart

为了便于你后续把“计数器 demo 首页”改造成真正业务首页,我会在讲解过程中把一些可扩展点标出来,但不会为了“看起来高级”去引入额外框架。

阅读前的小约定(很重要,但不复杂)

为了让首页后续好维护,我在文章里默认遵守几个约定,你也可以把它当成写页面时的“自检清单”:

  • 页面骨架要干净build() 里只组装 UI,不放初始化、不放请求、不放复杂计算。
  • 状态就近放置:只影响首页的状态,优先放在 _MyHomePageState;需要跨页面再上移。
  • 能复用的样式从主题拿:颜色、字号尽量来自 Theme.of(context),避免硬编码。

这些不算“规范强迫症”,更多是为了让你两周后回来看代码时,仍然能一眼读懂。

关键实现点

首页实现这件事,在 Flutter 里通常会被拆成两层:

  1. 应用壳层MaterialApp 负责主题、路由、页面栈等全局能力。
  2. 首页页面层MyHomePage 负责 UI 结构、交互逻辑、状态更新。

你现在的工程就采用了这种经典拆法,优点是:

首页逻辑不会污染到应用配置;同时页面内部的小状态可以就近维护,后续要替换成网络数据也更容易找到改动位置。

下面我们从入口一步步走到首页。

1) 从 main() 进入:首页的“第一跳”在哪里

先看 main.dart 的入口部分:

void main() {
  runApp(const MyApp());
}

这里不要只把它当“启动程序”。这段代码实际上定义了一个非常重要的边界:

runApp(...) 之后,一切都交给 Flutter 的渲染与事件系统;而 MyApp 会成为整个 widget 树的根节点,后续的路由、主题、字体、页面栈都从这里延伸。

我自己在项目里判断“首页从哪来”的习惯是:先找 main(),再顺着 runApp 的入参找到根 widget,然后再看 MaterialApphome 或路由表。这个路径很稳定,基本不会迷路。

如果你后面要做初始化(例如提前加载配置、初始化日志等),也建议仍然围绕 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 的配置参数在当前写法下是不可变的,能减少不必要的重建开销;第二,titleMyApp 传入 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 索引、是否展开。
  • 过程状态:比如 loadingrefreshingsubmitting
  • 异常状态:比如 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 或者用别的交互入口
  • 是否需要 drawerbottomNavigationBar

先把坑位占住,再慢慢往里填内容,比一开始就堆一坨 widget 好维护。

当你未来做首页导航(比如底部导航栏)时,仍然会沿着这个骨架扩展,所以用 Scaffold 作为首页根容器是非常稳妥的选择。

8) 顶部栏:AppBar 的标题来自 widget.title

appBar 部分:

appBar: AppBar(
  backgroundColor: Theme.of(context).colorScheme.inversePrimary,
  title: Text(widget.title),
),

这里有两个点特别适合“业务首页”沿用:

  1. 颜色从主题取值

    • Theme.of(context).colorScheme... 是统一风格的关键。
    • 后续换主题,不需要满项目找颜色。
  2. 标题来自 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) 静态文案:能 constconst

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 _loadingString? _errorList<Item> _items

而触发逻辑会变成:

Future<void> _loadHomeData()

但这些属于下一篇内容,这一篇先把页面骨架和状态更新的路径讲透。

代码位置

本篇涉及的代码全部来自你的工程文件:

flutter_application_2/lib/main.dart

首页相关类主要是:

MyAppMyHomePage_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

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐