albums grid, pagination

This commit is contained in:
austinried
2025-10-31 15:10:22 +09:00
parent cc168eefcd
commit 9f05ebb201
9 changed files with 984 additions and 21 deletions

View File

@@ -0,0 +1,61 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
PagingController<PageKeyType, ItemType>
usePagingController<PageKeyType, ItemType>({
required PageKeyType? Function(PagingState<PageKeyType, ItemType>)
getNextPageKey,
required FutureOr<List<ItemType>> Function(PageKeyType) fetchPage,
}) {
return use(
_PagingControllerHook<PageKeyType, ItemType>(
getNextPageKey: getNextPageKey,
fetchPage: fetchPage,
),
);
}
class _PagingControllerHook<PageKeyType, ItemType>
extends Hook<PagingController<PageKeyType, ItemType>> {
const _PagingControllerHook({
super.keys,
required this.getNextPageKey,
required this.fetchPage,
});
final PageKeyType? Function(PagingState<PageKeyType, ItemType>)
getNextPageKey;
final FutureOr<List<ItemType>> Function(PageKeyType) fetchPage;
@override
HookState<
PagingController<PageKeyType, ItemType>,
Hook<PagingController<PageKeyType, ItemType>>
>
createState() => _PagingControllerHookState<PageKeyType, ItemType>();
}
class _PagingControllerHookState<PageKeyType, ItemType>
extends
HookState<
PagingController<PageKeyType, ItemType>,
_PagingControllerHook<PageKeyType, ItemType>
> {
late final controller = PagingController<PageKeyType, ItemType>(
getNextPageKey: hook.getNextPageKey,
fetchPage: hook.fetchPage,
);
@override
PagingController<PageKeyType, ItemType> build(BuildContext context) =>
controller;
@override
void dispose() => controller.dispose();
@override
String get debugLabel => 'usePagingController';
}

View File

@@ -0,0 +1,40 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:go_router/go_router.dart';
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
import '../hooks/use_paging_controller.dart';
import 'list_items.dart';
class AlbumsGrid extends HookWidget {
const AlbumsGrid({super.key});
@override
Widget build(BuildContext context) {
final controller = usePagingController<int, String>(
getNextPageKey: (state) =>
state.lastPageIsEmpty ? null : state.nextIntPageKey,
fetchPage: (pageKey) => List.generate(30, (_) => pageKey.toString()),
);
return PagingListener(
controller: controller,
builder: (context, state, fetchNextPage) {
return PagedSliverGrid(
state: state,
fetchNextPage: fetchNextPage,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
),
builderDelegate: PagedChildBuilderDelegate<String>(
itemBuilder: (context, item, index) => AlbumGridTile(
onTap: () {
context.push('/album');
},
),
),
);
},
);
}
}

85
lib/lists/list_items.dart Normal file
View File

@@ -0,0 +1,85 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import '../util/clip.dart';
class AlbumGridTile extends StatelessWidget {
const AlbumGridTile({
super.key,
this.onTap,
});
final void Function()? onTap;
@override
Widget build(BuildContext context) {
return CardTheme(
clipBehavior: Clip.antiAlias,
shape: RoundedRectangleBorder(
borderRadius: BorderRadiusGeometry.circular(3),
),
margin: EdgeInsets.all(2),
child: ImageCard(
onTap: onTap,
child: CachedNetworkImage(
imageUrl: 'https://placehold.net/400x400.png',
placeholder: (context, url) => CircularProgressIndicator(),
errorWidget: (context, url, error) => Icon(Icons.error),
),
),
);
}
}
class ArtistListTile extends StatelessWidget {
const ArtistListTile({super.key});
@override
Widget build(BuildContext context) {
return ListTile(
leading: CircleClip(
child: CachedNetworkImage(
imageUrl: 'https://placehold.net/400x400.png',
placeholder: (context, url) => CircularProgressIndicator(),
errorWidget: (context, url, error) => Icon(Icons.error),
),
),
title: Text('Some Artist'),
);
}
}
class ImageCard extends StatelessWidget {
const ImageCard({
super.key,
required this.child,
this.onTap,
this.onLongPress,
});
final Widget child;
final void Function()? onTap;
final void Function()? onLongPress;
@override
Widget build(BuildContext context) {
return Card(
child: Stack(
fit: StackFit.passthrough,
alignment: Alignment.center,
children: [
child,
Positioned.fill(
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: onTap,
onLongPress: onLongPress,
),
),
),
],
),
);
}
}

View File

@@ -1,3 +1,4 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
@@ -18,6 +19,11 @@ class AlbumScreen extends StatelessWidget {
},
child: Text('Artist...'),
),
CachedNetworkImage(
imageUrl: 'https://placehold.net/400x400.png',
placeholder: (context, url) => CircularProgressIndicator(),
errorWidget: (context, url, error) => Icon(Icons.error),
),
],
),
),

View File

@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:material_symbols_icons/symbols.dart';
import '../lists/albums_grid.dart';
import '../util/custom_scroll_fix.dart';
class LibraryScreen extends StatefulWidget {
@@ -16,7 +17,7 @@ class _LibraryScreenState extends State<LibraryScreen>
late final TabController tabController;
final iconSize = 26.0;
final tabHeight = 32.0;
final tabHeight = 36.0;
late final List<(String, Widget)> tabs = [
('Home', Icon(Symbols.home_rounded, size: iconSize)),
@@ -202,20 +203,7 @@ class _NewWidgetState extends State<NewWidget>
),
SliverPadding(
padding: const EdgeInsets.all(8.0),
sliver: SliverFixedExtentList(
itemExtent: 48.0,
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return ListTile(
title: Text('Item $index'),
onTap: () {
context.push('/album');
},
);
},
childCount: 30,
),
),
sliver: AlbumsGrid(),
),
],
);

21
lib/util/clip.dart Normal file
View File

@@ -0,0 +1,21 @@
import 'package:flutter/material.dart';
class CircleClip extends StatelessWidget {
const CircleClip({
super.key,
required this.child,
});
final Widget child;
@override
Widget build(BuildContext context) {
return ClipOval(
clipBehavior: Clip.antiAlias,
child: AspectRatio(
aspectRatio: 1.0,
child: child,
),
);
}
}