rodydavis / signals.dart

Reactive programming made simple for Dart and Flutter
http://dartsignals.dev
Apache License 2.0
378 stars 44 forks source link

How to implement list pagination via signals? #212

Closed milansurelia closed 3 months ago

milansurelia commented 4 months ago

I'm interested in implementing pagination for large lists using Signals. Currently, I'm unsure about the best approach to achieve this functionality with signals.

Is there a recommended way to implement pagination using Signals?

Any insights or suggestions on how to approach pagination with Signals would be greatly appreciated.

rodydavis commented 4 months ago

I think this would be greats to show in the documentation!

Cavin6080 commented 3 months ago

+1

rodydavis commented 3 months ago

Do you mean infinite scroll? or a paged API request using async signal?

milansurelia commented 3 months ago

I was asking for paged API request, but it would be great if I could get documentation for both.

rodydavis commented 3 months ago

Here is an example with pocketbase:

import 'dart:async';

import 'package:flutter/material.dart';
import 'package:pocketbase/pocketbase.dart';
import 'package:signals/signals_flutter.dart';

class InfiniteScroll extends StatefulWidget {
  const InfiniteScroll({
    super.key,
    required this.service,
    required this.itemBuilder,
    required this.filter,
  });

  final RecordService service;
  final Widget Function(BuildContext, int, RecordModel model) itemBuilder;
  final String Function(String) filter;

  @override
  State<InfiniteScroll> createState() => InfiniteScrollState();
}

class InfiniteScrollState extends State<InfiniteScroll> {
  final scrollController = ScrollController();
  final currentPage = signal(0);
  final totalPages = signal(1);
  final loading = signal(false);
  final hasMore = signal(true);
  final items = listSignal<RecordModel>([]);
  final search = TextEditingController();

  @override
  void initState() {
    super.initState();
    scrollController.addListener(() {
      if (scrollController.position.pixels >=
          scrollController.position.maxScrollExtent) {
        fetchPage();
      }
    });
    fetchPage();
  }

  Future<void> startSearch() async {
    currentPage.value = 0;
    totalPages.value = 1;
    hasMore.value = true;
    items.clear();
    await fetchPage();
  }

  Future<void> fetchPage() async {
    if (loading.value) return;
    loading.value = true;
    final page = currentPage.value + 1;
    final q = search.text.trim();
    if (page <= totalPages.value) {
      try {
        final list = await widget.service.getList(
          page: page,
          filter: q.isEmpty ? null : widget.filter(q),
        );
        currentPage.value = page;
        totalPages.value = list.totalPages;
        hasMore.value = page < list.totalPages;
        items.addAll(list.items);
        // scrollController.jumpTo(scrollController.position.maxScrollExtent);
      } catch (e) {
        debugPrint('error fetching page: $e');
      }
    }
    loading.value = false;
  }

  @override
  Widget build(BuildContext context) {
    return Watch((context) {
      return CustomScrollView(
        controller: scrollController,
        slivers: [
          SliverToBoxAdapter(
            child: Padding(
              padding: const EdgeInsets.all(20),
              child: TextField(
                controller: search,
                decoration: InputDecoration(
                  labelText: 'Search',
                  border: const OutlineInputBorder(),
                  prefixIcon: InkWell(
                    onTap: startSearch,
                    child: const Icon(Icons.search),
                  ),
                ),
                onEditingComplete: startSearch,
              ),
            ),
          ),
          if (items.isEmpty)
            const SliverFillRemaining(
              child: Center(child: CircularProgressIndicator()),
            )
          else
            SliverList(
              delegate: SliverChildBuilderDelegate(
                (context, index) {
                  if (items().isEmpty) {
                    return const Center(child: Text('No results found'));
                  }
                  if (index == items().length && hasMore.value) {
                    return const Center(child: CircularProgressIndicator());
                  }
                  return widget.itemBuilder(
                    context,
                    index,
                    items.value[index],
                  );
                },
                childCount: items.value.length + (hasMore.value ? 1 : 0),
              ),
            ),
        ],
      );
    });
  }
}