前言
上一篇文章中,我们搭建了 Rolodex 通讯录项目并创建了数据模型。但应用目前只有一个"Hello Rolodex!"占位页面。
今天这篇文章基于官方教程的「Adaptive Layouts」章节,我们将学习一个非常实用的技能------自适应布局 。同一个应用,在手机上是导航跳转式的界面,在平板或电脑上自动变成侧边栏 + 详情面板的并排布局。而实现这一切的核心,就是 LayoutBuilder。
一、为什么需要自适应布局?
Flutter 可以运行在手机、平板、电脑和网页上,但这些设备的屏幕尺寸差异巨大。如果你只为手机设计界面,在大屏幕上就会浪费大量空间;如果只为大屏设计,在手机上又会挤成一团。
自适应布局的思路是:根据屏幕宽度,自动选择不同的布局方案。

二、认识 LayoutBuilder
LayoutBuilder 是实现自适应布局的核心组件。它能告诉你父组件给了多少可用空间,然后你根据这个空间大小决定返回什么 Widget。
kotlin
// LayoutBuilder 的基本用法
LayoutBuilder(
// builder 回调中的 constraints 包含了可用空间信息
// constraints.maxWidth → 最大可用宽度
// constraints.maxHeight → 最大可用高度
builder: (context, constraints) {
// 判断屏幕是否够宽
if (constraints.maxWidth > 600) {
// 宽屏:返回并排布局
return Row(children: [sidebar, details]);
} else {
// 窄屏:返回单页布局
return ContactGroupsPage();
}
},
)
你可以把 LayoutBuilder 想象成一把"量尺"------它先量一下有多少空间,然后你根据测量结果决定"摆什么家具"。
三、创建页面占位组件
在实现自适应布局之前,先创建两个占位页面。
3.1 联系人分组页面
创建 lib/screens/contact_groups.dart:
scala
// ContactGroupsPage:联系人分组页面
// 在小屏幕上作为主页面显示
// 在大屏幕上作为侧边栏的内容
import 'package:flutter/cupertino.dart';
class ContactGroupsPage extends StatelessWidget {
const ContactGroupsPage({super.key});
@override
Widget build(BuildContext context) {
// CupertinoPageScaffold → iOS 风格的页面脚手架
return const CupertinoPageScaffold(
// extraLightBackgroundGray → iOS 风格的浅灰色背景
backgroundColor: CupertinoColors.extraLightBackgroundGray,
child: Center(
// 占位文字,后续课程会替换为真正的分组列表
child: Text('Contact Groups will go here'),
),
);
}
}
3.2 联系人列表页面
创建 lib/screens/contacts.dart:
scala
// ContactListsPage:联系人列表页面
// 显示某个分组内的所有联系人
// listId 参数指定要显示哪个分组
import 'package:flutter/cupertino.dart';
class ContactListsPage extends StatelessWidget {
const ContactListsPage({super.key, required this.listId});
// 分组 ID:决定显示哪个分组的联系人
final int listId;
@override
Widget build(BuildContext context) {
return const CupertinoPageScaffold(
backgroundColor: CupertinoColors.extraLightBackgroundGray,
child: Center(
// 占位文字,后续课程会替换为真正的联系人列表
child: Text('Lists of contacts will go here'),
),
);
}
}
四、构建自适应布局组件
4.1 基本结构
创建 lib/screens/adaptive_layout.dart:
scala
import 'package:flutter/cupertino.dart';
import 'contact_groups.dart';
// 屏幕宽度断点:600 像素
// 低于此值视为小屏(手机),高于此值视为大屏(平板/电脑)
// 600px 是业界常用的手机/平板分界线
const largeScreenMinWidth = 600;
// AdaptiveLayout:自适应布局组件
// 使用 StatefulWidget 因为需要追踪当前选中的分组
class AdaptiveLayout extends StatefulWidget {
const AdaptiveLayout({super.key});
@override
State<AdaptiveLayout> createState() => _AdaptiveLayoutState();
}
class _AdaptiveLayoutState extends State<AdaptiveLayout> {
// 当前选中的联系人分组 ID(大屏幕布局需要知道右边显示哪个分组)
int selectedListId = 0;
// 当用户点击某个分组时调用
// 更新选中的分组 ID,触发界面重绘
void _onContactListSelected(int listId) {
setState(() {
selectedListId = listId;
});
}
@override
Widget build(BuildContext context) {
// ===== LayoutBuilder:核心!=====
// 它提供父组件的尺寸约束(constraints)
// 我们根据 maxWidth 判断屏幕大小,返回不同的布局
return LayoutBuilder(
builder: (context, constraints) {
// 判断:可用宽度是否大于 600 像素
final isLargeScreen = constraints.maxWidth > largeScreenMinWidth;
if (isLargeScreen) {
// 大屏幕 → 并排布局(侧边栏 + 详情面板)
return _buildLargeScreenLayout();
} else {
// 小屏幕 → 导航式布局(只显示分组列表)
return const ContactGroupsPage();
}
},
);
}
// ===== 大屏幕布局:侧边栏 + 详情面板 =====
Widget _buildLargeScreenLayout() {
return CupertinoPageScaffold(
backgroundColor: CupertinoColors.extraLightBackgroundGray,
// SafeArea 确保内容不会被系统 UI 遮挡
// 如状态栏、刘海屏的异形区域等
child: SafeArea(
child: Row(
children: [
// ===== 左侧:侧边栏(固定宽度 320px)=====
SizedBox(
width: 320,
// 后续会替换为真正的分组列表组件
child: Text('Sidebar placeholder'),
),
// ===== 中间:分隔线 =====
// 1 像素宽的垂直线,分隔左右两栏
Container(
width: 1,
color: CupertinoColors.separator,
),
// ===== 右侧:详情面板(占满剩余空间)=====
// Expanded 让详情面板自动填满 Row 中剩余的宽度
Expanded(
// 后续会替换为真正的联系人列表组件
child: Text('Details placeholder'),
),
],
),
),
);
}
}
4.2 布局结构解析
大屏幕布局的 Widget 树:
arduino
CupertinoPageScaffold
└── SafeArea
└── Row(横向排列三个部分)
├── SizedBox (width: 320) ← 侧边栏,固定 320px
│ └── 分组列表
├── Container (width: 1) ← 分隔线,1px
└── Expanded ← 详情面板,占满剩余空间
└── 联系人列表
这是一个经典的「主从布局」(master-detail)模式------左边是列表(master),右边是详情(detail),在桌面和平板应用中非常常见。
五、更新 main.dart
把 main.dart 中的占位页面替换为 AdaptiveLayout:
scala
import 'package:flutter/cupertino.dart';
import 'package:rolodex/data/contact_group.dart';
// 导入自适应布局组件
import 'package:rolodex/screens/adaptive_layout.dart';
final contactGroupsModel = ContactGroupsModel();
void main() {
runApp(const RolodexApp());
}
class RolodexApp extends StatelessWidget {
const RolodexApp({super.key});
@override
Widget build(BuildContext context) {
return const CupertinoApp(
title: 'Rolodex',
theme: CupertinoThemeData(
barBackgroundColor: CupertinoDynamicColor.withBrightness(
color: Color(0xFFF9F9F9),
darkColor: Color(0xFF1D1D1D),
),
),
// 使用 AdaptiveLayout 替代之前的占位页面
home: AdaptiveLayout(),
);
}
}
六、测试自适应效果
热重载后,在 Chrome 中测试:
- 拖宽浏览器窗口(> 600px) :看到"Sidebar placeholder"和"Details placeholder"并排显示
- 拖窄浏览器窗口(< 600px) :只看到"Contact Groups will go here"
拖动窗口边缘时,布局会实时切换------这就是 LayoutBuilder 的魔力。它在每次父组件尺寸变化时都会重新调用 builder 函数。
七、本节知识点小结
LayoutBuilder: 提供父组件的尺寸约束(BoxConstraints)。通过检查 constraints.maxWidth 判断屏幕大小,根据结果返回不同的布局 Widget。是实现自适应布局的核心工具。
断点(Breakpoint): 区分不同屏幕尺寸的阈值。600px 是常用的手机/平板分界线。低于 600px 用导航式布局,高于 600px 用并排式布局。
主从布局(Master-Detail): 大屏幕的经典模式------左侧固定宽度的列表(master),右侧 Expanded 的详情面板(detail)。用 Row + SizedBox + Expanded 实现。
SafeArea: 确保内容不被系统 UI 遮挡(状态栏、刘海屏、底部指示器等)。在全屏布局中应始终使用。
八、下一步学习
自适应布局的骨架已搭好,但侧边栏和详情面板还都是占位文字。下一课我们将学习 Scrolling and Slivers(滚动和 Sliver),用高级滚动技术填充联系人列表,实现可折叠的搜索栏和字母索引。
我们下篇文章见!