Implement persistent state across navigation views

Lifts scoring state from ScoringPage to MainNavigationPage level to preserve
scores and rule modes when switching between navigation views.

State management improvements:
- MainNavigationPage now manages persistent scoring state (arrows per end, scores array)
- ScoringPage accepts initial state parameters and callback functions
- All score modifications notify parent to maintain state consistency
- Deep copying ensures proper state isolation between parent and child

Persistence features:
- Scores remain intact when navigating to Settings and back to Main scorer
- Arrow count mode (3/6 arrows) preserved across view switches
- All sophisticated end interaction behaviors maintained within scoring context
- Confirmation dialogs for rule changes still work with persistent state

Technical implementation:
- State lifting pattern following Flutter best practices
- Callback functions for bidirectional communication between parent and child
- Deep copying of score arrays prevents reference sharing issues
- Clean separation of navigation state from scoring logic

This enhancement ensures professional UX where users can navigate between
app sections without losing their scoring progress or configuration settings.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Mikkeli Matlock
2025-08-23 22:51:33 +09:00
parent 8c86f9b38d
commit 398ac5af63
2 changed files with 81 additions and 10 deletions
@@ -12,8 +12,47 @@ class MainNavigationPage extends StatefulWidget {
class _MainNavigationPageState extends State<MainNavigationPage> { class _MainNavigationPageState extends State<MainNavigationPage> {
int _selectedIndex = 0; int _selectedIndex = 0;
final List<Widget> _pages = [ // Persistent scoring state
const ScoringPage(), int _arrowsPerEnd = 3;
int _endsPerSet = 12;
List<List<String>> _scores = [];
@override
void initState() {
super.initState();
_initializeScores();
}
void _initializeScores() {
_scores = List.generate(
_endsPerSet,
(index) => List.generate(_arrowsPerEnd, (index) => ''),
);
}
void _onArrowsPerEndChanged(int arrows) {
setState(() {
_arrowsPerEnd = arrows;
_endsPerSet = arrows == 3 ? 10 : 6;
// Note: The actual score preservation dialog logic will be handled by ScoringPage
// This is just to keep the navigation-level state in sync
});
}
void _onScoresUpdated(List<List<String>> newScores) {
setState(() {
_scores = newScores;
});
}
List<Widget> get _pages => [
ScoringPage(
initialArrowsPerEnd: _arrowsPerEnd,
initialEndsPerSet: _endsPerSet,
initialScores: _scores,
onArrowsPerEndChanged: _onArrowsPerEndChanged,
onScoresUpdated: _onScoresUpdated,
),
const SettingsPage(), const SettingsPage(),
]; ];
+40 -8
View File
@@ -4,18 +4,31 @@ import '../widgets/scoring_grid/scoring_keypad.dart';
import '../widgets/common/toggle_switches.dart'; import '../widgets/common/toggle_switches.dart';
class ScoringPage extends StatefulWidget { class ScoringPage extends StatefulWidget {
const ScoringPage({super.key}); final int initialArrowsPerEnd;
final int initialEndsPerSet;
final List<List<String>> initialScores;
final Function(int) onArrowsPerEndChanged;
final Function(List<List<String>>) onScoresUpdated;
const ScoringPage({
super.key,
required this.initialArrowsPerEnd,
required this.initialEndsPerSet,
required this.initialScores,
required this.onArrowsPerEndChanged,
required this.onScoresUpdated,
});
@override @override
State<ScoringPage> createState() => _ScoringPageState(); State<ScoringPage> createState() => _ScoringPageState();
} }
class _ScoringPageState extends State<ScoringPage> with SingleTickerProviderStateMixin { class _ScoringPageState extends State<ScoringPage> with SingleTickerProviderStateMixin {
int arrowsPerEnd = 3; late int arrowsPerEnd;
int endsPerSet = 12; late int endsPerSet;
int selectedRow = 0; int selectedRow = -1;
int selectedColumn = 0; int selectedColumn = -1;
List<List<String>> scores = []; late List<List<String>> scores;
bool isKeypadVisible = false; bool isKeypadVisible = false;
int? activeEndIndex; int? activeEndIndex;
late AnimationController _animationController; late AnimationController _animationController;
@@ -24,6 +37,11 @@ class _ScoringPageState extends State<ScoringPage> with SingleTickerProviderStat
@override @override
void initState() { void initState() {
super.initState(); super.initState();
// Initialize with values from parent
arrowsPerEnd = widget.initialArrowsPerEnd;
endsPerSet = widget.initialEndsPerSet;
scores = _deepCopyScores(widget.initialScores);
_animationController = AnimationController( _animationController = AnimationController(
duration: const Duration(milliseconds: 250), duration: const Duration(milliseconds: 250),
vsync: this, vsync: this,
@@ -32,7 +50,10 @@ class _ScoringPageState extends State<ScoringPage> with SingleTickerProviderStat
parent: _animationController, parent: _animationController,
curve: Curves.easeInOut, curve: Curves.easeInOut,
); );
_initializeScores(); }
List<List<String>> _deepCopyScores(List<List<String>> originalScores) {
return originalScores.map((end) => List<String>.from(end)).toList();
} }
@override @override
@@ -104,6 +125,10 @@ class _ScoringPageState extends State<ScoringPage> with SingleTickerProviderStat
} else { } else {
_initializeScores(); _initializeScores();
} }
// Notify parent of changes
widget.onArrowsPerEndChanged(arrows);
widget.onScoresUpdated(scores);
}); });
} else { } else {
// No scores exist, just switch modes normally // No scores exist, just switch modes normally
@@ -112,6 +137,10 @@ class _ScoringPageState extends State<ScoringPage> with SingleTickerProviderStat
// Infer ends per set based on ruleset // Infer ends per set based on ruleset
endsPerSet = arrows == 3 ? 10 : 6; endsPerSet = arrows == 3 ? 10 : 6;
_initializeScores(); _initializeScores();
// Notify parent of changes
widget.onArrowsPerEndChanged(arrows);
widget.onScoresUpdated(scores);
}); });
} }
} }
@@ -155,6 +184,9 @@ class _ScoringPageState extends State<ScoringPage> with SingleTickerProviderStat
// Update scores array // Update scores array
scores[endIndex] = sortedScores; scores[endIndex] = sortedScores;
// Notify parent of score changes
widget.onScoresUpdated(scores);
} }
int _findFirstEmptyCellInEnd(int endIndex) { int _findFirstEmptyCellInEnd(int endIndex) {
@@ -214,7 +246,7 @@ class _ScoringPageState extends State<ScoringPage> with SingleTickerProviderStat
// Clear the selected cell // Clear the selected cell
if (selectedColumn < scores[selectedRow].length && scores[selectedRow][selectedColumn].isNotEmpty) { if (selectedColumn < scores[selectedRow].length && scores[selectedRow][selectedColumn].isNotEmpty) {
scores[selectedRow][selectedColumn] = ''; scores[selectedRow][selectedColumn] = '';
_sortEndScores(selectedRow); _sortEndScores(selectedRow); // This already notifies parent
// Retract keypad and disarm after clearing // Retract keypad and disarm after clearing
_animationController.reverse().then((_) { _animationController.reverse().then((_) {
setState(() { setState(() {