Widgets

Navigation Hub

简介

Navigation Hub 是一个集中管理所有组件导航的中心枢纽。开箱即用,你可以在几秒钟内创建底部导航、顶部导航以及 Journey 导航布局。

假设你有一个应用,想要添加一个底部导航栏,让用户可以在应用中的不同标签页之间切换。

你可以使用 Navigation Hub 来构建这个功能。

接下来让我们深入了解如何在你的应用中使用 Navigation Hub。

基本用法

你可以使用以下命令创建一个 Navigation Hub。

metro make:navigation_hub base

该命令会引导你完成一个交互式设置流程:

  1. 选择布局类型 - 在 navigation_tabs(底部导航)和 journey_states(顺序流程)之间选择。
  2. 输入标签页/状态名称 - 提供逗号分隔的标签页或 Journey 状态名称。

这将在你的 resources/pages/navigation_hubs/base/ 目录下创建文件:

  • base_navigation_hub.dart - 主 Hub 组件
  • tabs/states/ - 包含每个标签页或 Journey 状态的子组件

以下是生成的 Navigation Hub 的示例:

import 'package:flutter/material.dart';
import 'package:nylo_framework/nylo_framework.dart';
import '/resources/pages/navigation_hubs/base/tabs/home_tab_widget.dart';
import '/resources/pages/navigation_hubs/base/tabs/settings_tab_widget.dart';

class BaseNavigationHub extends NyStatefulWidget with BottomNavPageControls {
  static RouteView path = ("/base", (_) => BaseNavigationHub());

  BaseNavigationHub()
      : super(
            child: () => _BaseNavigationHubState(),
            stateName: path.stateName());

  /// State actions
  static NavigationHubStateActions stateActions = NavigationHubStateActions(path.stateName());
}

class _BaseNavigationHubState extends NavigationHub<BaseNavigationHub> {

  /// Layout builder
  @override
  NavigationHubLayout? layout(BuildContext context) => NavigationHubLayout.bottomNav();

  /// Should the state be maintained
  @override
  bool get maintainState => true;

  /// The initial index
  @override
  int get initialIndex => 0;

  /// Navigation pages
  _BaseNavigationHubState() : super(() => {
      0: NavigationTab.tab(title: "Home", page: HomeTab()),
      1: NavigationTab.tab(title: "Settings", page: SettingsTab()),
  });

  /// Handle the tap event
  @override
  onTap(int index) {
    super.onTap(index);
  }
}

可以看到,这个 Navigation Hub 有两个标签页:Home 和 Settings。

layout 方法返回 Hub 的布局类型。它接收一个 BuildContext,因此你可以在配置布局时访问主题数据和媒体查询信息。

你可以通过向 Navigation Hub 添加更多 NavigationTab 来创建更多标签页。

首先,你需要使用 Metro 创建一个新组件。

metro make:stateful_widget news_tab

你也可以一次创建多个组件。

metro make:stateful_widget news_tab,notifications_tab

然后,将新组件添加到 Navigation Hub 中。

_BaseNavigationHubState() : super(() => {
    0: NavigationTab.tab(title: "Home", page: HomeTab()),
    1: NavigationTab.tab(title: "Settings", page: SettingsTab()),
    2: NavigationTab.tab(title: "News", page: NewsTab()),
});

要使用 Navigation Hub,请将其作为初始路由添加到路由器中:

import 'package:nylo_framework/nylo_framework.dart';

appRouter() => nyRoutes((router) {
    ...
    router.add(BaseNavigationHub.path).initialRoute();
});

// 或者从应用中的任何位置导航到 Navigation Hub

routeTo(BaseNavigationHub.path);

Navigation Hub 还有更多功能,让我们一起来了解一些特性。

底部导航

你可以在 layout 方法中返回 NavigationHubLayout.bottomNav 来设置底部导航栏布局。

class _MyNavigationHubState extends NavigationHub<MyNavigationHub> {
    ...
    @override
    NavigationHubLayout? layout(BuildContext context) => NavigationHubLayout.bottomNav();

你可以通过设置以下属性来自定义底部导航栏:

class _MyNavigationHubState extends NavigationHub<MyNavigationHub> {
    ...
    @override
    NavigationHubLayout? layout(BuildContext context) => NavigationHubLayout.bottomNav(
        backgroundColor: Colors.white,
        selectedItemColor: Colors.blue,
        unselectedItemColor: Colors.grey,
        elevation: 8.0,
        iconSize: 24.0,
        selectedFontSize: 14.0,
        unselectedFontSize: 12.0,
        showSelectedLabels: true,
        showUnselectedLabels: true,
        type: BottomNavigationBarType.fixed,
    );

你可以使用 style 参数为底部导航栏应用预设样式。

@override
NavigationHubLayout? layout(BuildContext context) => NavigationHubLayout.bottomNav(
    style: BottomNavStyle.material(), // 默认的 Flutter Material 样式
);

自定义导航栏构建器

如果你想完全掌控导航栏,可以使用 navBarBuilder 参数。

它允许你构建任何自定义组件,同时仍然可以接收导航数据。

class _MyNavigationHubState extends NavigationHub<MyNavigationHub> {
    ...
    @override
    NavigationHubLayout? layout(BuildContext context) => NavigationHubLayout.bottomNav(
        navBarBuilder: (context, data) {
            return MyCustomNavBar(
                items: data.items,
                currentIndex: data.currentIndex,
                onTap: data.onTap,
            );
        },
    );

NavBarData 对象包含以下内容:

属性 类型 描述
items List<BottomNavigationBarItem> 导航栏项目
currentIndex int 当前选中的索引
onTap ValueChanged<int> 标签页被点击时的回调

以下是一个完全自定义的毛玻璃导航栏示例:

NavigationHubLayout.bottomNav(
    navBarBuilder: (context, data) {
        return Padding(
            padding: EdgeInsets.all(16),
            child: ClipRRect(
                borderRadius: BorderRadius.circular(25),
                child: BackdropFilter(
                    filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
                    child: Container(
                        decoration: BoxDecoration(
                            color: Colors.white.withValues(alpha: 0.7),
                            borderRadius: BorderRadius.circular(25),
                        ),
                        child: BottomNavigationBar(
                            items: data.items,
                            currentIndex: data.currentIndex,
                            onTap: data.onTap,
                            backgroundColor: Colors.transparent,
                            elevation: 0,
                        ),
                    ),
                ),
            ),
        );
    },
)

注意: 使用 navBarBuilder 时,style 参数将被忽略。

顶部导航

你可以在 layout 方法中返回 NavigationHubLayout.topNav 来将布局更改为顶部导航栏。

class _MyNavigationHubState extends NavigationHub<MyNavigationHub> {
    ...
    @override
    NavigationHubLayout? layout(BuildContext context) => NavigationHubLayout.topNav();

你可以通过设置以下属性来自定义顶部导航栏:

class _MyNavigationHubState extends NavigationHub<MyNavigationHub> {
    ...
    @override
    NavigationHubLayout? layout(BuildContext context) => NavigationHubLayout.topNav(
        backgroundColor: Colors.white,
        labelColor: Colors.blue,
        unselectedLabelColor: Colors.grey,
        indicatorColor: Colors.blue,
        indicatorWeight: 3.0,
        isScrollable: false,
        hideAppBarTitle: true,
    );

Journey 导航

你可以在 layout 方法中返回 NavigationHubLayout.journey 来将布局更改为 Journey 导航。

这非常适合用于引导流程或多步表单。

class _MyNavigationHubState extends NavigationHub<MyNavigationHub> {
    ...
    @override
    NavigationHubLayout? layout(BuildContext context) => NavigationHubLayout.journey(
        progressStyle: JourneyProgressStyle(
          indicator: JourneyProgressIndicator.segments(),
        ),
    );

你也可以为 Journey 布局设置 backgroundGradient

@override
NavigationHubLayout? layout(BuildContext context) => NavigationHubLayout.journey(
    backgroundGradient: LinearGradient(
        colors: [Colors.blue, Colors.purple],
        begin: Alignment.topLeft,
        end: Alignment.bottomRight,
    ),
    progressStyle: JourneyProgressStyle(
      indicator: JourneyProgressIndicator.linear(),
    ),
);

注意: 当设置了 backgroundGradient 时,它优先于 backgroundColor

如果你想使用 Journey 导航布局,你的组件应该使用 JourneyState,因为它包含了大量辅助方法来帮助你管理 Journey 流程。

你可以使用 make:navigation_hub 命令并选择 journey_states 布局来创建整个 Journey:

metro make:navigation_hub onboarding
# 选择:journey_states
# 输入:welcome, personal_info, add_photos

这将在 resources/pages/navigation_hubs/onboarding/states/ 下创建 Hub 和所有 Journey 状态组件。

你也可以使用以下命令单独创建 Journey 组件:

metro make:journey_widget welcome,phone_number_step,add_photos_step

然后将新组件添加到 Navigation Hub 中。

_MyNavigationHubState() : super(() => {
    0: NavigationTab.journey(
        page: Welcome(),
    ),
    1: NavigationTab.journey(
        page: PhoneNumberStep(),
    ),
    2: NavigationTab.journey(
        page: AddPhotosStep(),
    ),
});

Journey 进度样式

你可以使用 JourneyProgressStyle 类来自定义进度指示器样式。

class _MyNavigationHubState extends NavigationHub<MyNavigationHub> {
    ...
    @override
    NavigationHubLayout? layout(BuildContext context) => NavigationHubLayout.journey(
        progressStyle: JourneyProgressStyle(
            indicator: JourneyProgressIndicator.linear(
                activeColor: Colors.blue,
                inactiveColor: Colors.grey,
                thickness: 4.0,
            ),
        ),
    );

你可以使用以下进度指示器:

  • JourneyProgressIndicator.none():不渲染任何内容 - 适合在特定标签页上隐藏指示器。
  • JourneyProgressIndicator.linear():线性进度条。
  • JourneyProgressIndicator.dots():基于圆点的进度指示器。
  • JourneyProgressIndicator.numbered():数字步骤进度指示器。
  • JourneyProgressIndicator.segments():分段式进度条样式。
  • JourneyProgressIndicator.circular():圆形进度指示器。
  • JourneyProgressIndicator.timeline():时间线样式进度指示器。
  • JourneyProgressIndicator.custom():使用构建器函数的自定义进度指示器。
@override
NavigationHubLayout? layout(BuildContext context) => NavigationHubLayout.journey(
    progressStyle: JourneyProgressStyle(
        indicator: JourneyProgressIndicator.custom(
            builder: (context, currentStep, totalSteps, percentage) {
                return LinearProgressIndicator(
                    value: percentage,
                    backgroundColor: Colors.grey[200],
                    color: Colors.blue,
                    minHeight: 4.0,
                );
            },
        ),
    ),
);

你可以在 JourneyProgressStyle 中自定义进度指示器的位置和内边距:

@override
NavigationHubLayout? layout(BuildContext context) => NavigationHubLayout.journey(
    progressStyle: JourneyProgressStyle(
        indicator: JourneyProgressIndicator.dots(),
        position: ProgressIndicatorPosition.bottom,
        padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
    ),
);

你可以使用以下进度指示器位置:

  • ProgressIndicatorPosition.top:进度指示器位于屏幕顶部。
  • ProgressIndicatorPosition.bottom:进度指示器位于屏幕底部。

单个标签页的进度样式覆盖

你可以使用 NavigationTab.journey(progressStyle: ...) 为单个标签页覆盖布局级别的 progressStyle。未设置自身 progressStyle 的标签页将继承布局的默认设置。既没有布局默认值也没有单独设置的标签页将不会显示进度指示器。

_MyNavigationHubState() : super(() => {
    0: NavigationTab.journey(
        page: Welcome(),
    ),
    1: NavigationTab.journey(
        page: PhoneNumberStep(),
        progressStyle: JourneyProgressStyle(
            indicator: JourneyProgressIndicator.numbered(),
        ), // 仅为此标签页覆盖布局默认值
    ),
    2: NavigationTab.journey(
        page: AddPhotosStep(),
    ),
});

JourneyState

JourneyState 类扩展了 NyState,添加了 Journey 专属功能,使创建引导流程和多步骤 Journey 变得更加容易。

你可以使用以下命令创建一个新的 JourneyState

metro make:journey_widget onboard_user_dob

如果你想一次创建多个组件,可以使用以下命令。

metro make:journey_widget welcome,phone_number_step,add_photos_step

以下是生成的 JourneyState 组件示例:

import 'package:flutter/material.dart';
import '/resources/pages/navigation_hubs/onboarding/onboarding_navigation_hub.dart';
import '/resources/widgets/buttons/buttons.dart';
import 'package:nylo_framework/nylo_framework.dart';

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

  @override
  createState() => _WelcomeState();
}

class _WelcomeState extends JourneyState<Welcome> {
  _WelcomeState() : super(
      navigationHubState: OnboardingNavigationHub.path.stateName());

  @override
  get init => () {
    // Your initialization logic here
  };

  @override
  Widget view(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(16),
      child: Column(
        children: [
          Expanded(
            child: Center(
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Text('Welcome', style: Theme.of(context).textTheme.headlineMedium),
                  const SizedBox(height: 20),
                  Text('This onboarding journey will help you get started.'),
                ],
              ),
            ),
          ),

          // Navigation buttons
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceBetween,
            children: [
              if (!isFirstStep)
                Flexible(
                  child: Button.textOnly(
                    text: "Back",
                    textColor: Colors.black87,
                    onPressed: onBackPressed,
                  ),
                )
              else
                const SizedBox.shrink(),
              Flexible(
                child: Button.primary(
                  text: "Continue",
                  onPressed: nextStep,
                ),
              ),
            ],
          ),
        ],
      ),
    );
  }

  /// Check if the journey can continue to the next step
  @override
  Future<bool> canContinue() async {
    return true;
  }

  /// Called before navigating to the next step
  @override
  Future<void> onBeforeNext() async {
    // E.g. save data to session
  }

  /// Called when the journey is complete (at the last step)
  @override
  Future<void> onComplete() async {}
}

你会注意到 JourneyState 类使用 nextStep 来向前导航,使用 onBackPressed 来返回。

nextStep 方法会经历完整的验证生命周期:canContinue() -> onBeforeNext() -> 导航(或在最后一步时调用 onComplete())-> onAfterNext()

你也可以使用 buildJourneyContent 来构建包含可选导航按钮的结构化布局:

@override
Widget view(BuildContext context) {
    return buildJourneyContent(
      content: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Text('Welcome', style: Theme.of(context).textTheme.headlineMedium),
          const SizedBox(height: 20),
          Text('This onboarding journey will help you get started.'),
        ],
      ),
      nextButton: Button.primary(
        text: isLastStep ? "Get Started" : "Continue",
        onPressed: nextStep,
      ),
      backButton: isFirstStep ? null : Button.textOnly(
        text: "Back",
        textColor: Colors.black87,
        onPressed: onBackPressed,
      ),
    );
}

以下是 buildJourneyContent 方法中可用的属性。

属性 类型 描述
content Widget 页面的主要内容。
nextButton Widget? "下一步"按钮组件。
backButton Widget? "返回"按钮组件。
contentPadding EdgeInsetsGeometry 内容的内边距。
header Widget? 头部组件。
footer Widget? 底部组件。
crossAxisAlignment CrossAxisAlignment 内容的交叉轴对齐方式。

JourneyState 辅助方法

JourneyState 类提供了辅助方法和属性,你可以用它们来自定义 Journey 的行为。

方法 / 属性 描述
nextStep() 带验证地导航到下一步。返回 Future<bool>
previousStep() 导航到上一步。返回 Future<bool>
onBackPressed() 导航到上一步的简便方法。
onComplete() 在 Journey 完成时调用(在最后一步)。
onBeforeNext() 在导航到下一步之前调用。
onAfterNext() 在导航到下一步之后调用。
canContinue() 导航到下一步之前的验证检查。
isFirstStep 如果是 Journey 的第一步则返回 true。
isLastStep 如果是 Journey 的最后一步则返回 true。
currentStep 返回当前步骤索引(从 0 开始)。
totalSteps 返回总步骤数。
completionPercentage 返回完成百分比(0.0 到 1.0)。
goToStep(int index) 按索引跳转到指定步骤。
goToNextStep() 跳转到下一步(不验证)。
goToPreviousStep() 跳转到上一步(不验证)。
goToFirstStep() 跳转到第一步。
goToLastStep() 跳转到最后一步。
exitJourney() 通过弹出根导航器退出 Journey。
resetCurrentStep() 重置当前步骤的状态。
onJourneyComplete Journey 完成时的回调(在最后一步中重写)。
buildJourneyPage() 构建带有 Scaffold 的全屏 Journey 页面。

nextStep

nextStep 方法带完整验证地导航到下一步。它经历以下生命周期:canContinue() -> onBeforeNext() -> 导航或 onComplete() -> onAfterNext()

你可以传入 force: true 来跳过验证直接导航。

@override
Widget view(BuildContext context) {
    return buildJourneyContent(
        content: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
                ...
            ],
        ),
        nextButton: Button.primary(
            text: isLastStep ? "Get Started" : "Continue",
            onPressed: nextStep, // 先验证再导航
        ),
    );
}

跳过验证:

onPressed: () => nextStep(force: true),

previousStep

previousStep 方法导航到上一步。成功返回 true,如果已在第一步则返回 false

onPressed: () async {
    bool success = await previousStep();
    if (!success) {
      // 已经在第一步了
    }
},

onBackPressed

onBackPressed 方法是一个简便辅助方法,内部调用 previousStep()

@override
Widget view(BuildContext context) {
    return buildJourneyContent(
        content: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
                ...
            ],
        ),
        backButton: isFirstStep ? null : Button.textOnly(
            text: "Back",
            textColor: Colors.black87,
            onPressed: onBackPressed,
        ),
    );
}

onComplete

onComplete 方法在最后一步触发 nextStep() 时调用(验证通过后)。

@override
Future<void> onComplete() async {
    print("Journey completed");
}

onBeforeNext

onBeforeNext 方法在导航到下一步之前调用。

例如,如果你想在导航到下一步之前保存数据,可以在这里操作。

@override
Future<void> onBeforeNext() async {
    // E.g. save data to session
    // session('onboarding', {
    //   'name': 'Anthony Gordon',
    //   'occupation': 'Software Engineer',
    // });
}

onAfterNext

onAfterNext 方法在导航到下一步之后调用。

@override
Future<void> onAfterNext() async {
    // print('Navigated to the next step');
}

canContinue

canContinue 方法在触发 nextStep() 时调用。返回 false 可以阻止导航。

@override
Future<bool> canContinue() async {
    // Perform your validation logic here
    // Return true if the journey can continue, false otherwise
    if (nameController.text.isEmpty) {
        showToastSorry(description: "Please enter your name");
        return false;
    }
    return true;
}

isFirstStep

isFirstStep 属性在当前是 Journey 的第一步时返回 true。

backButton: isFirstStep ? null : Button.textOnly(
    text: "Back",
    textColor: Colors.black87,
    onPressed: onBackPressed,
),

isLastStep

isLastStep 属性在当前是 Journey 的最后一步时返回 true。

nextButton: Button.primary(
    text: isLastStep ? "Get Started" : "Continue",
    onPressed: nextStep,
),

currentStep

currentStep 属性返回当前步骤索引(从 0 开始)。

Text("Step ${currentStep + 1} of $totalSteps"),

totalSteps

totalSteps 属性返回 Journey 中的总步骤数。

completionPercentage

completionPercentage 属性返回完成百分比,值范围为 0.0 到 1.0。

LinearProgressIndicator(value: completionPercentage),

goToStep

goToStep 方法按索引直接跳转到指定步骤。此操作不会触发验证。

nextButton: Button.primary(
    text: "Skip to photos",
    onPressed: () {
        goToStep(2); // 跳转到步骤索引 2
    },
),

goToNextStep

goToNextStep 方法不经过验证直接跳转到下一步。如果已经在最后一步,则不执行任何操作。

onPressed: () {
    goToNextStep(); // 跳过验证直接到下一步
},

goToPreviousStep

goToPreviousStep 方法不经过验证直接跳转到上一步。如果已经在第一步,则不执行任何操作。

onPressed: () {
    goToPreviousStep();
},

goToFirstStep

goToFirstStep 方法跳转到第一步。

onPressed: () {
    goToFirstStep();
},

goToLastStep

goToLastStep 方法跳转到最后一步。

onPressed: () {
    goToLastStep();
},

exitJourney

exitJourney 方法通过弹出根导航器来退出 Journey。

onPressed: () {
    exitJourney(); // 弹出根导航器
},

resetCurrentStep

resetCurrentStep 方法重置当前步骤的状态。

onPressed: () {
    resetCurrentStep();
},

onJourneyComplete

onJourneyComplete getter 可以在 Journey 的最后一步中被重写,用于定义用户完成流程后的行为。

class _CompleteStepState extends JourneyState<CompleteStep> {
  _CompleteStepState() : super(
      navigationHubState: OnboardingNavigationHub.path.stateName());

  /// Callback when journey completes
  @override
  void Function()? get onJourneyComplete => () {
    // Navigate to your home page or next destination
    routeTo(HomePage.path);
  };

  @override
  Widget view(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(16),
      child: Column(
        children: [
          ...
          Button.primary(
            text: "Get Started",
            onPressed: onJourneyComplete, // 触发完成回调
          ),
        ],
      ),
    );
  }
}

buildJourneyPage

buildJourneyPage 方法构建一个包裹在 ScaffoldSafeArea 中的全屏 Journey 页面。

@override
Widget view(BuildContext context) {
    return buildJourneyPage(
      content: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Text('Welcome', style: Theme.of(context).textTheme.headlineMedium),
        ],
      ),
      nextButton: Button.primary(
        text: "Continue",
        onPressed: nextStep,
      ),
      backgroundColor: Colors.white,
    );
}
属性 类型 描述
content Widget 页面的主要内容。
nextButton Widget? "下一步"按钮组件。
backButton Widget? "返回"按钮组件。
contentPadding EdgeInsetsGeometry 内容的内边距。
header Widget? 头部组件。
footer Widget? 底部组件。
backgroundColor Color? Scaffold 的背景颜色。
appBar Widget? 可选的 AppBar 组件。
crossAxisAlignment CrossAxisAlignment 内容的交叉轴对齐方式。

在标签页内导航到组件

你可以使用 pushTo 辅助方法在标签页内导航到其他组件。

在你的标签页内部,可以使用 pushTo 辅助方法导航到另一个组件。

_HomeTabState extends State<HomeTab> {
    ...
    void _navigateToSettings() {
        pushTo(SettingsPage());
    }
    ...
}

你也可以向目标组件传递数据。

_HomeTabState extends State<HomeTab> {
    ...
    void _navigateToSettings() {
        pushTo(SettingsPage(), data: {"name": "Anthony"});
    }
    ...
}

标签页

标签页是 Navigation Hub 的核心构建块。

你可以使用 NavigationTab 类及其命名构造函数来向 Navigation Hub 添加标签页。

class _MyNavigationHubState extends NavigationHub<MyNavigationHub> {
    ...
    @override
    NavigationHubLayout? layout(BuildContext context) => NavigationHubLayout.bottomNav();
    ...
    _MyNavigationHubState() : super(() => {
        0: NavigationTab.tab(
            title: "Home",
            page: HomeTab(),
            icon: Icon(Icons.home),
            activeIcon: Icon(Icons.home),
        ),
        1: NavigationTab.tab(
            title: "Settings",
            page: SettingsTab(),
            icon: Icon(Icons.settings),
            activeIcon: Icon(Icons.settings),
        ),
    });

在上面的示例中,我们向 Navigation Hub 添加了两个标签页:Home 和 Settings。

你可以使用不同类型的标签页:

  • NavigationTab.tab() - 标准导航标签页。
  • NavigationTab.badge() - 带有 Badge 计数的标签页。
  • NavigationTab.alert() - 带有 Alert 指示器的标签页。
  • NavigationTab.journey() - 用于 Journey 导航布局的标签页。

为标签页添加 Badge

我们让为标签页添加 Badge 变得非常简单。

Badge 是向用户展示标签页中有新内容的好方式。

例如,如果你有一个聊天应用,可以在聊天标签页中显示未读消息数量。

要为标签页添加 Badge,可以使用 NavigationTab.badge 构造函数。

class _MyNavigationHubState extends NavigationHub<MyNavigationHub> {
    ...
    @override
    NavigationHubLayout? layout(BuildContext context) => NavigationHubLayout.bottomNav();
    ...
    _MyNavigationHubState() : super(() => {
        0: NavigationTab.badge(
            title: "Chats",
            page: ChatTab(),
            icon: Icon(Icons.message),
            activeIcon: Icon(Icons.message),
            initialCount: 10,
        ),
        1: NavigationTab.tab(
            title: "Settings",
            page: SettingsTab(),
            icon: Icon(Icons.settings),
            activeIcon: Icon(Icons.settings),
        ),
    });

在上面的示例中,我们为 Chat 标签页添加了一个初始计数为 10 的 Badge。

你也可以通过编程方式更新 Badge 计数。

/// 增加 Badge 计数
BaseNavigationHub.stateActions.incrementBadgeCount(tab: 0);

/// 更新 Badge 计数
BaseNavigationHub.stateActions.updateBadgeCount(tab: 0, count: 5);

/// 清除 Badge 计数
BaseNavigationHub.stateActions.clearBadgeCount(tab: 0);

默认情况下,Badge 计数会被记住。如果你想在每次会话时清除 Badge 计数,可以将 rememberCount 设置为 false

0: NavigationTab.badge(
    title: "Chats",
    page: ChatTab(),
    icon: Icon(Icons.message),
    activeIcon: Icon(Icons.message),
    initialCount: 10,
    rememberCount: false,
),

为标签页添加 Alert

你可以为标签页添加 Alert。

有时候你可能不想显示 Badge 计数,但想向用户显示一个 Alert 指示器。

要为标签页添加 Alert,可以使用 NavigationTab.alert 构造函数。

class _MyNavigationHubState extends NavigationHub<MyNavigationHub> {
    ...
    @override
    NavigationHubLayout? layout(BuildContext context) => NavigationHubLayout.bottomNav();
    ...
    _MyNavigationHubState() : super(() => {
        0: NavigationTab.alert(
            title: "Chats",
            page: ChatTab(),
            icon: Icon(Icons.message),
            activeIcon: Icon(Icons.message),
            alertColor: Colors.red,
            alertEnabled: true,
            rememberAlert: false,
        ),
        1: NavigationTab.tab(
            title: "Settings",
            page: SettingsTab(),
            icon: Icon(Icons.settings),
            activeIcon: Icon(Icons.settings),
        ),
    });

这将为 Chat 标签页添加一个红色的 Alert。

你也可以通过编程方式更新 Alert。

/// 启用 Alert
BaseNavigationHub.stateActions.alertEnableTab(tab: 0);

/// 禁用 Alert
BaseNavigationHub.stateActions.alertDisableTab(tab: 0);

初始索引

默认情况下,Navigation Hub 从第一个标签页(索引 0)开始。你可以通过重写 initialIndex getter 来更改此设置。

class _MyNavigationHubState extends NavigationHub<MyNavigationHub> {
    ...
    @override
    int get initialIndex => 1; // 从第二个标签页开始
    ...
}

状态保持

默认情况下,Navigation Hub 的状态是保持的。

这意味着当你导航到一个标签页时,该标签页的状态会被保留。

如果你想在每次导航到标签页时清除其状态,可以将 maintainState 设置为 false

class _MyNavigationHubState extends NavigationHub<MyNavigationHub> {
    ...
    @override
    bool get maintainState => false;
    ...
}

onTap

你可以重写 onTap 方法,在标签页被点击时添加自定义逻辑。

class _MyNavigationHubState extends NavigationHub<MyNavigationHub> {
    ...
    @override
    onTap(int index) {
        // Add custom logic here
        // E.g. track analytics, show confirmation, etc.
        super.onTap(index); // 务必调用 super 来处理标签页切换
    }
}

State Actions

State Actions 是一种从应用中任何位置与 Navigation Hub 交互的方式。

以下是你可以使用的 State Actions:

/// 重置给定索引处的标签页
/// 例如:MyNavigationHub.stateActions.resetTabIndex(0);
resetTabIndex(int tabIndex);

/// 以编程方式切换当前标签页
/// 例如:MyNavigationHub.stateActions.currentTabIndex(2);
currentTabIndex(int tabIndex);

/// 更新 Badge 计数
/// 例如:MyNavigationHub.stateActions.updateBadgeCount(tab: 0, count: 2);
updateBadgeCount({required int tab, required int count});

/// 增加 Badge 计数
/// 例如:MyNavigationHub.stateActions.incrementBadgeCount(tab: 0);
incrementBadgeCount({required int tab});

/// 清除 Badge 计数
/// 例如:MyNavigationHub.stateActions.clearBadgeCount(tab: 0);
clearBadgeCount({required int tab});

/// 启用标签页的 Alert
/// 例如:MyNavigationHub.stateActions.alertEnableTab(tab: 0);
alertEnableTab({required int tab});

/// 禁用标签页的 Alert
/// 例如:MyNavigationHub.stateActions.alertDisableTab(tab: 0);
alertDisableTab({required int tab});

/// 在 Journey 布局中导航到下一页
/// 例如:await MyNavigationHub.stateActions.nextPage();
Future<bool> nextPage();

/// 在 Journey 布局中导航到上一页
/// 例如:await MyNavigationHub.stateActions.previousPage();
Future<bool> previousPage();

要使用 State Action,你可以按以下方式操作:

MyNavigationHub.stateActions.updateBadgeCount(tab: 0, count: 2);

MyNavigationHub.stateActions.resetTabIndex(0);

MyNavigationHub.stateActions.currentTabIndex(2); // 切换到标签页 2

await MyNavigationHub.stateActions.nextPage(); // Journey:跳转到下一页

加载样式

开箱即用,Navigation Hub 在标签页加载时会显示你的默认加载组件(resources/widgets/loader_widget.dart)。

你可以自定义 loadingStyle 来更新加载样式。

样式 描述
normal 默认加载样式
skeletonizer 骨架屏加载样式
none 无加载样式

你可以这样更改加载样式:

@override
LoadingStyle get loadingStyle => LoadingStyle.normal();
// 或
@override
LoadingStyle get loadingStyle => LoadingStyle.skeletonizer();

如果你想在某个样式中更新加载组件,可以向 LoadingStyle 传递一个 child

@override
LoadingStyle get loadingStyle => LoadingStyle.normal(
    child: Center(
        child: Text("Loading..."),
    ),
);

现在,当标签页加载时,将显示 "Loading..." 文本。

示例如下:

class _MyNavigationHubState extends NavigationHub<MyNavigationHub> {
    ...
    _MyNavigationHubState() : super(() async {

      await sleep(3); // simulate loading for 3 seconds

      return {
        0: NavigationTab.tab(
          title: "Home",
          page: HomeTab(),
        ),
        1: NavigationTab.tab(
          title: "Settings",
          page: SettingsTab(),
        ),
      };
    });

    @override
    LoadingStyle get loadingStyle => LoadingStyle.normal(
        child: Center(
            child: Text("Loading..."),
        ),
    );
    ...
}

创建 Navigation Hub

要创建 Navigation Hub,你可以使用 Metro,运行以下命令。

metro make:navigation_hub base

该命令会引导你完成一个交互式设置流程,你可以选择布局类型并定义标签页或 Journey 状态。

这将在你的 resources/pages/navigation_hubs/base/ 目录下创建 base_navigation_hub.dart 文件,子组件按照 tabs/states/ 子文件夹组织。