0

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.

1 Answer 1

0

I can see some issues with your code. You are calling an async function in initState without awaiting it. Also calling setState from initState makes little sense.

I recommend removing the state variable isLoading, and refactoring _viewModel to:

late Future<FlashcardViewModel> _viewModel;

and have _initializeDeckAndFlashcards return the _viewModel:

Future<FlashcardViewModel> _initializeDeckAndFlashcards() async {
    viewModel = FlashcardViewModel(FlashcardService(), FlashcardDeckDAO());
    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')),
      );
    }
    return viewModel;
  }

Your initState function would then become:

@override
  void initState() {
    super.initState();
    _viewModel = _initializeDeckAndFlashcards(); // _viewModel is a Future!
  }

Finally, you would use a FutureBuilder to build your widget. FutureBuilder provides an AsyncSnapshot to check the current state of the future so that you can display for example a loading indicator, the actual data, or an error message.

Widget build(BuildContext context) {
    return FutureBuilder<FlashcardViewModel>(
        future: _viewModel
        builder: (BuildContext context, AsyncSnapshot<FlashcardViewModel> snapshot) {
          if (snapshot.hasData) {
            final viewModel = snapshot.data;
            // ... build your widget
            // Refactor e.g. _buildHeader(context) -> _buildHeader(context, viewmodel) 
           
              

See also this question, dealing with a similar problem in a different context.

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.