Pagination을 사용하는 Screen에서는 가져온 데이터를 "DataName"Card라는 StatelessWidget 클래스를 통해 반복적으로 UI를 생성하고 있다.
Pagination Screen 공통 기능
- ScrollController에 listener를 등록해서 목록이 끝나갈때 쯤 paginate 실행
- 로딩, 에러 시 해당 UI 출력
Data에 해당하는 Card를 자동으로 생성하는 것은 파라미터를 통해 넘겨줄 수 있으나 그에 따른 행동(페이지 이동)나, 추가 행동에 관하여 모든 것을 일반화 할 순 없기 때문에 Itambuilder를 만들었다. 즉 추가 행동은 각각의 스크린에 맞게 개발할 수 있다.
typedef PaginationWidgetBuilder<T extends IModelWithId> = Widget Function(
BuildContext, int index, T model);
class PaginationListView<T extends IModelWithId>
extends ConsumerStatefulWidget {
final StateNotifierProvider<PaginationProvider, CursorPaginationBase>
provider;
final PaginationWidgetBuilder<T> itemBuilder;
const PaginationListView({
super.key,
required this.provider,
required this.itemBuilder,
});
ConsumerState<PaginationListView> createState() =>
_PaginationListViewState<T>();
}
class _PaginationListViewState<T extends IModelWithId>
extends ConsumerState<PaginationListView> {
final ScrollController controller = ScrollController();
void initState() {
super.initState();
controller.addListener(listener);
}
void listener() {
PaginationUtils.paginate(
controller: controller,
provider: ref.read(widget.provider.notifier),
);
}
void dispose() {
controller.removeListener(listener);
controller.dispose();
super.dispose();
}
Widget build(BuildContext context) {
final state = ref.watch(widget.provider);
if (state is CursorPaginationLoading) {
// 로딩 UI
}
if (state is CursorPaginationError) {
// Error UI
}
final cp = state as CursorPagination<T>;
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: ListView.separated(
controller: controller,
itemCount: cp.data.length + 1,
itemBuilder: (_, index) {
if (index == cp.data.length) {
// 추가 Paginate용 로딩 UI
}
final pItem = cp.data[index];
// 호출 시 받았던 itemBuilder에서 받은 return 함수에 들어갈 파라미터 추가
return widget.itemBuilder(
context,
index,
pItem,
);
},
separatorBuilder: (_, index) {
return const SizedBox(height: 16);
},
),
);
}
}
class ProductScreen extends StatelessWidget {
const ProductScreen({super.key});
Widget build(BuildContext context) {
return PaginationListView<ProductModel>(
provider: productProvider,
itemBuilder: <ProductModel>(context, index, model) {
return GestureDetector(
onTap: () {
Navigator.of(context).push(MaterialPageRoute(
builder: (_) => RestaurantDetailScreen(id: model.restaurant.id),
));
},
child: ProductCard.fromProductModel(
model: model,
),
);
},
);
}
}

이렇게 세부 행동에 대해서는 itembuilder를 직접 정의함으로써 커스텀이 가능하도록 유지하고, 공통 UI에 대해서는 일반화 할 수 있는 코드를 작성할 수 있도록 일반화 하는 버릇을 들여야 할 것 같다.