掌握 Flutter 中的 BLoC:深入探索 Cubit 模式

在 Flutter 开发中,状态管理是构建响应式和可维护应用的关键。BLoC(Business Logic Component)模式是一种广受欢迎的状态管理解决方案,而其中的 Cubit 变体因其简洁性和易用性,成为许多开发者的首选。本文将通过一个实际案例------获取并展示 GitHub 事件列表,带您深入了解如何在 Flutter 中使用 Cubit 模式。我们将从基础设置开始,逐步讲解数据获取、状态管理以及 UI 集成。

什么是 BLoC 和 Cubit?

在进入代码之前,先简单了解一下 BLoC 和 Cubit 的概念:

  • BLoC(业务逻辑组件):一种设计模式,用于将应用的展示层与业务逻辑分离。它通过流(Stream)管理状态变化,非常适合处理复杂的业务逻辑。
  • Cubit:BLoC 的一个简化版本,去掉了显式事件(Event)的概念,直接通过方法调用来改变状态。对于相对简单的状态管理需求,Cubit 是一个更轻量级的选择。

本文将聚焦 Cubit,展示其如何在实际项目中发挥作用。

项目设置

我们将构建一个简单的 Flutter 应用,用于展示 GitHub 的事件列表。为此,我们需要以下依赖:

  • flutter_bloc:提供 BLoC 和 Cubit 的核心功能。
  • http:用于发起 HTTP 请求以获取 GitHub API 数据。
  • equatable:简化状态对象的比较。

pubspec.yaml 中添加以下依赖:

yaml 复制代码
dependencies:
  flutter:
    sdk: flutter
  flutter_bloc: ^9.1.0
  http: ^1.3.0
  equatable: ^2.0.7

运行 flutter pub get 以安装这些包。

数据模型

首先,我们需要一个模型类来表示 GitHub 事件的数据。假设每个事件包含 idusernameavatarUrlrepoUrl 等字段。以下是 GithubEventModel 的实现:

dart 复制代码
import 'package:equatable/equatable.dart';

class GithubEventModel extends Equatable {
  final String id;
  final String username;
  final String avatarUrl;
  final String repoUrl;

  const GithubEventModel({
    required this.id,
    required this.username,
    required this.avatarUrl,
    required this.repoUrl,
  });

  factory GithubEventModel.fromJson(Map<String, dynamic> json) {
    return GithubEventModel(
      id: json['id'].toString(),
      username: json['actor']['login'],
      avatarUrl: json['actor']['avatar_url'],
      repoUrl: json['repo']['url'],
    );
  }

  @override
  List<Object> get props => [id, username, avatarUrl, repoUrl];
}

使用 Equatable 可以简化对象比较,确保状态更新时能正确触发 UI 重建。

数据仓库(Repository)

接下来,我们需要一个数据仓库类来处理与 GitHub API 的交互。GithubEventRepository 类负责从 GitHub API 获取事件数据,并支持分页:

dart 复制代码
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:flutter_bloc_test/cubit_app/core/model/github_event_model.dart';

class GithubEventRepository {
  Future<List<GithubEventModel>> fetchGithubEvents({int page = 1}) async {
    final response = await http.get(Uri.parse("https://api.github.com/events?page=$page"));
    if (response.statusCode != 200) {
      throw Exception("Failed to fetch Github events");
    }
    final List<dynamic> jsonData = json.decode(response.body);
    return jsonData.map((e) => GithubEventModel.fromJson(e)).toList();
  }
}

这个类通过 http 包发起 GET 请求,获取指定页码的事件数据,并将其转换为 GithubEventModel 对象的列表。

定义状态(State)

在 Cubit 模式中,状态(State)代表 UI 的不同情况。我们定义了以下四种状态:

  1. GithubEventInitial:初始状态。
  2. GithubEventLoading:数据加载中。
  3. GithubEventLoaded:数据加载成功。
  4. GithubEventError:加载出错。

以下是状态的实现:

dart 复制代码
import 'package:equatable/equatable.dart';
import 'package:flutter_bloc_test/cubit_app/core/model/github_event_model.dart';

sealed class GithubEventState extends Equatable {
  final List<GithubEventModel> githubEvents;
  const GithubEventState(this.githubEvents);

  @override
  List<Object> get props => [githubEvents];
}

final class GithubEventInitial extends GithubEventState {
  const GithubEventInitial() : super(const []);
}

final class GithubEventLoading extends GithubEventState {
  const GithubEventLoading() : super(const []);
}

final class GithubEventLoaded extends GithubEventState {
  const GithubEventLoaded(super.githubEvents);
}

final class GithubEventError extends GithubEventState {
  final String message;
  const GithubEventError(this.message, super.githubEvents);

  @override
  List<Object> get props => [message, ...githubEvents];
}
  • 使用 sealed class 定义状态的基类,确保类型安全。
  • githubEvents 字段存储当前的事件列表。
  • GithubEventError 额外包含错误信息 message,并保留当前的事件列表,以便在错误时仍能显示已有数据。

创建 Cubit

GithubEventCubit 是状态管理的核心,负责处理业务逻辑并发出状态变化:

dart 复制代码
import 'package:bloc/bloc.dart';
import 'package:flutter_bloc_test/cubit_app/core/model/github_event_model.dart';
import 'package:flutter_bloc_test/cubit_app/core/repository/github_event_repository.dart';
import 'github_event_state.dart';

class GithubEventCubit extends Cubit<GithubEventState> {
  final GithubEventRepository _githubEventRepository = GithubEventRepository();
  int _page = 1;
  bool _isLoadMore = false;

  GithubEventCubit() : super(const GithubEventInitial());

  Future<void> fetchGithubEvents() async {
    try {
      emit(const GithubEventLoading());
      _page = 1;
      final events = await _githubEventRepository.fetchGithubEvents(page: _page);
      emit(GithubEventLoaded(events));
    } on Exception catch (e) {
      emit(GithubEventError(e.toString(), state.githubEvents));
    }
  }

  Future<void> loadMoreGithubEvents() async {
    if (_isLoadMore) return;
    _isLoadMore = true;
    try {
      _page++;
      final events = await _githubEventRepository.fetchGithubEvents(page: _page);
      emit(GithubEventLoaded([...state.githubEvents, ...events]));
    } on Exception catch (e) {
      emit(GithubEventError(e.toString(), state.githubEvents));
    } finally {
      _isLoadMore = false;
    }
  }

  void removeGithubEventItem(GithubEventModel githubEventModel) {
    final List<GithubEventModel> githubEvents = List.from(state.githubEvents);
    githubEvents.remove(githubEventModel);
    emit(GithubEventLoaded(githubEvents));
  }
}

关键方法说明:

  • fetchGithubEvents :获取初始事件列表。重置页码为 1,发出加载状态,获取数据后发出 GithubEventLoaded,若出错则发出 GithubEventError
  • loadMoreGithubEvents :加载更多事件。使用 _isLoadMore 防止重复请求,增加页码并追加新数据到现有列表中。
  • removeGithubEventItem:从列表中移除指定事件,并发出更新后的状态。

UI 集成

我们将通过两个主要 widget 将 Cubit 集成到 UI 中:GithubEventPageGithubEventView

GithubEventPage

GithubEventPage 负责提供 Cubit 并初始化数据获取:

dart 复制代码
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_bloc_test/cubit_app/github_event/github_event.dart';
import 'github_event_view.dart';

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

  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (_) => GithubEventCubit()..fetchGithubEvents(),
      child: const GithubEventView(),
    );
  }
}
  • 使用 BlocProvider 创建并提供 GithubEventCubit
  • 在创建时立即调用 fetchGithubEvents 加载数据。

GithubEventView

GithubEventView 是一个有状态 widget,负责监听状态变化并渲染 UI:

dart 复制代码
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_bloc_test/cubit_app/core/model/github_event_model.dart';
import 'package:flutter_bloc_test/cubit_app/github_event/github_event.dart';

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

  @override
  State<GithubEventView> createState() => _GithubEventViewState();
}

class _GithubEventViewState extends State<GithubEventView> {
  late ScrollController _scrollController;
  bool _isShowTopBtn = false;

  @override
  void initState() {
    super.initState();
    _scrollController = ScrollController();
    _scrollController.addListener(_scroll);
  }

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

  void _scroll() {
    if (_scrollController.position.pixels > MediaQuery.of(context).size.height) {
      setState(() { _isShowTopBtn = true; });
    } else {
      setState(() { _isShowTopBtn = false; });
    }
    if (_scrollController.position.pixels >= _scrollController.position.maxScrollExtent - 100) {
      context.read<GithubEventCubit>().loadMoreGithubEvents();
    }
  }

  void _scrollToTop() {
    _scrollController.animateTo(0,
        duration: const Duration(milliseconds: 300), curve: Curves.easeInOut);
  }

  Future<void> _onCopy(GithubEventModel event) async {
    await Clipboard.setData(
      ClipboardData(
        text: "name: ${event.username}\navatar: ${event.avatarUrl}\nrepo: ${event.repoUrl}",
      ),
    );
    if (await Clipboard.hasStrings()) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: const Text("Copied Successfully"), duration: const Duration(seconds: 1)),
      );
    }
  }

  void _onShare(GithubEventModel event) {
    // Share.share(event.repoUrl);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text("Github Event")),
      body: BlocBuilder<GithubEventCubit, GithubEventState>(
        builder: (context, state) {
          if (state is GithubEventLoading) {
            return const Center(child: CircularProgressIndicator());
          }
          if (state is GithubEventError) {
            return Center(child: Text("Error: ${state.message}"));
          }
          if (state is GithubEventLoaded) {
            return RefreshIndicator(
              onRefresh: () => context.read<GithubEventCubit>().fetchGithubEvents(),
              child: ListView.builder(
                controller: _scrollController,
                itemCount: state.githubEvents.length,
                itemBuilder: (context, index) {
                  final event = state.githubEvents[index];
                  return Dismissible(
                    key: ValueKey(event.id),
                    direction: DismissDirection.endToStart,
                    background: Container(
                      color: Colors.red,
                      alignment: Alignment.centerRight,
                      padding: const EdgeInsets.only(right: 25.0),
                      child: const Icon(CupertinoIcons.trash, color: Colors.white),
                    ),
                    onDismissed: (_) {
                      context.read<GithubEventCubit>().removeGithubEventItem(event);
                    },
                    child: CupertinoContextMenu(
                      actions: [
                        CupertinoContextMenuAction(
                          child: const Text("Copy"),
                          onPressed: () {
                            _onCopy(event);
                            Navigator.pop(context);
                          },
                        ),
                        CupertinoContextMenuAction(
                          child: const Text("Share"),
                          onPressed: () => _onShare(event),
                        ),
                      ],
                      child: ListTile(
                        title: Text(event.username),
                        subtitle: Text(event.repoUrl),
                        leading: Image.network(event.avatarUrl),
                      ),
                    ),
                  );
                },
              ),
            );
          }
          return const Center(child: Text("No events"));
        },
      ),
      floatingActionButton: _isShowTopBtn
          ? FloatingActionButton(
              onPressed: _scrollToTop,
              child: const Icon(Icons.arrow_circle_up),
            )
          : null,
    );
  }
}

UI 功能亮点:

  • BlocBuilder:根据状态渲染不同 UI(加载中、错误、事件列表)。
  • 分页加载 :通过 ScrollController 监听滚动,当接近底部时调用 loadMoreGithubEvents
  • 刷新功能 :使用 RefreshIndicator 支持下拉刷新。
  • 滑动删除Dismissible 允许用户滑动删除事件。
  • 上下文菜单CupertinoContextMenu 提供复制和分享选项。
  • 回到顶部:当滚动超过一屏时显示浮动按钮,点击后平滑滚动到顶部。

最佳实践

  1. 使用 Equatable:确保状态比较高效,避免不必要的 UI 重建。
  2. 错误处理:保留当前数据并显示错误信息,提升用户体验。
  3. 分页管理 :通过 _isLoadMore 防止重复请求,确保性能。
  4. 状态隔离:将业务逻辑集中在 Cubit 中,使 UI 只负责展示。

总结

通过这个示例,我们展示了如何在 Flutter 中使用 Cubit 模式管理 GitHub 事件列表的状态。从数据获取到状态定义,再到 UI 集成,Cubit 提供了一种简单而强大的方式来组织代码。对于需要更复杂事件处理的场景,可以考虑完整的 BLoC 模式。但对于大多数简单应用,Cubit 已足够胜任。

希望这篇博客能帮助您更好地理解和应用 Cubit 模式,在 Flutter 开发中愉快coding!

相关推荐
北极象1 小时前
Flutter中实现拍照识题的功能
flutter·latex·数学公式
GeniuswongAir4 小时前
Flutter快速搭建聊天
flutter·im·聊天
马拉萨的春天6 小时前
mac 下配置flutter 总是失败,请参考文章重新配置flutter 环境MacOS Flutter环境配置和安装
flutter·macos
未来猫咪花7 小时前
Flutter 状态管理极速版:view_model
android·flutter
恋猫de小郭8 小时前
Android 转内部开发谁说是闭源?明明 AOSP 外部 PR 支持也会继续
android·前端·flutter
江上清风山间明月10 小时前
一周掌握Flutter开发--9. 与原生交互(上)
flutter·交互·与原生交互·methodchannel
Hello_Kid12 小时前
2025 Flutter Engine Source Setup
flutter
淡写成灰13 小时前
使用 Bloc 实现 Flutter 暗黑主题切换与持久化
flutter
wzj_what_why_how1 天前
Flutter TabBar 右侧渐变遮罩实现中的事件处理问题
flutter
Mmmm1 天前
Flutter SliverAppBar实现下拉显示bar 上拉隐藏
flutter