Flutter无限滚动列表插件bloc_infinity_list的使用
Flutter无限滚动列表插件bloc_infinity_list的使用
bloc_infinity_list
是一个用于 Flutter 应用程序的可定制无限滚动列表小部件。它通过与 BLoC 模式集成来简化分页列表的创建,当用户滚动时会无缝加载更多项目。
概述
bloc_infinity_list
是一个可定制的无限滚动列表小部件,专为 Flutter 构建,集成了 BLoC 模式。它简化了在 Flutter 应用程序中创建分页列表的过程,使用户在滚动时可以无缝加载更多项目。
文档
要获取详细的文档、使用示例等信息,请访问我们的Wiki。
开始使用
要开始使用 bloc_infinity_list
,请参阅我们的入门指南。
示例
查看各种示例,了解如何在您的 Flutter 项目中使用 bloc_infinity_list
。
贡献
我们欢迎贡献!有关更多信息,请参阅我们的贡献指南。
许可证
该项目受 MIT 许可证保护。详情请参阅LICENSE文件。
以下是一个完整的示例,展示了如何使用 bloc_infinity_list
创建自动和手动的无限滚动列表。
import 'dart:async';
import 'package:bloc_infinity_list/bloc_infinity_list.dart';
import 'package:bloc_infinity_list/infinite_list_bloc/infinite_list_bloc.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
/// A simple data class representing an item in the list.
class ListItem {
static int _staticId = 0;
final int id;
final String name;
final String description;
ListItem({required this.name, required this.description}) : id = ++_staticId;
/// Resets the static ID counter. Useful for testing.
static void resetIdCounter() {
_staticId = 0;
}
}
/// A custom BLoC that extends [InfiniteListBloc] to fetch [ListItem]s.
class MyCustomBloc extends InfiniteListBloc<ListItem> {
/// Constructor accepts an optional list of initial items.
MyCustomBloc({super.initialItems, super.limitFetch});
[@override](/user/override)
Future<List<ListItem>> fetchItems({
required int limit,
required int offset,
}) async {
// Simulate network delay
await Future.delayed(const Duration(seconds: 1));
// Simulate end of data
if (offset >= 50) {
return [];
}
// Generate dummy data
return List.generate(
limit,
(index) => ListItem(
name: 'Item ${offset + index + 1}',
description: 'Description for item ${offset + index + 1}',
),
);
}
}
void main() {
runApp(const MyApp());
}
/// The root widget of the application.
class MyApp extends StatelessWidget {
const MyApp({super.key});
[@override](/user/override)
Widget build(BuildContext context) {
return MaterialApp(
title: 'Infinite ListView Example',
theme: ThemeData(
primarySwatch: Colors.purple,
scaffoldBackgroundColor: const Color(0xFFF5F5F5),
textTheme: const TextTheme(
bodyMedium: TextStyle(fontSize: 16.0),
titleLarge: TextStyle(fontSize: 18.0, fontWeight: FontWeight.bold),
),
),
home: const HomePage(),
);
}
}
/// The home page that contains navigation to the four examples.
class HomePage extends StatefulWidget {
const HomePage({super.key});
[@override](/user/override)
State<HomePage> createState() => _HomePageState();
}
/// State class for [HomePage].
class _HomePageState extends State<HomePage> {
// Set Automatic (shrinkWrap = true) as default index = 0
int _selectedIndex = 0;
// We now have 4 pages:
final List<Widget> _pages = [
// Automatic with shrinkWrap = true
const AutomaticInfiniteListPage(),
// Automatic with shrinkWrap = false (NEW PAGE)
const AutomaticInfiniteListPageNoShrinkWrap(),
// Manual infinite list
const ManualInfiniteListPage(),
// Manual infinite list with initial items
const ManualInfiniteListPageWithInitialItems(),
];
[@override](/user/override)
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Infinite ListView Example'),
),
body: _pages[_selectedIndex],
bottomNavigationBar: BottomNavigationBar(
currentIndex: _selectedIndex,
selectedItemColor: Colors.purpleAccent,
unselectedItemColor: Colors.deepPurple,
items: const [
BottomNavigationBarItem(
icon: Icon(Icons.autorenew),
label: 'Auto ShrinkWrap',
),
BottomNavigationBarItem(
icon: Icon(Icons.vertical_align_bottom),
label: 'Auto Full',
),
BottomNavigationBarItem(
icon: Icon(Icons.touch_app),
label: 'Manual',
),
BottomNavigationBarItem(
icon: Icon(Icons.list_alt),
label: 'Default Manual',
),
],
onTap: (index) {
setState(() {
_selectedIndex = index;
});
},
),
);
}
}
/// A page demonstrating the automatic infinite list with shrinkWrap enabled.
class AutomaticInfiniteListPage extends StatefulWidget {
const AutomaticInfiniteListPage({super.key});
[@override](/user/override)
State<AutomaticInfiniteListPage> createState() => _AutomaticInfiniteListPageState();
}
class _AutomaticInfiniteListPageState extends State<AutomaticInfiniteListPage> {
late final MyCustomBloc _bloc;
[@override](/user/override)
void initState() {
super.initState();
_bloc = MyCustomBloc();
}
[@override](/user/override)
void dispose() {
_bloc.close();
super.dispose();
}
[@override](/user/override)
Widget build(BuildContext context) {
return BlocProvider<MyCustomBloc>(
create: (_) => _bloc,
child: InfiniteListView<ListItem>.automatic(
bloc: _bloc,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
backgroundColor: Colors.white,
padding: const EdgeInsets.all(16.0),
borderRadius: BorderRadius.circular(12.0),
borderColor: Colors.grey.shade300,
borderWidth: 1.0,
boxShadow: [
BoxShadow(
color: Colors.grey.shade200,
blurRadius: 6.0,
spreadRadius: 2.0,
offset: const Offset(0, 3),
),
],
itemBuilder: _buildListItem,
dividerWidget: const SizedBox(height: 0),
loadingWidget: _buildLoadingWidget,
errorWidget: _buildErrorWidget,
emptyWidget: _buildEmptyWidget,
noMoreItemWidget: _buildNoMoreItemWidget,
),
);
}
Widget _buildListItem(BuildContext context, ListItem item) {
return Card(
elevation: 2.0,
margin: const EdgeInsets.symmetric(vertical: 4.0),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10.0),
),
child: ListTile(
leading: CircleAvatar(
backgroundColor: Theme.of(context).primaryColor,
child: Text(
item.id.toString(),
style: const TextStyle(color: Colors.white),
),
),
title: Text(
item.name,
style: Theme.of(context).textTheme.titleLarge,
),
subtitle: Text(
item.description,
style: Theme.of(context).textTheme.bodyMedium,
),
trailing: const Icon(Icons.arrow_forward_ios),
onTap: () {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Tapped on ${item.name}')),
);
},
),
);
}
Widget _buildLoadingWidget(BuildContext context) => Padding(
padding: const EdgeInsets.all(12.0),
child: Center(
child: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(Theme.of(context).primaryColor),
),
),
);
Widget _buildErrorWidget(BuildContext context, String error) => Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.error_outline, color: Colors.red.shade300, size: 48),
const SizedBox(height: 8),
Text(
'Something went wrong!',
style: TextStyle(
color: Colors.red.shade300,
fontSize: 18,
),
),
const SizedBox(height: 8),
ElevatedButton.icon(
onPressed: () {
_bloc.add(LoadItemsEvent());
},
icon: const Icon(Icons.refresh),
label: const Text('Retry'),
style: ElevatedButton.styleFrom(
backgroundColor: Theme.of(context).primaryColor,
),
),
],
),
);
Widget _buildEmptyWidget(BuildContext context) => Center(
child: Text(
'No items available',
style: TextStyle(
color: Colors.grey.shade600,
fontSize: 18,
),
),
);
Widget _buildNoMoreItemWidget(BuildContext context) => Center(
child: Text(
'You have reached the end!',
style: TextStyle(
color: Colors.grey.shade600,
fontSize: 16,
),
),
);
}
class AutomaticInfiniteListPageNoShrinkWrap extends StatefulWidget {
const AutomaticInfiniteListPageNoShrinkWrap({super.key});
[@override](/user/override)
State<AutomaticInfiniteListPageNoShrinkWrap> createState() => _AutomaticInfiniteListPageNoShrinkWrapState();
}
class _AutomaticInfiniteListPageNoShrinkWrapState extends State<AutomaticInfiniteListPageNoShrinkWrap> {
late final MyCustomBloc _bloc;
[@override](/user/override)
void initState() {
super.initState();
_bloc = MyCustomBloc();
}
[@override](/user/override)
void dispose() {
_bloc.close();
super.dispose();
}
[@override](/user/override)
Widget build(BuildContext context) {
return BlocProvider<MyCustomBloc>(
create: (_) => _bloc,
child: InfiniteListView<ListItem>.automatic(
bloc: _bloc,
shrinkWrap: false,
physics: const BouncingScrollPhysics(),
backgroundColor: Colors.white,
padding: const EdgeInsets.all(16.0),
borderRadius: BorderRadius.circular(12.0),
borderColor: Colors.grey.shade300,
borderWidth: 1.0,
boxShadow: [
BoxShadow(
color: Colors.grey.shade200,
blurRadius: 6.0,
spreadRadius: 2.0,
offset: const Offset(0, 3),
),
],
itemBuilder: _buildListItem,
dividerWidget: const Divider(thickness: 2),
showLastDivider: () => true,
loadingWidget: _buildLoadingWidget,
errorWidget: _buildErrorWidget,
emptyWidget: _buildEmptyWidget,
noMoreItemWidget: _buildNoMoreItemWidget,
),
);
}
Widget _buildListItem(BuildContext context, ListItem item) {
return Card(
elevation: 2.0,
margin: const EdgeInsets.symmetric(vertical: 4.0),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10.0),
),
child: ListTile(
leading: CircleAvatar(
backgroundColor: Theme.of(context).primaryColor,
child: Text(
item.id.toString(),
style: const TextStyle(color: Colors.white),
),
),
title: Text(
item.name,
style: Theme.of(context).textTheme.titleLarge,
),
subtitle: Text(
item.description,
style: Theme.of(context).textTheme.bodyMedium,
),
trailing: const Icon(Icons.arrow_forward_ios),
onTap: () {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Tapped on ${item.name}')),
);
},
),
);
}
Widget _buildLoadingWidget(BuildContext context) => Padding(
padding: const EdgeInsets.all(12.0),
child: Center(
child: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(Theme.of(context).primaryColor),
),
),
);
Widget _buildErrorWidget(BuildContext context, String error) => Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.error_outline, color: Colors.red.shade300, size: 48),
const SizedBox(height: 8),
Text(
'Something went wrong!',
style: TextStyle(
color: Colors.red.shade300,
fontSize: 18,
),
),
const SizedBox(height: 8),
ElevatedButton.icon(
onPressed: () {
_bloc.add(LoadItemsEvent());
},
icon: const Icon(Icons.refresh),
label: const Text('Retry'),
style: ElevatedButton.styleFrom(
backgroundColor: Theme.of(context).primaryColor,
),
),
],
),
);
Widget _buildEmptyWidget(BuildContext context) => Center(
child: Text(
'No items available',
style: TextStyle(
color: Colors.grey.shade600,
fontSize: 18,
),
),
);
Widget _buildNoMoreItemWidget(BuildContext context) => Center(
child: Text(
'You have reached the end!',
style: TextStyle(
color: Colors.grey.shade600,
fontSize: 16,
),
),
);
}
/// A page demonstrating the manual infinite list with a "Load More" button.
class ManualInfiniteListPage extends StatefulWidget {
const ManualInfiniteListPage({super.key});
[@override](/user/override)
State<ManualInfiniteListPage> createState() => _ManualInfiniteListPageState();
}
class _ManualInfiniteListPageState extends State<ManualInfiniteListPage> {
late final MyCustomBloc _bloc;
[@override](/user/override)
void initState() {
super.initState();
// Initialize the bloc without initial items
_bloc = MyCustomBloc();
}
[@override](/user/override)
void dispose() {
_bloc.close();
super.dispose();
}
[@override](/user/override)
Widget build(BuildContext context) {
return BlocProvider<MyCustomBloc>(
create: (_) => _bloc,
child: InfiniteListView<ListItem>.manual(
bloc: _bloc,
backgroundColor: Colors.white,
padding: const EdgeInsets.all(16.0),
borderRadius: BorderRadius.circular(12.0),
borderColor: Colors.grey.shade300,
borderWidth: 1.0,
boxShadow: [
BoxShadow(
color: Colors.grey.shade200,
blurRadius: 6.0,
spreadRadius: 2.0,
offset: const Offset(0, 3),
),
],
physics: const BouncingScrollPhysics(),
itemBuilder: _buildListItem,
loadMoreButtonBuilder: _buildLoadMoreButton,
dividerWidget: const Divider(
height: 2,
thickness: 1,
indent: 20,
),
showLastDivider: () => _bloc.state is! NoMoreItemsState,
loadingWidget: _buildLoadingWidget,
errorWidget: _buildErrorWidget,
emptyWidget: _buildEmptyWidget,
noMoreItemWidget: _buildNoMoreItemWidget,
),
);
}
Widget _buildListItem(BuildContext context, ListItem item) {
return Card(
elevation: 2.0,
margin: const EdgeInsets.symmetric(vertical: 8.0),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10.0),
),
child: ListTile(
leading: CircleAvatar(
backgroundColor: Theme.of(context).primaryColor,
child: Text(
item.id.toString(),
style: const TextStyle(color: Colors.white),
),
),
title: Text(
item.name,
style: Theme.of(context).textTheme.titleLarge,
),
subtitle: Text(
item.description,
style: Theme.of(context).textTheme.bodyMedium,
),
trailing: const Icon(Icons.arrow_forward_ios),
onTap: () {
// Handle item tap
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Tapped on ${item.name}')),
);
},
),
);
}
Widget _buildLoadMoreButton(BuildContext context) {
final state = _bloc.state;
final isLoading = state is LoadingState<ListItem>;
return Padding(
padding: const EdgeInsets.all(16.0),
child: ElevatedButton(
key: const Key('loadMoreButton'), // Assigning a unique key here
onPressed: isLoading
? null
: () {
_bloc.add(LoadMoreItemsEvent());
},
style: ElevatedButton.styleFrom(
minimumSize: const Size(double.infinity, 50),
backgroundColor: Theme.of(context).primaryColor,
),
child: isLoading
? const SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
strokeWidth: 2.0,
),
)
: const Text(
'Load More',
style: TextStyle(fontSize: 18, color: Colors.white),
),
),
);
}
Widget _buildLoadingWidget(BuildContext context) => Padding(
padding: const EdgeInsets.all(16.0),
child: Center(
child: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(Theme.of(context).primaryColor),
),
),
);
Widget _buildErrorWidget(BuildContext context, String error) => Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.error_outline, color: Colors.red.shade300, size: 48),
const SizedBox(height: 8),
Text(
'Something went wrong!',
style: TextStyle(
color: Colors.red.shade300,
fontSize: 18,
),
),
const SizedBox(height: 8),
ElevatedButton.icon(
onPressed: () {
_bloc.add(LoadItemsEvent());
},
icon: const Icon(Icons.refresh),
label: const Text('Retry'),
style: ElevatedButton.styleFrom(
backgroundColor: Theme.of(context).primaryColor,
),
),
],
),
);
Widget _buildEmptyWidget(BuildContext context) => Center(
child: Text(
'No items available',
style: TextStyle(
color: Colors.grey.shade600,
fontSize: 18,
),
),
);
Widget _buildNoMoreItemWidget(BuildContext context) => Center(
child: Text(
'You have reached the end!',
style: TextStyle(
color: Colors.grey.shade600,
fontSize: 16,
),
),
);
}
/// A second manual infinite list page demonstrating another manual list with initial items.
/// **This page is updated to dynamically grow in height and can be embedded within another scrollable widget.**
class ManualInfiniteListPageWithInitialItems extends StatefulWidget {
const ManualInfiniteListPageWithInitialItems({super.key});
[@override](/user/override)
State<ManualInfiniteListPageWithInitialItems> createState() => _ManualInfiniteListPageWithInitialItemsState();
}
class _ManualInfiniteListPageWithInitialItemsState extends State<ManualInfiniteListPageWithInitialItems> {
late final MyCustomBloc _bloc;
[@override](/user/override)
void initState() {
super.initState();
// Define 5 initial items
final initialItems = List.generate(
5,
(index) => ListItem(
name: 'Secondary Preloaded Item ${index + 1}',
description: 'Description for secondary preloaded item ${index + 1}',
),
);
// Initialize the bloc with initial items
_bloc = MyCustomBloc(initialItems: initialItems, limitFetch: 5);
}
[@override](/user/override)
void dispose() {
_bloc.close();
super.dispose();
}
[@override](/user/override)
Widget build(BuildContext context) {
// **Embedding within SingleChildScrollView and Column for dynamic height**
return SingleChildScrollView(
child: Column(
children: [
// Other widgets above the InfiniteListView
Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
'Manual Infinite List with Initial Items',
style: Theme.of(context).textTheme.titleLarge,
),
),
// The updated InfiniteListView.manual with shrinkWrap enabled
BlocProvider<MyCustomBloc>(
create: (_) => _bloc,
child: InfiniteListView<ListItem>.manual(
bloc: _bloc,
shrinkWrap: true,
// Enable shrink wrapping
backgroundColor: Colors.white,
padding: const EdgeInsets.all(16.0),
borderRadius: BorderRadius.circular(12.0),
borderColor: Colors.grey.shade300,
borderWidth: 1.0,
boxShadow: [
BoxShadow(
color: Colors.grey.shade200,
blurRadius: 6.0,
spreadRadius: 2.0,
offset: const Offset(0, 3),
),
],
itemBuilder: _buildListItem,
loadMoreButtonBuilder: _buildLoadMoreButton,
dividerWidget: const SizedBox(height: 0),
loadingWidget: _buildLoadingWidget,
errorWidget: _buildErrorWidget,
emptyWidget: _buildEmptyWidget,
noMoreItemWidget: _buildNoMoreItemWidget,
),
),
// Other widgets below the InfiniteListView
Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
'Footer Widget',
style: Theme.of(context).textTheme.titleLarge,
),
),
],
),
);
}
Widget _buildListItem(BuildContext context, ListItem item) {
return Card(
elevation: 2.0,
margin: const EdgeInsets.symmetric(vertical: 8.0),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10.0),
),
child: ListTile(
leading: CircleAvatar(
backgroundColor: Colors.deepPurple,
child: Text(
item.id.toString(),
style: const TextStyle(color: Colors.white),
),
),
title: Text(
item.name,
style: Theme.of(context).textTheme.titleLarge,
),
subtitle: Text(
item.description,
style: Theme.of(context).textTheme.bodyMedium,
),
trailing: const Icon(Icons.arrow_forward_ios),
onTap: () {
// Handle item tap
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Tapped on ${item.name}')),
);
},
),
);
}
Widget _buildLoadMoreButton(BuildContext context) {
final state = _bloc.state;
final isLoading = state is LoadingState<ListItem>;
return Padding(
padding: const EdgeInsets.all(16.0),
child: ElevatedButton(
key: const Key('loadMoreButton'), // Assigning a unique key here
onPressed: isLoading
? null
: () {
_bloc.add(LoadMoreItemsEvent());
},
style: ElevatedButton.styleFrom(
minimumSize: const Size(double.infinity, 50),
backgroundColor: Colors.deepPurple,
),
child: isLoading
? const SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
strokeWidth: 2.0,
),
)
: const Text(
'Load More',
style: TextStyle(fontSize: 18, color: Colors.white),
),
),
);
}
Widget _buildLoadingWidget(BuildContext context) => const Padding(
padding: EdgeInsets.all(16.0),
child: Center(
child: CircularProgressIndicator(),
),
);
Widget _buildErrorWidget(BuildContext context, String error) => Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.error_outline, color: Colors.red.shade300, size: 48),
const SizedBox(height: 8),
Text(
'Something went wrong!',
style: TextStyle(
color: Colors.red.shade300,
fontSize: 18,
),
),
const SizedBox(height: 8),
ElevatedButton.icon(
onPressed: () {
_bloc.add(LoadItemsEvent());
},
icon: const Icon(Icons.refresh),
label: const Text('Retry'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.deepPurple,
),
),
],
),
);
Widget _buildEmptyWidget(BuildContext context) => Center(
child: Text(
'No items available',
style: TextStyle(
color: Colors.grey.shade600,
fontSize: 18,
),
),
);
Widget _buildNoMoreItemWidget(BuildContext context) => Center(
child: Text(
'No more items',
style: TextStyle(
color: Colors.grey.shade600,
fontSize: 16,
),
),
);
}
更多关于Flutter无限滚动列表插件bloc_infinity_list的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html
更多关于Flutter无限滚动列表插件bloc_infinity_list的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html
当然,下面是一个关于如何使用 bloc_infinity_list
插件来实现 Flutter 中无限滚动列表的示例代码。bloc_infinity_list
是一个非常有用的 Flutter 插件,它结合了 flutter_bloc
状态管理和无限滚动功能。
首先,确保你的 pubspec.yaml
文件中已经添加了必要的依赖:
dependencies:
flutter:
sdk: flutter
flutter_bloc: ^8.0.0 # 确保版本与你的项目兼容
bloc_infinity_list: ^0.3.0 # 确保版本与你的项目兼容
然后,运行 flutter pub get
来安装这些依赖。
接下来,我们来看一个简单的例子,展示如何使用 bloc_infinity_list
。
创建数据模型和事件
首先,我们需要定义数据模型和事件。在这个例子中,我们将模拟一个简单的数据获取。
// data_model.dart
class Item {
final String id;
final String content;
Item({required this.id, required this.content});
}
// events.dart
import 'package:equatable/equatable.dart';
abstract class InfinityListEvent extends Equatable {
const InfinityListEvent();
@override
List<Object?> get props => [];
}
class FetchItemsEvent extends InfinityListEvent {
final int page;
const FetchItemsEvent({required this.page});
@override
List<Object?> get props => [page];
}
创建Bloc状态
接下来,我们定义Bloc的状态。
// states.dart
import 'package:equatable/equatable.dart';
import 'data_model.dart';
abstract class InfinityListState extends Equatable {
const InfinityListState();
@override
List<Object?> get props => [];
}
class InitialInfinityListState extends InfinityListState {}
class ItemsLoadedState extends InfinityListState {
final List<Item> items;
final bool hasReachedMax;
const ItemsLoadedState({required this.items, required this.hasReachedMax});
@override
List<Object?> get props => [items, hasReachedMax];
}
创建Bloc
然后,我们创建Bloc来处理事件并生成状态。
// infinity_list_bloc.dart
import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'data_model.dart';
import 'events.dart';
import 'states.dart';
class InfinityListBloc extends Bloc<InfinityListEvent, InfinityListState> {
InfinityListBloc() : super(InitialInfinityListState());
@override
Stream<InfinityListState> mapEventToState(
InfinityListEvent event,
) async* {
if (event is FetchItemsEvent) {
yield* _mapFetchItemsEventToState(event);
}
}
Stream<InfinityListState> _mapFetchItemsEventToState(FetchItemsEvent event) async* {
// Simulate a network call or data fetching
await Future.delayed(const Duration(seconds: 1));
List<Item> items = List.generate(
10,
(index) => Item(
id: '${event.page}_$index',
content: 'Item ${event.page}_$index',
),
);
bool hasReachedMax = event.page >= 3; // Simulate a max page
if (state is InitialInfinityListState) {
yield ItemsLoadedState(items: items, hasReachedMax: hasReachedMax);
} else if (state is ItemsLoadedState) {
yield ItemsLoadedState(
items: [...state.items, ...items],
hasReachedMax: hasReachedMax,
);
}
}
}
创建UI
最后,我们创建UI组件来使用 bloc_infinity_list
。
// main.dart
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:bloc_infinity_list/bloc_infinity_list.dart';
import 'infinity_list_bloc.dart';
import 'data_model.dart';
void main() {
runApp(
BlocProvider<InfinityListBloc>(
create: (_) => InfinityListBloc(),
child: MyApp(),
),
);
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(),
);
}
}
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Infinity List Demo'),
),
body: BlocInfinityList<InfinityListEvent, InfinityListState, Item>(
bloc: context.read<InfinityListBloc>(),
fetchingState: (state) => state is ItemsLoadedState,
hasReachedMax: (state) => state is ItemsLoadedState && state.hasReachedMax,
itemBuilder: (context, item) {
return ListTile(
title: Text(item.content),
);
},
onLoadMore: (context, state) {
context.read<InfinityListBloc>().add(FetchItemsEvent(page: state.items.length ~/ 10 + 1));
},
initialState: const InitialInfinityListState(),
listBuilder: (context, state) {
if (state is ItemsLoadedState) {
return state.items;
} else {
return [];
}
},
),
);
}
}
在这个例子中,我们创建了一个简单的 InfinityListBloc
,它处理 FetchItemsEvent
事件并生成 InfinityListState
。然后,我们在 MyHomePage
中使用 BlocInfinityList
组件来显示数据,并在用户滚动到底部时加载更多数据。
希望这个示例代码能帮助你理解如何在 Flutter 中使用 bloc_infinity_list
插件来实现无限滚动列表。