掌握 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!

相关推荐
勤劳打代码11 小时前
妙笔生花 —— Flutter 实现飞入动画
前端·flutter·设计模式
我想吃辣条14 小时前
flutter mapbox_maps_flutter 应用不支持 16 KB
flutter
Aftery的博客1 天前
flutter项目打包macOS桌面程序dmg
flutter·macos
庞哈哈121381 天前
Flutter 仿网易云音乐播放器:唱片旋转 + 歌词滚动实现记录
flutter
心随雨下1 天前
Flutter中新手需要掌握的几种Widget
android·flutter·ios
weixin_438732101 天前
Flutter 开发环境安装
flutter
monika_yu1 天前
关于flutter插件的存储位置问题
flutter
程序员老刘·1 天前
2025年Flutter状态管理新趋势:AI友好度成为技术选型第一标准
flutter·ai编程·跨平台开发·客户端开发