Flutter: jak zbudować quiz

AKTUALIZACJA (01.06.2019): alternatywną wersję można znaleźć, korzystając z pakietu odbudowy tutaj.

Wprowadzenie

W tym artykule chciałbym pokazać, jak zbudowałem ten przykład ciekawostki z Flutter i pakietem frideos (zapoznaj się z tymi dwoma przykładami, aby dowiedzieć się, jak to działa przykład 1, przykład 2). Jest to dość prosta gra, ale obejmuje różne interesujące argumenty.

Aplikacja ma cztery ekrany:

  • Strona główna, na której użytkownik wybiera kategorię i rozpoczyna grę.
  • Strona ustawień, na której użytkownik może wybrać liczbę pytań, typ bazy danych (lokalny lub zdalny), limit czasowy dla każdego pytania i poziom trudności.
  • Strona z ciekawostkami, na której wyświetlane są pytania, wynik, liczba poprawek, pomyłek i brak odpowiedzi.
  • Strona podsumowująca, która pokazuje wszystkie pytania z poprawnymi / błędnymi odpowiedziami.

Oto wynik końcowy:

Tutaj możesz zobaczyć lepszy gif.
  • Część 1: Konfiguracja projektu
  • Część 2: Architektura aplikacji
  • Część 3: API i JSON
  • Część 4: Strona główna i inne ekrany
  • Część 5: TriviaBloc
  • Część 6: Animacje
  • Część 7: Strona podsumowania
  • Wniosek
Część 1 - Przygotowanie projektu

1 - Utwórz nowy projekt trzepotania:

trzepotać utwórz swoją nazwę projektu

2 - Edytuj plik „pubspec.yaml” i dodaj pakiety http i frideos:

zależności:
  trzepotanie:
    sdk: trzepotanie
  http: ^ 0.12.0
  frideos: ^ 0,6

3- Usuń zawartość pliku main.dart

4- Utwórz strukturę projektu jako następujący obraz:

Szczegóły struktury

  • API: tutaj będą pliki dart do obsługi interfejsu API „Otwartej bazy danych ciekawostek” oraz pozorny interfejs API do testowania lokalnego: api_interface.dart, mock_api.dart, trivia_api.dart.
  • Bloki: miejsce jedynego BLoC aplikacji trivia_bloc.dart.
  • Modele: appstate.dart, category.dart, models.dart, question.dart, theme.dart, trivia_stats.dart.
  • Ekrany: main_page.dart, settings_page.dart, summary_page.dart, trivia_page.dart.
Część 2 - Architektura aplikacji

W ostatnim artykule pisałem o różnych sposobach wysyłania i udostępniania danych w wielu widżetach i stronach. W tym przypadku zastosujemy nieco bardziej zaawansowane podejście: wystąpienie klasy singleton o nazwie appState zostanie dostarczone do drzewa widżetów za pomocą dostawcy InheritedWidget (AppStateProvider), który będzie utrzymywał stan aplikacji, niektóre firmy logika i instancja jedynego BLoC, który obsługuje „część quizu” aplikacji. Tak więc ostatecznie będzie to rodzaj mieszanki singletonu i wzorca BLoC.

Wewnątrz każdego widżetu można uzyskać instancję klasy AppState, wywołując:

final appState = AppStateProvider.of  (w kontekście);

1 - dart główny

To jest punkt wejścia aplikacji. Klasa App jest bezstanowym widgetem, w którym jest zadeklarowana jako instancja klasy AppState i gdzie za pomocą AppStateProvider jest ona następnie przekazywana do drzewa widgetów. Instancja appState zostanie usunięta, zamykając wszystkie strumienie, w metodzie dispose klasy AppStateProvider.

Widżet MaterialApp jest zawinięty w widżet ValueBuilder, dzięki czemu za każdym razem, gdy wybierany jest nowy motyw, całe drzewo widżetów odbudowuje się, aktualizując motyw.

2 - zarządzanie państwem

Jak powiedziano wcześniej, instancja appState przechowuje stan aplikacji. Ta klasa będzie używana do:

  • Ustawienia: aktualnie używany motyw, wczytaj / zapisz go z SharedPreferences. Implementacja interfejsu API, próbna lub zdalna (przy użyciu interfejsu API z opentdb.com). Czas ustalony dla każdego pytania.
  • Wyświetlanie bieżącej karty: strona główna, ciekawostki, podsumowanie.
  • Ładowanie pytań.
  • (jeśli na zdalnym interfejsie API) Zapisz ustawienia kategorii, liczby i stopnia trudności pytań.

W konstruktorze klasy:

  • _createThemes buduje motywy aplikacji.
  • _loadKategorie ładują kategorie pytań, które mają zostać wybrane z menu głównego strony.
  • odliczanie to StreamedTransformed pakietu frideos typu , używany do pobierania z pola tekstowego wartości do ustawienia odliczania.
  • questionAmount przechowuje liczbę pytań, które zostaną wyświetlone podczas gry z ciekawostkami (domyślnie 5).
  • Instancja classTriviaBloc jest inicjowana, przekazując do niej strumienie obsługujące odliczanie, listę pytań i stronę do wyświetlenia.
Część 3 - API i JSON

Aby umożliwić użytkownikowi wybór między lokalną i zdalną bazą danych, stworzyłem interfejs QuestionApi za pomocą dwóch metod i dwóch klas, które go implementują: MockApi i TriviaApi.

pytania abstrakcyjne klasyAPI {
  Przyszłe  getCategories (StreamedList  kategorie);
  
  Przyszłe  getQuestions (
    {StreamedList  pytania,
     liczba całkowita,
     Kategoria kategorii,
     Pytanie Trudność,
     Typ pytania});
}

Implementacja MockApi jest ustawiona domyślnie (można ją zmienić na stronie ustawień aplikacji) w appState:

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

Podczas gdy apiType jest tylko wyliczeniem do obsługi zmiany bazy danych na stronie ustawień:

enum ApiType {mock, remote}

mock_api.dart:

trivia_api.dart:

1 - Wybór interfejsu API

Na stronie ustawień użytkownik może wybrać bazę danych, która ma być używana, poprzez menu rozwijane:

ValueBuilder  (
  streamowane: appState.apiType,
  builder: (kontekst, migawka) {
    return DropdownButton  (
      wartość: snapshot.data,
      onChanged: appState.setApiType,
      przedmiotów: [
        const DropdownMenuItem  (
          wartość: ApiType.mock,
          child: Text („Demo”),
        ),
        const DropdownMenuItem  (
          wartość: ApiType.remote,
          dziecko: tekst („opentdb.com”),
       ),
    ]);
}),

Za każdym razem, gdy wybierana jest nowa baza danych, metoda setApiType zmienia implementację interfejsu API, a kategorie są aktualizowane.

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

2 - Kategorie

Aby uzyskać listę kategorii, nazywamy ten adres URL:

https://opentdb.com/api_category.php

Wyciąg odpowiedzi:

{„trivia_categories”: [{„id”: 9, „name”: „General Knowledge”}, {„id”: 10, „name”: „Entertainment: Books”}]

Po dekodowaniu JSON przy użyciu funkcji jsonDecode biblioteki dart: convert:

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

mamy tę strukturę:

  • jsonResponse ['trivia_categories']: lista kategorii
  • jsonResponse ['trivia_categories'] [INDEX] ['id']: id kategorii
  • jsonResponse ['trivia_categories'] [INDEX] ['name']: nazwa kategorii

Model będzie więc:

Kategoria klasy {
  Kategoria ({this.id, this.name});
  factory Category.fromJson (Mapa  json) {
    return Category (id: json [„id”], name: json [„name”]);
  }
  int id;
  Nazwa łańcucha;
}

3 - pytania

Jeśli nazwiemy ten adres URL:

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

to będzie odpowiedź:

{„response_code": 0, „results”: [{„category”: „Entertainment: Music”, „type”: „multiple”, „trudność”: „medium”, „pytanie”: „Jaki francuski artysta / zespół” jest znany z grania na instrumencie midi „Launchpad”? ”,„ correct_answer ”:„ Madeon ”,„ invalid_answers ”: [„ Daft Punk ”,„ Disclosure ”,„ David Guetta ”]}, {„ category ”:„ Sport ”,„ typ ”:„ wielokrotność ”,„ trudność ”:„ średni ”,„ pytanie ”:„ Kto wygrał Mistrzostwa Krajowe College Football Playoff (CFP) 2015? ”,„ Correct_answer ”:„ Ohio State Buckeyes ”,„ invalid_answers ": [„ „Alabama Crimson Tide”, „Clemson Tigers”, „Wisconsin Badgers”]}]}

W tym przypadku dekodując JSON mamy następującą strukturę:

  • jsonResponse [„wyniki”]: lista pytań.
  • jsonResponse [„wyniki”] [INDEKS] [„kategoria”]: kategoria pytania.
  • jsonResponse [„wyniki”] [INDEKS] [„typ”]: typ pytania, wielokrotność lub wartość logiczna.
  • jsonResponse [„wyniki”] [INDEKS] [„pytanie”]: pytanie.
  • jsonResponse ['results'] [INDEX] ['correct_answer']: poprawna odpowiedź.
  • jsonResponse ['Wyniki'] [INDEKS] ['Nieprawidłowe odpowiedzi']: lista niepoprawnych odpowiedzi.

Model:

klasa QuestionModel {
  QuestionModel ({this.question, this.correctAnswer, this.incorrectAnswers});
  Pytanie FactoryModel.fromJson (mapa  json) {
    return QuestionModel (
      pytanie: json [„pytanie”],
      correctAnswer: json [„correct_answer”],
      Nieprawidłowe odpowiedzi: (json [„nieprawidłowa odpowiedź”] jako lista)
        .map ((answer) => answer.toString ())
        .notować());
  }
  Pytanie o ciąg;
  String correctAnswer;
  Lista  niepoprawnaAnswers;
}

4 - klasa TriviaApi

Klasa implementuje dwie metody interfejsu PytaniaApi, getCategories i getQuestions:

  • Uzyskiwanie kategorii

W pierwszej części JSON jest dekodowany, a następnie przy użyciu modelu, analizowany w celu uzyskania listy kategorii Kategoria, w końcu wynik jest przekazywany do kategorii (StreamedList typu kategorii używany do zapełniania listy kategorii na stronie głównej ).

final jsonResponse = convert.jsonDecode (response.body);
wynik końcowy = (jsonResponse [„trivia_categories” ”jako lista)
.map ((kategoria) => Category.fromJson (kategoria));
kategorie.value = [];
kategorie
..addAll (wynik)
..addElement (kategoria (id: 0, nazwa: „Dowolna kategoria”));
  • Uzyskiwanie pytań

Coś podobnego dzieje się w przypadku pytań, ale w tym przypadku używamy modelu (pytania), aby „przekonwertować” oryginalną strukturę (model pytań) modelu JSON na wygodniejszą strukturę do użycia w aplikacji.

final jsonResponse = convert.jsonDecode (response.body);
wynik końcowy = (jsonResponse [„wyniki” jako lista)
.map ((pytanie) => QuestionModel.fromJson (pytanie));
pytania. wartość = wynik
.map ((pytanie) => Pytanie.fromQuestionModel (pytanie))
.notować();

5 - Klasa pytań

Jak powiedziano w poprzednim akapicie, aplikacja używa innej struktury pytań. W tej klasie mamy cztery właściwości i dwie metody:

Pytanie klasowe {
  Pytanie ({this.question, this.answers, this.correctAnswerIndex});
  fabryka Question.fromQuestionModel (model QuestionModel) {
    końcowa lista  odpowiedzi = []
      ..add (model.correctAnswer)
      ..addAll (model.incorrectAnswers)
      ..człapać();
    indeks końcowy = Answer.indexOf (model.correctAnswer);
    return Pytanie (pytanie: model.question, odpowiedzi: odpowiedzi, correctAnswerIndex: indeks);
  }
  Pytanie o ciąg;
  Lista  odpowiedzi;
  int correctAnswerIndex;
  int selectedAnswerIndex;
  bool isCorrect (ciąg odpowiedzi) {
    return answer.indexOf (answer) == correctAnswerIndex;
  }
  bool isChosen (ciąg odpowiedzi) {
    return answer.indexOf (answer) == selectedAnswerIndex;
  }
}

W fabryce lista odpowiedzi jest najpierw zapełniana wszystkimi odpowiedziami, a następnie tasowana, dzięki czemu kolejność jest zawsze inna. Tutaj otrzymujemy nawet indeks poprawnej odpowiedzi, abyśmy mogli przypisać ją do correctAnswerIndex za pomocą konstruktora Pytanie. Te dwie metody służą do ustalenia, czy odpowiedź przekazana jako parametr jest poprawna, czy wybrana (zostaną one lepiej wyjaśnione w jednym z kolejnych akapitów).

Część 4 - Strona główna i inne ekrany

1 - Widżet HomePage

W AppPState można zobaczyć właściwość o nazwie tabController, która jest wartością StreamedValue typu AppTab (wyliczenie), używaną do przesyłania strumieniowego strony w celu wyświetlenia w widżecie HomePage (bezstanowy). Działa to w ten sposób: za każdym razem, gdy inny zestaw AppTabis, widget ValueBuilder odbudowuje ekran pokazujący nową stronę.

  • Klasa HomePage:
Kompilacja widgetów (kontekst BuildContext) {
  final appState = AppStateProvider.of  (w kontekście);
  
  zwraca ValueBuilder (
    streaming: appState.tabController,
    builder: (kontekst, migawka) => Scaffold (
      appBar: snapshot.data! = AppTab.main? null: AppBar (),
      drawer: DrawerWidget (),
      body: _switchTab (snapshot.data, appState),
      ),
  );
}

N.B. W takim przypadku pasek aplikacji będzie wyświetlany tylko na stronie głównej.

  • Metoda _switchTab:
Widżet _switchTab (karta AppTab, AppState appState) {
  przełącznik (karta) {
    case AppTab.main:
      return MainPage ();
      przerwa;
    sprawa AppTab.trivia:
      return TriviaPage ();
      przerwa;
    case AppTab.summary:
      return SummaryPage (stats: appState.triviaBloc.stats);
      przerwa;
    domyślna:
    return MainPage ();
  }
}

2 - Ustawienia Strona

Na stronie Ustawienia możesz wybrać liczbę pytań do wyświetlenia, trudność, ilość czasu na odliczanie i jakiego rodzaju bazy danych użyć. Na stronie głównej możesz wybrać kategorię i wreszcie rozpocząć grę. Dla każdego z tych ustawień używam StreamedValue, aby widżet ValueBuilder mógł odświeżyć stronę za każdym razem, gdy ustawiana jest nowa wartość.

Część 5 - TriviaBloc

Logika biznesowa aplikacji znajduje się w jedynym BLoC o nazwie TriviaBloc. Przyjrzyjmy się tej klasie.

W konstruktorze mamy:

TriviaBloc ({this.countdownStream, this.questions, this.tabController}) {
// Pobieranie pytań z interfejsu API
  pytania.onZmień ((dane) {
    if (data.isNotEmpty) {
      ostatnie pytania = data..shuffle ();
     _startTrivia (pytania);
    }
  });
  countdownStream.outTransformed.listen ((dane) {
     odliczanie = liczba całkowita (dane) * 1000;
  });
}

Tutaj właściwość pytania (StreamedList typu Pytanie) nasłuchuje zmian, gdy do strumienia wysyłana jest lista pytań, wywoływana jest metoda _startTrivia, uruchamiająca grę.

Zamiast tego countdownStream po prostu nasłuchuje zmian wartości odliczania na stronie Ustawienia, aby mógł zaktualizować właściwość odliczania używaną w klasie TriviaBloc.

  • _startTrivia (Lista Dane)

Ta metoda rozpoczyna grę. Zasadniczo resetuje stan właściwości, ustawia pierwsze pytanie do wyświetlenia i po jednej sekundzie wywołuje metodę playTrivia.

void _startTrivia (Lista  dane) {
  indeks = 0;
  triviaState.value.questionIndex = 1;
  // Aby wyświetlić przyciski strony głównej i podsumowania
  triviaState.value.isTriviaEnd = false;
  // Zresetuj statystyki
  stats.reset ();
  // Aby ustawić pytanie początkowe (w tym przypadku odliczanie)
  // animacja paska nie uruchomi się).
  currentQuestion.value = data.first;
  Timer (czas trwania (w milisekundach: 1000), () {
    // Ustawienie tej flagi na true po zmianie pytania
    // rozpoczyna się animacja paska odliczania.
    triviaState.value.isTriviaPlaying = true;
  
    // Ponownie przesyłaj pierwsze pytanie za pomocą paska odliczania
    // animacja.
    currentQuestion.value = data [index];
  
    playTrivia ();
  });
}

triviaState to StreamedValue typu TriviaState, klasa używana do obsługi stanu ciekawostek.

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

Po wywołaniu tej metody licznik okresowo aktualizuje licznik i sprawdza, czy upływający czas jest dłuższy niż ustawienie odliczania, w tym przypadku anuluje licznik, zaznacza bieżące pytanie jako nie udzielone odpowiedzi i wywołuje metodę _nextQuestion, aby wyświetlić nowe pytanie .

void playTrivia () {
  timer = Timer.periodic (Czas trwania (milisekundy: refreshTime), (Timer t) {
    currentTime.value = refreshTime * t.tick;
    if (currentTime.value> odliczanie) {
      currentTime.value = 0;
      timer.cancel ();
      notAnsabled (currentQuestion.value);
     _następne pytanie();
    }
  });
}
  • brak odpowiedzi (pytanie pytanie)

Ta metoda wywołuje metodę addNoAnswer instancji statystyki klasy TriviaStats dla każdego pytania bez odpowiedzi w celu zaktualizowania statystyk.

void notAnsided (Pytanie do pytania) {
  stats.addNoAnswer (pytanie);
}
  • _następne pytanie()

W tej metodzie indeks pytań jest zwiększany, a jeśli na liście znajdują się inne pytania, do strumienia currentQuestion wysyłane jest nowe pytanie, dzięki czemu ValueBuilder aktualizuje stronę o nowe pytanie. W przeciwnym razie wywoływana jest metoda _endTriva, która kończy grę.

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

W tym przypadku licznik czasu jest anulowany, a flaga ma wartość TriviaEnd i ma wartość true. Po 1,5 sekundy po zakończeniu gry wyświetlana jest strona podsumowania.

void _endTrivia () {
  // RESETOWANIE
  timer.cancel ();
  currentTime.value = 0;
  triviaState.value.isTriviaEnd = true;
  triviaState.refresh ();
  stopTimer ();
  Timer (Czas trwania (w milisekundach: 1500), () {
     // resetuje się tutaj, aby nie uruchamiać początku
     // animacja odliczania podczas oczekiwania na stronę podsumowania.
     triviaState.value.isAnswerChosen = false;
     // Pokaż stronę podsumowania po 1,5 s
     tabController.value = AppTab.summary;
     // Wyczyść ostatnie pytanie, aby się nie pojawiło
     // w następnej grze
     currentQuestion.value = null;
  });
}
  • checkAnswer (pytanie pytanie, odpowiedź ciągu)

Gdy użytkownik kliknie odpowiedź, ta metoda sprawdza, czy jest poprawna, i wywołuje metodę, aby dodać wynik dodatni lub ujemny do statystyk. Następnie licznik czasu jest resetowany i ładowane jest nowe pytanie.

void checkAnswer (pytanie pytanie, ciąg odpowiedzi) {
  if (! triviaState.value.isTriviaEnd) {
     question.chosenAnswerIndex = question.answers.indexOf (odpowiedź);
     if (question.isCorrect (answer)) {
       stats.addCorrect (pytanie);
     } else {
       stats.addWrong (pytanie);
     }
     timer.cancel ();
     currentTime.value = 0;
    _następne pytanie();
  }
}
  • stopTimer ()

Po wywołaniu tej metody czas jest anulowany, a flagą jestAnswerChosen ustawiona na wartość true, aby powiedzieć CountdownWidget, aby zatrzymał animację.

void stopTimer () {
  // Zatrzymaj stoper
  timer.cancel ();
  // Po ustawieniu tej flagi na true animacja odliczania zostanie zatrzymana
  triviaState.value.isAnswerChosen = true;
  triviaState.refresh ();
}
  • onChosenAnswer (ciąg odpowiedzi)

Po wybraniu odpowiedzi licznik czasu jest anulowany, a indeks odpowiedzi jest zapisywany we właściwości selectedAnswerIndex instancji AnsAntation klasy AnswerAnimation. Ten indeks służy do umieszczenia tej odpowiedzi na końcu widżetu, aby uniknąć pokrycia przez wszystkie pozostałe odpowiedzi.

void onChosenAnswer (ciąg odpowiedzi) {
  selectedAnswer = odpowiedź;
  stopTimer ();
  // Ustaw wybraną odpowiedź, aby widżet odpowiedzi mógł umieścić ją jako ostatnią na
  // stos.
  
  answerAnimation.value.chosenAnswerIndex =
  currentQuestion.value.answers.indexOf (odpowiedź);
  answerAnimation.refresh ();
}

Klasa AnswerAnimation:

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

Kiedy animacja odpowiedzi się kończy, flaga isAnswerChosen jest ustawiona na false, aby umożliwić odliczanie animacji od nowa, a następnie wywoływana jest metoda checkAnswer w celu sprawdzenia, czy odpowiedź jest poprawna.

void onChosenAnwserAnimationEnd () {
  // Zresetuj flagę, aby rozpocząć animację odliczania
  triviaState.value.isAnswerChosen = false;
  triviaState.refresh ();
  checkAnswer (currentQuestion.value, selectedAnswer);
}
  • Klasa TriviaStats

Metody tej klasy są używane do przypisywania wyniku. Jeśli użytkownik wybierze prawidłową odpowiedź, wynik zostanie zwiększony o dziesięć punktów, a bieżące pytania dodane do listy poprawek, aby można je było wyświetlić na stronie podsumowania, jeśli odpowiedź jest nieprawidłowa, wówczas wynik zostanie zmniejszony o cztery, w końcu jeśli brak odpowiedzi wynik jest pomniejszony o dwa punkty.

klasa TriviaStats {
  TriviaStats () {
    poprawia = [];
    krzywdy = [];
    noAns odpowiedział = [];
    wynik = 0;
  }
Lista  poprawia;
  Lista nieprawidłowości ;
  Lista  nie Odpowiedzi;
  wynik int;
void addCorrect (Pytanie do pytania) {
    corrects.add (pytanie);
    wynik + = 10;
  }
void addWrong (Pytanie do pytania) {
    errs.add (pytanie);
    wynik - = 4;
  }
void addNoAnswer (Pytanie do pytania) {
    noAnsided.add (pytanie);
    wynik - = 2;
  }
void reset () {
    poprawia = [];
    krzywdy = [];
    noAns odpowiedział = [];
    wynik = 0;
  }
}
Część 6 - Animacje

W tej aplikacji mamy dwa rodzaje animacji: animowany pasek poniżej odpowiedzi wskazuje czas pozostały do ​​odpowiedzi, a animacja odtwarzana po wybraniu odpowiedzi.

1 - Animacja paska odliczania

To dość prosta animacja. Widżet przyjmuje jako parametr szerokość paska, czas trwania i stan gry. Animacja rozpoczyna się za każdym razem, gdy widget zostanie odbudowany, i zatrzymuje się, jeśli zostanie wybrana odpowiedź.

Początkowy kolor jest zielony i stopniowo zmienia kolor na czerwony, sygnalizując, że czas się kończy.

2 - Odpowiedzi na animację

Ta animacja jest uruchamiana za każdym razem, gdy zostanie wybrana odpowiedź. Dzięki prostemu obliczeniu pozycji odpowiedzi każda z nich jest stopniowo przenoszona do pozycji wybranej odpowiedzi. Aby wybrana odpowiedź pozostała na górze stosu, zostaje ona zamieniona na ostatni element listy widżetów.

// Zamień ostatni element na wybraną odpowiedź, aby mógł
// być pokazany jako ostatni na stosie.
ostatnia ostatnia = widgets.last;
final selected = widgets [widget.answerAnimation.chosenAnswerIndex]; final selectedIndex = widgets.indexOf (wybrano);
widgets.last = wybrano;
widżety [selectedIndex] = ostatnie;
zwrot pojemnika (
   dziecko: Stack (
      dzieci: widżety,
   ),
);

Kolor pól zmienia się na zielony, jeśli odpowiedź jest poprawna, i czerwony, jeśli jest błędny.

var newColor;
if (isCorrect) {
  newColor = Colors.green;
} else {
  newColor = Colors.red;
}
colorAnimation = ColorTween (
  początek: answerBoxColor,
  end: newColor,
) .animate (kontroler);
czekaj na controller.forward ();
Część 7 - Strona podsumowująca

1 - Podsumowanie Strona

Ta strona przyjmuje jako parametr instancję klasy TriviaStats, która zawiera listę poprawnych pytań, błędów i odpowiedzi bez wybranej odpowiedzi, i buduje ListView pokazujący każde pytanie we właściwym miejscu. Bieżące pytanie jest następnie przekazywane do widżetu SummaryAnswers, który tworzy listę odpowiedzi.

2 - Podsumowanie odpowiedzi

Ten widget przyjmuje jako parametr indeks pytania i samo pytanie oraz buduje listę odpowiedzi. Prawidłowa odpowiedź jest zaznaczona na zielono, a jeśli użytkownik wybierze niepoprawną odpowiedź, ta zostanie podświetlona na czerwono, pokazując zarówno poprawną, jak i niepoprawną odpowiedź.

Wniosek

Ten przykład jest zdecydowanie doskonały lub ostateczny, ale może być dobrym punktem wyjścia do pracy. Na przykład można to ulepszyć, tworząc stronę statystyk z wynikami każdej gry lub sekcję, w której użytkownik może tworzyć niestandardowe pytania i kategorie (może to być świetne ćwiczenie do ćwiczenia baz danych). Mam nadzieję, że to może być przydatne, zachęcamy do proponowania ulepszeń, sugestii lub innych.

Możesz znaleźć kod źródłowy w tym repozytorium GitHub.