How I built a math game using Flutter & Firebase
Tags: flutter, firebase, tutorial, gamedev
Published: 2021-03-14 05:29:23


You might know that I am currently learning Flutter. I watched few YouTube tutorials, wrote few posts about my learnings, and signed up for the #30DaysOfFlutter. I decided to take my learning journey to the next level by actually implementing something


{% link https://dev.to/offlineprogrammer/the-best-way-to-learn-something-is-by-building-something-1hg3 %}


BrainTrainer is a math game where you have 30 seconds to solve as many questions as possible. Your highest score will be displayed on the app bar and saved in Firebase.



![BrainTrainer](https://i.imgur.com/n8HI7yY.png)

The game has two screens in total. On main.dart we watch the firebase user to determine which screen to display.



```dart

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final firebaseUser = context.watch<User>();
    return Scaffold(
        appBar: AppBar(
          title: Text('BrainTrainer'),
        ),
        body: firebaseUser != null ? loadGameScreen(context) : LoginPage());
  }

  Future<Object> loadGameScreen(BuildContext context) {
    Future.delayed(Duration.zero, () {
      return Navigator.of(context).pushNamedAndRemoveUntil(
          GameScreen.routeName, (Route<dynamic> route) => false);
    });
  }
}

```

We are using a provider to enable (Apple Sign In) using Firebase. check my previous post on that

{% link https://dev.to/offlineprogrammer/flutter-firebase-authentication-apple-sign-in-1m64 %}

We are using few widgets in the game:

**ActionButtons Widget:** This is where we show the play button to start the game. we also use to indicate to the player if their selected answer is correct or now

```dart

import 'package:brain_trainer_app/models/game.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

class ActionButtons extends StatefulWidget {
  const ActionButtons({
    Key key,
  }) : super(key: key);

  @override
  _ActionButtonsState createState() => _ActionButtonsState();
}

class _ActionButtonsState extends State<ActionButtons> {
  @override
  Widget build(BuildContext context) {
    final _game = Provider.of<Game>(context);
    return SizedBox(
      height: 100,
      child: GestureDetector(
        onTap: () {
          _game.playTheGame();
        },
        child: Card(
          margin: EdgeInsets.all(10),
          elevation: 8,
          child: Container(
            decoration: BoxDecoration(
              borderRadius: BorderRadius.circular(10),
              boxShadow: [
                BoxShadow(color: Colors.white, spreadRadius: 3),
              ],
            ),
            padding: EdgeInsets.all(10),
            child: Consumer<Game>(
              builder: (context, game, child) {
                return Image.asset(
                  _game.actionButtonImage,
                  fit: BoxFit.cover,
                );
              },
            ),
          ),
        ),
      ),
    );
  }
}


```

**AnswerItem Widget:** This represents an item on a grid to display a number for the player to select. One of those items will be the correct answer.

```dart

import 'package:brain_trainer_app/models/game.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

class AnswerItem extends StatelessWidget {
  final Answer answer;
  final int index;

  static const _answercolot = [
    Color.fromRGBO(224, 81, 98, 1),
    Color.fromRGBO(84, 160, 86, 1),
    Color.fromRGBO(68, 150, 224, 1),
    Color.fromRGBO(111, 64, 222, 1),
  ];

  const AnswerItem({Key key, this.answer, this.index}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    final _game = Provider.of<Game>(context);
    return ClipRRect(
      borderRadius: BorderRadius.circular(10),
      child: GridTile(
        child: GestureDetector(
          onTap: () {
            _game.answerSelected(this.answer);
          },
          child: Container(
            decoration: BoxDecoration(
              borderRadius: BorderRadius.circular(10),
              color: _answercolot[this.index],
              boxShadow: [
                BoxShadow(color: Colors.white, spreadRadius: 3),
              ],
            ),
            padding: EdgeInsets.all(10),
            child: Center(
                child: Text(
              answer.value.toString(),
              style: TextStyle(
                  fontSize: 24,
                  fontStyle: FontStyle.italic,
                  fontWeight: FontWeight.bold),
            )),
          ),
        ),
      ),
    );
  }
}


```

**AnswersGrid Widget:** A grid of 4 AnswerItem widgets. 

```dart

import 'package:brain_trainer_app/models/game.dart';
import 'package:brain_trainer_app/widgets/answer_item.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

class AnswersGrid extends StatelessWidget {
  @override
  @override
  Widget build(BuildContext context) {
    final gameData = Provider.of<Game>(context);
    final answers = gameData.answers;
    return GridView.builder(
        padding: const EdgeInsets.all(10.0),
        itemCount: answers.length,
        gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
            crossAxisCount: 2,
            childAspectRatio: 3 / 2,
            crossAxisSpacing: 10,
            mainAxisSpacing: 10),
        itemBuilder: (BuildContext context, int index) {
          return AnswerItem(
            answer: answers[index],
            index: index,
          );
        });
  }
}


```

**GameAds Widget:** This is where we display Google AdMob Ads

```dart

import 'package:flutter/material.dart';
import 'package:google_mobile_ads/google_mobile_ads.dart';

class GameAds extends StatefulWidget {
  @override
  _GameAdsState createState() => _GameAdsState();
}

class _GameAdsState extends State<GameAds> {
  BannerAd _bannerAd;

  @override
  void initState() {
    super.initState();
    MobileAds.instance.initialize();
    _bannerAd = BannerAd(
      size: AdSize.banner,
      adUnitId: BannerAd.testAdUnitId,
      listener: AdListener(),
      request: AdRequest(),
    );
    _bannerAd..load();
  }

  @override
  void dispose() {
    super.dispose();
    _bannerAd.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return SizedBox(height: 50, child: AdWidget(ad: _bannerAd));
  }
}


```

**GameConfetti Widget:** We use this fun widget to display a celebration animation when the play finish the game

```dart


import 'package:brain_trainer_app/models/game.dart';
import 'package:confetti/confetti.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:rflutter_alert/rflutter_alert.dart';

class GameConfetti extends StatefulWidget {
  @override
  _GameConfettiState createState() => _GameConfettiState();
}

class _GameConfettiState extends State<GameConfetti> {
  ConfettiController _controllerCenter;

  @override
  void initState() {
    _controllerCenter =
        ConfettiController(duration: const Duration(seconds: 10));
    _controllerCenter.play();
    super.initState();
  }

  @override
  void dispose() {
    _controllerCenter.dispose();
    super.dispose();
  }

  _showAlert(context) {
    Future.delayed(Duration.zero, () async {
      final String _gameMsg =
          Provider.of<Game>(context, listen: false).completionMsg;
      var alertStyle = AlertStyle(
        animationType: AnimationType.fromTop,
        isCloseButton: false,
        isOverlayTapDismiss: false,
        descStyle: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
        descTextAlign: TextAlign.start,
        animationDuration: Duration(milliseconds: 400),
        overlayColor: Colors.transparent,
        alertBorder: RoundedRectangleBorder(
          borderRadius: BorderRadius.circular(0.0),
          side: BorderSide(
            color: Colors.grey,
          ),
        ),
        titleStyle: TextStyle(
          color: Colors.green,
        ),
        alertAlignment: Alignment.center,
      );

      Alert(
        style: alertStyle,
        context: context,
        type: AlertType.success,
        title: "Well done",
        desc: _gameMsg,
        buttons: [
          DialogButton(
            child: Text(
              "Brilliant!!!",
              style: TextStyle(color: Colors.white, fontSize: 16),
            ),
            onPressed: () => Navigator.pop(context),
            width: 120,
          )
        ],
      ).show();
    });
  }

  @override
  Widget build(BuildContext context) {
    return Align(
      alignment: Alignment.topCenter,
      child: ConfettiWidget(
          confettiController: _controllerCenter,
          blastDirectionality: BlastDirectionality
              .explosive, // don't specify a direction, blast randomly
          shouldLoop: false,
          gravity: 0.5,
          emissionFrequency: 0.05,
          numberOfParticles:
              20, // start again as soon as the animation is finished
          colors: const [
            Colors.green,
            Colors.blue,
            Colors.pink,
            Colors.orange,
            Colors.purple
          ], // manually specify the colors to be used
          child: _showAlert(context)
          ),
    );
  }
}


```

**TimerQuestionScoreRow Widget:** Here, we show the 30s timer, The math question, and we keep track of the player score/

```dart

import 'package:brain_trainer_app/models/game.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

class TimerQuestionScoreRow extends StatefulWidget {
  const TimerQuestionScoreRow({
    Key key,
  }) : super(key: key);

  @override
  _TimerQuestionScoreRowState createState() => _TimerQuestionScoreRowState();
}

class _TimerQuestionScoreRowState extends State<TimerQuestionScoreRow> {
  @override
  Widget build(BuildContext context) {
    final _game = Provider.of<Game>(context);
    return Row(
      mainAxisAlignment: MainAxisAlignment.spaceEvenly,
      children: [
        Expanded(
          flex: 3,
          child: SizedBox(
            height: 100,
            child: Card(
              margin: EdgeInsets.all(10),
              elevation: 8,
              child: Container(
                decoration: BoxDecoration(
                  borderRadius: BorderRadius.circular(10),
                  color: Color.fromRGBO(255, 152, 0, 1),
                  boxShadow: [
                    BoxShadow(color: Colors.white, spreadRadius: 3),
                  ],
                ),
                padding: EdgeInsets.all(10),
                child: Consumer<Game>(
                  builder: (context, game, child) {
                    return Center(
                        child: Text(
                      '${game.timer}s',
                      style:
                          TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
                    ));
                  },
                ),
              ),
            ),
          ),
        ),
        Expanded(
          flex: 4,
          child: SizedBox(
            height: 100,
            child: Card(
              margin: EdgeInsets.all(10),
              elevation: 8,
              child: Container(
                decoration: BoxDecoration(
                  borderRadius: BorderRadius.circular(10),
                  boxShadow: [
                    BoxShadow(color: Colors.white, spreadRadius: 3),
                  ],
                ),
                padding: EdgeInsets.all(10),
                child: Consumer<Game>(
                  builder: (context, game, child) {
                    return Center(
                        child: Text(
                      '${game.question}',
                      style:
                          TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
                    ));
                  },
                ),
              ),
            ),
          ),
        ),
        Expanded(
          flex: 3,
          child: SizedBox(
            height: 100,
            child: Card(
              margin: EdgeInsets.all(10),
              elevation: 8,
              child: Container(
                decoration: BoxDecoration(
                  borderRadius: BorderRadius.circular(10),
                  color: Color.fromRGBO(3, 169, 244, 1),
                  boxShadow: [
                    BoxShadow(color: Colors.white, spreadRadius: 3),
                  ],
                ),
                padding: EdgeInsets.all(10),
                child: Consumer<Game>(
                  builder: (context, game, child) {
                    return Center(
                        child: Text(
                      '${game.score}',
                      style:
                          TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
                    ));
                  },
                ),
              ),
            ),
          ),
        ),
      ],
    );
  }
}



```

The game use three models:

**Game:** This is the primary model where we set up the game. Here we also verify the selected answer and keep track of the timer.

**Answer:** This is a simple model to represent the answer value

**Player:** This is where we have the player information we get from Firebase, and we also use it to track the high score.

We are using DataRepository class to retrieves and saves the data.

```dart

import 'package:brain_trainer_app/models/player.dart';
import 'package:cloud_firestore/cloud_firestore.dart';

class DataRepository {
  final CollectionReference collection =
      FirebaseFirestore.instance.collection('players');

  Future<void> addPlayer(Player player) {
    return collection.doc(player.uid).set(player.toJson());
  }

  updatePlayer(Player player) async {
    await collection.doc(player.uid).update(player.toJson());
  }

  Future<Player> getPlayer(String uid) async {
    var doc = await collection.doc(uid).get();
    if (doc.data() == null) {
      return null;
    }
    return Player.fromJson(doc.data());
  }
}


```


{% youtube aU9NE8zUgG0 %}

The game is on Apple Store <a href="http://apple.co/3lgpSqQ">here</a> and Google Play <a href="http://bit.ly/3qLzFG8">here</a>

Check the code here

{% github offlineprogrammer/brain_trainer_flutter  %}


> Follow me on <a href="https://twitter.com/_Mo_Malaka_">Twitter</a> for more tips about #coding, #learning, #technology...etc.

> Check my Apps on <a href="https://bit.ly/3h05gQ7">Google Play</a>



> <span>Cover image <a href="https://unsplash.com/@amoltyagi2?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Amol Tyagi</a> on <a href="/s/photos/calculator?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Unsplash</a></span>