惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
AWS News Blog
AWS News Blog
V
Vulnerabilities – Threatpost
D
Darknet – Hacking Tools, Hacker News & Cyber Security
量子位
博客园 - 叶小钗
AI
AI
T
Tor Project blog
Forbes - Security
Forbes - Security
W
WeLiveSecurity
博客园_首页
爱范儿
爱范儿
J
Java Code Geeks
B
Blog
G
GRAHAM CLULEY
aimingoo的专栏
aimingoo的专栏
Cloudbric
Cloudbric
C
CXSECURITY Database RSS Feed - CXSecurity.com
TaoSecurity Blog
TaoSecurity Blog
L
LINUX DO - 热门话题
阮一峰的网络日志
阮一峰的网络日志
有赞技术团队
有赞技术团队
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Simon Willison's Weblog
Simon Willison's Weblog
云风的 BLOG
云风的 BLOG
Google DeepMind News
Google DeepMind News
H
Help Net Security
博客园 - 三生石上(FineUI控件)
C
Cisco Blogs
C
Cybersecurity and Infrastructure Security Agency CISA
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
P
Palo Alto Networks Blog
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
Recent Commits to openclaw:main
Recent Commits to openclaw:main
博客园 - 司徒正美
The Last Watchdog
The Last Watchdog
Blog — PlanetScale
Blog — PlanetScale
T
The Blog of Author Tim Ferriss
S
Secure Thoughts
Spread Privacy
Spread Privacy
F
Fortinet All Blogs
月光博客
月光博客
大猫的无限游戏
大猫的无限游戏
S
SegmentFault 最新的问题
H
Hackread – Cybersecurity News, Data Breaches, AI and More
A
About on SuperTechFans
Security Latest
Security Latest
Webroot Blog
Webroot Blog
Scott Helme
Scott Helme
Hugging Face - Blog
Hugging Face - Blog

Supabase Blog

AI Agents Know About Supabase. They Don't Always Use It Right. Custom OIDC Providers for Supabase Auth 100,000 GitHub stars Supabase docs over SSH Navigating Regional Network Blocks Supabase Joins the Stripe Projects Developer Preview Log Drains: Now available on Pro Supabase Storage: major performance, security, and reliability updates Supabase incident on February 12, 2026 Hydra joins Supabase X / Twitter OAuth 2.0 is now available for Supabase Auth BKND joins Supabase Supabase is now an official Claude connector Supabase PrivateLink is now available Introducing: Postgres Best Practices When to use Read Replicas vs. bigger compute Introducing TRAE SOLO integration with Supabase Supabase Security Retro: 2025 Sync Stripe Data to Your Supabase Database in One Click Building ChatGPT Apps with Supabase Edge Functions and mcp-use Own Your Observability: Supabase Metrics API Introducing iceberg-js: A JavaScript Client for Apache Iceberg Introducing Supabase for Platforms Adding Async Streaming to Postgres Foreign Data Wrappers Build "Sign in with Your App" using Supabase Auth Introducing Seven New Email Templates for Supabase Auth The new Supabase power for Kiro Introducing Supabase ETL Introducing Analytics Buckets Introducing Vector Buckets Snap, Inc. Launches Snap Cloud, Powered by Supabase Triplit joins Supabase Supabase Series E 1000 Y Combinator Founders Choose Supabase gm 👋 web3, welcome aboard to Sign in with Web3 (Solana, Ethereum) Announcing the Supabase Remote MCP Server Enterprise speed, enterprise standards with Bolt Cloud + Supabase PostgREST 13 Lovable Cloud + Supabase: The Default Platform for AI Builders Processing large jobs with Edge Functions, Cron, and Queues Defense in Depth for MCP Servers OrioleDB Patent: now freely available to the Postgres community Supabase Launch Week 15 Hackathon Winner Announcement The Vibe Coder's Guide to Supabase Environments Testing for Vibe Coders: From Zero to Production Confidence The Vibe Coding Master Checklist Vibe Coding: Best Practices for Prompting Supabase Auth: Build vs. Buy Top 10 Launches of Launch Week 15 Supabase Launch Week 15 Hackathon Storage: 10x Larger Uploads, 3x Cheaper Cached Egress, and 2x Egress Quota Persistent Storage and 97% Faster Cold Starts for Edge Functions Algolia Connector for Supabase New Observability Features in Supabase Improved Security Controls and A New Home for Security Introducing Branching 2.0 Stripe-To-Postgres Sync Engine as standalone Library Supabase Analytics Buckets with Iceberg Support Create a Supabase backend using Figma Make Introducing JWT Signing Keys Supabase UI: Platform Kit Build a Personalized AI Assistant with Postgres Announcing Multigres: Vitess for Postgres Building on open table formats Open Data Standards: Postgres, OTel, and Iceberg Simplifying back-end complexity with Supabase Data APIs PostgreSQL Event Triggers without superuser access Top 10 Launches of Launch Week 14 Supabase MCP Server Data API Routes to Nearest Read Replica Declarative Schemas for Simpler Database Management Realtime: Broadcast from Database Keeping Tabs on What's New in Supabase Studio Edge Functions: Deploy from the Dashboard + Deno 2.1 Automatic Embeddings in Postgres Introducing the Supabase UI Library Supabase Auth: Bring Your Own Clerk Postgres Language Server: Initial Release Migrating from Fauna to Supabase Migrating from the MongoDB Data API to Supabase Dedicated Poolers Postgres as a Graph Database: (Ab)using pgRouting AI Hackathon at Y Combinator Calendars in Postgres using Foreign Data Wrappers Supabase Launch Week 13 Hackathon Winners How to Hack the Base! Running Durable Workflows in Postgres using DBOS database.build v2: Bring-your-own-LLM Restore to a New Project Hack the Base! with Supabase Top 10 Launches of Launch Week 13 Supabase Queues High Performance Disk Supabase Cron Supabase CLI v2: Config as Code Supabase Edge Functions: Introducing Background Tasks, Ephemeral Storage, and WebSockets Supabase AI Assistant v2 OrioleDB Public Alpha Executing Dynamic JavaScript Code on Supabase with Edge Functions ClickHouse Partnership, improved Postgres Replication, and Disk Management
Flutter Tutorial: building a Flutter chat app
Tyler Shukert · 2022-06-30 · via Supabase Blog

Flutter Tutorial: building a Flutter chat app

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 the second part, I will show you how to implement Authentication and Authorization (with RLS) to the chat app.

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.

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.

Create a blank Flutter application#

We will start out by creating an empty Flutter project.

First, open your terminal and type


_10

flutter create my_chat_app


Once it is done, you can go into your app and run it.


_10

cd my_chat_app

_10

flutter run


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.


_10

supabase_flutter: ^1.0.0

_10

timeago: ^3.1.0


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 Plan 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.


_17

create table if not exists public.profiles (

_17

id uuid references auth.users on delete cascade not null primary key,

_17

username varchar(24) not null unique,

_17

created_at timestamp with time zone default timezone('utc' :: text, now()) not null,

_17

_17

-- username should be 3 to 24 characters long containing alphabets, numbers and underscores

_17

constraint username_validation check (username ~* '^[A-Za-z0-9_]{3,24}$')

_17

);

_17

comment on table public.profiles is 'Holds all of users profile information';

_17

_17

create table if not exists public.messages (

_17

id uuid not null primary key default gen_random_uuid(),

_17

profile_id uuid default auth.uid() references public.profiles(id) on delete cascade not null,

_17

content varchar(500) not null,

_17

created_at timestamp with time zone default timezone('utc' :: text, now()) not null

_17

);

_17

comment on table public.messages is 'Holds individual messages sent on the app.';


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.


_10

-- *** Add tables to the publication to enable real time subscription ***

_10

alter publication supabase_realtime add table public.messages;


Now that we have defined what our data looks like, let’s have some fun writing Flutter code!

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.


_83

import 'package:flutter/material.dart';

_83

import 'package:supabase_flutter/supabase_flutter.dart';

_83

_83

/// Supabase client

_83

final supabase = Supabase.instance.client;

_83

_83

/// Simple preloader inside a Center widget

_83

const preloader =

_83

Center(child: CircularProgressIndicator(color: Colors.orange));

_83

_83

/// Simple sized box to space out form elements

_83

const formSpacer = SizedBox(width: 16, height: 16);

_83

_83

/// Some padding for all the forms to use

_83

const formPadding = EdgeInsets.symmetric(vertical: 20, horizontal: 16);

_83

_83

/// Error message to display the user when unexpected error occurs.

_83

const unexpectedErrorMessage = 'Unexpected error occurred.';

_83

_83

/// Basic theme to change the look and feel of the app

_83

final appTheme = ThemeData.light().copyWith(

_83

primaryColorDark: Colors.orange,

_83

appBarTheme: const AppBarTheme(

_83

elevation: 1,

_83

backgroundColor: Colors.white,

_83

iconTheme: IconThemeData(color: Colors.black),

_83

titleTextStyle: TextStyle(

_83

color: Colors.black,

_83

fontSize: 18,

_83

),

_83

),

_83

primaryColor: Colors.orange,

_83

textButtonTheme: TextButtonThemeData(

_83

style: TextButton.styleFrom(

_83

foregroundColor: Colors.orange,

_83

),

_83

),

_83

elevatedButtonTheme: ElevatedButtonThemeData(

_83

style: ElevatedButton.styleFrom(

_83

foregroundColor: Colors.white,

_83

backgroundColor: Colors.orange,

_83

),

_83

),

_83

inputDecorationTheme: InputDecorationTheme(

_83

floatingLabelStyle: const TextStyle(

_83

color: Colors.orange,

_83

),

_83

border: OutlineInputBorder(

_83

borderRadius: BorderRadius.circular(12),

_83

borderSide: const BorderSide(

_83

color: Colors.grey,

_83

width: 2,

_83

),

_83

),

_83

focusColor: Colors.orange,

_83

focusedBorder: OutlineInputBorder(

_83

borderRadius: BorderRadius.circular(12),

_83

borderSide: const BorderSide(

_83

color: Colors.orange,

_83

width: 2,

_83

),

_83

),

_83

),

_83

);

_83

_83

/// Set of extension methods to easily display a snackbar

_83

extension ShowSnackBar on BuildContext {

_83

/// Displays a basic snackbar

_83

void showSnackBar({

_83

required String message,

_83

Color backgroundColor = Colors.white,

_83

}) {

_83

ScaffoldMessenger.of(this).showSnackBar(SnackBar(

_83

content: Text(message),

_83

backgroundColor: backgroundColor,

_83

));

_83

}

_83

_83

/// Displays a red snackbar indicating error

_83

void showErrorSnackBar({required String message}) {

_83

showSnackBar(message: message, backgroundColor: Colors.red);

_83

}

_83

}


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.


_29

import 'package:flutter/material.dart';

_29

import 'package:my_chat_app/utils/constants.dart';

_29

import 'package:supabase_flutter/supabase_flutter.dart';

_29

import 'package:my_chat_app/pages/splash_page.dart';

_29

_29

Future<void> main() async {

_29

WidgetsFlutterBinding.ensureInitialized();

_29

_29

await Supabase.initialize(

_29

// TODO: Replace credentials with your own

_29

url: 'SUPABASE_URL',

_29

anonKey: 'SUPABASE_ANON_KEY',

_29

);

_29

runApp(const MyApp());

_29

}

_29

_29

class MyApp extends StatelessWidget {

_29

const MyApp({Key? key}) : super(key: key);

_29

_29

@override

_29

Widget build(BuildContext context) {

_29

return MaterialApp(

_29

debugShowCheckedModeBanner: false,

_29

title: 'My Chat App',

_29

theme: appTheme,

_29

home: const SplashPage(),

_29

);

_29

}

_29

}


Step 3: Redirect users depending on auth state using splash page#

When a 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. Within the _redirect() method, we are fetching the auth state and navigating the user accordingly.


_39

import 'package:flutter/material.dart';

_39

import 'package:my_chat_app/pages/chat_page.dart';

_39

import 'package:my_chat_app/pages/register_page.dart';

_39

import 'package:my_chat_app/utils/constants.dart';

_39

_39

/// Page to redirect users to the appropriate page depending on the initial auth state

_39

class SplashPage extends StatefulWidget {

_39

const SplashPage({Key? key}) : super(key: key);

_39

_39

@override

_39

SplashPageState createState() => SplashPageState();

_39

}

_39

_39

class SplashPageState extends State<SplashPage> {

_39

@override

_39

void initState() {

_39

super.initState();

_39

_redirect();

_39

}

_39

_39

Future<void> _redirect() async {

_39

// await for for the widget to mount

_39

await Future.delayed(Duration.zero);

_39

_39

final session = supabase.auth.currentSession;

_39

if (session == null) {

_39

Navigator.of(context)

_39

.pushAndRemoveUntil(RegisterPage.route(), (route) => false);

_39

} else {

_39

Navigator.of(context)

_39

.pushAndRemoveUntil(ChatPage.route(), (route) => false);

_39

}

_39

}

_39

_39

@override

_39

Widget build(BuildContext context) {

_39

return const Scaffold(body: preloader);

_39

}

_39

}


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.


_21

class Profile {

_21

Profile({

_21

required this.id,

_21

required this.username,

_21

required this.createdAt,

_21

});

_21

_21

/// User ID of the profile

_21

final String id;

_21

_21

/// Username of the profile

_21

final String username;

_21

_21

/// Date and time when the profile was created

_21

final DateTime createdAt;

_21

_21

Profile.fromMap(Map<String, dynamic> map)

_21

: id = map['id'],

_21

username = map['username'],

_21

createdAt = DateTime.parse(map['created_at']);

_21

}



_33

class Message {

_33

Message({

_33

required this.id,

_33

required this.profileId,

_33

required this.content,

_33

required this.createdAt,

_33

required this.isMine,

_33

});

_33

_33

/// ID of the message

_33

final String id;

_33

_33

/// ID of the user who posted the message

_33

final String profileId;

_33

_33

/// Text content of the message

_33

final String content;

_33

_33

/// Date and time when the message was created

_33

final DateTime createdAt;

_33

_33

/// Whether the message is sent by the user or not.

_33

final bool isMine;

_33

_33

Message.fromMap({

_33

required Map<String, dynamic> map,

_33

required String myUserId,

_33

}) : id = map['id'],

_33

profileId = map['profile_id'],

_33

content = map['content'],

_33

createdAt = DateTime.parse(map['created_at']),

_33

isMine = myUserId == map['profile_id'];

_33

}


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.


_126

import 'package:flutter/material.dart';

_126

import 'package:my_chat_app/pages/chat_page.dart';

_126

import 'package:my_chat_app/pages/login_page.dart';

_126

import 'package:my_chat_app/utils/constants.dart';

_126

import 'package:supabase_flutter/supabase_flutter.dart';

_126

_126

class RegisterPage extends StatefulWidget {

_126

const RegisterPage({Key? key, required this.isRegistering}) : super(key: key);

_126

_126

static Route<void> route({bool isRegistering = false}) {

_126

return MaterialPageRoute(

_126

builder: (context) => RegisterPage(isRegistering: isRegistering),

_126

);

_126

}

_126

_126

final bool isRegistering;

_126

_126

@override

_126

State<RegisterPage> createState() => _RegisterPageState();

_126

}

_126

_126

class _RegisterPageState extends State<RegisterPage> {

_126

final bool _isLoading = false;

_126

_126

final _formKey = GlobalKey<FormState>();

_126

_126

final _emailController = TextEditingController();

_126

final _passwordController = TextEditingController();

_126

final _usernameController = TextEditingController();

_126

_126

Future<void> _signUp() async {

_126

final isValid = _formKey.currentState!.validate();

_126

if (!isValid) {

_126

return;

_126

}

_126

final email = _emailController.text;

_126

final password = _passwordController.text;

_126

final username = _usernameController.text;

_126

try {

_126

await supabase.auth.signUp(

_126

email: email, password: password, data: {'username': username});

_126

Navigator.of(context)

_126

.pushAndRemoveUntil(ChatPage.route(), (route) => false);

_126

} on AuthException catch (error) {

_126

context.showErrorSnackBar(message: error.message);

_126

} catch (error) {

_126

context.showErrorSnackBar(message: unexpectedErrorMessage);

_126

}

_126

}

_126

_126

@override

_126

Widget build(BuildContext context) {

_126

return Scaffold(

_126

appBar: AppBar(

_126

title: const Text('Register'),

_126

),

_126

body: Form(

_126

key: _formKey,

_126

child: ListView(

_126

padding: formPadding,

_126

children: [

_126

TextFormField(

_126

controller: _emailController,

_126

decoration: const InputDecoration(

_126

label: Text('Email'),

_126

),

_126

validator: (val) {

_126

if (val == null || val.isEmpty) {

_126

return 'Required';

_126

}

_126

return null;

_126

},

_126

keyboardType: TextInputType.emailAddress,

_126

),

_126

formSpacer,

_126

TextFormField(

_126

controller: _passwordController,

_126

obscureText: true,

_126

decoration: const InputDecoration(

_126

label: Text('Password'),

_126

),

_126

validator: (val) {

_126

if (val == null || val.isEmpty) {

_126

return 'Required';

_126

}

_126

if (val.length < 6) {

_126

return '6 characters minimum';

_126

}

_126

return null;

_126

},

_126

),

_126

formSpacer,

_126

TextFormField(

_126

controller: _usernameController,

_126

decoration: const InputDecoration(

_126

label: Text('Username'),

_126

),

_126

validator: (val) {

_126

if (val == null || val.isEmpty) {

_126

return 'Required';

_126

}

_126

final isValid = RegExp(r'^[A-Za-z0-9_]{3,24}$').hasMatch(val);

_126

if (!isValid) {

_126

return '3-24 long with alphanumeric or underscore';

_126

}

_126

return null;

_126

},

_126

),

_126

formSpacer,

_126

ElevatedButton(

_126

onPressed: _isLoading ? null : _signUp,

_126

child: const Text('Register'),

_126

),

_126

formSpacer,

_126

TextButton(

_126

onPressed: () {

_126

Navigator.of(context).push(LoginPage.route());

_126

},

_126

child: const Text('I already have an account'),

_126

)

_126

],

_126

),

_126

),

_126

);

_126

}

_126

}


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 data. By passing data to your sign up method, you can populate the meta data field of your authenticated users. 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.


_16

-- Function to create a new row in profiles table upon signup

_16

-- Also copies the username value from metadata

_16

create or replace function handle_new_user() returns trigger as $$

_16

begin

_16

insert into public.profiles(id, username)

_16

values(new.id, new.raw_user_meta_data->>'username');

_16

_16

return new;

_16

end;

_16

$$ language plpgsql security definer;

_16

_16

-- Trigger to call `handle_new_user` when new user signs up

_16

create trigger on_auth_user_created

_16

after insert on auth.users

_16

for each row

_16

execute function handle_new_user();


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.


_79

import 'package:flutter/material.dart';

_79

import 'package:my_chat_app/pages/chat_page.dart';

_79

import 'package:my_chat_app/utils/constants.dart';

_79

import 'package:supabase_flutter/supabase_flutter.dart';

_79

_79

class LoginPage extends StatefulWidget {

_79

const LoginPage({Key? key}) : super(key: key);

_79

_79

static Route<void> route() {

_79

return MaterialPageRoute(builder: (context) => const LoginPage());

_79

}

_79

_79

@override

_79

_LoginPageState createState() => _LoginPageState();

_79

}

_79

_79

class _LoginPageState extends State<LoginPage> {

_79

bool _isLoading = false;

_79

final _emailController = TextEditingController();

_79

final _passwordController = TextEditingController();

_79

_79

Future<void> _signIn() async {

_79

setState(() {

_79

_isLoading = true;

_79

});

_79

try {

_79

await supabase.auth.signInWithPassword(

_79

email: _emailController.text,

_79

password: _passwordController.text,

_79

);

_79

Navigator.of(context)

_79

.pushAndRemoveUntil(ChatPage.route(), (route) => false);

_79

} on AuthException catch (error) {

_79

context.showErrorSnackBar(message: error.message);

_79

} catch (_) {

_79

context.showErrorSnackBar(message: unexpectedErrorMessage);

_79

}

_79

if (mounted) {

_79

setState(() {

_79

_isLoading = true;

_79

});

_79

}

_79

}

_79

_79

@override

_79

void dispose() {

_79

_emailController.dispose();

_79

_passwordController.dispose();

_79

super.dispose();

_79

}

_79

_79

@override

_79

Widget build(BuildContext context) {

_79

return Scaffold(

_79

appBar: AppBar(title: const Text('Sign In')),

_79

body: ListView(

_79

padding: formPadding,

_79

children: [

_79

TextFormField(

_79

controller: _emailController,

_79

decoration: const InputDecoration(labelText: 'Email'),

_79

keyboardType: TextInputType.emailAddress,

_79

),

_79

formSpacer,

_79

TextFormField(

_79

controller: _passwordController,

_79

decoration: const InputDecoration(labelText: 'Password'),

_79

obscureText: true,

_79

),

_79

formSpacer,

_79

ElevatedButton(

_79

onPressed: _isLoading ? null : _signIn,

_79

child: const Text('Login'),

_79

),

_79

],

_79

),

_79

);

_79

}

_79

}


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.


_232

import 'dart:async';

_232

_232

import 'package:flutter/material.dart';

_232

_232

import 'package:my_chat_app/models/message.dart';

_232

import 'package:my_chat_app/models/profile.dart';

_232

import 'package:my_chat_app/utils/constants.dart';

_232

import 'package:supabase_flutter/supabase_flutter.dart';

_232

import 'package:timeago/timeago.dart';

_232

_232

/// Page to chat with someone.

_232

///

_232

/// Displays chat bubbles as a ListView and TextField to enter new chat.

_232

class ChatPage extends StatefulWidget {

_232

const ChatPage({Key? key}) : super(key: key);

_232

_232

static Route<void> route() {

_232

return MaterialPageRoute(

_232

builder: (context) => const ChatPage(),

_232

);

_232

}

_232

_232

@override

_232

State<ChatPage> createState() => _ChatPageState();

_232

}

_232

_232

class _ChatPageState extends State<ChatPage> {

_232

late final Stream<List<Message>> _messagesStream;

_232

final Map<String, Profile> _profileCache = {};

_232

_232

@override

_232

void initState() {

_232

final myUserId = supabase.auth.currentUser!.id;

_232

_messagesStream = supabase

_232

.from('messages')

_232

.stream(primaryKey: ['id'])

_232

.order('created_at')

_232

.map((maps) => maps

_232

.map((map) => Message.fromMap(map: map, myUserId: myUserId))

_232

.toList());

_232

super.initState();

_232

}

_232

_232

Future<void> _loadProfileCache(String profileId) async {

_232

if (_profileCache[profileId] != null) {

_232

return;

_232

}

_232

final data =

_232

await supabase.from('profiles').select().eq('id', profileId).single();

_232

final profile = Profile.fromMap(data);

_232

setState(() {

_232

_profileCache[profileId] = profile;

_232

});

_232

}

_232

_232

@override

_232

Widget build(BuildContext context) {

_232

return Scaffold(

_232

appBar: AppBar(title: const Text('Chat')),

_232

body: StreamBuilder<List<Message>>(

_232

stream: _messagesStream,

_232

builder: (context, snapshot) {

_232

if (snapshot.hasData) {

_232

final messages = snapshot.data!;

_232

return Column(

_232

children: [

_232

Expanded(

_232

child: messages.isEmpty

_232

? const Center(

_232

child: Text('Start your conversation now :)'),

_232

)

_232

: ListView.builder(

_232

reverse: true,

_232

itemCount: messages.length,

_232

itemBuilder: (context, index) {

_232

final message = messages[index];

_232

_232

/// I know it's not good to include code that is not related

_232

/// to rendering the widget inside build method, but for

_232

/// creating an app quick and dirty, it's fine 😂

_232

_loadProfileCache(message.profileId);

_232

_232

return _ChatBubble(

_232

message: message,

_232

profile: _profileCache[message.profileId],

_232

);

_232

},

_232

),

_232

),

_232

const _MessageBar(),

_232

],

_232

);

_232

} else {

_232

return preloader;

_232

}

_232

},

_232

),

_232

);

_232

}

_232

}

_232

_232

/// Set of widget that contains TextField and Button to submit message

_232

class _MessageBar extends StatefulWidget {

_232

const _MessageBar({

_232

Key? key,

_232

}) : super(key: key);

_232

_232

@override

_232

State<_MessageBar> createState() => _MessageBarState();

_232

}

_232

_232

class _MessageBarState extends State<_MessageBar> {

_232

late final TextEditingController _textController;

_232

_232

@override

_232

Widget build(BuildContext context) {

_232

return Material(

_232

color: Colors.grey[200],

_232

child: SafeArea(

_232

child: Padding(

_232

padding: const EdgeInsets.all(8.0),

_232

child: Row(

_232

children: [

_232

Expanded(

_232

child: TextFormField(

_232

keyboardType: TextInputType.text,

_232

maxLines: null,

_232

autofocus: true,

_232

controller: _textController,

_232

decoration: const InputDecoration(

_232

hintText: 'Type a message',

_232

border: InputBorder.none,

_232

focusedBorder: InputBorder.none,

_232

contentPadding: EdgeInsets.all(8),

_232

),

_232

),

_232

),

_232

TextButton(

_232

onPressed: () => _submitMessage(),

_232

child: const Text('Send'),

_232

),

_232

],

_232

),

_232

),

_232

),

_232

);

_232

}

_232

_232

@override

_232

void initState() {

_232

_textController = TextEditingController();

_232

super.initState();

_232

}

_232

_232

@override

_232

void dispose() {

_232

_textController.dispose();

_232

super.dispose();

_232

}

_232

_232

void _submitMessage() async {

_232

final text = _textController.text;

_232

final myUserId = supabase.auth.currentUser!.id;

_232

if (text.isEmpty) {

_232

return;

_232

}

_232

_textController.clear();

_232

try {

_232

await supabase.from('messages').insert({

_232

'profile_id': myUserId,

_232

'content': text,

_232

});

_232

} on PostgrestException catch (error) {

_232

context.showErrorSnackBar(message: error.message);

_232

} catch (_) {

_232

context.showErrorSnackBar(message: unexpectedErrorMessage);

_232

}

_232

}

_232

}

_232

_232

class _ChatBubble extends StatelessWidget {

_232

const _ChatBubble({

_232

Key? key,

_232

required this.message,

_232

required this.profile,

_232

}) : super(key: key);

_232

_232

final Message message;

_232

final Profile? profile;

_232

_232

@override

_232

Widget build(BuildContext context) {

_232

List<Widget> chatContents = [

_232

if (!message.isMine)

_232

CircleAvatar(

_232

child: profile == null

_232

? preloader

_232

: Text(profile!.username.substring(0, 2)),

_232

),

_232

const SizedBox(width: 12),

_232

Flexible(

_232

child: Container(

_232

padding: const EdgeInsets.symmetric(

_232

vertical: 8,

_232

horizontal: 12,

_232

),

_232

decoration: BoxDecoration(

_232

color: message.isMine

_232

? Theme.of(context).primaryColor

_232

: Colors.grey[300],

_232

borderRadius: BorderRadius.circular(8),

_232

),

_232

child: Text(message.content),

_232

),

_232

),

_232

const SizedBox(width: 12),

_232

Text(format(message.createdAt, locale: 'en_short')),

_232

const SizedBox(width: 60),

_232

];

_232

if (message.isMine) {

_232

chatContents = chatContents.reversed.toList();

_232

}

_232

return Padding(

_232

padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 18),

_232

child: Row(

_232

mainAxisAlignment:

_232

message.isMine ? MainAxisAlignment.end : MainAxisAlignment.start,

_232

children: chatContents,

_232

),

_232

);

_232

}

_232

}


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.

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.