arch-atlas

Pagination States part 4

Presentation (Integration)

08:31

Summary

This lesson demonstrates how to build a custom skeleton loading indicator for the featured books list to replace a generic circular loader. The instructor constructs a specialized widget (FeaturedBooksListViewLoadingIndicator) that mimics the exact layout of the actual list, using hardcoded empty items (CustomBookImageLoadingIndicator) wrapped in a fading animation (CustomFadingWidget). This approach enhances the user experience by providing a structural preview of the content layout while the Bloc state is still loading data from the repository.

Key Ideas

  • Skeleton loaders provide a better user experience than generic spinners by previewing the layout of incoming data.
  • To create a skeleton list, duplicate the UI structure of the target ListView.builder and remove data-dependent properties like scroll controllers.
  • Hardcode an itemCount (e.g., 15) to fill the screen with placeholder items during the loading state.
  • Replace dynamic image widgets (like CachedNetworkImage) with simple, static shapes (like a gray Container) in the placeholder item.
  • Wrap the entire skeleton list in a fading animation widget to create a smooth, pulsing visual effect.
  • AnimationController instances require an explicit Duration to avoid runtime errors during widget initialization.

Code Snippets

Creates a gray placeholder container with the same aspect ratio and border radius as the actual book image.dart
import 'package:flutter/material.dart';

class CustomBookImageLoadingIndicator extends StatelessWidget {
  const CustomBookImageLoadingIndicator({super.key});

  @override
  Widget build(BuildContext context) {
    return AspectRatio(
      aspectRatio: 2.6 / 4,
      child: ClipRRect(
        borderRadius: BorderRadius.circular(12),
        child: Container(
          color: Colors.grey[50],
        ),
      ),
    );
  }
}
Builds a horizontally scrolling list of 15 fading placeholder items to simulate the featured books list loading state.dart
import 'package:bookly/features/home/presentation/views/widgets/custom_book_image_loading_indicator.dart';
import 'package:bookly/core/widgets/custom_fading_widget.dart';
import 'package:flutter/material.dart';

class FeaturedBooksListViewLoadingIndicator extends StatelessWidget {
  const FeaturedBooksListViewLoadingIndicator({super.key});

  @override
  Widget build(BuildContext context) {
    return CustomFadingWidget(
      child: SizedBox(
        height: MediaQuery.of(context).size.height * .3,
        child: ListView.builder(
          itemCount: 15,
          scrollDirection: Axis.horizontal,
          itemBuilder: (context, index) {
            return const Padding(
              padding: EdgeInsets.symmetric(horizontal: 8),
              child: CustomBookImageLoadingIndicator(),
            );
          },
        ),
      ),
    );
  }
}
Adds the missing duration to the AnimationController in the custom fading widget to fix runtime initialization errors.dart
@override
void initState() {
  animationController = AnimationController(
    vsync: this,
    duration: const Duration(milliseconds: 800),
  );
  // ... (animation setup continues)
}
Updates the BlocBuilder to return the new skeleton loader when the Bloc state indicates a loading phase.dart
if (state is FeaturedBooksPaginationLoading) {
  return const FeaturedBooksListViewLoadingIndicator();
}

My Notes

The instructor opted to copy-paste the entire ListView.builder configuration from the actual UI into a new "skeleton" widget to maintain identical spacing and dimensions.

  • Gotcha: Don't forget to remove the ScrollController from the copy-pasted ListView.builder when creating the loader, as the skeleton doesn't need to trigger pagination events.
  • Gotcha: The AnimationController threw a runtime error because the duration was initially omitted. Always define explicit durations for custom animations.
  • [Editor's note] While duplicating the UI code is an easy way to get exact dimensions, it can lead to maintenance issues if the core layout changes later (you'd have to remember to update both the real UI and the skeleton widget). A more robust approach in large applications might involve creating a generic skeleton builder or sharing layout parameters via a constants file. Furthermore, the instructor's custom fading approach is lightweight, but utilizing the shimmer package remains the standard industry practice for this design pattern.