Flutter无限滚动列表插件bloc_infinity_list的使用

发布于 1周前 作者 h691938207 来自 Flutter

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

1 回复

更多关于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 插件来实现无限滚动列表。

回到顶部