Featured list view
Presentation (Building the UI)
Summary
This lesson covers the implementation of the horizontal ListView for the featured books section. The primary focus is resolving the common "unbounded height" exception that occurs when nesting a horizontal ListView inside a Column. The instructor demonstrates why wrapping the list in an Expanded widget is not always the best solution, and instead uses a SizedBox with a dynamic height based on MediaQuery to properly constrain the list while matching the design. Finally, visual spacing is fine-tuned using Padding and class names are refactored for clarity.
Key Ideas
- Placing a horizontal
ListViewdirectly inside aColumnthrows an unbounded height exception because both widgets defer to their children for size constraints in the cross-axis. - Wrapping a
ListViewin anExpandedwidget gives it all remaining vertical space, which prevents the crash but may distort the UI if a specific maximum height is required by the design. SizedBoxcan be used to strictly enforce a layout height on aListView.MediaQuery.of(context).size.height * 0.3is used to ensure the list takes up exactly 30% of the screen height regardless of the device.- Padding is added to the individual items within the
itemBuilderrather than theListViewitself to space out the elements evenly while maintaining scroll behavior.
Code Snippets
class FeaturedBooksListView extends StatelessWidget {
const FeaturedBooksListView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return SizedBox(
height: MediaQuery.of(context).size.height * .3,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemBuilder: (context, index) {
return const Padding(
padding: EdgeInsets.symmetric(horizontal: 8),
child: FeaturedListViewItem(),
);
},
),
);
}
}class HomeViewBody extends StatelessWidget {
const HomeViewBody({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return const Column(
children: [
CustomAppBar(),
FeaturedBooksListView(),
],
);
}
}class CustomAppBar extends StatelessWidget {
const CustomAppBar({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(left: 24, right: 24, top: 40, bottom: 20),
child: Row(
children: [
Image.asset(
AssetsData.logo,
height: 18,
),
const Spacer(),
IconButton(
onPressed: () {},
icon: const Icon(
FontAwesomeIcons.magnifyingGlass,
size: 24,
),
)
],
),
);
}
}Architecture Insight
Right now this list is hardcoded. That's fine — but be aware: when you wire data later (section 5), the widget should still render the same way regardless of whether the data came from cache, network, or a stub. That invariance is the CA promise.
The widget's contract should be: "give me a List<Book>, I render it." Where the list came from is somebody else's problem (a Cubit + use case + repository).
Looking ahead — Section 5 replaces the hardcoded list with a Cubit + FetchFeaturedBooksUseCase pipeline.
My Notes
- The Unbounded Height Dilemma: The video gives a practical look at the classic Flutter
ListViewinside aColumnerror. The instructor initially reaches forExpanded, which is the instinctual fix for many developers, but correctly pivots toSizedBoxto maintain the design's intended aspect ratio. - [Editor's note] Missing `itemCount`: The
ListView.builderin this lesson is technically infinite and will keep building items if you scroll endlessly. When hooking this up to aBloc/Cubitstate later, ensuringitemCount: state.books.lengthis critical to prevent crashes. - [Editor's note] `MediaQuery` vs `LayoutBuilder`: Hardcoding
MediaQuery.of(context).size.height * .3ties the presentation logic directly to screen size globally. While it works for a quick layout, Clean Architecture UI practices generally favor passing down constraints viaLayoutBuilderso widgets remain isolated and testable without a full screen context.