flutter 使用Bloc+PageView+BottomNavigationBar实现传统首页布局


前言

本篇文章主要记录首页框架搭配bloc的使用示例,本篇文章将会使用上一篇文章中的代码,有兴趣的朋友可以去参考一下实现,除了使用pageview还有另外一种实现,但是最后发现那种方式有两个问题,一个是进入首页后会加载所有PageWidget,第二个是每次切换PageWidget时都会走一遍build方法,这显然不符合实际使用场景,所以这里参考部分文章对PageView的使用,也引入了PageView的页面缓存与懒加载完美的符合实际使用,下面看示例。


一、目录结构

├── bloc
│   ├── cubit.dart
│   └── state.dart
└── view.dart

二、具体步骤

1. 使用state定义BottomNavigationBar切换状态

代码如下:

part of 'cubit.dart';

enum HomeTab {
  home('首页'),
  classification('分类'),
  dynamics('动态'),
  me('我的');

  final String title;
  const HomeTab(this.title);
}
final class HomeState extends Equatable {
  final HomeTab tab;

  const HomeState({this.tab = HomeTab.home});

  
  List<Object?> get props => [tab];

  HomeState copyWith({
    HomeTab? tab,
  }) {
    return HomeState(
      tab: tab ?? this.tab,
    );
  }
}

2.使用Cubit定义简单的事件处理

代码如下:

import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

part 'state.dart';

class HomeCubit extends Cubit<HomeState> {
  HomeCubit() : super(const HomeState());

  void setTab(HomeTab tab) => emit(state.copyWith(tab: tab));
}

3.封装PageView带缓存子类,listview也可以使用(需要慎重)

代码如下:

part of lib_rock_utils;

/// {@template keep_alive_wrapper}
/// 帮助 PageView或ListView等滑动控件实现缓存
/// 使用方式,只需要将需要缓存的子项用该类包裹即可
///
/// {@endtemplate}
final class KeepAliveWrapper extends StatefulWidget {
  /// {@macro keep_alive_wrapper}
  const KeepAliveWrapper({
    super.key,
    this.keepAlive = true,
    required this.child,
  });

  /// 是否缓存, 默认缓存
  final bool keepAlive;
  /// 布局
  final Widget child;

  
  State<KeepAliveWrapper> createState() => _KeepAliveWrapperState();
}

class _KeepAliveWrapperState extends State<KeepAliveWrapper>
    with AutomaticKeepAliveClientMixin {
  
  Widget build(BuildContext context) {
    super.build(context); // 必须调用
    return widget.child;
  }

  
  void didUpdateWidget(covariant KeepAliveWrapper oldWidget) {
    if (oldWidget.keepAlive != widget.keepAlive) {
      // keepAlive 状态需要更新,实现在 AutomaticKeepAliveClientMixin 中
      updateKeepAlive();
    }
    super.didUpdateWidget(oldWidget);
  }

  
  bool get wantKeepAlive => widget.keepAlive; // 是否需要缓存
}

4.实际的布局, 直接将布局放到main中即可看到效果

代码如下:

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:lib_rock_utils/lib_rock_utils.dart';

import '../test_http2/view.dart';
import 'bloc/cubit.dart';

class HomePage extends StatelessWidget {
  /// 首页
  const HomePage({super.key});

  
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (BuildContext context) => HomeCubit(),
      child: const _HomePageView(),
    );
  }
}

class _HomePageView extends StatefulWidget {
  const _HomePageView();

  
  State<_HomePageView> createState() => _HomePageViewState();
}

class _HomePageViewState extends State<_HomePageView> {
  final PageController _pageController = PageController();
  // 记录两次点击返回键的时间,如果在1秒内点击两次就退出
  DateTime? lastPopTime;
  // 当前需要切换的页面,需要与BottomNavigationBar个数对应
  final List<Widget> pageList = [
    KeepAliveWrapper(child: HomeTwoPage()),
    KeepAliveWrapper(
        child: TestHomeItemPage(txt: HomeTab.classification.title)),
    KeepAliveWrapper(child: TestHomeItemPage(txt: HomeTab.dynamics.title)),
    KeepAliveWrapper(child: TestHomeItemPage(txt: HomeTab.me.title)),
  ];

  
  Widget build(BuildContext context) {
    final selectedTab = context.select((HomeCubit cubit) => cubit.state.tab);

    return Scaffold(
      body: WillPopScope(
        onWillPop: () async {
          // 如果当前tab不在首页, 则按返回触发切换到首页
          if (selectedTab != HomeTab.home) {
            _pageController.jumpToPage(0);
            context.read<HomeCubit>().setTab(HomeTab.home);
          } else {
            // 双击退出判断
            lastPopTime ??= DateTime.now();
            if (DateTime.now().difference(lastPopTime!) <= const Duration(seconds: 1)) {
              lastPopTime = DateTime.now();
              LogUtil.error('再按一次退出APP');
            } else {
              lastPopTime = DateTime.now();
              // 退出app
              await SystemChannels.platform.invokeMethod('SystemNavigator.pop');
            }
          }
          return false;
        },
        child: PageView(
          // 禁止左右滑动
          physics: const NeverScrollableScrollPhysics(),
          controller: _pageController,
          children: pageList,
        ),
      ),
      // 底部导航
      bottomNavigationBar: _buildBottomNavigationBar(context, selectedTab),
    );
  }

  // 底部导航栏布局
  BottomNavigationBar _buildBottomNavigationBar(
      BuildContext context, HomeTab selectedTab) {
    return BottomNavigationBar(
      currentIndex: selectedTab.index,
      type: BottomNavigationBarType.fixed,
      fixedColor: Theme.of(context).colorScheme.inversePrimary,
      onTap: (index) {
        // 切换页面
        _pageController.jumpToPage(index);
        final tab = HomeTab.values[index];
        // 发送切换指令
        context.read<HomeCubit>().setTab(tab);
      },
      items: [
        BottomNavigationBarItem(
          label: HomeTab.home.title,
          icon: const Icon(Icons.home),
        ),
        BottomNavigationBarItem(
          label: HomeTab.classification.title,
          icon: const Icon(Icons.my_library_books),
        ),
        BottomNavigationBarItem(
          label: HomeTab.dynamics.title,
          icon: const Icon(Icons.cloud),
        ),
        BottomNavigationBarItem(
          label: HomeTab.me.title,
          icon: const Icon(Icons.person),
        ),
      ],
    );
  }
}

// 其他布局,用于演示使用
class TestHomeItemPage extends StatelessWidget {
  final String txt;

  const TestHomeItemPage({super.key, required this.txt});

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(txt),
      ),
      body: Center(
        child: Text(txt),
      ),
    );
  }
}

总结

在开始写这个页面之前,也踩了一些坑,比如大多数文章都是使用简单的方式实现了效果,可以用吗?可以用,但是我认为该控件的使用场景并不在首页,也许做轮播合适,毕竟以目前的app来看,首页是比较复杂的功能,也不太可能出现一次加载多个布局,每次显示都重绘一次,这样对于复杂的功能来讲性能堪忧,所以当时写完测试之后发现了不对,然后想到也许flutter有类似于ViewPage这样的组件,按照这个思路确实很容易就实现这个页面的简单效果,当然由于项目使用了bloc,所以对bloc的依赖比较高,如果项目中使用的其它状态管理框架,照着修改即可,如果有更好的思路欢迎一起讨论与学习。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
使用 Flutter 的 TabBar 和 PageView 可以轻松实现多个页面切换的效果,以下是一个简单的示例: ```dart class MyTabbedPage extends StatefulWidget { @override _MyTabbedPageState createState() => _MyTabbedPageState(); } class _MyTabbedPageState extends State<MyTabbedPage> with SingleTickerProviderStateMixin { final List<Tab> myTabs = <Tab>[ Tab(text: 'Tab 1'), Tab(text: 'Tab 2'), Tab(text: 'Tab 3'), ]; TabController _tabController; @override void initState() { super.initState(); _tabController = TabController(vsync: this, length: myTabs.length); } @override void dispose() { _tabController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('My Tabbed Page'), bottom: TabBar( controller: _tabController, tabs: myTabs, ), ), body: TabBarView( controller: _tabController, children: myTabs.map((Tab tab) { final String label = tab.text.toLowerCase(); return Center( child: Text( 'This is the $label tab', style: const TextStyle(fontSize: 36), ), ); }).toList(), ), ); } } ``` 在这个示例中,我们创建了一个 `_MyTabbedPageState` 类来管理 TabBar 和 PageView 的状态。在 `initState` 中,我们创建了一个 `TabController` 对象,将其与 `TabBar` 和 `TabBarView` 绑定。在 `dispose` 中,我们释放了 `TabController` 对象。在 `build` 方法中,我们创建了一个 `Scaffold`,其中包含一个 `AppBar` 和一个 `TabBarView`。`TabBarView` 包含了一个 `children` 列表,其中每个元素都是一个 `Center` widget,包含一个 `Text` widget,用于显示当前选中的标签。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值