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

相关推荐
古希腊被code拿捏的神13 小时前
【Flutter】面试记录
flutter·面试·职场和发展
nc_kai13 小时前
Flutter 之 table_calendar 控件
flutter
0wioiw013 小时前
Flutter基础(前端教程⑨-图片)
前端·flutter
Engandend13 小时前
Flutter与iOS混合开发交互
flutter·ios·程序员
浅忆无痕14 小时前
Flutter抓包
前端·flutter
火柴就是我14 小时前
每日见闻之尝试大白话说清Flutter的事件传递
flutter
Lucifer晓16 小时前
记录一次Flutter项目上传App Store Connect出现“Validation failed”错误的问题
flutter·ios
江上清风山间明月18 小时前
一周掌握Flutter开发--10. 结构与设计模式
flutter·设计模式·快速
_小猪睡枕头_21 小时前
鸿蒙与Flutter的混合开发
flutter·harmonyos
0wioiw01 天前
Flutter基础(前端教程⑦-Http和卡片)
前端·flutter·http