Flutter 自适应布局一套代码适配手机和平板(十二)

10 阅读6分钟

前言

上一篇文章中,我们搭建了 Rolodex 通讯录项目并创建了数据模型。但应用目前只有一个"Hello Rolodex!"占位页面。

今天这篇文章基于官方教程的「Adaptive Layouts」章节,我们将学习一个非常实用的技能——自适应布局。同一个应用,在手机上是导航跳转式的界面,在平板或电脑上自动变成侧边栏 + 详情面板的并排布局。而实现这一切的核心,就是 LayoutBuilder


一、为什么需要自适应布局?

Flutter 可以运行在手机、平板、电脑和网页上,但这些设备的屏幕尺寸差异巨大。如果你只为手机设计界面,在大屏幕上就会浪费大量空间;如果只为大屏设计,在手机上又会挤成一团。

自适应布局的思路是:根据屏幕宽度,自动选择不同的布局方案。

image.png


二、认识 LayoutBuilder

LayoutBuilder 是实现自适应布局的核心组件。它能告诉你父组件给了多少可用空间,然后你根据这个空间大小决定返回什么 Widget。

// 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

// 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

// 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

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 树:

CupertinoPageScaffold
  └── SafeArea
        └── Row(横向排列三个部分)
              ├── SizedBox (width: 320)    ← 侧边栏,固定 320px
              │     └── 分组列表
              ├── Container (width: 1)     ← 分隔线,1px
              └── Expanded                 ← 详情面板,占满剩余空间
                    └── 联系人列表

这是一个经典的「主从布局」(master-detail)模式——左边是列表(master),右边是详情(detail),在桌面和平板应用中非常常见。


五、更新 main.dart

main.dart 中的占位页面替换为 AdaptiveLayout

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),用高级滚动技术填充联系人列表,实现可折叠的搜索栏和字母索引。

我们下篇文章见!

参考资料:Flutter 官方教程 - Adaptive Layouts