feat(ui): Implement Developer Menu & A/B Testing (Grid/FAB)

This commit is contained in:
Ponshu Developer 2026-01-13 18:33:47 +09:00
parent d0ce82f59a
commit fbeefd9456
5 changed files with 137 additions and 14 deletions

View File

@ -0,0 +1,39 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
// UI実験設定クラス
class UiExperimentSettings {
final int gridColumns; // 2 or 3
final String fabAnimation; // 'rotate' or 'bounce'
const UiExperimentSettings({
this.gridColumns = 2,
this.fabAnimation = 'rotate',
});
UiExperimentSettings copyWith({
int? gridColumns,
String? fabAnimation,
}) {
return UiExperimentSettings(
gridColumns: gridColumns ?? this.gridColumns,
fabAnimation: fabAnimation ?? this.fabAnimation,
);
}
}
// Provider
final uiExperimentProvider = StateNotifierProvider<UiExperimentNotifier, UiExperimentSettings>(
(ref) => UiExperimentNotifier(),
);
class UiExperimentNotifier extends StateNotifier<UiExperimentSettings> {
UiExperimentNotifier() : super(const UiExperimentSettings());
void setGridColumns(int columns) {
state = state.copyWith(gridColumns: columns);
}
void setFabAnimation(String animation) {
state = state.copyWith(fabAnimation: animation);
}
}

View File

@ -0,0 +1,52 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:lucide_icons/lucide_icons.dart';
import '../providers/ui_experiment_provider.dart';
class DevMenuScreen extends ConsumerWidget {
const DevMenuScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final experiment = ref.watch(uiExperimentProvider);
return Scaffold(
appBar: AppBar(
title: const Text('🔬 開発者メニュー'),
),
body: ListView(
children: [
const ListTile(
leading: Icon(LucideIcons.flaskConical),
title: Text('UI実験'),
subtitle: Text('新しいデザインのテスト'),
),
Card(
margin: const EdgeInsets.all(16),
child: Column(
children: [
SwitchListTile(
secondary: const Text('📱', style: TextStyle(fontSize: 24)),
title: const Text('グリッド3列表示'),
subtitle: Text('瓶型の縦長カード (現在: ${experiment.gridColumns}列)'),
value: experiment.gridColumns == 3,
onChanged: (val) => ref.read(uiExperimentProvider.notifier)
.setGridColumns(val ? 3 : 2),
),
const Divider(),
SwitchListTile(
secondary: const Text('', style: TextStyle(fontSize: 24)),
title: const Text('FABバウンス'),
subtitle: Text('ぷるんとした動き (現在: ${experiment.fabAnimation == 'bounce' ? 'ON' : 'OFF'})'),
value: experiment.fabAnimation == 'bounce',
onChanged: (val) => ref.read(uiExperimentProvider.notifier)
.setFabAnimation(val ? 'bounce' : 'rotate'),
),
],
),
),
],
),
);
}
}

View File

@ -24,9 +24,10 @@ import '../widgets/onboarding_dialog.dart';
import '../widgets/home/sake_filter_chips.dart';
import '../widgets/home/home_empty_state.dart';
import '../widgets/home/sake_no_match_state.dart';
import '../widgets/home/sake_list_view.dart';
import '../widgets/home/sake_grid_view.dart';
import '../widgets/add_set_item_dialog.dart';
import '../models/user_profile.dart'; // UserProfile
import '../widgets/analyzing_dialog.dart';
import '../widgets/empty_state.dart'; // Generic empty state
import '../providers/ui_experiment_provider.dart'; // A/B Test
import 'package:flutter_speed_dial/flutter_speed_dial.dart';
import 'package:lucide_icons/lucide_icons.dart';
import '../widgets/prefecture_filter_sheet.dart';
@ -210,6 +211,15 @@ class HomeScreen extends ConsumerWidget {
activeBackgroundColor: Colors.grey[800],
overlayColor: Colors.black,
overlayOpacity: 0.5,
// A/B Test Animation
animationCurve: ref.watch(uiExperimentProvider).fabAnimation == 'bounce'
? Curves.elasticOut
: Curves.linear,
animationDuration: ref.watch(uiExperimentProvider).fabAnimation == 'bounce'
? const Duration(milliseconds: 400)
: const Duration(milliseconds: 250),
spacing: 12,
spaceBetweenChildren: 12,
children: [

View File

@ -5,6 +5,7 @@ import 'package:flutter/services.dart'; // HapticFeedback
import '../../models/sake_item.dart';
import '../../providers/sake_list_provider.dart'; // For sakeOrderControllerProvider
import '../../providers/ui_experiment_provider.dart'; // A/B Test
import 'sake_grid_item.dart';
class SakeGridView extends ConsumerWidget {
@ -23,15 +24,17 @@ class SakeGridView extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final list = sakeList.cast<SakeItem>();
final experiment = ref.watch(uiExperimentProvider);
// If reorder is disabled (Menu Creation Screen), use standard GridView
if (!enableReorder || isMenuMode) {
return GridView.builder(
padding: const EdgeInsets.all(4),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: experiment.gridColumns,
crossAxisSpacing: 4,
mainAxisSpacing: 4,
childAspectRatio: 1.0,
childAspectRatio: experiment.gridColumns == 3 ? (1.0 / 1.5) : 1.0,
),
itemCount: list.length,
itemBuilder: (context, index) {
@ -47,11 +50,11 @@ class SakeGridView extends ConsumerWidget {
// Standard ReorderableGridView for Home Screen
return ReorderableGridView.builder(
padding: const EdgeInsets.all(4),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: experiment.gridColumns,
crossAxisSpacing: 4,
mainAxisSpacing: 4,
childAspectRatio: 1.0,
childAspectRatio: experiment.gridColumns == 3 ? (1.0 / 1.5) : 1.0,
),
itemCount: list.length,
onReorder: (oldIndex, newIndex) {

View File

@ -2,6 +2,8 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:lucide_icons/lucide_icons.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:flutter/services.dart';
import '../../screens/dev_menu_screen.dart';
import '../../providers/theme_provider.dart';
class OtherSettingsSection extends ConsumerStatefulWidget {
@ -20,6 +22,7 @@ class OtherSettingsSection extends ConsumerStatefulWidget {
class _OtherSettingsSectionState extends ConsumerState<OtherSettingsSection> {
String _appVersion = 'Loading...';
int _devTapCount = 0;
@override
void initState() {
@ -60,11 +63,27 @@ class _OtherSettingsSectionState extends ConsumerState<OtherSettingsSection> {
),
const Divider(height: 1),
],
ListTile(
leading: Icon(LucideIcons.info, color: isDark ? Colors.grey[400] : null),
title: const Text('アプリバージョン'),
subtitle: Text(_appVersion),
),
ListTile(
leading: Icon(LucideIcons.info, color: isDark ? Colors.grey[400] : null),
title: const Text('アプリバージョン'),
subtitle: Text(_appVersion),
onTap: () {
setState(() {
_devTapCount++;
if (_devTapCount >= 5) {
_devTapCount = 0;
HapticFeedback.heavyImpact();
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const DevMenuScreen()),
);
} else {
// Optional: Small feedback
HapticFeedback.lightImpact();
}
});
},
),
],
),
),