Chess – a game as old as time – and yet most people disagree whether the king goes on the right or left.
Chess has transitioned from the traditional board and made its way to our screens. It makes quite a frequent appearance in chat applications since most people know at least the basics of the game.
Having spent several years going from one long-drawn tournament to the next, I definitely do know where the king goes – and also happen to work at Stream. This bit of serendipity makes for some interesting possibilities.
Adding extra functionality to chat – whether that be games like Messenger or visual effects like those in iMessage – improves user retention and makes the chat experience more memorable.
The Stream Chat Flutter SDK allows you to easily add custom attachments to in-app chat.
In this article, we’ll demonstrate the versatility of custom attachments using our Flutter Chat SDK by adding a full-fledged chess game to a chat app.
Ready? Let’s code.
Planning Our App
In most in-app chat chess game implementations, the interface is quite complicated: It usually consists of drawing a new chess board every time a player makes a move. Due to this, the channel fills up with chessboards and the messages already sent become quite hard to reach.
For this reason, this guide focuses on building a chessboard with a game that can be played on one chat message. This makes the game a seamless part of the chat interface and avoids flooding the channel with chessboards.
Here is the end result of what you are going to build:
A few things to notice:
- There is a game icon on the bottom message input bar to start a chess game.
- Clicking the icon adds a chess game as an attachment to the message - which we display using a custom thumbnail.
- The previously sent custom attachment is displayed as a full-fledged chessboard with interactivity once the message is sent.
Starting Out
Let’s start by creating a basic Stream Chat implementation. Here, the stream_chat_flutter
package is used to easily add chat functionality. Along with this, the flutter_chess_board
package is used to create the chessboards.
Your pubspec.yaml
should have these new dependencies added to it:
1234dependencies: stream_chat_flutter: ^3.5.1 flutter_chess_board: ^1.0.1
⚠️ There may be updated versions of these packages at the time you’re reading this article. If you want to follow along exactly, it will be safer to use the versions mentioned above. Once you are familiar with all of the code, you can run
flutter pub outdated
to see which packages require updating.
Open main.dart
and set up your chat functionality:
- Initialize Stream Chat Flutter, connect a user, and watch a channel for changes.
- Display the channel messages using the MessageListView widget.
- Add a MessageInput widget to be able to send a message to the aforementioned channel.
Here is what that looks like in code:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); final client = StreamChatClient( 'YOUR_API_KEY', logLevel: Level.INFO, ); await client.connectUser( User(id: 'YOUR_USER_ID'), '''YOUR_USER_TOKEN''', ); final channel = client.channel('messaging', id: 'YOUR_CHANNEL_ID'); await channel.watch(); runApp( MyApp( client: client, channel: channel, ), ); } class MyApp extends StatelessWidget { const MyApp({ Key? key, required this.client, required this.channel, }) : super(key: key); final StreamChatClient client; /// Instance of the Channel final Channel channel; Widget build(BuildContext context) => MaterialApp( theme: ThemeData.light(), darkTheme: ThemeData.dark(), builder: (context, widget) => StreamChat( client: client, child: widget, ), home: StreamChannel( channel: channel, child: const ChannelPage(), ), ); } class ChannelPage extends StatefulWidget { const ChannelPage({ Key? key, }) : super(key: key); State<ChannelPage> createState() => _ChannelPageState(); } class _ChannelPageState extends State<ChannelPage> { Widget build(BuildContext context) { return Scaffold( appBar: const ChannelHeader(), body: Column( children: <Widget>[ const Expanded( child: MessageListView(), ), MessageInput( attachmentLimit: 3, ), ], ), ); } }
This gives a single channel with an AppBar
, the MessageListView
below it displaying the messages in the channel, as well as the MessageInput
at the bottom for sending/editing messages:
Sending a Chess Board Attachment
Now that the base chat UI is set, the intended chess functionality can be integrated.
Modeling the Chessboard Attachment
To create our custom Chessboard Attachment, we will need to create a custom attachment to display the chessboard.
We also need to create a message custom attachment that will include data about the current game being played and the board orientation so that the piece colors are assigned correctly.
To upload the Chess game, we will use the Forsyth-Edwards Notation (FEN) format for storing details about the current chess game. FEN stores all information necessary about any board position so we can initialize the chessboard to a given position.
As an example, this is the standard notation for the starting position:
1rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1
Without going too deep into the details (the implementation does not demand them), here are a few things you should know:
- FEN divides positions into the usual eight ranks of a chessboard
- Lowercase letters denote black pieces while uppercase letters denote white pieces
- The “w” indicates that it is white’s move to play
- There is a full- and half-move counter at the end of the ASCII string
The Chess
class in the flutter_chess_board
packages gives us a .fen
getter which converts the current game into FEN format. You can convert this back to a Chess
object using Chess.fromFEN()
.
Along with this, the ID of the user playing the white pieces needs to be stored so the correct orientation of the board can be displayed. If you need to, you can also store the player playing the black pieces, however, this implementation keeps it open so that the code does not break for group channels that may have more than two members.
Here is the model class for the attachment which is to be added to a message:
1234567891011121314151617181920212223242526272829303132import 'package:flutter_chess_board/flutter_chess_board.dart'; class ChessAttachment { final Chess game; final String whiteUserId; ChessAttachment({required this.game, required this.whiteUserId}); factory ChessAttachment.fromJson(Map<String, dynamic> json) { return ChessAttachment( game: Chess.fromFEN(json['game'] as String), whiteUserId: json['white_user_id'] as String, ); } Map<String, dynamic> toJson() { return <String, dynamic>{ 'game': game.fen, 'white_user_id': whiteUserId, }; } ChessAttachment copyWith({ Chess? game, String? whiteUserId, }) { return ChessAttachment( game: game ?? this.game, whiteUserId: whiteUserId ?? this.whiteUserId, ); } }
You can add any other data you may need to this – a few examples include:
- Defining the other player or players
- Adding the score between players, etc.
Setting Up the MessageInput
Widget
Before we build out the UI required for playing chess, there needs to be a way to trigger the start of a game. This will take the form of a button in the MessageInput
actions argument. Let’s start with adding this action button first:
12345678910111213141516171819202122232425262728293031GlobalKey<MessageInputState> _mipKey = GlobalKey(); Widget build(BuildContext context) { return Scaffold( appBar: const ChannelHeader(), body: Column( children: <Widget>[ const Expanded( child: MessageListView(), ), MessageInput( key: _mipKey, attachmentLimit: 3, actions: [ IconButton( onPressed: () {}, icon: const Icon(Icons.videogame_asset_outlined), padding: const EdgeInsets.all(0), constraints: const BoxConstraints.tightFor( height: 24, width: 24, ), splashRadius: 24, ), ], ), ], ), ); }
Your MessageInputWidget should now look like this:
Add an Attachment to the MessageInput
State
There is now a button to trigger adding an attachment to the MessageInput
. However, the logic for adding it still needs to be implemented.
Creating a new Chess
object to store our chess game is a good start. You can get the current user ID to set the player playing white (you can change this in your implementation to have someone else as white or the current player playing as black). Finally, you can create a ChessAttachment
model object which takes in the two aforementioned objects.
The last section attached a GlobalKey
to the MessageInput
. This key is now used for accessing the current state of the MessageInput
and adding an attachment.
Here is the code for this step:
1234567891011121314151617181920212223IconButton( onPressed: () { var newGame = Chess(); var userId = StreamChat.of(context).currentUser!.id; var attachment = ChessAttachment(game: newGame, whiteUserId: userId); _mipKey.currentState?.addAttachment( Attachment( type: 'chess', uploadState: const UploadState.success(), extraData: attachment.toJson(), ), ); }, icon: const Icon(Icons.videogame_asset_outlined), padding: const EdgeInsets.all(0), constraints: const BoxConstraints.tightFor( height: 24, width: 24, ), splashRadius: 24, ),
Even though the MessageInput
has an attachment now, it is not a predefined type (image, video, GIF, etc.). The widget needs to be told how we want the thumbnail for the attachment to be rendered.
Adding a Custom Chessboard Thumbnail
The previous section focused on adding a button to the MessageInput
, which allowed you to add the attachment of type chess
. When a user adds this attachment, there needs to be a preview of this attachment on the MessageInput
composer. A chessboard itself would be a good preview of this, which is what this section tries to achieve.
The MessageInput
contains an attachmentThumbnailBuilders
property to which a Map
is supplied where the keys are the attachment types and the values are the attachment builder methods.
Adding the attachmentThumbnailBuilders
property and defining the builder for the chess
attachment type, the code looks like:
12345678910111213141516171819202122232425262728293031323334MessageInput( key: _mipKey, attachmentLimit: 3, actions: [ IconButton( onPressed: () { var newGame = Chess(); var userId = StreamChat.of(context).currentUser!.id; var attachment = ChessAttachment(game: newGame, whiteUserId: userId); _mipKey.currentState?.addAttachment( Attachment( type: 'chess', uploadState: const UploadState.success(), extraData: attachment.toJson(), ), ); }, icon: const Icon(Icons.videogame_asset_outlined), ), ], attachmentThumbnailBuilders: { 'chess': (context, attachment) { return SizedBox( height: 75, width: 75, child: ChessBoard( controller: ChessBoardController(), ), ); }, }, ),
Now, when the button is clicked, the thumbnail displays above the TextField
which shows a chessboard:
Displaying the Chessboard Attachment in a Message
After the previous sections, the message gets sent with the chess attachment which contains the current board position and game metadata. Now, those attachments need to be rendered as a full chessboard in the message.
The MessageListView
by default (obviously) does not support a chess
attachment type. To add this, similar logic to the MessageInput
can be used – albeit with attachments instead of thumbnails. Here, however, the customAttachmentBuilders
parameter is used to tell the MessageListView
how to render the custom attachment.
To do this, you can retrieve the chess attachment by checking message.attachments
. Since chess attachment data is added in the extraData
parameter of the attachment, it can be recreated using ChessAttachment.fromJson(attachment.extraData)
.
You can also instantiate a ChessBoardController
using the game in the ChessAttachment
. This now displays the current game position on the board. Additionally, the whiteUserId
parameter helps us determine which side of the board faces which user. We can change the orientation of the board if the ID does not match:
1234567return ChessBoard( controller: chessBoardController, boardOrientation: StreamChat.of(context).currentUser!.id == chessAttachment.whiteUserId ? PlayerColor.white : PlayerColor.black, );
The overall code for this is:
12345678910111213141516171819202122232425MessageListView( messageBuilder: (context, details, list, defaultWidget) { return defaultWidget.copyWith( customAttachmentBuilders: { 'chess': (context2, message, list) { var attachment = message.attachments .firstWhere((e) => e.type == 'chess'); var chessAttachment = ChessAttachment.fromJson(attachment.extraData); var chessBoardController = ChessBoardController.fromGame(chessAttachment.game); return ChessBoard( controller: chessBoardController, boardOrientation: StreamChat.of(context).currentUser!.id == chessAttachment.whiteUserId ? PlayerColor.white : PlayerColor.black, ); }, }, ); }, ),
This now renders the chessboard as an attachment to the message itself:
The main thing to note at this moment is that the chessboard does not react to any changes – making moves, for example, does not update the board for the other player since the move is never sent. Let’s tackle this next.
Watching for New Moves on the Board
The current code can add a ChessAtachment
to a message, display the thumbnail, and display the attachment as a chessboard in the MessageListView
. When a user now plays a move, the board needs to update the attachment with the new position on the board, which also updates the other device automatically.
To do this, the ChessBoardController
allows listening to updates on the board position. When this happens, we can update the message with the new attachment data.
1234567891011121314151617181920212223242526272829303132333435363738394041424344MessageListView( messageBuilder: (context, details, list, defaultWidget) { return defaultWidget.copyWith( customAttachmentBuilders: { 'chess': (context2, message, list) { var attachment = message.attachments .firstWhere((e) => e.type == 'chess'); var chessAttachment = ChessAttachment.fromJson(attachment.extraData); var chessBoardController = ChessBoardController.fromGame(chessAttachment.game); chessBoardController.addListener( () { StreamChannel.of(context).channel.updateMessage( message.copyWith( attachments: [ attachment.copyWith( uploadState: const UploadState.success(), extraData: chessAttachment .copyWith( game: chessBoardController.game, ) .toJson(), ), ], ), ); }, ); return ChessBoard( controller: chessBoardController, boardOrientation: StreamChat.of(context).currentUser!.id == chessAttachment.whiteUserId ? PlayerColor.white : PlayerColor.black, ); }, }, ); }, ),
Conclusion
With all of the above implemented, the final result will allow two players to play with each other without having to leave chat or send chess invites that don’t reach the other player.
We want you to spend time increasing your ELO, not your development time.
To find the full working code for this project, you can go to the GitHub link below.