Flutter无限滚动分页插件riverpod_infinite_scroll_pagination的使用

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

Flutter无限滚动分页插件riverpod_infinite_scroll_pagination的使用

示例代码

import 'package:flutter/material.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:dio/dio.dart';
import 'package:example/src/env/env.dart';
import 'package:example/src/features/movies/data/tmdb_repository.dart';

part 'tmdb_movie.freezed.dart';
part 'tmdb_movie.g.dart';

[@freezed](/user/freezed)
class TmdbMovie with _$TmdbMovie {
  factory TmdbMovie({
    bool? adult,
    @JsonKey(name: 'backdrop_path') String? backdropPath,
    int? id,
    @JsonKey(name: 'original_language') String? originalLanguage,
    @JsonKey(name: 'original_title') String? originalTitle,
    String? overview,
    double? popularity,
    @JsonKey(name: 'poster_path') String? posterPath,
    @JsonKey(name: 'release_date') String? releaseDate,
    String? title,
    bool? video,
    @JsonKey(name: 'vote_average') double? voteAverage,
    @JsonKey(name: 'vote_count') int? voteCount,
  }) = _TmdbMovie;

  factory TmdbMovie.fromJson(Map<String, dynamic> json) => _$TmdbMovieFromJson(json);
}

class TmdbRepository {
  const TmdbRepository({
    required this.dio,
  });

  final Dio dio;

  Future<PaginatedResponse<TmdbMovie>> getTrendingMovies({
    int page = 1,
    String? query,
  }) async {
    final results = await dio.get<Map<String, dynamic>>({
      'trendin/movie/day?language=en-US&'page=$page${query != null ? '&amp;$query' : ''}',
    });
    return PaginatedResponse.fromJson(
      results.data!,
      dataMapper: TmdbMovie.fromJson,
      dataField: 'results',
      paginationParser: (data) =&gt; Pagination(
        totalNumber: data['total_results'] as int,
        currentPage: data['page'] as int,
        lastPage: data['total_pages'] as int,
      ),
    );
  }
}

[@riverpod](/user/riverpod)
class TrendingMoviesList extends _$TrendingMoviesList with PaginatedDataMixin&lt;TmdbMovie&gt; implements PaginatedNotifier&lt;TmdbMovie&gt; {
  @override
  FutureOr&lt;List&lt;TmdbMovie&gt;&gt build() async {
    return init(
      dataFetcher: PaginatedDataRepository(
        fetcher: ref.watch(tmdbRepositoryProvider).getTrendingMovies,
      ),
    );
  }
}

class MovieList extends ConsumerWidget {
  const MovieList({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final movies = ref.watch(trendingMoviesListProvider);

    return Scaffold(
      appBar: AppBar(title: const Text('Trending Movies')),
      body: PaginatedListView(
        state: movies,
        itemBuilder: (_, data) =&gt; MovieItem(movie: data),
        notifier: ref.read(trendingMoviesListProvider.notifier),
      ),
    );
  }
}

使用说明

  1. 初始化数据源

    • 首先,我们需要初始化一个 AsyncNotifier 来管理状态。在这个例子中,我们使用了 TrendingMoviesList 提供器来获取 trending movies 的列表。
    import 'package:flutter_riverpod/flutter_riverpod.dart';
    import 'package:riverpod_annotation/riverpod_annotation.dart';
    import 'package:example/src/features/movies/providers/trending_movies_list_provider.dart';
    
    void main() {
      runApp(RiverpodApp());
    }
    
    class RiverpodApp extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          home: MyHomePage(),
        );
      }
    }
    
    class MyHomePage extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(title: const Text('Trending Movies')),
          body: Provider(
            child: MovieList(),
          ),
        );
      }
    }
    
  2. 创建模型类和仓库

    • 我们使用 freezed 包来创建电影模型类,并使用 dio 库来处理网络请求。
    import 'package:freezed_annotation/freezed_annotation.dart';
    import 'package:dio/dio.dart';
    import 'package:example/src/features/movies/data/tmdb_repository.dart';
    
    part 'tmdb_movie.freezed.dart';
    part 'tmdb_movie.g.dart';
    
    [@freezed](/user/freezed)
    class TmdbMovie with _$TmdbMovie {
      factory TmdbMovie({
        bool? adult,
        @JsonKey(name: 'backdrop_path') String? backdropPath,
        int? id,
        @JsonKey(name: 'original_language') String? originalLanguage,
        @JsonKey(name: 'original_title') String? originalTitle,
        String? overview,
        double? popularity,
        @JsonKey(name: 'poster_path') String? posterPath,
        @JsonKey(name: 'release_date') String? releaseDate,
        String? title,
        bool? video,
        @JsonKey(name: 'vote_average') double? voteAverage,
        @JsonKey(name: 'vote_count') int? voteCount,
      }) = _TmdbMovie;
    
      factory TmdbMovie.fromJson(Map<String, dynamic> json) =&gt;
        _$TmdbMovieFromJson(json);
    }
    
    class TmdbRepository {
      const TmdbRepository({
        required this.dio,
      });
    
      final Dio dio;
    
      Future&lt;PaginatedResponse&lt;TmdbMovie&gt;&gt; getTrendingMovies({
        int page = 1,
        String? query,
      }) async {
        final results = await dio.get&lt;Map&lt;String, dynamic&gt;&gt;({
          'trendin/movie/day?language=en-US&amp;page=$page${query != null ? '&amp;$query' : ''}',
        });
        return PaginatedResponse.fromJson(
          results.data!,
          dataMapper: TmdbMovie.fromJson,
          dataField: 'results',
          paginationParser: (data) =&gt; Pagination(
            totalNumber: data['total_results'] as int,
            currentPage: data['page'] as int,
            lastPage: data['total_pages'] as int,
          ),
        );
      }
    }
    
  3. 设置提供器

    • 创建 TrendingMoviesList 提供器来初始化 TmdbRepository 并进行数据加载。
    import 'package:flutter_riverpod/flutter_riverpod.dart';
    import 'package:riverpod_annotation/riverpod_annotation.dart';
    import 'package:example/src/features/movies/data/tmdb_repository.dart';
    
    part 'tmdb_repository_provider.g.dart';
    
    [@riverpod](/user/riverpod)
    TmdbRepository tmdbRepository(TmdbRepositoryRef ref) {
      return TmdbRepository(
        dio: Dio(
          BaseOptions(
            baseUrl: 'https://api.themoviedb.org/3/',
            headers: &lt;String, dynamic&gt;{
              'Accept': 'application/json',
              'Authorization': 'Bearer ${Env.tmdbApiKey}',
            },
          ),
        ),
      );
    }
    
  4. 构建 UI

    • MovieList 消费者 widget 中使用 PaginatedListView 来展示数据。
    import 'package:flutter/material.dart';
    import 'package:flutter_riverpod/flutter_riverpod.dart';
    
    class MovieList extends ConsumerWidget {
      const MovieList({super.key});
    
      @override
      Widget build(BuildContext context, WidgetRef ref) {
        final movies = ref.watch(trendingMoviesListProvider);
    
        return Scaffold(
          appBar: AppBar(title: const Text('Trending Movies')),
          body: PaginatedListView(
            state: movies,
            itemBuilder: (_, data) =&gt; MovieItem(movie: data),
            notifier: ref.read(trendingMoviesListProvider.notifier),
          ),
        );
      }
    }
    
  5. 添加搜索功能

    • 如果需要添加搜索功能,可以修改 SearchMovies 提供器来处理查询参数。
    import 'package:flutter_riverpod/flutter_riverpod.dart';
    import 'package:riverpod_annotation/riverpod_annotation.dart';
    import 'package:example/src/features/movies/data/tmdb_repository.dart';
    
    part 'search_movies_provider.g.dart';
    
    [@riverpod](/user/riverpod)
    class SearchMovies extends _$SearchMovies with PaginatedDataMixin&lt;TmdbMovie&gt; implements PaginatedNotifier&lt;TmdbMovie&gt; {
      @override
      FutureOr&lt;List&lt;TmdbMovie&gt;&gt; build() async {
        final dataFetcher = PaginatedDataRepository(
          fetcher: ref.watch(tmdbRepositoryProvider).searchMovies,
          queryFilter: queryFilter,
        );
    
        return init(
          dataFetcher: PaginatedDataRepository(
            fetcher: ref.watch(tmdbRepositoryProvider).searchMovies,
            queryFilter: queryFilter,
          ),
        );
      }
    }
    
  6. 使用 Slivers 和 Skeleton 动画

    • 可以通过设置 useSliver 参数为 true 来使用 Slivers,并传递一个 ScrollController 实例。

更多关于Flutter无限滚动分页插件riverpod_infinite_scroll_pagination的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html

1 回复

更多关于Flutter无限滚动分页插件riverpod_infinite_scroll_pagination的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html


当然,以下是一个关于如何使用 riverpod_infinite_scroll_pagination 插件来实现 Flutter 应用中的无限滚动分页功能的代码示例。这个插件结合了 Riverpod 状态管理,可以非常方便地实现分页加载数据。

首先,确保在你的 pubspec.yaml 文件中添加以下依赖:

dependencies:
  flutter:
    sdk: flutter
  flutter_riverpod: ^1.0.0  # 确保版本与riverpod_infinite_scroll_pagination兼容
  riverpod_infinite_scroll_pagination: ^3.0.0  # 使用最新版本

然后运行 flutter pub get 来获取依赖。

接下来,设置你的 Flutter 应用。以下是一个完整的示例,展示如何使用 riverpod_infinite_scroll_pagination

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_infinite_scroll_pagination/riverpod_infinite_scroll_pagination.dart';

void main() {
  runApp(
    ProviderScope(
      child: MyApp(),
    ),
  );
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Infinite Scroll Pagination',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: InfiniteScrollPage(),
    );
  }
}

final itemsProvider = StateNotifierProvider<ItemsNotifier, List<String>>(
  (ref) => ItemsNotifier([]),
);

class ItemsNotifier extends StateNotifier<List<String>> {
  ItemsNotifier(List<String> items) : super(items);

  void addItems(List<String> newItems) {
    state = [...state, ...newItems];
  }
}

final paginationProvider = PaginationProvider<int, String>(
  key: 'items',
  pageKeyExtractor: (item, index) => item, // 用于唯一标识每一项,这里我们假设每一项是一个唯一的int
  pageFunction: (pageKey, previousItems, fetchFunction) async {
    // 模拟从网络获取数据
    await Future.delayed(const Duration(seconds: 2));
    final newItems = List.generate(
      20,
      (index) => 'Item ${(pageKey - 1) * 20 + index + 1}',
    );
    return PaginationResult.success(data: newItems);
  },
  itemBuilder: (context, item, index) => ListTile(
    title: Text(item),
  ),
);

class InfiniteScrollPage extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final itemsNotifier = ref.watch(itemsProvider.notifier);
    final paginationController = ref.watch(paginationProvider.controller);

    ref.onDispose(() {
      paginationController.dispose();
    });

    return Scaffold(
      appBar: AppBar(
        title: Text('Infinite Scroll Pagination'),
      ),
      body: NotificationListener<ScrollNotification>(
        onNotification: (scrollNotification) {
          if (scrollNotification is ScrollEndNotification &&
              scrollNotification.metrics.pixels ==
                  scrollNotification.metrics.maxScrollExtent) {
            // 滚动到底部时加载更多数据
            paginationController.nextPage();
          }
          return true;
        },
        child: Padding(
          padding: const EdgeInsets.all(8.0),
          child: PaginationView<int, String>(
            paginationProvider: paginationProvider,
            loadMore: () async {
              // 这里可以添加加载更多的逻辑,但paginationProvider已经处理了这部分
              // 只是为了演示,这里可以注释或保留为空
            },
            errorBuilder: (context, error, stackTrace, retry) {
              return Center(
                child: Column(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: [
                    Text('An error occurred: $error'),
                    SizedBox(height: 12),
                    ElevatedButton(
                      onPressed: retry,
                      child: Text('Retry'),
                    ),
                  ],
                ),
              );
            },
            emptyListBuilder: (context) {
              return Center(
                child: Text('No items found'),
              );
            },
            // 将分页结果添加到itemsNotifier中(可选,视需求而定)
            onPageLoaded: (pageKey, newItems) {
              itemsNotifier.addItems(newItems);
            },
          ),
        ),
      ),
    );
  }
}

解释

  1. 依赖注入:使用 ProviderScope 包裹整个应用,确保 Riverpod 正常工作。
  2. 状态管理:使用 StateNotifierProvider 管理项目的列表状态。
  3. 分页逻辑:使用 PaginationProvider 配置分页逻辑,包括如何获取下一页的数据和如何构建每一项。
  4. 滚动监听:使用 NotificationListener 监听滚动事件,当滚动到底部时调用 paginationController.nextPage() 加载更多数据。
  5. 分页视图:使用 PaginationView 展示分页数据,并处理错误和空列表的情况。

这个示例演示了如何使用 riverpod_infinite_scroll_pagination 插件结合 Riverpod 实现无限滚动分页。根据实际需求,你可能需要调整分页逻辑、数据获取方式和UI展示。

回到顶部