在这里插入图片描述

主框架是应用的骨架,包含底部导航栏和页面容器。核心功能是Tab切换和页面状态保持。做好底部导航需要考虑很多细节,比如状态保持、流畅动画、视觉反馈等。

底部导航功能需求

在写代码之前,先明确底部导航要实现的功能:

页面切换 - 点击不同的Tab显示不同的页面
状态保持 - 切换Tab后再切回来,之前的滚动位置、输入内容等状态应该保持
视觉反馈 - 当前选中的Tab要有明显的视觉区分
流畅动画 - 切换Tab时要有平滑的过渡
性能优化 - 不能因为有多个页面就占用太多内存

选择导航组件

Flutter提供了几种底部导航的实现方式:

BottomNavigationBar - Flutter早期的组件,稳定但样式老旧
NavigationBar - Material Design 3的新组件,样式现代,动画流畅
CupertinoTabBar - iOS风格的导航,适合iOS风格应用

我们选择NavigationBar,因为它代表了Flutter的未来方向,而且会自动适配深色模式。

创建主框架文件

lib/screens目录下创建main_screen.dart

import 'package:flutter/material.dart';
import 'home/home_screen.dart';
import 'categories/categories_screen.dart';
import 'favorites/favorites_screen.dart';
import 'profile/profile_screen.dart';

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

  
  State<MainScreen> createState() => _MainScreenState();
}

代码解析

  • 导入四个页面:首页、分类、收藏、个人中心
  • 使用StatefulWidget管理当前选中的Tab索引
  • 为什么叫main_screen而不是bottom_navigation?因为这个文件不只是底部导航,它是整个应用的主框架

定义状态和页面列表

class _MainScreenState extends State<MainScreen> {
  int _currentIndex = 0;

  final List<Widget> _screens = [
    const HomeScreen(),
    const CategoriesScreen(),
    const FavoritesScreen(),
    const ProfileScreen(),
  ];

代码解析

  • _currentIndex - 当前选中的Tab索引,0表示首页,1表示分类,以此类推
  • _screens - 四个页面的列表,通过索引访问
  • 使用const构造函数优化性能,Flutter会复用Widget实例
  • 变量名前面的下划线表示这是私有变量,只能在这个文件内访问

页面顺序很重要:List的顺序要和底部导航按钮的顺序一致,否则点击按钮会跳到错误的页面。

构建主框架UI

  
  Widget build(BuildContext context) {
    return Scaffold(
      body: IndexedStack(
        index: _currentIndex,
        children: _screens,
      ),

代码解析

  • Scaffold - 提供Material Design的基本布局结构
  • IndexedStack - 关键组件,同时创建所有子Widget但只显示指定index的那个
  • 其他Widget虽然不可见但依然存在,状态得以保持
  • 切换Tab时页面的滚动位置、输入内容等不会丢失

为什么不用_screens[_currentIndex]

如果直接用索引访问,每次切换Tab都会销毁旧页面、创建新页面,页面状态就丢失了。用户在首页浏览新闻,看到第10条时切换到收藏Tab,然后又切回首页,如果页面重新加载,又要从第1条开始看,用户会很烦。

IndexedStack虽然占用更多内存(同时创建4个页面),但换来的是更好的用户体验。对于4-5个Tab的应用,这点内存开销是值得的。

实现底部导航栏

      bottomNavigationBar: NavigationBar(
        selectedIndex: _currentIndex,
        onDestinationSelected: (index) {
          setState(() {
            _currentIndex = index;
          });
        },

代码解析

  • NavigationBar - Material Design 3的底部导航组件
  • selectedIndex - 当前选中的Tab,与IndexedStack的index保持一致
  • onDestinationSelected - 点击Tab时触发,参数index就是被点击的Tab索引
  • setState - 通知Flutter重新执行build方法,更新UI

setState的作用:这是Flutter状态管理的基础。调用setState告诉Flutter:“我的状态变了,请重新执行build方法”。如果不用setState包裹,_currentIndex虽然会改变,但界面不会更新。

配置导航按钮

        destinations: const [
          NavigationDestination(
            icon: Icon(Icons.home_outlined),
            selectedIcon: Icon(Icons.home),
            label: '首页',
          ),
          NavigationDestination(
            icon: Icon(Icons.category_outlined),
            selectedIcon: Icon(Icons.category),
            label: '分类',
          ),
          NavigationDestination(
            icon: Icon(Icons.favorite_outline),
            selectedIcon: Icon(Icons.favorite),
            label: '收藏',
          ),
          NavigationDestination(
            icon: Icon(Icons.person_outline),
            selectedIcon: Icon(Icons.person),
            label: '我的',
          ),
        ],
      ),
    );
  }
}

代码解析

  • icon - 未选中时显示的图标(轮廓风格)
  • selectedIcon - 选中时显示的图标(填充风格)
  • label - 图标下方的文字,简短有力,最好两个字
  • 图标选择遵循Material Design规范,用户一看就懂

图标的选择原则

  • 首页用房子图标(home)
  • 分类用分类图标(category)
  • 收藏用心形图标(favorite)
  • 个人中心用人形图标(person)

这些都是通用的设计语言,用户看到就知道是什么功能。

完整代码

完整的main_screen.dart文件:

import 'package:flutter/material.dart';
import 'home/home_screen.dart';
import 'categories/categories_screen.dart';
import 'favorites/favorites_screen.dart';
import 'profile/profile_screen.dart';

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

  
  State<MainScreen> createState() => _MainScreenState();
}

class _MainScreenState extends State<MainScreen> {
  int _currentIndex = 0;

  final List<Widget> _screens = [
    const HomeScreen(),
    const CategoriesScreen(),
    const FavoritesScreen(),
    const ProfileScreen(),
  ];

  
  Widget build(BuildContext context) {
    return Scaffold(
      body: IndexedStack(
        index: _currentIndex,
        children: _screens,
      ),
      bottomNavigationBar: NavigationBar(
        selectedIndex: _currentIndex,
        onDestinationSelected: (index) {
          setState(() {
            _currentIndex = index;
          });
        },
        destinations: const [
          NavigationDestination(
            icon: Icon(Icons.home_outlined),
            selectedIcon: Icon(Icons.home),
            label: '首页',
          ),
          NavigationDestination(
            icon: Icon(Icons.category_outlined),
            selectedIcon: Icon(Icons.category),
            label: '分类',
          ),
          NavigationDestination(
            icon: Icon(Icons.favorite_outline),
            selectedIcon: Icon(Icons.favorite),
            label: '收藏',
          ),
          NavigationDestination(
            icon: Icon(Icons.person_outline),
            selectedIcon: Icon(Icons.person),
            label: '我的',
          ),
        ],
      ),
    );
  }
}

代码不到70行,但实现了完整的底部导航功能。代码简洁、逻辑清晰、易于维护。

创建临时页面

为了让应用能运行,需要创建四个临时页面。以首页为例:

import 'package:flutter/material.dart';

class HomeScreen extends StatelessWidget {
  const HomeScreen({super.key});

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('首页')),
      body: const Center(child: Text('首页内容')),
    );
  }
}

代码解析

  • 最简单的页面实现,只显示标题和文字
  • 足够测试底部导航是否正常工作
  • 后续会逐步完善这些页面

用同样方式创建其他三个页面(CategoriesScreen、FavoritesScreen、ProfileScreen),只需要改改类名和显示的文字。

运行测试

flutter run

测试要点:

  1. 启动页显示2-3秒后自动跳转
  2. 底部显示四个导航按钮
  3. 点击不同按钮切换页面
  4. 切换后再切回来,页面状态保持

试着在首页滚动一下(虽然现在还没内容可滚动),然后切换到其他Tab,再切回首页,你会发现页面状态保持了。这就是IndexedStack的作用。

进阶优化

双击Tab回到顶部

有些应用支持双击当前Tab回到顶部,这是个不错的功能:

onDestinationSelected: (index) {
  if (index == _currentIndex) {
    // 双击当前Tab,滚动到顶部
    _scrollToTop(index);
  } else {
    setState(() {
      _currentIndex = index;
    });
  }
},

具体的滚动实现要在各个页面里处理,这里只是提供一个思路。

添加角标提示

有时候需要在Tab上显示未读消息数量:

NavigationDestination(
  icon: Stack(
    children: [
      Icon(Icons.favorite_outline),
      if (unreadCount > 0)
        Positioned(
          right: 0,
          top: 0,
          child: Container(
            padding: EdgeInsets.all(2),
            decoration: BoxDecoration(
              color: Colors.red,
              shape: BoxShape.circle,
            ),
            child: Text(
              unreadCount > 99 ? '99+' : '$unreadCount',
              style: TextStyle(fontSize: 10, color: Colors.white),
            ),
          ),
        ),
    ],
  ),
  label: '收藏',
)

代码解析

  • 使用Stack叠加图标和角标
  • 红色圆点显示未读数量
  • 数量超过99显示"99+",避免数字太长

处理返回键

从主页面跳转到详情页时,底部导航栏不应该显示。这是自动的,因为用Navigator.push会在当前页面上叠加新页面,新页面有自己的Scaffold。

如果想在主页面按返回键时退出应用:

WillPopScope(
  onWillPop: () async {
    return true; // 允许返回,退出应用
  },
  child: Scaffold(...),
)

设计原则

做底部导航时要遵循的原则:

Tab数量 - 3-5个最佳,太少功能不够,太多用户记不住
图标选择 - 直观易懂,使用用户熟悉的图标
文字标签 - 虽然图标很直观,但文字标签也不能省
颜色使用 - 使用主题颜色,自动适配深色模式
性能考虑 - IndexedStack同时创建4个页面,内存开销可接受

状态保持的重要性

用户在首页浏览新闻,看到第10条时切换到收藏Tab,然后又切回首页。如果页面重新加载,又要从第1条开始看,用户会很烦。所以状态保持不是可选项,而是必须项。

IndexedStack会同时创建所有页面,占用更多内存。但换来的是更好的用户体验。这是个典型的空间换时间的例子。在移动设备上,内存是宝贵的资源,但对于4-5个Tab的应用,这点内存开销是值得的。

简单就是美

我见过有些应用,底部导航做得很复杂,各种动画、特效。但用户要的不是炫酷的效果,而是快速、流畅的切换。所以建议不要过度设计,用Flutter提供的标准组件,遵循Material Design规范,就能做出很好的效果。


欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

在这里你可以找到更多Flutter开发资源,与其他开发者交流经验,共同进步。

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐