This is the first of a series of Flutter Tutorials. During the series, you will learn how to build cross-platform apps without worrying about the backend.
In the first part, I will show you how to build a chat application, where users can sign up and talk to other users in a single chat room. The exchange of messages will happen in real time, meaning that you do not have to refresh the page to load new messages.
In order to build this chat app, we will need a database to store all of the chat information. We will be using Supabase to store the chat information. Supabase is a back end as a service that provides auth, database, storage and functions to easily create a scalable application. Supabase is a suitable backend for this app, because it provides a nice set of APIs on Postgres database that we can easily call by using the SDK. It is also perfect for chat apps like the one we are about to create, because we can subscribe to real time changes on the database.
This article also assumes that you have installed Flutter already on your machine. If not, you can go to the official Flutter page to start the installation.
You can find the complete version of this app in this Github repository.
Overview of the final chat app
The application we are creating today is a simple real time chat application. Users can sign up/ sign in using email and password. Once they are in, they can read and send messages to a shared room across all users of the application. Because we are using Flutter, the app can run on iOS, Android, or on the web.
Techstack will be fairly simple. We have Flutter on the frontend, Supabase on the backend and that is it! Since Supabase provides nice APIs to access the Postgres database, we don't need to create our own. We will access Supabase through the supabase_flutter package, which provides an intuitive way of reading and writing data to the database.
Setting up the scene
Create a blank Flutter application
We will start out by creating an empty Flutter project.
First, open your terminal and type
1flutter create my_chat_app 2
Once it is done, you can go into your app and run it.
1cd my_chat_app 2flutter run 3
You should be able to now see the default counter app that every Flutter project starts with. With that, let's open the app in your favorite code editor and get started with coding!
Install dependencies
Open pubspec.yaml file and let's install a few dependencies of this app.
1supabase_flutter: ^0.2.12
2timeago: ^3.1.0
3
supabase_flutter
will provide us easy access to our Postgres database hosted on Supabase. timeago
is a simple library that takes a DateTime
and returns nice strings displaying how long ago the time was. This will be used to display the timestamps of each chat bubble.
Run flutter pub get
to install the packages. Note that you will have to terminate flutter run
and re-run it again after this package installation.
Creating a new Supabase project
If you do not have a Supabase account yet, do not worry, you can get started for free.
You will be prompted to sign in using your Github account with a big green button, so let's go ahead and press it. Proceed with the sign up process and once you are done, you will be taken to a list of projects. You can go ahead and create a new project by pressing the “New Project” button at the top.
You will be entering a few things here like the name of the project. You can call it “chat” for now. For the database password, go ahead and hit the “Generate a password” button to generate a random password. We won't use this password in this app, but if you ever need it, you can always override it later to whatever you want it to be. You can leave the pricing plan for free as Supabase has a very generous free tier that will be way more than enough for our chat app. Once you have entered everything, you can press the “Create new Project” button. Spinning up a brand new Supabase project could take a few minutes.
Once your project is ready, we can dive into setting up our project!
Setting up tables in Supabase
Once your project is ready, we can dive into setting up our project!
In order to create the chat app, we will create 2 tables.
- profiles - stores user profile data
- messages - contains the contents of each message along with who sent it.
Each message is associated with one profile to represent who posted the message.
You can run the following SQL in your SQL editor of your Supabase dashboard.
1create table if not exists public.profiles (
2 id uuid references auth.users on delete cascade not null primary key,
3 username varchar(24) not null unique,
4 created_at timestamp with time zone default timezone('utc' :: text, now()) not null,
5
6 -- username should be 3 to 24 characters long containing alphabets, numbers and underscores
7 constraint username_validation check (username ~* '^[A-Za-z0-9_]{3,24}$')
8);
9comment on table public.profiles is 'Holds all of users profile information';
10
11create table if not exists public.messages (
12 id uuid not null primary key default uuid_generate_v4(),
13 profile_id uuid default auth.uid() references public.profiles(id) on delete cascade not null,
14 content varchar(500) not null,
15 created_at timestamp with time zone default timezone('utc' :: text, now()) not null
16);
17comment on table public.messages is 'Holds individual messages sent on the app.';
18
After running the SQL, you should see the tables in your table editor on your Supabase dashboard. You can click any of the tables to view the stored data, note that all of the tables should be empty at this point.
Supabase allows us to listen to real time changes on the database with additional configuration. We want to enable real time on our messages
table, so that we can display the chats when new data is added. Go back to SQL editor and run the following SQL to enable real time for messages
.
1-- *** Add tables to the publication to enable real time subscription *** 2alter publication supabase_realtime add table public.messages; 3
Now that we have defined what our data looks like, let’s have some fun writing Flutter code!
Building the Flutter chat application
Step 1: Define constants that to be used throughout the application
We will start out by creating a constants.dart file and define a few constants that will make things easier down the line. We will use the supabase variable to access our database and auth features.
1import 'package:flutter/material.dart';
2import 'package:supabase_flutter/supabase_flutter.dart';
3
4/// Supabase client
5final supabase = Supabase.instance.client;
6
7/// Simple preloader inside a Center widget
8const preloader =
9 Center(child: CircularProgressIndicator(color: Colors.orange));
10
11/// Simple sized box to space out form elements
12const formSpacer = SizedBox(width: 16, height: 16);
13
14/// Some padding for all the forms to use
15const formPadding = EdgeInsets.symmetric(vertical: 20, horizontal: 16);
16
17/// Basic theme to change the look and feel of the app
18final appTheme = ThemeData.light().copyWith(
19 primaryColorDark: Colors.orange,
20 appBarTheme: const AppBarTheme(
21 elevation: 1,
22 backgroundColor: Colors.white,
23 iconTheme: IconThemeData(color: Colors.black),
24 titleTextStyle: TextStyle(
25 color: Colors.black,
26 fontSize: 18,
27 ),
28 ),
29 primaryColor: Colors.orange,
30 textButtonTheme: TextButtonThemeData(
31 style: TextButton.styleFrom(
32 primary: Colors.orange,
33 ),
34 ),
35 elevatedButtonTheme: ElevatedButtonThemeData(
36 style: ElevatedButton.styleFrom(
37 onPrimary: Colors.white,
38 primary: Colors.orange,
39 ),
40 ),
41 inputDecorationTheme: InputDecorationTheme(
42 floatingLabelStyle: const TextStyle(
43 color: Colors.orange,
44 ),
45 border: OutlineInputBorder(
46 borderRadius: BorderRadius.circular(12),
47 borderSide: const BorderSide(
48 color: Colors.grey,
49 width: 2,
50 ),
51 ),
52 focusColor: Colors.orange,
53 focusedBorder: OutlineInputBorder(
54 borderRadius: BorderRadius.circular(12),
55 borderSide: const BorderSide(
56 color: Colors.orange,
57 width: 2,
58 ),
59 ),
60 ),
61);
62
63/// Set of extension methods to easily display a snackbar
64extension ShowSnackBar on BuildContext {
65 /// Displays a basic snackbar
66 void showSnackBar({
67 required String message,
68 Color backgroundColor = Colors.white,
69 }) {
70 ScaffoldMessenger.of(this).showSnackBar(SnackBar(
71 content: Text(message),
72 backgroundColor: backgroundColor,
73 ));
74 }
75
76 /// Displays a red snackbar indicating error
77 void showErrorSnackBar({required String message}) {
78 showSnackBar(message: message, backgroundColor: Colors.red);
79 }
80}
81
Step 2: Initialize Supabase
In order to use Supabase, we need to initialize it at the top of the main function. Let’s edit the main.dart file so that we can initialize Supabase. Note that within the build method of MyApp, we are loading the theme data created in the constants.dart
file and the home is set to SplashPage(), which we will create in later sections.
You can find your Supabase URL and Supabase anon key under settings -> API in your dashboard.
1import 'package:flutter/material.dart';
2import 'package:my_chat_app/utils/constants.dart';
3import 'package:supabase_flutter/supabase_flutter.dart';
4import 'package:my_chat_app/pages/splash_page.dart';
5
6Future<void> main() async {
7 WidgetsFlutterBinding.ensureInitialized();
8
9 await Supabase.initialize(
10 // TODO: Replace credentials with your own
11 url: 'SUPABASE_URL',
12 anonKey: 'SUPABASE_ANON_KEY',
13 );
14 runApp(const MyApp());
15}
16
17class MyApp extends StatelessWidget {
18 const MyApp({Key? key}) : super(key: key);
19
20 @override
21 Widget build(BuildContext context) {
22 return MaterialApp(
23 debugShowCheckedModeBanner: false,
24 title: 'My Chat App',
25 theme: appTheme,
26 home: const SplashPage(),
27 );
28 }
29}
30
Step 3: Redirect users depending on auth state using splash page
When the user launches the app, we want to redirect those who have already signed in to the Chat page and those who have not signed in yet to the register page. In order to achieve this, we will create a splash page, which is just a page with a preloader at the middle from the user, but takes care of fetching auth state and redirects users accordingly behind the scenes. We are using the onAuthenticated
and onUnauthenticated
to redirect the user to the proper pages.
1import 'package:flutter/material.dart';
2import 'package:my_chat_app/pages/chat_page.dart';
3import 'package:my_chat_app/pages/register_page.dart';
4import 'package:my_chat_app/utils/constants.dart';
5import 'package:supabase_flutter/supabase_flutter.dart';
6
7/// Page to redirect users to the appropriate page depending on the initial auth state
8class SplashPage extends StatefulWidget {
9 const SplashPage({Key? key}) : super(key: key);
10
11 @override
12 SplashPageState createState() => SplashPageState();
13}
14
15class SplashPageState extends SupabaseAuthState<SplashPage> {
16 @override
17 void initState() {
18 super.initState();
19 recoverSupabaseSession();
20 }
21
22 @override
23 Widget build(BuildContext context) {
24 return const Scaffold(body: preloader);
25 }
26
27 @override
28 void onAuthenticated(Session session) {
29 Navigator.of(context).pushAndRemoveUntil(ChatPage.route(), (_) => false);
30 }
31
32 @override
33 void onUnauthenticated() {
34 Navigator.of(context)
35 .pushAndRemoveUntil(RegisterPage.route(), (_) => false);
36 }
37
38 @override
39 void onErrorAuthenticating(String message) {}
40
41 @override
42 void onPasswordRecovery(Session session) {}
43}
44
Step 4 : Define data models to be used within the app
We need to create data model classes that we will use within our app. We will map the tables we had creating Profile and Message class. They will also contain a fromMap constructor to easily create them from the return value of Supabase.
1class Profile {
2 Profile({
3 required this.id,
4 required this.username,
5 required this.createdAt,
6 });
7
8 /// User ID of the profile
9 final String id;
10
11 /// Username of the profile
12 final String username;
13
14 /// Date and time when the profile was created
15 final DateTime createdAt;
16
17 Profile.fromMap(Map<String, dynamic> map)
18 : id = map['id'],
19 username = map['username'],
20 createdAt = DateTime.parse(map['created_at']);
21}
22
1class Message {
2 Message({
3 required this.id,
4 required this.profileId,
5 required this.content,
6 required this.createdAt,
7 required this.isMine,
8 });
9
10 /// ID of the message
11 final String id;
12
13 /// ID of the user who posted the message
14 final String profileId;
15
16 /// Text content of the message
17 final String content;
18
19 /// Date and time when the message was created
20 final DateTime createdAt;
21
22 /// Whether the message is sent by the user or not.
23 final bool isMine;
24
25 Message.fromMap({
26 required Map<String, dynamic> map,
27 required String myUserId,
28 }) : id = map['id'],
29 profileId = map['profile_id'],
30 content = map['content'],
31 createdAt = DateTime.parse(map['created_at']),
32 isMine = myUserId == map['profile_id'];
33}
34
Step 5: Create register page with email, password and username
Now that we have defined a few handy constants, it is time to dive into creating pages. The first page we will create is the register page. This page will take an email address, password, and username within a form widget. The username will be the primary identifier when users search for other users within the app. Once a user performs registration, they will be taken to the chat page. Let's create a lib/pages/register_page.dart
file and paste the following code.
1import 'package:flutter/material.dart';
2import 'package:my_chat_app/pages/chat_page.dart';
3import 'package:my_chat_app/pages/login_page.dart';
4import 'package:my_chat_app/utils/constants.dart';
5
6class RegisterPage extends StatefulWidget {
7 const RegisterPage({Key? key, required this.isRegistering}) : super(key: key);
8
9 static Route<void> route({bool isRegistering = false}) {
10 return MaterialPageRoute(
11 builder: (context) => RegisterPage(isRegistering: isRegistering),
12 );
13 }
14
15 final bool isRegistering;
16
17 @override
18 State<RegisterPage> createState() => _RegisterPageState();
19}
20
21class _RegisterPageState extends State<RegisterPage> {
22 final bool _isLoading = false;
23
24 final _formKey = GlobalKey<FormState>();
25
26 final _emailController = TextEditingController();
27 final _passwordController = TextEditingController();
28 final _usernameController = TextEditingController();
29
30 Future<void> _signUp() async {
31 final isValid = _formKey.currentState!.validate();
32 if (!isValid) {
33 return;
34 }
35 final email = _emailController.text;
36 final password = _passwordController.text;
37 final username = _usernameController.text;
38 final res = await supabase.auth
39 .signUp(email, password, userMetadata: {'username': username});
40 final error = res.error;
41 if (error != null) {
42 context.showErrorSnackBar(message: error.message);
43 return;
44 }
45 Navigator.of(context)
46 .pushAndRemoveUntil(ChatPage.route(), (route) => false);
47 }
48
49 @override
50 Widget build(BuildContext context) {
51 return Scaffold(
52 appBar: AppBar(
53 title: const Text('Register'),
54 ),
55 body: Form(
56 key: _formKey,
57 child: ListView(
58 padding: formPadding,
59 children: [
60 TextFormField(
61 controller: _emailController,
62 decoration: const InputDecoration(
63 label: Text('Email'),
64 ),
65 validator: (val) {
66 if (val == null || val.isEmpty) {
67 return 'Required';
68 }
69 return null;
70 },
71 keyboardType: TextInputType.emailAddress,
72 ),
73 formSpacer,
74 TextFormField(
75 controller: _passwordController,
76 obscureText: true,
77 decoration: const InputDecoration(
78 label: Text('Password'),
79 ),
80 validator: (val) {
81 if (val == null || val.isEmpty) {
82 return 'Required';
83 }
84 if (val.length < 6) {
85 return '6 characters minimum';
86 }
87 return null;
88 },
89 ),
90 formSpacer,
91 TextFormField(
92 controller: _usernameController,
93 decoration: const InputDecoration(
94 label: Text('Username'),
95 ),
96 validator: (val) {
97 if (val == null || val.isEmpty) {
98 return 'Required';
99 }
100 final isValid = RegExp(r'^[A-Za-z0-9_]{3,24}$').hasMatch(val);
101 if (!isValid) {
102 return '3-24 long with alphanumeric or underscore';
103 }
104 return null;
105 },
106 ),
107 formSpacer,
108 ElevatedButton(
109 onPressed: _isLoading ? null : _signUp,
110 child: const Text('Register'),
111 ),
112 formSpacer,
113 TextButton(
114 onPressed: () {
115 Navigator.of(context).push(LoginPage.route());
116 },
117 child: const Text('I already have an account'),
118 )
119 ],
120 ),
121 ),
122 );
123 }
124}
125
If you look at the validator function of the username field, you notice that we are enforcing the same regular expression check as what we defined in our table definition of profiles.
If you take a closer look at the _signup()
method, you notice that the username is passed as a userMetadata. We will need to copy this username into our profiles table so that other users can find you. In order to do this, we will utilize a Postgres function and Postgres trigger. Run the following SQL to create a Postgres function that will automatically run when a new user signs up to our application. Since we have set a unique constraint on the username column of our profiles table, the sign up will fail if a user chooses a username that is already taken.
1-- Function to create a new row in profiles table upon signup
2-- Also copies the username value from metadata
3create or replace function handle_new_user() returns trigger as $$
4 begin
5 insert into public.profiles(id, username)
6 values(new.id, new.raw_user_meta_data->>'username');
7
8 return new;
9 end;
10$$ language plpgsql security definer;
11
12-- Trigger to call `handle_new_user` when new user signs up
13create trigger on_auth_user_created
14 after insert on auth.users
15 for each row
16 execute function handle_new_user();
17
Also, Supabase has email confirmation turned on by default, meaning that every time someone signs up, they have to click the confirmation link they receive in their email. This is ideal for a production app, but for our sample app, we can turn it off since we want to get up and running with building a functioning chat app. We will cover secure authentications using Supabase in later articles. Go to authentication → settings and turn off the switch of Enable email confirmations
.
Step 6: Create login page
Login page will also be a simple page with an email and password field. Once they have signed in, the user will be taken to the rooms page.
1import 'package:flutter/material.dart';
2import 'package:my_chat_app/pages/chat_page.dart';
3import 'package:my_chat_app/utils/constants.dart';
4
5class LoginPage extends StatefulWidget {
6 const LoginPage({Key? key}) : super(key: key);
7
8 static Route<void> route() {
9 return MaterialPageRoute(builder: (context) => const LoginPage());
10 }
11
12 @override
13 _LoginPageState createState() => _LoginPageState();
14}
15
16class _LoginPageState extends State<LoginPage> {
17 bool _isLoading = false;
18 final _emailController = TextEditingController();
19 final _passwordController = TextEditingController();
20
21 Future<void> _signIn() async {
22 setState(() {
23 _isLoading = true;
24 });
25 final response = await supabase.auth.signIn(
26 email: _emailController.text,
27 password: _passwordController.text,
28 );
29 final error = response.error;
30 if (error != null) {
31 context.showErrorSnackBar(message: error.message);
32 }
33 Navigator.of(context)
34 .pushAndRemoveUntil(ChatPage.route(), (route) => false);
35 }
36
37 @override
38 void dispose() {
39 _emailController.dispose();
40 _passwordController.dispose();
41 super.dispose();
42 }
43
44 @override
45 Widget build(BuildContext context) {
46 return Scaffold(
47 appBar: AppBar(title: const Text('Sign In')),
48 body: ListView(
49 padding: formPadding,
50 children: [
51 TextFormField(
52 controller: _emailController,
53 decoration: const InputDecoration(labelText: 'Email'),
54 keyboardType: TextInputType.emailAddress,
55 ),
56 formSpacer,
57 TextFormField(
58 controller: _passwordController,
59 decoration: const InputDecoration(labelText: 'Password'),
60 obscureText: true,
61 ),
62 formSpacer,
63 ElevatedButton(
64 onPressed: _isLoading ? null : _signIn,
65 child: const Text('Login'),
66 ),
67 ],
68 ),
69 );
70 }
71}
72
Step 7: Create a chat page to receive and send real time messages
Last, we create the Chat page. This page will load the messages in real time and display them to the users. Users will also be able to send messages to everyone else using the app. We are using the stream() method on Supabase SDK to load the messages in realtime. As those messages come in, we are lazily loading the profiles of each message’s sender. We will display the user icon as soon as their profile data is available.
1import 'dart:async';
2
3import 'package:flutter/material.dart';
4
5import 'package:my_chat_app/models/message.dart';
6import 'package:my_chat_app/models/profile.dart';
7import 'package:my_chat_app/utils/constants.dart';
8import 'package:timeago/timeago.dart';
9
10/// Page to chat with someone.
11///
12/// Displays chat bubbles as a ListView and TextField to enter new chat.
13class ChatPage extends StatefulWidget {
14 const ChatPage({Key? key}) : super(key: key);
15
16 static Route<void> route() {
17 return MaterialPageRoute(
18 builder: (context) => const ChatPage(),
19 );
20 }
21
22 @override
23 State<ChatPage> createState() => _ChatPageState();
24}
25
26class _ChatPageState extends State<ChatPage> {
27 late final Stream<List<Message>> _messagesStream;
28 final Map<String, Profile> _profileCache = {};
29
30 @override
31 void initState() {
32 final myUserId = supabase.auth.currentUser!.id;
33 _messagesStream = supabase
34 .from('messages')
35 .stream(['id'])
36 .order('created_at')
37 .execute()
38 .map((maps) => maps
39 .map((map) => Message.fromMap(map: map, myUserId: myUserId))
40 .toList());
41 super.initState();
42 }
43
44 Future<void> _loadProfileCache(String profileId) async {
45 if (_profileCache[profileId] != null) {
46 return;
47 }
48 final res = await supabase
49 .from('profiles')
50 .select()
51 .match({'id': profileId})
52 .single()
53 .execute();
54 final data = res.data;
55 if (data != null) {
56 final profile = Profile.fromMap(data);
57 setState(() {
58 _profileCache[profileId] = profile;
59 });
60 }
61 }
62
63 @override
64 Widget build(BuildContext context) {
65 return Scaffold(
66 appBar: AppBar(title: const Text('Chat')),
67 body: StreamBuilder<List<Message>>(
68 stream: _messagesStream,
69 builder: (context, snapshot) {
70 if (snapshot.hasData) {
71 final messages = snapshot.data!;
72 return Column(
73 children: [
74 Expanded(
75 child: messages.isEmpty
76 ? const Center(
77 child: Text('Start your conversation now :)'),
78 )
79 : ListView.builder(
80 reverse: true,
81 itemCount: messages.length,
82 itemBuilder: (context, index) {
83 final message = messages[index];
84
85 /// I know it's not good to include code that is not related
86 /// to rendering the widget inside build method, but for
87 /// creating an app quick and dirty, it's fine 😂
88 _loadProfileCache(message.profileId);
89
90 return _ChatBubble(
91 message: message,
92 profile: _profileCache[message.profileId],
93 );
94 },
95 ),
96 ),
97 const _MessageBar(),
98 ],
99 );
100 } else {
101 return preloader;
102 }
103 },
104 ),
105 );
106 }
107}
108
109/// Set of widget that contains TextField and Button to submit message
110class _MessageBar extends StatefulWidget {
111 const _MessageBar({
112 Key? key,
113 }) : super(key: key);
114
115 @override
116 State<_MessageBar> createState() => _MessageBarState();
117}
118
119class _MessageBarState extends State<_MessageBar> {
120 late final TextEditingController _textController;
121
122 @override
123 Widget build(BuildContext context) {
124 return Material(
125 color: Colors.grey[200],
126 child: SafeArea(
127 child: Padding(
128 padding: const EdgeInsets.all(8.0),
129 child: Row(
130 children: [
131 Expanded(
132 child: TextFormField(
133 keyboardType: TextInputType.text,
134 maxLines: null,
135 autofocus: true,
136 controller: _textController,
137 decoration: const InputDecoration(
138 hintText: 'Type a message',
139 border: InputBorder.none,
140 focusedBorder: InputBorder.none,
141 contentPadding: EdgeInsets.all(8),
142 ),
143 ),
144 ),
145 TextButton(
146 onPressed: () => _submitMessage(),
147 child: const Text('Send'),
148 ),
149 ],
150 ),
151 ),
152 ),
153 );
154 }
155
156 @override
157 void initState() {
158 _textController = TextEditingController();
159 super.initState();
160 }
161
162 @override
163 void dispose() {
164 _textController.dispose();
165 super.dispose();
166 }
167
168 void _submitMessage() async {
169 final text = _textController.text;
170 final myUserId = supabase.auth.currentUser!.id;
171 if (text.isEmpty) {
172 return;
173 }
174 _textController.clear();
175
176 final res = await supabase.from('messages').insert({
177 'profile_id': myUserId,
178 'content': text,
179 }).execute();
180 final error = res.error;
181 if (error != null) {
182 context.showErrorSnackBar(message: error.message);
183 }
184 }
185}
186
187class _ChatBubble extends StatelessWidget {
188 const _ChatBubble({
189 Key? key,
190 required this.message,
191 required this.profile,
192 }) : super(key: key);
193
194 final Message message;
195 final Profile? profile;
196
197 @override
198 Widget build(BuildContext context) {
199 List<Widget> chatContents = [
200 if (!message.isMine)
201 CircleAvatar(
202 child: profile == null
203 ? preloader
204 : Text(profile!.username.substring(0, 2)),
205 ),
206 const SizedBox(width: 12),
207 Flexible(
208 child: Container(
209 padding: const EdgeInsets.symmetric(
210 vertical: 8,
211 horizontal: 12,
212 ),
213 decoration: BoxDecoration(
214 color: message.isMine
215 ? Theme.of(context).primaryColor
216 : Colors.grey[300],
217 borderRadius: BorderRadius.circular(8),
218 ),
219 child: Text(message.content),
220 ),
221 ),
222 const SizedBox(width: 12),
223 Text(format(message.createdAt, locale: 'en_short')),
224 const SizedBox(width: 60),
225 ];
226 if (message.isMine) {
227 chatContents = chatContents.reversed.toList();
228 }
229 return Padding(
230 padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 18),
231 child: Row(
232 mainAxisAlignment:
233 message.isMine ? MainAxisAlignment.end : MainAxisAlignment.start,
234 children: chatContents,
235 ),
236 );
237 }
238}
239
With that, we are done creating our application. If you kept your flutter run
running, you should now see a fully functional application on your device or simulator. You can install it on another device or simulator to chat with each other in real time.
Conclusion / Future improvements
We saw how easily it is to create a chat application when you combine amazing tools like Flutter and Supabase. One thing that was missing from this chat application is authorization. We did implement registration, but that was only to distinguish different users. In the coming up article, we will cover how you can add authorization using row level security in Supabase to secure this chat application. With authorization, we can create private chat rooms so that messages can only be seen by those inside those rooms.
If you have any questions please reach out via Twitter or join our Discord.