在 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 事件的数据。假设每个事件包含 id
、username
、avatarUrl
和 repoUrl
等字段。以下是 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 的不同情况。我们定义了以下四种状态:
- GithubEventInitial:初始状态。
- GithubEventLoading:数据加载中。
- GithubEventLoaded:数据加载成功。
- 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 中:GithubEventPage
和 GithubEventView
。
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
提供复制和分享选项。 - 回到顶部:当滚动超过一屏时显示浮动按钮,点击后平滑滚动到顶部。
最佳实践
- 使用 Equatable:确保状态比较高效,避免不必要的 UI 重建。
- 错误处理:保留当前数据并显示错误信息,提升用户体验。
- 分页管理 :通过
_isLoadMore
防止重复请求,确保性能。 - 状态隔离:将业务逻辑集中在 Cubit 中,使 UI 只负责展示。
总结
通过这个示例,我们展示了如何在 Flutter 中使用 Cubit 模式管理 GitHub 事件列表的状态。从数据获取到状态定义,再到 UI 集成,Cubit 提供了一种简单而强大的方式来组织代码。对于需要更复杂事件处理的场景,可以考虑完整的 BLoC 模式。但对于大多数简单应用,Cubit 已足够胜任。
希望这篇博客能帮助您更好地理解和应用 Cubit 模式,在 Flutter 开发中愉快coding!