I am building an flashcard app with Flutter and encountering a problem if you want to flip the front and backside of the card. The "flipping" is an AnimatedSwitcher and after it the text of the opposite side should appear. The problem is that you can see the text of the opposite side right after tapping, even before the animation starts. I really don't know how to fix that.
This is the code of the Widget:
import 'dart:math';
imports ...
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
class FlashcardWidget extends StatefulWidget {
final String deckId; // Deck-ID hinzufügen
final List<Flashcard> flashcards;
const FlashcardWidget({super.key, required this.deckId, required this.flashcards});
@override
FlashcardWidgetState createState() => FlashcardWidgetState();
}
class FlashcardWidgetState extends State<FlashcardWidget> {
late FlashcardViewModel _viewModel;
bool isFront = true; // Zustand für Vorder- oder Rückseite
bool isLoading = true; // Ladezustand
@override
void initState() {
super.initState();
_viewModel = FlashcardViewModel(FlashcardService(), FlashcardDeckDAO());
_initializeDeckAndFlashcards();
}
Future<void> _initializeDeckAndFlashcards() async {
await _viewModel.loadDeck(widget.deckId); // Deck-ID verwenden, um Deck zu laden
_viewModel.flashcards = widget.flashcards;
if (_viewModel.flashcards.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('No flashcards to display')),
);
}
setState(() {
isLoading = false; // Ladezustand beenden
});
}
Future<void> _toggleCardSide() async {
if (_viewModel.flashcards.isNotEmpty && _viewModel.flashcards[_viewModel.currentIndex].frontText.isNotEmpty) {
setState(() {
isFront = !isFront; // Umschalten der Seite
});
}
await Future.delayed(const Duration(milliseconds: 500));
// Textwechsel nach der Animation
setState(() {
if (!isFront) {
// Bei Erfolg oder Misserfolg wird die nächste Karte geladen
if (_viewModel.flashcards.isNotEmpty) {
_viewModel.currentIndex = (_viewModel.currentIndex + 1) % _viewModel.flashcards.length;
}
}
});
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => FocusScope.of(context).unfocus(),
child: Scaffold(
backgroundColor: Colors.white,
body: SafeArea(
child: Column(
children: [
_buildHeader(context), // Header oben
const SizedBox(height: 10), // Optionaler Abstand
if (isLoading)
const Expanded(
child: Center(
child: CircularProgressIndicator(), // Ladeanzeige
),
)
else
_buildFlashcard(context), // Flashcard anzeigen
const SizedBox(height: 20), // Abstand zu den Buttons
if (!isLoading) _buildActionButtons(context), // Buttons nur anzeigen, wenn geladen
],
),
),
),
);
}
Widget _buildHeader(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0),
decoration: const BoxDecoration(
color: Colors.white, // Weißer Hintergrund
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// Zurück-Button
IconButton(
icon: Icon(Icons.arrow_back, color: Colors.grey[700]), // Grau (#808080)
onPressed: () {
Navigator.pop(context);
},
),
// Deckname (zentriert)
Expanded(
child: Text(
_viewModel.deckName,
textAlign: TextAlign.center,
style: GoogleFonts.outfit(
fontWeight: FontWeight.w500,
fontSize: 32,
color: Colors.black,
),
overflow: TextOverflow.ellipsis, // Kürzen, falls zu lang
),
),
// Fortschrittsanzeige (rechts)
Container(
padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0),
decoration: BoxDecoration(
color: const Color(0xFF4B39EF), // Neuer Hintergrund (#4b39ef)
borderRadius: BorderRadius.circular(20),
),
child: Text(
'${_viewModel.currentIndex}/${_viewModel.totalCards}', // Fortschrittsanzeige
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Colors.white, // Weißer Text
fontWeight: FontWeight.bold,
),
),
),
],
),
);
}
Widget _buildFlashcard(BuildContext context) {
if (_viewModel.flashcards.isEmpty) {
return const Expanded(
child: Center(
child: Text(
'No flashcard available',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
),
);
}
return Expanded(
child: GestureDetector(
onTap: _toggleCardSide,
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 500),
transitionBuilder: (child, animation) {
final rotate = Tween(begin: pi, end: 0.0).animate(animation);
return AnimatedBuilder(
animation: rotate,
child: child,
builder: (context, child) {
final isUnder = (ValueKey(isFront) != child?.key);
final value = isUnder ? min(rotate.value, pi / 2) : rotate.value;
return Transform(
transform: Matrix4.rotationY(value),
alignment: Alignment.center,
child: child,
);
},
);
},
child: Container(
key: ValueKey(isFront),
width: MediaQuery.of(context).size.width * 0.9,
height: MediaQuery.of(context).size.height * 0.6,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.grey[300],
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: const Color(0xFF4B39EF)
),
boxShadow: const [
BoxShadow(
color: Colors.black26,
blurRadius: 4,
offset: Offset(0, 2),
),
],
),
child: Center(
child: Text(
isFront
? _viewModel.frontText
: _viewModel.flashcards[_viewModel.currentIndex].backText,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodyLarge,
),
),
),
),
),
);
}
Widget _buildActionButtons(BuildContext context) {
if (_viewModel.flashcards.isEmpty) {
return const SizedBox(); // Keine Buttons anzeigen, wenn keine Karten mehr vorhanden sind
}
final double buttonWidth = MediaQuery.of(context).size.width * 0.45; // Hälfte der Flashcard-Breite
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, // Buttons nebeneinander mit Abstand
children: [
ElevatedButton.icon(
onPressed: () {
setState(() {
isFront = true; // Karte auf die Vorderseite zurücksetzen
_viewModel.markAsFailure(); // Nächste Karte laden
});
},
icon: const Icon(Icons.cancel, color: Colors.white),
label: const Text('Failure'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(24),
),
padding: const EdgeInsets.symmetric(vertical: 12),
fixedSize: Size(buttonWidth, 50), // Breite anpassen
),
),
ElevatedButton.icon(
onPressed: () {
setState(() {
isFront = true; // Karte auf die Vorderseite zurücksetzen
_viewModel.markAsSuccess(); // Nächste Karte laden
});
},
icon: const Icon(Icons.check_circle, color: Colors.white),
label: const Text('Success'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(24),
),
padding: const EdgeInsets.symmetric(vertical: 12),
fixedSize: Size(buttonWidth, 50), // Breite anpassen
),
),
],
),
);
}
}
I thought i can solve it with the 500 ms delay, but nothing changes.