You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何优雅实现带动态AppBar操作的Flutter持久侧边栏仪表盘

问题描述

我需要实现一个带持久侧边栏的仪表盘,核心需求:

  • 侧边栏持久显示,点击侧边栏选项切换主页面
  • 每个主页面有独立的AppBar操作按钮

当前实现方案:
用一个中心DashboardPage承载Scaffold和Drawer,主内容用PageView管理多个页面;通过Dashboard Cubit维护状态,on page requested方法接收页面索引,中心页面监听状态变化切换页面。

现存问题:
中心页面根据当前页面动态生成AppBar时,部分按钮需要触发页面内方法,目前用GlobalKey直接调用页面State的方法,导致强耦合,还出现重建异常。

当前代码:

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import 'package:my_way_app/Features/Dashboard/presentation/manager/Agencies/my_agency_cubit.dart';
import 'package:my_way_app/Features/Dashboard/presentation/manager/shared/dashboard_cubit.dart';
import 'package:my_way_app/Features/Dashboard/presentation/pages/dashboard_stats_page.dart';
import 'package:my_way_app/Features/Dashboard/presentation/pages/service_settings.dart';
import 'package:my_way_app/Features/Dashboard/presentation/pages/voyages_management.dart';
import 'package:my_way_app/Features/Dashboard/presentation/widgets/app_bars/base_dashboard_app_bar.dart';
import 'package:my_way_app/Features/Dashboard/presentation/widgets/app_bars/voyages_app_bar.dart';
import 'package:my_way_app/Features/Dashboard/presentation/widgets/shared/side_bar/dashboard_side_bar.dart';
import 'package:my_way_app/Features/MyServices/domain/entities/my_service.dart';
import 'package:my_way_app/Shared/Widgets/Buttons/app_bar_check_button.dart';
import 'package:my_way_app/Theme/theme_shortcuts.dart';

class DashboardPage extends StatefulWidget {
  const DashboardPage({super.key});

  @override
  State<DashboardPage> createState() => _DashboardPageState();
}

class _DashboardPageState extends State<DashboardPage> {
  late MyService myService;
  final PageController pageController = PageController();

  final GlobalKey<ServiceSettingsState> serviceSettingsState = GlobalKey();

  late final List<Widget> pages;
  late final List<Widget> sharedDashboardPages;

  late final List<Widget> hotelDashboardPages;

  late final List<Widget> agencyDashboardPages;

  @override
  void initState() {
    final targetService = context.read<DashboardCubit>().myService;

    myService = targetService;
    hotelDashboardPages = [];
    agencyDashboardPages = [const VoyagesManagement()];
    sharedDashboardPages = [
      const DashboardStatsPage(),
      ServiceSettings(key: serviceSettingsState),
    ];

    pages = switch (myService.type) {
      ServiceType.agency => [
        sharedDashboardPages[0],
        ...agencyDashboardPages,
        sharedDashboardPages[1],
      ],

      ServiceType.hotel => [...sharedDashboardPages, ...hotelDashboardPages],
      ServiceType.restaurant => throw UnimplementedError(),
      ServiceType.guide => throw UnimplementedError(),
    };

    super.initState();
  }

  @override
  void dispose() {
    pageController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return BlocListener<DashboardCubit, DashboardState>(
      listener: (context, state) {
        if (state.state == DashboardCubitStates.pageRequested) {
          final index = state.pageIndex;
          pageController.jumpToPage(index);
          context.pop();
        }
      },

      child: Scaffold(
        appBar: PreferredSize(
          preferredSize: const Size.fromHeight(kToolbarHeight),
          child: BlocBuilder<DashboardCubit, DashboardState>(
            buildWhen:
                (previous, current) =>
                    current.state == DashboardCubitStates.pageRequested,
            builder: (context, state) {
              return getAppBar(context, state.url);
            },
          ),
        ),
        drawer: const DashboardSideBar(),
        body: Column(
          children: [
            Expanded(
              child: SafeArea(
                child: PageView.builder(
                  physics: const NeverScrollableScrollPhysics(),
                  controller: pageController,
                  itemBuilder: (BuildContext context, int index) {
                    return pages[index];
                  },
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }

  PreferredSizeWidget getAppBar(BuildContext context, String url) {
    final textTheme = getTextTheme(context);

    PreferredSizeWidget targetAppBar = const BaseDashboardAppBar(
      title: 'Dashboard',
    );
    final textStyle = textTheme.bodyMedium?.copyWith(
      fontWeight: FontWeight.w400,
    );

    if (url.contains('stats')) {
      targetAppBar = const BaseDashboardAppBar(title: 'Statistics');
    }
    if (url.contains('agencies/voyages')) {
      targetAppBar = const VoyagesAppBar();
    }
    if (url.contains('/dashboard/settings')) {
      targetAppBar = BaseDashboardAppBar(
        title: 'Settings',
        actions: [
          BlocBuilder<MyAgencyCubit, MyAgencyState>(
            builder: (context, state) {
              return AppBarSubmitButton(
                isLoading:
                    state.myAgencyStatus ==
                    MyAgencyCubitStatus.updateAgencyLoading,
                label: 'Save',
                hasIcon: false,
                onTap: () {
                  serviceSettingsState.currentState?.onSubmit();
                },
              );
            },
          ),
        ],
      );
    }

    return targetAppBar;
  }
}
优化方案

核心思路是让每个页面自己管理对应的AppBar配置和交互逻辑,把AppBar的控制权下放到页面,消除中心页面和子页面的耦合。

1. 定义页面元数据模型

创建一个DashboardPageConfig类,封装每个页面的核心信息:

class DashboardPageConfig {
  final String title;
  final PreferredSizeWidget Function(BuildContext) appBarBuilder;
  final Widget page;

  DashboardPageConfig({
    required this.title,
    required this.appBarBuilder,
    required this.page,
  });
}

2. 重构子页面,自主提供AppBar

以ServiceSettings为例,直接在页面内部实现AppBar的构建和交互逻辑:

class ServiceSettings extends StatefulWidget {
  const ServiceSettings({super.key});

  // 对外暴露当前页面的AppBar构建方法
  PreferredSizeWidget buildAppBar(BuildContext context) {
    return BaseDashboardAppBar(
      title: 'Settings',
      actions: [
        BlocBuilder<MyAgencyCubit, MyAgencyState>(
          builder: (context, state) {
            return AppBarSubmitButton(
              isLoading: state.myAgencyStatus == MyAgencyCubitStatus.updateAgencyLoading,
              label: 'Save',
              hasIcon: false,
              onTap: () => _handleSubmit(context),
            );
          },
        ),
      ],
    );
  }

  void _handleSubmit(BuildContext context) {
    // 页面内部的提交逻辑,直接访问自身依赖的Cubit
    context.read<MyAgencyCubit>().updateAgency(...);
  }

  @override
  State<ServiceSettings> createState() => _ServiceSettingsState();
}

class _ServiceSettingsState extends State<ServiceSettings> {
  // 页面内部状态和逻辑
}

其他页面如VoyagesManagement、DashboardStatsPage同理,各自实现自己的buildAppBar方法。

3. 重构中心DashboardPage

基于页面配置列表管理页面和AppBar,彻底移除GlobalKey:

class DashboardPage extends StatefulWidget {
  const DashboardPage({super.key});

  @override
  State<DashboardPage> createState() => _DashboardPageState();
}

class _DashboardPageState extends State<DashboardPage> {
  late final PageController pageController;
  late final List<DashboardPageConfig> pageConfigs;

  @override
  void initState() {
    super.initState();
    pageController = PageController();
    final myService = context.read<DashboardCubit>().myService;

    // 根据服务类型生成页面配置列表
    pageConfigs = switch (myService.type) {
      ServiceType.agency => [
          DashboardPageConfig(
            title: 'Statistics',
            appBarBuilder: (_) => const BaseDashboardAppBar(title: 'Statistics'),
            page: const DashboardStatsPage(),
          ),
          DashboardPageConfig(
            title: 'Voyages',
            appBarBuilder: (_) => const VoyagesAppBar(),
            page: const VoyagesManagement(),
          ),
          DashboardPageConfig(
            title: 'Settings',
            appBarBuilder: (context) => const ServiceSettings().buildAppBar(context),
            page: const ServiceSettings(),
          ),
        ],
      ServiceType.hotel => [
          // 酒店类页面配置
        ],
      _ => throw UnimplementedError(),
    };
  }

  @override
  void dispose() {
    pageController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return BlocListener<DashboardCubit, DashboardState>(
      listener: (context, state) {
        if (state.state == DashboardCubitStates.pageRequested) {
          pageController.jumpToPage(state.pageIndex);
          context.pop();
        }
      },
      child: BlocBuilder<DashboardCubit, DashboardState>(
        builder: (context, state) {
          final currentConfig = pageConfigs[state.pageIndex];
          return Scaffold(
            appBar: currentConfig.appBarBuilder(context),
            drawer: const DashboardSideBar(),
            body: SafeArea(
              child: PageView.builder(
                physics: const NeverScrollableScrollPhysics(),
                controller: pageController,
                itemCount: pageConfigs.length,
                itemBuilder: (_, index) => pageConfigs[index].page,
              ),
            ),
          );
        },
      ),
    );
  }
}

4. 可选:进一步解耦状态管理

把页面配置的生成逻辑移到DashboardCubit中,让Cubit统一维护页面列表和当前选中索引:

// DashboardCubit内部新增
final List<DashboardPageConfig> pageConfigs;
int currentPageIndex = 0;

// 初始化时生成配置列表
DashboardCubit(this.myService) : pageConfigs = _generatePageConfigs(myService);

static List<DashboardPageConfig> _generatePageConfigs(MyService service) {
  switch (service.type) {
    case ServiceType.agency:
      return [
        // 代理类页面配置
      ];
    case ServiceType.hotel:
      return [
        // 酒店类页面配置
      ];
    default:
      throw UnimplementedError();
  }
}

// 切换页面方法
void navigateToPage(int index) {
  emit(state.copyWith(
    pageIndex: index,
    state: DashboardCubitStates.pageRequested,
  ));
}

关键优势

  • 消除耦合:中心页面不再依赖子页面的State,子页面自主管理AppBar和交互
  • 可维护性:新增页面只需添加对应配置,无需修改中心页面的分支判断逻辑
  • 避免异常:彻底移除GlobalKey,解决页面重建导致的空指针或状态异常

内容的提问来源于stack exchange,提问作者Michael Bervo

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.13 01:12:33