Flutter+go_router实现带侧边栏与AppBar的局部导航求助
Complete Flutter Web Dashboard Implementation
Here's a fully runnable Flutter Web solution that meets all your requirements:
Core Components:
- Fixed Sidebar: 20 menu items with active page highlighting
- Top AppBar: Displays switch/close buttons for all opened pages
- State-Preserving Pages: Uses
AutomaticKeepAliveClientMixinto retain state (e.g., counter values) when switching between pages - Data Table with Detail Retention: Tracks which detail view was open, so switching back shows the detail instead of the list
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: 'Flutter Web Dashboard', theme: ThemeData( primarySwatch: Colors.blue, visualDensity: VisualDensity.adaptivePlatformDensity, ), home: const MainLayout(), ); } } class PageInfo { final String key; final String title; final Widget widget; PageInfo({required this.key, required this.title, required this.widget}); } class MainLayout extends StatefulWidget { const MainLayout({super.key}); @override State<MainLayout> createState() => _MainLayoutState(); } class _MainLayoutState extends State<MainLayout> { final List<PageInfo> _openedPages = []; String? _currentPageKey; final Map<String, int?> _dataTableSelectedItems = {}; @override void initState() { super.initState(); _openPage('page_1', 'Page 1', const Page1()); } void _openPage(String key, String title, Widget widget) { final existingPage = _openedPages.firstWhere( (p) => p.key == key, orElse: () => PageInfo(key: '', title: '', widget: const SizedBox()), ); if (existingPage.key.isNotEmpty) { setState(() => _currentPageKey = key); return; } setState(() { _openedPages.add(PageInfo(key: key, title: title, widget: widget)); _currentPageKey = key; }); } void _switchPage(String key) => setState(() => _currentPageKey = key); void _closePage(String key) { if (_openedPages.length == 1) return; final currentIndex = _openedPages.indexWhere((p) => p.key == key); setState(() { _openedPages.removeWhere((p) => p.key == key); _currentPageKey = _openedPages[currentIndex > 0 ? currentIndex - 1 : 0].key; }); } void _updateDataTableSelectedItem(String pageKey, int? itemId) { setState(() => _dataTableSelectedItems[pageKey] = itemId); } @override Widget build(BuildContext context) { final currentPage = _openedPages.firstWhere( (p) => p.key == _currentPageKey, orElse: () => _openedPages.first, ); return Scaffold( body: Row( children: [ // Fixed Sidebar Container( width: 250, color: Colors.grey[800], child: ListView.builder( itemCount: 20, itemBuilder: (context, index) { final pageNum = index + 1; final pageKey = 'page_$pageNum'; final title = 'Menu Item $pageNum'; Widget pageWidget; // Use DataTablePage for 5th menu item as example if (pageNum == 5) { pageWidget = DataTablePage( pageKey: pageKey, initialSelectedItemId: _dataTableSelectedItems[pageKey], onSelectedItemChanged: (id) => _updateDataTableSelectedItem(pageKey, id), ); } else { pageWidget = GenericPage(title: title); } return ListTile( leading: const Icon(Icons.list, color: Colors.white), title: Text(title, style: const TextStyle(color: Colors.white)), selected: _currentPageKey == pageKey, selectedTileColor: Colors.grey[700], onTap: () => _openPage(pageKey, title, pageWidget), ); }, ), ), // Main Content Area Expanded( child: Column( children: [ // Top AppBar with Page Controls AppBar( title: Text(currentPage.title), backgroundColor: Colors.blue, actions: _openedPages.map((page) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 4), child: ElevatedButton( style: ElevatedButton.styleFrom( backgroundColor: _currentPageKey == page.key ? Colors.grey[700] : Colors.blue[800], ), onPressed: () => _switchPage(page.key), child: Row( mainAxisSize: MainAxisSize.min, children: [ Text(page.title), if (_openedPages.length > 1) IconButton( icon: const Icon(Icons.close, size: 16, color: Colors.white), onPressed: () => _closePage(page.key), ), ], ), ), ); }).toList(), ), // Page Content Expanded( child: PageStorage( bucket: PageStorageBucket(), child: currentPage.widget, ), ), ], ), ), ], ), ); } } // Generic State-Preserving Page class GenericPage extends StatefulWidget { final String title; const GenericPage({super.key, required this.title}); @override State<GenericPage> createState() => _GenericPageState(); } class _GenericPageState extends State<GenericPage> with AutomaticKeepAliveClientMixin { int _counter = 0; @override bool get wantKeepAlive => true; @override Widget build(BuildContext context) { super.build(context); return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text(widget.title, style: const TextStyle(fontSize: 24)), const SizedBox(height: 20), Text('Counter: $_counter', style: const TextStyle(fontSize: 20)), ElevatedButton( onPressed: () => setState(() => _counter++), child: const Text('Increment Counter'), ), ], ), ); } } // Data Table Page with Detail View Retention class DataTablePage extends StatefulWidget { final String pageKey; final int? initialSelectedItemId; final Function(int?) onSelectedItemChanged; const DataTablePage({ super.key, required this.pageKey, required this.initialSelectedItemId, required this.onSelectedItemChanged, }); @override State<DataTablePage> createState() => _DataTablePageState(); } class _DataTablePageState extends State<DataTablePage> with AutomaticKeepAliveClientMixin { int? _selectedItemId; final List<Map<String, dynamic>> _data = List.generate( 50, (index) => {'id': index + 1, 'name': 'Item ${index + 1}', 'value': (index + 1) * 10}, ); @override bool get wantKeepAlive => true; @override void initState() { super.initState(); _selectedItemId = widget.initialSelectedItemId; } void _onRowSelected(int itemId) { setState(() => _selectedItemId = itemId); widget.onSelectedItemChanged(itemId); } void _goBackToList() { setState(() => _selectedItemId = null); widget.onSelectedItemChanged(null); } @override Widget build(BuildContext context) { super.build(context); if (_selectedItemId != null) { final item = _data.firstWhere((d) => d['id'] == _selectedItemId); return Padding( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ ElevatedButton( onPressed: _goBackToList, child: const Text('Back to List'), ), const SizedBox(height: 20), Text('Detail for ${item['name']}', style: const TextStyle(fontSize: 24)), const SizedBox(height: 10), Text('ID: ${item['id']}'), Text('Value: ${item['value']}'), const SizedBox(height: 20), const Text('This detail state persists when switching pages!', style: TextStyle(color: Colors.blue)), ], ), ); } else { return SingleChildScrollView( child: PaginatedDataTable( header: const Text('Data Table Page'), columns: const [ DataColumn(label: Text('ID')), DataColumn(label: Text('Name')), DataColumn(label: Text('Value')), ], source: _DataTableSource(data: _data, onRowSelected: _onRowSelected), rowsPerPage: 10, ), ); } } } class _DataTableSource extends DataTableSource { final List<Map<String, dynamic>> data; final Function(int) onRowSelected; _DataTableSource({required this.data, required this.onRowSelected}); @override DataRow? getRow(int index) { if (index >= data.length) return null; final item = data[index]; return DataRow( onSelectChanged: (_) => onRowSelected(item['id']), cells: [ DataCell(Text(item['id'].toString())), DataCell(Text(item['name'])), DataCell(Text(item['value'].toString())), ], ); } @override bool get isRowCountApproximate => false; @override int get rowCount => data.length; @override int get selectedRowCount => 0; } // Dummy Page1 (same as GenericPage, just for initial load) class Page1 extends GenericPage { const Page1({super.key}) : super(title: 'Page 1'); }
How to Use:
- Create a new Flutter Web project
- Replace the contents of
lib/main.dartwith this code - Run with
flutter run -d chrome
Key Notes:
- State Retention: All pages use
AutomaticKeepAliveClientMixinto preserve their state (try incrementing the counter on a page, switch to another, then come back—counter stays the same) - Data Table Detail: Click any row in the 5th menu item's table to open the detail view. Switch to another page and come back—it will still show the detail view
- Page Management: The
MainLayouttracks all opened pages, allowing switching and closing (can't close the last open page) - Sidebar: Fixed width, highlights the active page
内容的提问来源于stack exchange,提问作者Biagio63
相关产品推荐
相关产品推荐

