Creating Tic Tac Toe Using Bloc And GetX In Flutter

By Mohammed Yaseen, 20 min read, May 09 2026

Creating Tic Tac Toe Using Bloc And GetX In Flutter

Tic Tac Toe is one of the best beginner projects for learning Flutter game development because the rules are simple but the project still teaches many important programming concepts. When I first started learning state management in Flutter, I used Tic Tac Toe to understand how data changes inside games. Even though the game itself is small, it contains turns, user interaction, game logic, win detection, state updates, and UI rebuilding. These systems exist in larger games too.

One thing I quickly realized while building Flutter games was that state management becomes extremely important. At first, using only setState looks easy. But once games become larger, the code can become messy very fast. That was when I started learning Bloc and GetX.

Both Bloc and GetX solve state management problems, but they do it differently. Bloc focuses heavily on structure and predictable updates. GetX focuses more on simplicity and speed. Both approaches are useful depending on the type of project being created.

Before building the actual game, it is important to understand the logic behind Tic Tac Toe. The board contains nine spaces arranged in a three by three layout. Two players alternate turns placing X and O symbols. The first player to complete a horizontal, vertical, or diagonal line wins the game.

Even simple games like this require organized state handling because every tap changes the board. The interface must rebuild correctly after every move.

Tic Tac Toe Using Bloc

Let us first understand Bloc because it teaches structured application architecture very clearly.

Bloc stands for Business Logic Component. The main idea behind Bloc is separating interface code from game logic. Instead of changing state directly inside widgets, events are sent into the Bloc and the Bloc produces updated states.

At first this system looked complicated to me because there are events, states, and Bloc classes working together. But after some practice, I understood why many large applications use Bloc.

The first step is creating events. Events represent actions happening inside the game.

abstract class TicTacToeEvent {} class TapBoxEvent extends TicTacToeEvent { final int index; TapBoxEvent(this.index); } class ResetGameEvent extends TicTacToeEvent {}

These events describe what the player is doing. One event handles tapping a square while another resets the game.

Next comes the game state. The state stores the current board data and game information.

class TicTacToeState { final List<String> board; final bool xTurn; final String winner; TicTacToeState({ required this.board, required this.xTurn, required this.winner, }); }

The board stores the symbols. The xTurn variable tracks whose turn it is. The winner variable stores the winning result.

After creating events and states, the Bloc itself handles the game logic.

class TicTacToeBloc extends Bloc<TicTacToeEvent, TicTacToeState> { TicTacToeBloc() : super( TicTacToeState( board: List.filled(9, ""), xTurn: true, winner: "", ), ) { on<TapBoxEvent>(onTapBox); on<ResetGameEvent>(onResetGame); } }

The Bloc starts with an empty board. Every time an event happens, the corresponding function runs.

Now we can implement the movement logic.

void onTapBox( TapBoxEvent event, Emitter<TicTacToeState> emit, ) { List<String> newBoard = List.from(state.board); if (newBoard[event.index] != "") { return; } newBoard[event.index] = state.xTurn ? "X" : "O"; emit( TicTacToeState( board: newBoard, xTurn: !state.xTurn, winner: "", ), ); }

The Bloc updates the board and emits a new state. The UI automatically rebuilds after the state changes.

One thing I really like about Bloc is predictability. Since every change passes through events and states, debugging becomes easier. You can track exactly how the game reached a specific situation.

Another advantage of Bloc is scalability. Large multiplayer games can become very complicated, but Bloc keeps logic organized. Different systems stay separated cleanly.

Bloc also improves testing because the game logic exists independently from the interface. This allows automated tests to verify movement systems and rules easily.

But Bloc also has disadvantages. The biggest problem for beginners is complexity. Even a small Tic Tac Toe project requires multiple files and boilerplate code. Writing events, states, and Bloc classes can feel repetitive during smaller projects.

Another disadvantage is development speed. For quick prototypes, Bloc may slow development because extra structure is required before features even work.

Despite these disadvantages, Bloc remains extremely powerful for large production projects because maintainability becomes very important as applications grow.

Now let us build the interface using BlocBuilder.

BlocBuilder<TicTacToeBloc, TicTacToeState>( builder: (context, state) { return GridView.builder( itemCount: 9, gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 3, ), itemBuilder: (context, index) { return GestureDetector( onTap: () { context.read<TicTacToeBloc>().add( TapBoxEvent(index), ); }, child: Container( margin: const EdgeInsets.all(4), color: Colors.blue.shade200, child: Center( child: Text( state.board[index], style: const TextStyle( fontSize: 40, ), ), ), ), ); }, ); }, )

Every tap sends an event into the Bloc. The state updates and the board rebuilds automatically.

Win detection is another important part of the game.

String checkWinner(List<String> board) { List<List<int>> wins = [ [0,1,2], [3,4,5], [6,7,8], [0,3,6], [1,4,7], [2,5,8], [0,4,8], [2,4,6], ]; for (var pattern in wins) { String first = board[pattern[0]]; if ( first != "" && first == board[pattern[1]] && first == board[pattern[2]] ) { return first; } } return ""; }

This function checks every possible winning pattern. If a winner exists, the game displays the result.

Tic Tac Toe Using GetX

After building the Bloc version, I started experimenting with GetX. The difference was immediately noticeable because GetX feels much simpler.

GetX focuses on reactive programming and lightweight syntax. Instead of events and states, variables become observable and the interface updates automatically whenever values change.

One major advantage of GetX is speed. Development becomes much faster because less code is required.

Another advantage is simplicity. Beginners often understand GetX faster than Bloc because there are fewer architectural layers.

GetX also combines routing, dependency injection, and state management together in one package. This reduces setup complexity.

But GetX also has disadvantages. Large projects can become difficult to organize if developers are careless. Since updates happen very freely, architecture discipline becomes the responsibility of the developer.

Another disadvantage is debugging complexity in poorly structured projects. Without strong separation rules, logic can spread across many files unpredictably.

Even with these disadvantages, GetX works extremely well for many games because game development often requires rapid experimentation and quick updates.

Let us now build the Tic Tac Toe game using GetX.

First we create the controller.

class TicTacToeController extends GetxController { RxList<String> board = List.filled(9, "").obs; RxBool xTurn = true.obs; RxString winner = "".obs; }

The obs keyword makes variables reactive. Whenever these variables change, widgets listening to them rebuild automatically.

Next comes the movement system.

void tapBox(int index) { if (board[index] != "") { return; } board[index] = xTurn.value ? "X" : "O"; String result = checkWinner(); if (result != "") { winner.value = result; } xTurn.value = !xTurn.value; }

This logic feels much shorter compared to Bloc because there are no events or emitted states.

The winner checking function looks very similar.

String checkWinner() { List<List<int>> wins = [ [0,1,2], [3,4,5], [6,7,8], [0,3,6], [1,4,7], [2,5,8], [0,4,8], [2,4,6], ]; for (var pattern in wins) { String first = board[pattern[0]]; if ( first != "" && first == board[pattern[1]] && first == board[pattern[2]] ) { return first; } } return ""; }

Building the interface with GetX is also very direct.

final controller = Get.put(TicTacToeController()); Obx(() { return GridView.builder( itemCount: 9, gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 3, ), itemBuilder: (context, index) { return GestureDetector( onTap: () { controller.tapBox(index); }, child: Container( margin: const EdgeInsets.all(4), color: Colors.green.shade200, child: Center( child: Text( controller.board[index], style: const TextStyle( fontSize: 40, ), ), ), ), ); }, ); })

The Obx widget rebuilds automatically whenever observable values change. This makes reactive programming feel extremely smooth.

Reset functionality is also easy.

void resetGame() { board.value = List.filled(9, ""); xTurn.value = true; winner.value = ""; }

One thing I noticed during development was how different the programming experience feels between Bloc and GetX.

Bloc feels structured and enterprise focused. Every state transition becomes controlled carefully. Large teams often appreciate this because projects stay organized.

GetX feels lightweight and flexible. Small teams and solo developers often enjoy the faster workflow because experimentation becomes easier.

Neither system is universally better. The best choice depends on project goals and development style.

For small browser games and prototypes, I often use GetX because development speed matters heavily. For larger long term projects, Bloc provides stronger architecture.

Another important lesson from Tic Tac Toe development is understanding reactive updates. Games constantly change state during gameplay. Proper state management prevents confusing bugs and unnecessary rebuilds.

I also experimented with animations after completing the basic versions.

AnimatedContainer( duration: const Duration( milliseconds: 300, ), child: Text( controller.board[index], ), )

Even simple animations improve game feel significantly.

Sound effects also make the experience more satisfying. Small tap sounds and victory sounds create stronger feedback for players.

Later I added score tracking between rounds.

RxInt xScore = 0.obs; RxInt oScore = 0.obs;

Persistent scoring transforms Tic Tac Toe from a single match into a longer competitive experience.

Another interesting improvement was adding single player mode with computer movement logic.

void computerMove() { for (int i = 0; i < board.length; i++) { if (board[i] == "") { board[i] = "O"; break; } } }

Even simple artificial intelligence systems make games feel much more interactive.

Responsive layouts also matter because Flutter games may run across phones, tablets, desktops, and browsers.

double size = MediaQuery.of(context).size.width / 3.5;

Responsive sizing keeps the board visually balanced across different screen sizes.

Looking back, Tic Tac Toe became much more than a simple beginner project. It taught me architecture design, state management, reactive programming, user interaction, and game logic structuring.

Bloc and GetX both helped me understand different approaches to managing game data. Bloc taught discipline and organization. GetX taught simplicity and rapid development.

For beginners, I recommend trying both systems. Building the same game twice teaches valuable lessons because you experience how architecture choices affect development style.

Flutter game development becomes much easier once state management concepts become comfortable. Larger projects like chess games, multiplayer systems, arcade shooters, and racing games all depend heavily on reliable state handling.

At the end of the day, Tic Tac Toe may look simple on the surface, but it provides one of the best learning experiences for Flutter developers entering game development. Small projects often teach the biggest lessons because they allow developers to focus deeply on architecture and logic without becoming overwhelmed by massive systems.