Flutter实现M3 List-detail规范布局:寻求Navigator替代布局操控方案
问题背景
我在实现M3规范中的List-detail经典布局时遇到了问题,这种布局类似Gmail左侧显示列表、右侧展示选中项详情,或是WhatsApp左侧聊天列表、右侧显示对话内容的样式。
我当前的实现是通过直接操控布局来切换列表和详情页,但想改用Navigator和独立页面的方式实现,希望找到更优的方案。
当前实现代码
import 'package:flutter/material.dart'; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return const MaterialApp( title: 'List-detail', home: ListDetailScreen(), ); } } class ListDetailScreen extends StatefulWidget { const ListDetailScreen({super.key}); @override State<ListDetailScreen> createState() => _ListDetailScreenState(); } class _ListDetailScreenState extends State<ListDetailScreen> { bool largeScreen = false; int? selectedIndex; @override void initState() { super.initState(); } @override void didChangeDependencies() { super.didChangeDependencies(); final double width = MediaQuery.of(context).size.width; largeScreen = width > 600; } @override Widget build(BuildContext context) { if (largeScreen) { return Row( children: [ Flexible( flex: 1, child: ListScreen( onSelectedItem: onSelectedItem, ), ), Flexible( flex: 1, child: DetailScreen( index: selectedIndex, ), ), ], ); } else { if (selectedIndex == null) { return ListScreen( onSelectedItem: onSelectedItem, ); } else { return DetailScreen( index: selectedIndex, ); } } } void onSelectedItem(int index) { setState(() { selectedIndex = index; }); } } class ListScreen extends StatefulWidget { final void Function(int) onSelectedItem; const ListScreen({super.key, required this.onSelectedItem}); @override State<ListScreen> createState() => _ListScreenState(); } class _ListScreenState extends State<ListScreen> { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('List'), elevation: 0, ), body: ListView( children: List.generate(100, (index) { return ListTile( title: Text('Item $index'), onTap: () => widget.onSelectedItem(index), ); }), ), ); } } class DetailScreen extends StatefulWidget { final int? index; const DetailScreen({super.key, this.index}); @override State<DetailScreen> createState() => _DetailScreenState(); } class _DetailScreenState extends State<DetailScreen> { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Detail'), elevation: 0, ), body: Padding( padding: const EdgeInsets.all(16), child: _buildContent(), ), ); } Widget _buildContent() { if (widget.index == null) { return const Text('Please select an item...'); } return Text('Selected item ${widget.index}'); } }
基于Navigator的优化实现
我们可以通过路由管理结合自适应布局来实现,让大屏下保持双栏布局,小屏下用Navigator跳转详情页,同时保持路由栈的一致性。核心思路是:
- 大屏时:在右侧详情栏嵌套独立Navigator,既保留双栏布局,又能通过路由管理详情页状态
- 小屏时:直接通过全局Navigator跳转详情页
优化后的完整代码
import 'package:flutter/material.dart'; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( title: 'List-detail with Navigator', home: const AdaptiveListDetailContainer(), routes: { '/detail': (context) => DetailScreen( index: ModalRoute.of(context)?.settings.arguments as int?, ), }, ); } } class ListScreen extends StatelessWidget { const ListScreen({super.key}); @override Widget build(BuildContext context) { final isLargeScreen = MediaQuery.of(context).size.width > 600; return Scaffold( appBar: AppBar(title: const Text('List')), body: ListView.builder( itemCount: 100, itemBuilder: (context, index) { return ListTile( title: Text('Item $index'), onTap: () { if (isLargeScreen) { // 大屏下:在右侧详情栏的Navigator中更新路由 Navigator.of(context, rootNavigator: false).pushReplacementNamed( '/detail', arguments: index, ); } else { // 小屏下:全局Navigator跳转详情页 Navigator.pushNamed( context, '/detail', arguments: index, ); } }, ); }, ), ); } } class DetailScreen extends StatelessWidget { final int? index; const DetailScreen({super.key, this.index}); @override Widget build(BuildContext context) { final isLargeScreen = MediaQuery.of(context).size.width > 600; return Scaffold( appBar: AppBar( title: const Text('Detail'), // 大屏下隐藏返回按钮,符合双栏交互逻辑 automaticallyImplyLeading: !isLargeScreen, ), body: Padding( padding: const EdgeInsets.all(16), child: index == null ? const Text('请选择一个项目...') : Text('选中的项目:$index'), ), ); } } // 自适应容器:根据屏幕尺寸切换布局 class AdaptiveListDetailContainer extends StatelessWidget { const AdaptiveListDetailContainer({super.key}); @override Widget build(BuildContext context) { final isLargeScreen = MediaQuery.of(context).size.width > 600; if (isLargeScreen) { return Row( children: [ // 左侧列表:占1/3宽度 const Expanded(flex: 1, child: ListScreen()), // 右侧详情栏:占2/3宽度,嵌套独立Navigator Expanded( flex: 2, child: Navigator( initialRoute: '/detail', onGenerateRoute: (settings) { return MaterialPageRoute( builder: (context) => DetailScreen( index: settings.arguments as int?, ), ); }, ), ), ], ); } else { // 小屏下直接显示列表页 return const ListScreen(); } } }
关键优化点
- 路由统一管理:通过
MaterialApp的routes配置统一路由,避免硬编码跳转路径 - 自适应Navigator嵌套:大屏下右侧详情栏使用独立Navigator,实现双栏布局下的路由状态管理,不影响左侧列表
- 交互细节优化:大屏下详情页自动隐藏返回按钮,贴合M3双栏布局的交互规范
- 规范状态传递:通过
settings.arguments传递选中项索引,替代回调传参,符合Flutter路由设计原则
额外优化建议
- 使用
LayoutBuilder替代MediaQuery监听布局变化,支持窗口 resize 时自动切换布局(比如平板横竖屏切换) - 大屏下给详情栏添加分割线,增强视觉区分度
- 结合状态管理工具(如Provider、Riverpod)统一管理选中状态,避免页面间频繁传参
内容的提问来源于stack exchange,提问作者Alessandro
相关产品推荐
相关产品推荐

