Flattern: Wie baue ich ein Quizspiel?

UPDATE (01/06/2019): Eine alternative Version mit dem Rebuilder-Paket finden Sie hier.

Einführung

In diesem Artikel möchte ich Ihnen zeigen, wie ich dieses Beispiel für ein Quizspiel mit Flutter und dem Paket frideos erstellt habe (sehen Sie sich diese beiden Beispiele an, um zu erfahren, wie es funktioniert, Beispiel 1, Beispiel 2). Es ist ein ziemlich einfaches Spiel, aber es deckt verschiedene interessante Argumente ab.

Die App verfügt über vier Bildschirme:

  • Eine Hauptseite, auf der der Benutzer eine Kategorie auswählt und das Spiel startet.
  • Eine Einstellungsseite, auf der der Benutzer die Anzahl der Fragen, den Datenbanktyp (lokal oder remote), das Zeitlimit für jede Frage und den Schwierigkeitsgrad auswählen kann.
  • Eine Trivia-Seite, auf der die Fragen, die Punktzahl, die Anzahl der Korrekturen, Fehler und nicht beantworteten Fragen angezeigt werden.
  • Eine Zusammenfassungsseite, auf der alle Fragen mit den richtigen / falschen Antworten angezeigt werden.

Dies ist das Endergebnis:

Du kannst hier ein besseres GIF sehen.
  • Teil 1: Projekteinrichtung
  • Teil 2: App-Architektur
  • Teil 3: API und JSON
  • Teil 4: Homepage und andere Bildschirme
  • Teil 5: TriviaBloc
  • Teil 6: Animationen
  • Teil 7: Zusammenfassungsseite
  • Fazit
Teil 1 - Projekteinrichtung

1 - Erstelle ein neues Flatterprojekt:

flattere erstelle deinen_projektnamen

2 - Bearbeiten Sie die Datei „pubspec.yaml“ und fügen Sie die http- und frideos-Pakete hinzu:

Abhängigkeiten:
  flattern:
    sdk: flattern
  http: ^ 0.12.0
  frideos: ^ 0.6.0

3- Löschen Sie den Inhalt der Datei main.dart

4- Erstellen Sie die Projektstruktur wie folgt:

Strukturdetails

  • API: Hier finden Sie die Dart-Dateien für die API der „Open Trivia Database“ und eine nachgebildete API für lokale Tests: api_interface.dart, mock_api.dart, trivia_api.dart.
  • Blöcke: der Ort des einzigen BLoC der App trivia_bloc.dart.
  • Modelle: appstate.dart, category.dart, models.dart, question.dart, theme.dart, trivia_stats.dart.
  • Bildschirme: main_page.dart, settings_page.dart, summary_page.dart, trivia_page.dart.
Teil 2 - App-Architektur

In meinem letzten Artikel habe ich verschiedene Methoden zum Senden und Freigeben von Daten für mehrere Widgets und Seiten beschrieben. In diesem Fall verwenden wir einen etwas fortgeschritteneren Ansatz: Eine Instanz einer Singleton-Klasse mit dem Namen appState wird dem Widgets-Baum mithilfe eines InheritedWidget-Providers (AppStateProvider) zur Verfügung gestellt, der den Status der App enthält, einige Unternehmen Logik und die Instanz des einzigen BLoC, der den „Quiz-Teil“ der App verwaltet. Am Ende wird es also eine Art Mischung zwischen dem Singleton- und dem BLoC-Muster sein.

In jedem Widget können Sie die Instanz der AppState-Klasse abrufen, indem Sie Folgendes aufrufen:

final appState = AppStateProvider.of  (Kontext);

1 - main.dart

Dies ist der Einstiegspunkt der App. Die Klasse Appist ein zustandsloses Widget, in dem sie als Instanz der AppState-Klasse deklariert und mithilfe des AppStateProviders für den Widgets-Baum bereitgestellt wird. Die AppState-Instanz wird in der dispose-Methode der AppStateProvider-Klasse entsorgt, wobei alle Streams geschlossen werden.

Das MaterialApp-Widget ist in ein ValueBuilder-Widget eingebunden, sodass bei jeder Auswahl eines neuen Themas der gesamte Widgets-Baum neu erstellt und das Thema aktualisiert wird.

2 - Staatsverwaltung

Wie bereits erwähnt, enthält die AppState-Instanz den Status der App. Diese Klasse wird verwendet für:

  • Einstellungen: aktuell verwendetes Theme, laden / speichern mit den SharedPreferences. API-Implementierung, mock oder remote (unter Verwendung der API von opentdb.com). Die für jede Frage festgelegte Zeit.
  • Anzeigen der aktuellen Registerkarte: Hauptseite, Wissenswertes, Zusammenfassung.
  • Laden Sie die Fragen.
  • (wenn auf der Remote-API) Speichern Sie die Einstellungen für Kategorie, Nummer und Schwierigkeitsgrad der Fragen.

Im Konstruktor der Klasse:

  • _createThemes erstellt die Themen der App.
  • _loadCategories lädt die Kategorien der Fragen, die im Dropdown-Menü der Hauptseite ausgewählt werden sollen.
  • countdown ist ein StreamedTransformed des frideos-Pakets vom Typ , mit dem aus dem Textfeld der Wert zum Festlegen des Countdowns abgerufen wird.
  • questionsAmount enthält die Anzahl der Fragen, die während des Quizspiels angezeigt werden sollen (standardmäßig 5).
  • Die Instanz von classTriviaBloc wird initialisiert. Die Streams verarbeiten den Countdown, die Liste der Fragen und die anzuzeigende Seite.
Teil 3 - API und JSON

Damit der Benutzer zwischen einer lokalen und einer entfernten Datenbank wählen kann, habe ich die QuestionApi-Schnittstelle mit zwei Methoden und zwei Klassen erstellt, die sie implementieren: MockApi und TriviaApi.

abstrakte Klasse FragenAPI {
  Zukünftige  getCategories (StreamedList  -Kategorien);
  
  Future  getQuestions (
    Fragen zu {StreamedList ,
     int nummer,
     Kategorie Kategorie,
     Schwierigkeitsgrad,
     QuestionType type});
}

Die MockApi-Implementierung ist standardmäßig im AppState festgelegt (kann auf der Einstellungsseite der App geändert werden):

// API
QuestionsAPI api = MockAPI ();
final apiType = StreamedValue  (initialData: ApiType.mock);

Während apiTyp nur eine Aufzählung für das Ändern der Datenbank auf der Einstellungsseite ist:

enum ApiType {mock, remote}

mock_api.dart:

trivia_api.dart:

1 - API-Auswahl

Auf der Einstellungsseite kann der Benutzer über ein Dropdown-Menü auswählen, welche Datenbank verwendet werden soll:

ValueBuilder  (
  gestreamt: appState.apiType,
  Erbauer: (Kontext, Momentaufnahme) {
    return DropdownButton  (
      value: snapshot.data,
      onChanged: appState.setApiType,
      Artikel: [
        const DropdownMenuItem  (
          value: ApiType.mock,
          Kind: Text ("Demo"),
        ),
        const DropdownMenuItem  (
          value: ApiType.remote,
          Kind: Text (‘opentdb.com’),
       ),
    ]);
}),

Jedes Mal, wenn eine neue Datenbank ausgewählt wird, ändert die Methode setApiType die Implementierung der API und die Kategorien werden aktualisiert.

void setApiType (ApiType-Typ) {
  if (apiType.value! = type) {
    apiType.value = type;
    if (type == ApiType.mock) {
      api = MockAPI ();
    } else {
      api = TriviaAPI ();
    }
    _loadCategories ();
  }
}

2 - Kategorien

Um die Liste der Kategorien zu erhalten, rufen wir diese URL auf:

https://opentdb.com/api_category.php

Auszug der Antwort:

{"trivia_categories": [{"id": 9, "name": "Allgemeinwissen"}, {"id": 10, "name": "Unterhaltung: Bücher"}]

Nach dem Dekodieren des JSON mit der jsonDecode-Funktion von dart: convert library:

final jsonResponse = convert.jsonDecode (response.body);

Wir haben diese Struktur:

  • jsonResponse ['trivia_categories']: Liste der Kategorien
  • jsonResponse ['trivia_categories'] [INDEX] ['id']: ID der Kategorie
  • jsonResponse ['trivia_categories'] [INDEX] ['name']: Name der Kategorie

Das Modell wird also sein:

Klasse Kategorie {
  Kategorie ({this.id, this.name});
  factory Category.fromJson (Map  json) {
    Rückgabekategorie (id: json [‘id’], name: json [’name’]);
  }
  int id;
  String name;
}

3 - Fragen

Wenn wir diese URL aufrufen:

https://opentdb.com/api.php?amount=2&difficulty=medium&type=multiple

Das wird die Antwort sein:

{"response_code": 0, "results": [{"category": "Entertainment: Music", "type": "multiple", "difficult": "medium", "question": "What French artist \ / band ist bekannt für das Spielen auf dem MIDI-Instrument "Launchpad"? "," correct_answer ":" Madeon "," wrong_answers ": [" Daft Punk "," Disclosure "," David Guetta "]}, {" category ":" Sport "," Typ ":" Mehrfach "," Schwierigkeitsgrad ":" Mittel "," Frage ":" Wer hat die nationale Meisterschaft der College Football Playoffs (CFP) 2015 gewonnen? "," Correct_answer ":" Ohio State Buckeyes "," wrong_answers ": [" Alabama Crimson Tide "," Clemson Tigers "," Wisconsin Badgers "]}]}

In diesem Fall haben wir beim Dekodieren von JSON die folgende Struktur:

  • jsonResponse ['results']: Liste der Fragen.
  • jsonResponse ['results'] [INDEX] ['category']: Die Kategorie der Frage.
  • jsonResponse ['results'] [INDEX] ['type']: Art der Frage, multiple oder boolean.
  • jsonResponse ['results'] [INDEX] ['question']: die Frage.
  • jsonResponse ['results'] [INDEX] ['correct_answer']: die richtige Antwort.
  • jsonResponse ['results'] [INDEX] ['wrong_answers']: Liste der falschen Antworten.

Modell:

Klasse QuestionModel {
  QuestionModel ({this.question, this.correctAnswer, this.incorrectAnswers});
  factory QuestionModel.fromJson (Map  json) {
    return QuestionModel (
      frage: json [‘frage’],
      correctAnswer: json [‘correct_answer’],
      wrongAnswers: (json [‘wrong_answers’] als Liste)
        .map ((answer) => answer.toString ())
        .auflisten());
  }
  String-Frage;
  String correctAnswer;
  List  wrongAnswers;
}

4 - TriviaApi Klasse

Die Klasse implementiert die beiden Methoden der QuestionsApi-Schnittstelle, getCategories und getQuestions:

  • Kategorien abrufen

Im ersten Teil wird der JSON-Code mithilfe des Modells decodiert. Anschließend wird eine Liste mit dem Typ "Category" analysiert. Das Ergebnis wird dann den Kategorien übergeben (eine Streaming-Liste mit dem Typ "Category", mit der die Kategorienliste auf der Hauptseite ausgefüllt wird ).

final jsonResponse = convert.jsonDecode (response.body);
Endergebnis = (jsonResponse [‘trivia_categories’] als Liste)
.map ((category) => Category.fromJson (category));
categories.value = [];
Kategorien
..addAll (Ergebnis)
..addElement (Kategorie (ID: 0, Name: „Beliebige Kategorie“));
  • Fragen bekommen

Ähnliches passiert für die Fragen, aber in diesem Fall verwenden wir ein Modell (Frage), um die ursprüngliche Struktur (QuestionModel) des JSON in eine bequemere Struktur für die Verwendung in der App umzuwandeln.

final jsonResponse = convert.jsonDecode (response.body);
Endergebnis = (jsonResponse [‘results’] as List)
.map ((question) => QuestionModel.fromJson (question));
questions.value = result
.map ((question) => Question.fromQuestionModel (question))
.auflisten();

5 - Fragenklasse

Wie im vorherigen Absatz erwähnt, verwendet die App eine andere Struktur für die Fragen. In dieser Klasse haben wir vier Eigenschaften und zwei Methoden:

Klasse Frage {
  Question ({this.question, this.answers, this.correctAnswerIndex});
  factory Question.fromQuestionModel (QuestionModel-Modell) {
    endgültige Liste  answers = []
      ..add (model.correctAnswer)
      ..addAll (model.incorrectAnswers)
      ..Mischen();
    final index = answers.indexOf (model.correctAnswer);
    return Question (Frage: model.question, Antworten: answers, correctAnswerIndex: index);
  }
  String-Frage;
  List  answers;
  int correctAnswerIndex;
  int chosenAnswerIndex;
  bool isCorrect (String-Antwort) {
    return answers.indexOf (answer) == correctAnswerIndex;
  }
  bool isChosen (String-Antwort) {
    return answers.indexOf (answer) == chosenAnswerIndex;
  }
}

In der Fabrik wird die Liste der Antworten zuerst mit allen Antworten gefüllt und dann gemischt, so dass die Reihenfolge immer unterschiedlich ist. Hier erhalten wir sogar den Index der richtigen Antwort, damit wir ihn über den Fragekonstruktor dem korrekten Antwortindex zuordnen können. Die beiden Methoden werden verwendet, um zu bestimmen, ob die als Parameter übergebene Antwort die richtige oder die ausgewählte Antwort ist (sie werden in einem der nächsten Absätze genauer erläutert).

Teil 4 - Homepage und andere Bildschirme

1 - HomePage-Widget

In AppState wird eine Eigenschaft mit dem Namen tabControll angezeigt, die ein Streaming-Wert vom Typ AppTab (eine Aufzählung) ist und zum Streamen der Seite verwendet wird, die im HomePage-Widget angezeigt werden soll (statuslos). Dies funktioniert folgendermaßen: Jedes Mal, wenn ein anderes AppTabis-Set erstellt wird, erstellt das ValueBuilder-Widget den Bildschirm mit der neuen Seite neu.

  • HomePage-Klasse:
Widget-Erstellung (BuildContext-Kontext) {
  final appState = AppStateProvider.of  (Kontext);
  
  return ValueBuilder (
    gestreamt: appState.tabController,
    Erbauer: (Kontext, Momentaufnahme) => Gerüst (
      appBar: snapshot.data! = AppTab.main? null: AppBar (),
      Schublade: DrawerWidget (),
      body: _switchTab (snapshot.data, appState),
      ),
  );
}

N.B. In diesem Fall wird die AppBar nur auf der Hauptseite angezeigt.

  • _switchTab-Methode:
Widget _switchTab (Registerkarte AppTab, AppState appState) {
  Schalter (Tab) {
    case AppTab.main:
      return MainPage ();
      brechen;
    case AppTab.trivia:
      return TriviaPage ();
      brechen;
    case AppTab.summary:
      return SummaryPage (stats: appState.triviaBloc.stats);
      brechen;
    Standard:
    return MainPage ();
  }
}

2 - Einstellungsseite

Auf der Seite Einstellungen können Sie die Anzahl der anzuzeigenden Fragen, den Schwierigkeitsgrad, die Zeitdauer für den Countdown und den zu verwendenden Datenbanktyp auswählen. Auf der Hauptseite kannst du dann eine Kategorie auswählen und schließlich das Spiel starten. Für jede dieser Einstellungen verwende ich einen StreamedValue, damit das ValueBuilder-Widget die Seite jedes Mal aktualisieren kann, wenn ein neuer Wert festgelegt wird.

Teil 5 - TriviaBloc

Die Geschäftslogik der App befindet sich im einzigen BLoC mit dem Namen TriviaBloc. Lassen Sie uns diese Klasse untersuchen.

Im Konstruktor haben wir:

TriviaBloc ({this.countdownStream, this.questions, this.tabController}) {
// Fragen von der API abrufen
  questions.onChange ((data) {
    if (data.isNotEmpty) {
      letzte Fragen = Daten..shuffle ();
     _startTrivia (Fragen);
    }
  });
  countdownStream.outTransformed.listen ((data) {
     Countdown = int.parse (Daten) * 1000;
  });
}

Hier wartet die Eigenschaft questions (eine StreamedList vom Typ Question) auf Änderungen, wenn eine Liste von Fragen an den Stream gesendet wird, und die Methode _startTrivia wird aufgerufen, um das Spiel zu starten.

Stattdessen wartet der countdownStream auf der Seite "Einstellungen" nur auf Änderungen des Countdown-Werts, damit die in der TriviaBloc-Klasse verwendete Countdown-Eigenschaft aktualisiert werden kann.

  • _startTrivia (List data)

Diese Methode startet das Spiel. Grundsätzlich wird der Status der Eigenschaften zurückgesetzt, die erste anzuzeigende Frage festgelegt und nach einer Sekunde die playTrivia-Methode aufgerufen.

void _startTrivia (List  data) {
  Index = 0;
  triviaState.value.questionIndex = 1;
  // Zum Anzeigen der Schaltflächen Hauptseite und Zusammenfassung
  triviaState.value.isTriviaEnd = false;
  // Setze die Statistiken zurück
  stats.reset ();
  // Um ​​die erste Frage zu stellen (in diesem Fall den Countdown
  // Balkenanimation startet nicht).
  currentQuestion.value = data.first;
  Timer (Dauer (Millisekunden: 1000), () {
    // Setzen Sie dieses Flag auf true, wenn Sie die Frage ändern
    // Die Countdown-Balkenanimation startet.
    triviaState.value.isTriviaPlaying = true;
  
    // Die erste Frage erneut mit der Countdown-Leiste streamen
    // Animation.
    currentQuestion.value = data [index];
  
    playTrivia ();
  });
}

triviaState ist ein StreamedValue vom Typ TriviaState, eine Klasse, die zum Behandeln des Status der Trivia verwendet wird.

class TriviaState {
  bool isTriviaPlaying = false;
  bool isTriviaEnd = false;
  bool isAnswerChosen = false;
  int questionIndex = 1;
}
  • playTrivia ()

Wenn diese Methode aufgerufen wird, aktualisiert ein Timer den Timer regelmäßig und überprüft, ob die verstrichene Zeit größer als die Countdown-Einstellung ist. In diesem Fall bricht er den Timer ab, markiert die aktuelle Frage als nicht beantwortet und ruft die _nextQuestion-Methode auf, um eine neue Frage anzuzeigen .

nichtig playTrivia () {
  timer = Timer.periodic (Dauer (Millisekunden: refreshTime), (Timer t) {
    currentTime.value = refreshTime * t.tick;
    if (currentTime.value> countdown) {
      currentTime.value = 0;
      timer.cancel ();
      notAnswered (currentQuestion.value);
     _nächste Frage();
    }
  });
}
  • nicht beantwortet (Frage Frage)

Diese Methode ruft die Methode addNoAnswer der Statistikinstanz der TriviaStats-Klasse für jede Frage ohne Antwort auf, um die Statistiken zu aktualisieren.

nichtig nicht beantwortet (Frage Frage) {
  stats.addNoAnswer (question);
}
  • _nächste Frage()

Bei dieser Methode wird der Index der Fragen erhöht. Wenn die Liste weitere Fragen enthält, wird eine neue Frage an den Stream currentQuestion gesendet, damit der ValueBuilder die Seite mit der neuen Frage aktualisiert. Andernfalls wird die _endTriva-Methode aufgerufen und das Spiel beendet.

void _nextQuestion () {
  index ++;
   if (index 
  • endTrivia ()

Hier wird der Timer abgebrochen und das Flag isTriviaEnd auf true gesetzt. Nach 1,5 Sekunden nach Spielende wird die Übersichtsseite angezeigt.

void _endTrivia () {
  // RESET
  timer.cancel ();
  currentTime.value = 0;
  triviaState.value.isTriviaEnd = true;
  triviaState.refresh ();
  stopTimer ();
  Timer (Dauer (Millisekunden: 1500), () {
     // Hier wird dies zurückgesetzt, um den Start von nicht auszulösen
     // Countdown-Animation, während auf die Zusammenfassungsseite gewartet wird.
     triviaState.value.isAnswerChosen = false;
     // Zeige die Übersichtsseite nach 1.5s
     tabController.value = AppTab.summary;
     // Lösche die letzte Frage, damit sie nicht erscheint
     // im nächsten Spiel
     currentQuestion.value = null;
  });
}
  • checkAnswer (Frage Frage, String Antwort)

Wenn der Benutzer auf eine Antwort klickt, prüft diese Methode, ob sie korrekt ist, und ruft die Methode auf, um den Statistiken eine positive oder negative Punktzahl hinzuzufügen. Dann wird der Timer zurückgesetzt und eine neue Frage geladen.

void checkAnswer (Fragenfrage, Stringantwort) {
  if (! triviaState.value.isTriviaEnd) {
     question.chosenAnswerIndex = question.answers.indexOf (Antwort);
     if (question.isCorrect (answer)) {
       stats.addCorrect (question);
     } else {
       stats.addWrong (question);
     }
     timer.cancel ();
     currentTime.value = 0;
    _nächste Frage();
  }
}
  • stopTimer ()

Wenn diese Methode aufgerufen wird, wird die Zeit abgebrochen und das Flag isAnswerChosen auf true gesetzt, um CountdownWidget anzuweisen, die Animation zu stoppen.

void stopTimer () {
  // Timer anhalten
  timer.cancel ();
  // Wenn Sie dieses Flag auf true setzen, wird die Countdown-Animation gestoppt
  triviaState.value.isAnswerChosen = true;
  triviaState.refresh ();
}
  • onChosenAnswer (String-Antwort)

Wenn eine Antwort ausgewählt wird, wird der Timer abgebrochen und der Index der Antwort in der Eigenschaft chosenAnswerIndex der answersAnimation-Instanz der AnswerAnimation-Klasse gespeichert. Dieser Index wird verwendet, um diese Antwort auf den Widgets-Stapel zu setzen, um zu vermeiden, dass sie von allen anderen Antworten abgedeckt wird.

void onChosenAnswer (String answer) {
  selectedAnswer = answer;
  stopTimer ();
  // Setze die gewählte Antwort so, dass das Antwort-Widget sie auf die letzte setzen kann
  // Stapel.
  
  answersAnimation.value.chosenAnswerIndex =
  currentQuestion.value.answers.indexOf (answer);
  answersAnimation.refresh ();
}

AnswerAnimation-Klasse:

Klasse AnswerAnimation {
  AnswerAnimation ({this.chosenAnswerIndex, this.startPlaying});
  int chosenAnswerIndex;
  bool startPlaying = false;
}
  • onChosenAnswerAnimationEnd ()

Wenn die Animation der Antworten endet, wird das Flag isAnswerChosen auf false gesetzt, damit die Countdown-Animation erneut gestartet werden kann. Anschließend wird die checkAnswer-Methode aufgerufen, um zu überprüfen, ob die Antwort korrekt ist.

void onChosenAnwserAnimationEnd () {
  // Setzen Sie das Flag zurück, damit die Countdown-Animation gestartet werden kann
  triviaState.value.isAnswerChosen = false;
  triviaState.refresh ();
  checkAnswer (currentQuestion.value, chosenAnswer);
}
  • TriviaStats-Klasse

Die Methoden dieser Klasse werden verwendet, um die Punktzahl zuzuweisen. Wenn der Benutzer die richtige Antwort auswählt, wird die Punktzahl um zehn Punkte erhöht und die aktuellen Fragen zur Korrekturliste hinzugefügt, sodass diese auf der Zusammenfassungsseite angezeigt werden können. Wenn eine Antwort nicht korrekt ist, wird die Punktzahl um vier verringert Ohne Antwort wird die Punktzahl um zwei Punkte verringert.

class TriviaStats {
  TriviaStats () {
    korrigiert = [];
    falsch = [];
    noAnswered = [];
    score = 0;
  }
Liste  korrigiert;
  List  wrongs;
  Liste  noAnswered;
  int score;
void addCorrect (Fragenfrage) {
    corrects.add (frage);
    Punktzahl + = 10;
  }
void addWrong (Fragenfrage) {
    wrongs.add (frage);
    Punktzahl - = 4;
  }
nichtig addNoAnswer (Frage Frage) {
    noAnswered.add (Frage);
    Punktzahl - = 2;
  }
void reset () {
    korrigiert = [];
    falsch = [];
    noAnswered = [];
    score = 0;
  }
}
Teil 6 - Animationen

In dieser App gibt es zwei Arten von Animationen: Die animierte Leiste unter den Antworten gibt die verbleibende Zeit für die Antwort an und die Animation, die abgespielt wird, wenn eine Antwort ausgewählt wird.

1 - Countdown-Balkenanimation

Dies ist eine ziemlich einfache Animation. Das Widget verwendet als Parameter die Breite des Balkens, die Dauer und den Status des Spiels. Die Animation startet jedes Mal, wenn das Widget neu erstellt wird, und stoppt, wenn eine Antwort ausgewählt wird.

Die anfängliche Farbe ist grün und wechselt allmählich zu rot, um das Ende der Zeit anzuzeigen.

2 - Antworten Animation

Diese Animation wird jedes Mal gestartet, wenn eine Antwort ausgewählt wird. Mit einer einfachen Berechnung der Position der Antworten wird jede schrittweise an die Position der ausgewählten Antwort verschoben. Damit die gewählte Antwort oben im Stapel bleibt, wird diese mit dem letzten Element der Widgets-Liste ausgetauscht.

// Tauschen Sie das letzte Element mit dem ausgewählten Antwortelement aus, damit dies möglich ist
// als letzter auf dem Stapel angezeigt werden.
final last = widgets.last;
final chosen = widgets [widget.answerAnimation.chosenAnswerIndex]; final chosenIndex = widgets.indexOf (gewählt);
widgets.last = gewählt;
Widgets [chosenIndex] = last;
Rückgabecontainer (
   Kind: Stack (
      Kinder: Widgets,
   ),
);

Die Farbe der Kästchen wird grün, wenn die Antwort richtig ist, und rot, wenn sie falsch ist.

var newColor;
if (isCorrect) {
  newColor = Colors.green;
} else {
  newColor = Colors.red;
}
colorAnimation = ColorTween (
  begin: answerBoxColor,
  Ende: newColor,
) .animate (Controller);
warte auf controller.forward ();
Teil 7 - Zusammenfassungsseite

1 - SummaryPage

Diese Seite verwendet als Parameter eine Instanz der TriviaStats-Klasse, die die Liste der korrekten, falschen und nicht ausgewählten Fragen enthält, und erstellt eine ListView, in der jede Frage an der richtigen Stelle angezeigt wird. Die aktuelle Frage wird dann an das Widget SummaryAnswers übergeben, das die Liste der Antworten erstellt.

2 - ZusammenfassungAntworten

Dieses Widget verwendet als Parameter den Index der Frage und die Frage selbst und erstellt die Liste der Antworten. Die richtige Antwort ist grün markiert. Wenn der Benutzer eine falsche Antwort ausgewählt hat, wird diese rot hervorgehoben und zeigt sowohl die richtigen als auch die falschen Antworten an.

Fazit

Dieses Beispiel ist weit davon entfernt, perfekt oder endgültig zu sein, aber es kann ein guter Ausgangspunkt für die Arbeit sein. Zum Beispiel kann es verbessert werden, indem eine Statistikseite mit den Ergebnissen jedes gespielten Spiels oder ein Abschnitt erstellt wird, in dem der Benutzer benutzerdefinierte Fragen und Kategorien erstellen kann (dies kann eine großartige Übung sein, um mit Datenbanken zu üben). Hoffe, dies kann nützlich sein, zögern Sie nicht, Verbesserungen, Vorschläge oder anderes vorzuschlagen.

Sie finden den Quellcode in diesem GitHub-Repository.