arch-atlas

Pagination states part 1

Presentation (Integration)

11:14

Summary

This lesson addresses a critical UX flaw in the initial pagination implementation: the entire list disappears and is replaced by a loading indicator when fetching subsequent pages. To solve this, a new FeaturedBooksPaginationLoading state is introduced to differentiate between the initial load and pagination loads. The UI is then refactored to use a StatefulWidget and a BlocConsumer to accumulate fetched books locally, allowing the application to rebuild the list seamlessly with new items without disrupting the user's scroll position or wiping the existing items from the screen.

Key Ideas

  • Emitting a generic loading state during pagination replaces the existing list with a loading indicator, creating a poor user experience.
  • Creating distinct states for initial loading (FeaturedBooksLoading) and subsequent pagination loading (FeaturedBooksPaginationLoading) allows the UI to handle them differently.
  • The Cubit checks the pageNumber parameter (e.g., pageNumber == 0) to determine which specific loading state to emit to the UI.
  • BlocConsumer is utilized to handle both state-driven side effects (listening) and UI rendering (building) simultaneously.
  • The listener callback in BlocConsumer appends newly fetched books to a locally managed state list when a success state is emitted.
  • StatefulWidget is used to maintain the accumulated list of BookEntity objects across widget rebuilds.

Code Snippets

Adding a specific state for pagination loadingdart
abstract class FeaturedBooksState {}

class FeaturedBooksInitial extends FeaturedBooksState {}

class FeaturedBooksLoading extends FeaturedBooksState {}
class FeaturedBooksPaginationLoading extends FeaturedBooksState {}

class FeaturedBooksFailure extends FeaturedBooksState {
  final String errMessage;
  FeaturedBooksFailure(this.errMessage);
}

class FeaturedBooksSuccess extends FeaturedBooksState {
  final List<BookEntity> books;
  FeaturedBooksSuccess(this.books);
}
Emitting different loading states based on page numberdart
Future<void> fetchFeaturedBooks({int pageNumber = 0}) async {
  if (pageNumber == 0) {
    emit(FeaturedBooksLoading());
  } else {
    emit(FeaturedBooksPaginationLoading());
  }

  var result = await fetchFeaturedBooksUseCase.call(pageNumber);
  result.fold((failure) {
    emit(FeaturedBooksFailure(failure.errMessage));
  }, (books) {
    emit(FeaturedBooksSuccess(books));
  });
}
Refactoring to StatefulWidget and BlocConsumer to accumulate booksdart
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../domain/entities/book_entity.dart';
import '../featured_books_cubit/featured_books_cubit.dart';
import 'featured_books_list_view.dart';

class FeaturedBooksListViewBlocBuilder extends StatefulWidget {
  const FeaturedBooksListViewBlocBuilder({super.key});

  @override
  State<FeaturedBooksListViewBlocBuilder> createState() =>
      _FeaturedBooksListViewBlocBuilderState();
}

class _FeaturedBooksListViewBlocBuilderState
    extends State<FeaturedBooksListViewBlocBuilder> {
  List<BookEntity> books = [];

  @override
  Widget build(BuildContext context) {
    return BlocConsumer<FeaturedBooksCubit, FeaturedBooksState>(
      listener: (context, state) {
        if (state is FeaturedBooksSuccess) {
          books.addAll(state.books);
        }
      },
      builder: (context, state) {
        if (state is FeaturedBooksSuccess ||
            state is FeaturedBooksPaginationLoading) {
          return FeaturedBooksListView(
            books: books,
          );
        } else if (state is FeaturedBooksFailure) {
          return Text(state.errMessage);
        } else {
          return const CircularProgressIndicator();
        }
      },
    );
  }
}

Architecture Insight

Architecture insight

Pagination needs more states than the standard four — and that's the right reason to subclass. PaginationLoading is distinct from initial Loading because the UI keeps showing the existing list while loading the next page; PaginationFailure shows a "retry" affordance at the list bottom instead of replacing the whole screen with an error.

The general lesson: when the UI needs to render two situations differently, they're two states. Don't shoehorn them into one with a flag.

My Notes

The instructor tackles the common "flickering list" issue in pagination. The solution involves introducing a specific FeaturedBooksPaginationLoading state and using a BlocConsumer inside a StatefulWidget to maintain a cumulative list of BookEntity objects.

  • [Editor's note] While this solves the UI flicker, moving the domain data accumulation (List<BookEntity> books = [];) into the UI's StatefulWidget is an anti-pattern in Clean Architecture. The UI should strictly reflect the current state. The Cubit should hold and append to the master list of books, and FeaturedBooksSuccess should emit the updated, combined list. This would allow us to keep FeaturedBooksListViewBlocBuilder as a StatelessWidget and stick to a simple BlocBuilder.
  • The instructor correctly points out that without this separation of loading states, the ListView gets destroyed and completely replaced by the progress indicator.