enchance
enchance

Reputation: 30501

Riverpod: Implementing an infinite loading provider the right way

As a Provider user I've always wondered about using Riverpod since they're from the same author. I've been playing around with it and like the use of with() but I'm having problems implementing an infinite loading scroll in my test environment.

I'm pretty sure I'm thinking about this the wrong way but there are some things I find confusing. I'm able to load my first data correctly but loading any succeeding pages from there won't work. These 3 things come to mind:

I realize this is a rundown topic that's already been discussed at length but this is just me kicking the wheels trying to get a firm grip on its features.

My RecipeData provider

@riverpod
class RecipeData extends _$RecipeData {
  int page = 1;
  final int _limit = 8;

  @override
  Future<List<Recipe>> build() async {
    final int offset = (page - 1) * _limit;
    final List<Recipe> data = await RecipeService.fetchFromApi(ref, _limit, offset);
    return data;
  }

  Future<List<Recipe>> fetchNextBatch() async {

    // TODO: Don't fetch if another fetch is in progress

    incrPage();
    final int offset = (page - 1) * _limit;
    final awaiteddata = await RecipeService.fetchFromApi(ref, _limit, offset);

    // Why is this wrong?
    state = [...state, ...awaiteddata];
  } 

  void incrPage() => page++;

  void resetPage() => page = 0;
}

Widget

@override
Widget build(BuildContext context) {
  final recipes = ref.watch(recipeDataProvider);

  return Scaffold(
    appBar: AppBar(
      title: const Text('Playground')
    ),
    body: Column(
      children: [
        ...
        // when() is great
        recipes.when(
          data: (data) => ListView.builder(...),
          loading: () => const CircularProgressIndicator(),
          error: (err, _) => Text(err.toString()),
        ),

      ],
    ),
  );
}

Related code

Basic Recipe model

@freezed
class Recipe with _$Recipe {
  const factory Recipe({
    required String name,
    required String cuisine,
    required String image,
  }) = _Recipe;

  factory Recipe.fromJson(Map<String, Object?> json) => _$RecipeFromJson(json);
}

I also created this in case I needed it which is just another provider that has alldata and page in it.

@freezed
class RecipeResponse with _$RecipeResponse {
  const factory RecipeResponse({
    @Default([]) final List<Recipe> alldata,
    @Default(1) final int page,
  }) = _RecipeResponse;
}

Upvotes: 0

Views: 369

Answers (1)

Leo Chen
Leo Chen

Reputation: 340

in your case

Future<void> fetchNextBatch() async {

  incrPage();
  final int offset = (page - 1) * _limit;
  final awaiteddata = await RecipeService.fetchFromApi(ref, _limit, offset);
  final previousState = await future;
  state = AsyncData([...previousState, ...awaiteddata]);
}

there is my sample

enum ProductFilterEnum {
  onSale,
  review,
  reject,
  notSale,
}

//filter state
@riverpod
class ProductFilter extends _$ProductFilter {
  @override
  ProductFilterEnum build() {
    return ProductFilterEnum.onSale;
  }
}

//search state
@riverpod
class ProductSearch extends _$ProductSearch {
  @override
  String build() {
    return "";
  }
}

//sort state
@riverpod
class ProductSort extends _$ProductSort {
  @override
  bool build() {
    return false;
  }
}

@riverpod
class Product extends _$Product {
  int _page = 0;

  @override
  Future<List<ProducModel>> build() async {
    final result = await fetch(_page);
    return result;
  }

  Future<void> getNextPage() async {
    _page++;
    final result = await fetch(_page);
    final previousState = await future;
    state = AsyncData([...previousState, ...result]);
  }

  //api
  Future<List<ProducModel>> fetch(int page) async {
    final filter = ref.watch(productFilterProvider);
    final search = ref.watch(productSearchProvider);
    final sort = ref.watch(productSortProvider);
    await Future.delayed(Duration(seconds: 1));
    return [ProducModel(name: "$page $search $filter")];
  }
}

in widget

//watch for change
Widget tabBarView() {
  final providerList = ref.watch(productProvider);
  final search = ref.watch(productSearchProvider);
  final list = providerList.value ?? [];
  if (providerList.isLoading) {
    return const Center(child: CircularProgressIndicator());
  }
  //when search empty
  if (search.isNotEmpty && list.isEmpty) {
    return Center(
      child: Text(
        AppLocalizations.of(context)!.translate('product_search_empty'),
        style: TextStyle(fontSize: 14.0, color: AppStyle.grayColor2, fontWeight: FontWeight.w300),
      ),
    );
  }
  return ListView.builder(
    itemCount: list.length,
    itemBuilder: (context, index) {
      final item = list[index];
      return listItem(item);
    },
  );
}


//tabBarView will change after read
Textfield(
  controller: _search,
  onEditingComplete: () {
    ref.read(productSearchProvider.notifier).state = _search.text;
  },
  textInputAction: TextInputAction.search,
),

Upvotes: 0

Related Questions