feat: refactor RecipesScreen and RecipesViewNotifier to support dynamic view modes and column selection

This commit is contained in:
Nils-Johan Gynther
2026-04-25 07:09:15 +02:00
parent fe3d8581a8
commit ba4e931f5c
3 changed files with 110 additions and 52 deletions
+37 -12
View File
@@ -4,6 +4,9 @@ import 'package:go_router/go_router.dart';
import '../../features/auth/data/auth_providers.dart'; import '../../features/auth/data/auth_providers.dart';
import '../../features/recipes/data/recipes_grid_provider.dart'; import '../../features/recipes/data/recipes_grid_provider.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
const _adminDestination = _AppDestination( const _adminDestination = _AppDestination(
path: '/admin', path: '/admin',
@@ -102,18 +105,40 @@ class AppShell extends ConsumerWidget {
appBar: AppBar( appBar: AppBar(
title: Text(selectedDestination.title), title: Text(selectedDestination.title),
actions: [ actions: [
if (isRecipesRoute) if (isRecipesRoute) ...existing code...
PopupMenuButton<int>( Consumer(
icon: const Icon(Icons.grid_view), builder: (context, ref, child) {
tooltip: 'Välj antal kolumner', final view = ref.watch(recipesViewProvider).valueOrNull ??
onSelected: (columns) => (mode: RecipesViewMode.grid, columns: 2);
ref.read(recipesGridProvider.notifier).setColumns(columns), return Row(
itemBuilder: (context) => const [ mainAxisSize: MainAxisSize.min,
PopupMenuItem(value: 2, child: Text('2 kolumner')), children: [
PopupMenuItem(value: 4, child: Text('4 kolumner')), IconButton(
PopupMenuItem(value: 6, child: Text('6 kolumner')), tooltip: view.mode == RecipesViewMode.grid
PopupMenuItem(value: 8, child: Text('8 kolumner')), ? 'Visa som lista'
], : 'Visa som grid',
icon: Icon(view.mode == RecipesViewMode.grid
? Icons.view_list
: Icons.grid_view),
onPressed: () =>
ref.read(recipesViewProvider.notifier).toggleMode(),
),
if (view.mode == RecipesViewMode.grid)
PopupMenuButton<int>(
icon: const Icon(Icons.grid_view),
tooltip: 'Välj antal kolumner',
onSelected: (columns) =>
ref.read(recipesViewProvider.notifier).setColumns(columns),
itemBuilder: (context) => const [
PopupMenuItem(value: 2, child: Text('2 kolumner')),
PopupMenuItem(value: 4, child: Text('4 kolumner')),
PopupMenuItem(value: 6, child: Text('6 kolumner')),
PopupMenuItem(value: 8, child: Text('8 kolumner')),
],
),
],
);
},
), ),
IconButton( IconButton(
tooltip: 'Logga ut', tooltip: 'Logga ut',
@@ -1,21 +1,36 @@
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
const _prefsKey = 'recipes_grid_columns'; enum RecipesViewMode { grid, list }
class RecipesViewNotifier extends AsyncNotifier<({RecipesViewMode mode, int columns})> {
static const _modeKey = 'recipes_view_mode';
static const _columnsKey = 'recipes_grid_columns';
class RecipesGridNotifier extends AsyncNotifier<int> {
@override @override
Future<int> build() async { Future<({RecipesViewMode mode, int columns})> build() async {
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
return prefs.getInt(_prefsKey) ?? 2; final mode = RecipesViewMode.values[prefs.getInt(_modeKey) ?? 0];
final columns = prefs.getInt(_columnsKey) ?? 2;
return (mode: mode, columns: columns);
}
Future<void> toggleMode() async {
final prefs = await SharedPreferences.getInstance();
final current = state.valueOrNull ?? (mode: RecipesViewMode.grid, columns: 2);
final newMode = current.mode == RecipesViewMode.grid ? RecipesViewMode.list : RecipesViewMode.grid;
await prefs.setInt(_modeKey, newMode.index);
state = AsyncData((mode: newMode, columns: current.columns));
} }
Future<void> setColumns(int columns) async { Future<void> setColumns(int columns) async {
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
await prefs.setInt(_prefsKey, columns); final current = state.valueOrNull ?? (mode: RecipesViewMode.grid, columns: 2);
state = AsyncData(columns); await prefs.setInt(_columnsKey, columns);
state = AsyncData((mode: current.mode, columns: columns));
} }
} }
final recipesGridProvider = final recipesViewProvider =
AsyncNotifierProvider<RecipesGridNotifier, int>(RecipesGridNotifier.new); AsyncNotifierProvider<RecipesViewNotifier, ({RecipesViewMode mode, int columns})>(
RecipesViewNotifier.new);
@@ -13,10 +13,8 @@ class RecipesScreen extends ConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final recipesAsync = ref.watch(recipesProvider); final recipesAsync = ref.watch(recipesProvider);
final columns = ref.watch(recipesGridProvider).maybeWhen( final view = ref.watch(recipesViewProvider).valueOrNull ??
data: (v) => v, (mode: RecipesViewMode.grid, columns: 2);
orElse: () => 2,
);
return Stack( return Stack(
children: [ children: [
@@ -34,36 +32,56 @@ class RecipesScreen extends ConsumerWidget {
); );
} }
return GridView.builder( if (view.mode == RecipesViewMode.grid) {
padding: const EdgeInsets.only(bottom: 88), return GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( padding: const EdgeInsets.only(bottom: 88),
crossAxisCount: columns, gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisSpacing: 4, crossAxisCount: view.columns,
mainAxisSpacing: 4, crossAxisSpacing: 4,
), mainAxisSpacing: 4,
itemCount: recipes.length, ),
itemBuilder: (context, index) { itemCount: recipes.length,
final recipe = recipes[index]; itemBuilder: (context, index) {
return GestureDetector( final recipe = recipes[index];
onTap: () => context.push('/recipes/${recipe.id}'), return GestureDetector(
child: Container( onTap: () => context.push('/recipes/${recipe.id}'),
decoration: BoxDecoration( child: Container(
borderRadius: BorderRadius.circular(8.0), decoration: BoxDecoration(
color: Colors.grey[200], borderRadius: BorderRadius.circular(8.0),
image: recipe.imageUrl != null color: Colors.grey[200],
? DecorationImage( image: recipe.imageUrl != null
image: NetworkImage(recipe.imageUrl!), ? DecorationImage(
fit: BoxFit.cover, image: NetworkImage(recipe.imageUrl!),
) fit: BoxFit.cover,
)
: null,
),
child: recipe.imageUrl == null
? const Center(child: Icon(Icons.restaurant, size: 32))
: null, : null,
), ),
child: recipe.imageUrl == null );
? const Center(child: Icon(Icons.restaurant, size: 32)) },
: null, );
), } else {
); return ListView.builder(
}, padding: const EdgeInsets.only(bottom: 88),
); itemCount: recipes.length,
itemBuilder: (context, index) {
final recipe = recipes[index];
return ListTile(
leading: recipe.imageUrl != null
? CircleAvatar(
backgroundImage: NetworkImage(recipe.imageUrl!),
)
: const CircleAvatar(child: Icon(Icons.restaurant)),
title: Text(recipe.name),
subtitle: Text(recipe.description ?? ''),
onTap: () => context.push('/recipes/${recipe.id}'),
);
},
);
}
}, },
), ),
Positioned( Positioned(