Build a production app in Flutter: Tic Tac Toe game


Davide Agostini /

9 min read

Hi, and welcome in this second post about the Flutter world. In this post I will explore how to build the UI and the business logic for the famous game Tic Tac Toe.

I build from scratch the entire app and I will deepen some concepts of this framework.

Let's start and create the main.dart file

import 'package:flutter/material.dart';

import 'home_screen.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      debugShowCheckedModeBanner: false,
      home: HomeScreen(),
    );
  }
}

In Flutter everything that appears on the screen is a widget. When we create user interfaces we make a composition of widgets. When we nest widgets one inside the other we create a hierarchy called widget tree.

the main() function is the entry point and the runApp() function takes an instance of a Widget and makes it the root of the widget tree.

MyApp is a class that extends a StatelessWidget. Inside the build method I use MaterialApp widget provided by Flutter. It represents the skeleton of a UI following the material design guidelines. The home of my widget is another widget called HomeScreen that I will create shortly.

Stateless and Stateful widgets

In Flutter a widget can extend or StatelessWidget or StatefulWidget.

  1. StatelessWidget is a kind of widget that not require a mutable state, that is the UI is not going to change over the time. (more info)
  2. StatefulWidget is a widget that has a mutable state, that is the UI is going to change over the time. (more info)

Keys

You might have noticed that any widget provided by Flutter has the optional key parameter. The key uniquely identify a widget in the tree.

Home screen UI

Now I created another file called home_screen.dart

class HomeScreen extends StatefulWidget {
  @override
  _HomeScreenState createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
  @override
  Widget build(BuildContext context) {
    return Container();
  }
}

this class extends a StatefulWidget. This widget is inserted in the widget tree. _HomeScreenState is the mutable state of the HomeScreen widget.

When Flutter rebuilds the widget tree to refresh UI, the build() method is called. I will see in detail later how to refresh our view, now let's build our UI.

Inside the build method replace the Container widget with this code.

class HomeScreen extends StatefulWidget {
  @override
  _HomeScreenState createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        elevation: 0,
        backgroundColor: Colors.grey[900],
        actions: [
          IconButton(
            icon: const Icon(Icons.refresh),
            onPressed: () {
              _clearBoard();
            },
          )
        ],
        title: Text(
          'Tic Tac Toe',
          style: kCustomText(
              fontSize: 20.0, color: Colors.white, fontWeight: FontWeight.w800),
        ),
      ),
      backgroundColor: Colors.grey[900],
      body: Column(
        children: [
          _buildPointsTable(),
          _buildGrid(),
          _buildTurn(),
        ],
      ),
    );
  }
}

The Scaffold widget implements the basic material design layout structure. It has the AppBar placed at the top of the screen with the relative actions button, the title, and the body that in our case is a Column widget.

Inside the actions I inserted an IconButton widget, which I will use to restart the game when the icon is pressed.

Inside the Column widget I will insert three children in the vertical axis with the given space constraints.

Points table

_buildPointsTable() method, build the section of the points of the game.

It's a simple Container and inside it there is a Row that places more children in horizontal axis. Widgets in rows can be placed in different ways according to the value of mainAxisAlignment. In our case I placed our widgets in the center of the row.

Then I will use a Column widget to enter the player o and his relative score. The same thing is repeated for the player x and his relative score.

Widget _buildPointsTable() {
    return Expanded(
      child: Container(
        child: Row(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Padding(
              padding: const EdgeInsets.all(
                20.0,
              ),
              child: Column(
                children: [
                  Text(
                    'Player O',
                    style: kCustomText(
                        fontSize: 22.0,
                        color: Colors.white,
                        fontWeight: FontWeight.w800),
                  ),
                  const SizedBox(
                    height: 20,
                  ),
                  Text(
                    _scoreO.toString(),
                    style: kCustomText(
                        color: Colors.white,
                        fontSize: 25.0,
                        fontWeight: FontWeight.bold),
                  )
                ],
              ),
            ),
            Padding(
              padding: const EdgeInsets.all(
                20.0,
              ),
              child: Column(
                children: [
                  Text(
                    'Player X',
                    style: kCustomText(
                        fontSize: 22.0,
                        color: Colors.white,
                        fontWeight: FontWeight.w800),
                  ),
                  const SizedBox(
                    height: 20,
                  ),
                  Text(
                    _scoreX.toString(),
                    style: kCustomText(
                        color: Colors.white,
                        fontSize: 25.0,
                        fontWeight: FontWeight.bold),
                  ),
                ],
              ),
            )
          ],
        ),
      ),
    );
  }

Build the grid

The _buildGrid() method, build the grid of the game. Essentially it is a 3 rows x 3 columns grid, with a border decoration. To each item in the grid, I will add a click event, that draw a o or a x according to the player's turn.

Also I differentiate the two players with the color white or red.

Widget _buildGrid() {
    return Expanded(
      flex: 3,
      child: GridView.builder(
          itemCount: 9,
          gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
            crossAxisCount: 3,
          ),
          itemBuilder: (BuildContext context, int index) {
            return GestureDetector(
              onTap: () {
                _tapped(index);
              },
              child: Container(
                decoration:
                    BoxDecoration(border: Border.all(color: Colors.grey[700])),
                child: Center(
                  child: Text(
                    _xOrOList[index],
                    style: TextStyle(
                      color:
                          _xOrOList[index] == 'x' ? Colors.white : Colors.red,
                      fontSize: 40,
                    ),
                  ),
                ),
              ),
            );
          }),
    );
  }

Build the turn

Finally in the _buildTurn() method, I build a simple widget that show the player's turn.

Widget _buildTurn() {
    return Container(
      child: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Center(
          child: Text(
            _turnOfO ? 'Turn of O' : 'Turn of X',
            style: kCustomText(color: Colors.white),
          ),
        ),
      ),
    );
  }

At this point this if I launch the application on the emulator I will have the following result.

tic-tac-toe-screen

Business logic

Before moving on to the part of logic that will manage our game, inside our build() method, I inserted the following variables.

The _ before each variables means that they are private.

The _xOrOList represents our grid that will contain all the various moves of the players.

The first cell of the grid matches the first element of the array, the second cell of the grid matches the second element of the array and so on.

  int _scoreX = 0;
  int _scoreO = 0;
  bool _turnOfO = true;
  int _filledBoxes = 0;
  final List<String> _xOrOList = [
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
    '',
  ];

The _tapped() method, it is invoked whenever any player clicks on a cell of the grid.

At each click is checked whether it is the turn of the player o or player x and if the selected cell is empty.

Depending on the case, the index of the corresponding array is valued with o or x and the number of cells filled is increased.

Then I will change the turn and check if there is a winner by calling the _checkTheWinner() method.

Subclasses of State<T> gain access to setState() method which rebuilds the widget and consequently refresh the UI.

void _tapped(int index) {
    setState(() {
      if (_turnOfO && _xOrOList[index] == '') {
        _xOrOList[index] = 'o';
        _filledBoxes += 1;
      } else if (!_turnOfO && _xOrOList[index] == '') {
        _xOrOList[index] = 'x';
        _filledBoxes += 1;
      }

      _turnOfO = !_turnOfO;
      _checkTheWinner();
    });
  }

The _checkTheWinner() method, check if there is a winner just check that there is three times the same symbol o or x in diagonal, horizontal, or vertical row and show an alert dialog with a message.

If none of the conditions are met and all 9 cells are full, comes an alert dialog with a draw message.

The check takes place on:

  • first row;
  • second row;
  • third row;
tic tac toe rows
  • first column;
  • second column;
  • third column;
tic tac toe columns
  • the two diagonals
tic tac toe diagonals
void _checkTheWinner() {
    // check first row
    if (_xOrOList[0] == _xOrOList[1] &&
        _xOrOList[0] == _xOrOList[2] &&
        _xOrOList[0] != '') {
      _showAlertDialog('Winner', _xOrOList[0]);
      return;
    }

    // check second row
    if (_xOrOList[3] == _xOrOList[4] &&
        _xOrOList[3] == _xOrOList[5] &&
        _xOrOList[3] != '') {
      _showAlertDialog('Winner', _xOrOList[3]);
      return;
    }

    // check third row
    if (_xOrOList[6] == _xOrOList[7] &&
        _xOrOList[6] == _xOrOList[8] &&
        _xOrOList[6] != '') {
      _showAlertDialog('Winner', _xOrOList[6]);
      return;
    }

    // check first column
    if (_xOrOList[0] == _xOrOList[3] &&
        _xOrOList[0] == _xOrOList[6] &&
        _xOrOList[0] != '') {
      _showAlertDialog('Winner', _xOrOList[0]);
      return;
    }

    // check second column
    if (_xOrOList[1] == _xOrOList[4] &&
        _xOrOList[1] == _xOrOList[7] &&
        _xOrOList[1] != '') {
      _showAlertDialog('Winner', _xOrOList[1]);
      return;
    }

    // check third column
    if (_xOrOList[2] == _xOrOList[5] &&
        _xOrOList[2] == _xOrOList[8] &&
        _xOrOList[2] != '') {
      _showAlertDialog('Winner', _xOrOList[2]);
      return;
    }

    // check diagonal
    if (_xOrOList[0] == _xOrOList[4] &&
        _xOrOList[0] == _xOrOList[8] &&
        _xOrOList[0] != '') {
      _showAlertDialog('Winner', _xOrOList[0]);
      return;
    }

    // check diagonal
    if (_xOrOList[2] == _xOrOList[4] &&
        _xOrOList[2] == _xOrOList[6] &&
        _xOrOList[2] != '') {
      _showAlertDialog('Winner', _xOrOList[2]);
      return;
    }

    if (_filledBoxes == 9) {
      _showAlertDialog('Draw', '');
    }
  }

These are simple accessory methods to display the alert dialog and to reset all cells.

void _showAlertDialog(String title, String winner) {
    showAlertDialog(
        context: context,
        title: title,
        content: winner == ''
            ? 'The match ended in a draw'
            : 'The winner is ${winner.toUpperCase()}',
        defaultActionText: 'OK',
        onOkPressed: () {
          _clearBoard();
          Navigator.of(context).pop();
        });

    if (winner == 'o') {
      _scoreO += 1;
    } else if (winner == 'x') {
      _scoreX += 1;
    }
  }

  void _clearBoard() {
    setState(() {
      for (int i = 0; i < 9; i++) {
        _xOrOList[i] = '';
      }
    });

    _filledBoxes = 0;
  }

Well with very few lines of code I was able to build a complete one that works on both iOS and Android operating systems.

tic-tac-toe-start-screen

Link to my source code https://github.com/davideagostini/tic_tac_toe

Before concluding I recommend you to like this post and subscribe to my newsletter to stay updated on new posts.

See you in the next tutorial. 😉

← Back to the blog