Build a Real-Time Flutter Chat App for Free (2026 Tutorial)
Ryan Yang
Nexconn Infrastructure Engineer. Optimizes latency and scales microservices for hundreds of millions of concurrent users. Shares technical deep dives and backend lessons for zero-latency communication.
Flutter makes it possible to build one product experience for iOS and Android, but a reliable chat feature still needs more than a message list and a WebSocket connection. Mobile networks change, apps move between foreground and background, users sign in on multiple devices, and missed messages must be synchronized after reconnecting.
To support production-oriented integrations from day one, this tutorial uses the Nexconn Production-Ready Free Plan. This plan supports up to 10,000 MAU with full Pro-tier features—including multi-channel push notifications, 10GB storage/upload, and a 180-day history—with no credit card required. It is sized for real product launches, not just small-scale prototypes.
This tutorial uses the Nexconn Chat SDK for Flutter to build the core of a production-oriented chat integration: initialize the SDK, receive real-time and offline messages, securely connect a user, send a direct text message, expose events to Flutter widgets, and clean up the session correctly.
Getting Started & Architecture Overview
By the end, your Flutter app will have:
one centralized owner for the Nexconn engine lifecycle;
a secure token flow through your application backend;
connection status and incoming message streams;
direct text message sending with local-save and final-send callbacks;
explicit handler removal, disconnect, and engine destruction;
a foundation for group chat, media messages, unread state, push, and moderation.
The tutorial uses the lower-level Chat SDK. It does not generate a complete chat UI automatically. You can build your own screens on top of these APIs or use Nexconn Chat UI when you want prebuilt conversation and message components.
Prerequisites
The current Flutter SDK source requires:
Dart ^3.7.2;
Flutter >=3.29.2;
a Nexconn developer account;
a Development App Key;
an application backend that obtains a user token from the Nexconn Server API;
two test users if you want to verify a real message exchange.
Register in the Nexconn Console. The console creates a Development application and App Key. Development and Production environments have separate keys and isolated data.
Installation & Workflow Overview
1. Add the Flutter Package
Run this command in your terminal:
flutter pub add ai_nexconn_chat_plugin
2. High-Level Implementation Order
Follow this sequence to integrate the core messaging capabilities. Do not skip steps, especially handler registration.
NCEngine.initialize
-> add connection and message handlers
-> request a user token from your backend
-> NCEngine.connect
-> DirectChannel(...).sendMessage(...)
-> remove handlers and disconnect when the session ends
Understand the Authentication Boundary
Nexconn uses two different credential types:
Credential
Where it belongs
Purpose
App Key
Flutter client only
Identifies the Nexconn application
App Secret
Backend only
Authorizes Nexconn Server API calls
User Token
Returned by your backend to the signed-in client
Connects and authenticates one Nexconn user
The production flow should look like this:
Flutter app -> GET /api/chat/token with app session
Backend -> verifies the current app user
Backend -> maps app user ID to a stable Nexconn user ID
Backend -> calls Nexconn user registration/token API using App Secret
Backend -> returns { userId, token }
Flutter app -> NCEngine.connect(ConnectParams(token: token))
Do not call the Nexconn Server API directly from Dart. A mobile binary can be inspected, so any App Secret included in it should be considered exposed.
📋 Before you continue
You’ll need a Nexconn App Key to initialize the SDK in the next steps. If you haven’t already, create a free account and claim the Production-Ready Free Plan (10K MAU) now.
Using flutter pub add is preferable for a new project because it resolves the latest compatible published version instead of freezing an article's version number indefinitely.
Step 2: SDK Initialization
Call NCEngine.initialize() before any other SDK API. Here's a simple app-level setup:
InitParams also supports custom navigation, file, statistics, and log servers, push options, compression options, and reconnect device behavior. Most public-cloud integrations only need the App Key and the correct AreaCode.
Use build-time configuration for Development and Production App Keys:
flutter run --dart-define=NEXCONN_APP_KEY=your-development-app-key
An App Key is not a signing secret, but environment configuration still helps prevent Development and Production data from being mixed.
Step 3: Registering Event Handlers
Register message and connection handlers before calling connect. The server can begin delivering connection state and offline messages as soon as the session is established.
The Flutter wrapper can report a successful connection as NCError(code: 0). For that reason, checking only error == null is too strict. Use error.isSuccess or accept both null and code 0 as success.
The SDK handles automatic reconnection. Observe connection status to update banners or disable sending when necessary, but do not call connect again for every temporary network interruption.
Step 5: Send a Direct Text Message
A one-to-one conversation is represented by DirectChannel, whose channel ID is the other user's ID.
final channel = DirectChannel('recipient-user-id');
await channel.sendMessage(
SendMessageParams(
messageParams: TextMessageParams(
text: 'Hello from Nexconn Flutter!',
),
),
callback: SendMessageCallback(
onMessageSaved: (message) {
// The message is stored locally. Insert or update it in the UI.
print('Saved locally: ${message?.messageId}');
},
onMessageSent: (code, message) {
if (code == 0) {
print('Sent: ${message?.messageId}');
} else {
print('Send failed with code $code');
}
},
),
);
The two callbacks support an optimistic UI:
onMessageSaved gives you the locally persisted message.
Render it immediately with a sending state.
onMessageSent changes the bubble to sent or failed.
Keep the message ID stable so the UI updates the existing item instead of inserting a duplicate.
Step 6: Handling Various Message Types
The SDK exports typed message classes. A basic renderer can branch on the received message type:
String messagePreview(Message message) {
if (message is TextMessage) {
return message.text ?? '';
}
if (message is ImageMessage) {
return '[Image]';
}
if (message is FileMessage) {
return '[File]';
}
if (message is HDVoiceMessage) {
return '[Voice]';
}
if (message is ShortVideoMessage) {
return '[Video]';
}
if (message is CustomMessage) {
return '[Custom message]';
}
return '[Unsupported message]';
}
Nexconn also exposes GIF, location, reference, combined, command, stream, custom media, group notification, and other message types. Add only the types your product can render, and provide a safe fallback for messages introduced by a newer client.
Step 7: Creating a Reusable Service
Initializing and registering listeners from individual widgets creates duplicate connections and lifecycle bugs. A better Flutter pattern is one app-level service that owns the engine and exposes streams to your state-management layer.
Provide this service through Riverpod, Provider, get_it, Bloc, or your existing dependency injection system. The important decision is ownership: only one object should initialize and destroy the global engine.
Step 8: Connecting the Service to Your UI
A chat screen normally combines three sources of state:
initial paginated history from the channel query APIs;
real-time messages from NexconnChatService.messages;
local outgoing messages and their sending/failed state.
Keep SDK Message objects in a repository or map them into your own view model:
class ChatMessageViewData {
final String id;
final String text;
final bool isMine;
final bool isSending;
final bool hasFailed;
const ChatMessageViewData({
required this.id,
required this.text,
required this.isMine,
required this.isSending,
required this.hasFailed,
});
}
This separation makes it easier to:
preserve scroll position while older messages load;
merge local and remote updates by message ID;
retry failed sends;
render unsupported custom messages safely;
test the widget layer without a live SDK connection.
For a first UI, use a reverse ListView, a composer with a send command, explicit loading/error states, and a connection banner. Avoid placing SDK initialization inside build() or page-level initState() if navigating between pages can create more than one owner.
Beyond Text Chat: Groups, Media, & Push
Group Chat and Other Channel Types
Once direct messaging is stable, the same send pattern works with other channel objects:
final group = GroupChannel('group-id');
final open = OpenChannel('open-channel-id');
final community = CommunityChannel('community-id');
The relevant product rules are different:
group channels need membership, roles, invites, kicks, mutes, and profile updates;
open channels need enter/leave state, high-volume behavior, moderation, and message priority;
community channels need subchannel navigation, permissions, and large-scale member design.
Do not treat these as a UI-only switch. Define authorization and moderation behavior on your backend before exposing the channel to users.
Rich Media Messages
The SDK separates regular messages from media upload flows. For images, files, voice, video, GIF, and custom media messages, use sendMediaMessage with the corresponding message params and handle:
local save/attachment;
upload progress;
final send result;
cancellation;
local path and remote URL state;
file size, compression, retry, and permission failures.
Test media on real iOS and Android devices. Simulators are not enough for camera, microphone, photo-library permissions, background behavior, and push notifications.
Planning Offline Push Notifications
Real-time delivery works while the client is connected. Mobile engagement also requires APNs and FCM configuration, device token lifecycle handling, notification payloads, deep links, muted-channel behavior, and badge reconciliation.
Push is a native platform integration even when the app is written in Flutter. Complete the Nexconn Android and iOS push setup, then test:
app in foreground;
app in background;
app terminated;
token refresh;
user logout and account switching;
tapping a notification for an existing or unavailable channel.
Common Integration Mistakes
Initializing from more than one widget
NCEngine is global. Initialize it in one app-level owner, not every screen.
Connecting before registering handlers
Register handlers first so initial connection and offline synchronization events are observable.
Treating error != null as connection failure
The current wrapper can return NCError(code: 0) on success. Check isSuccess.
Calling connect after every reconnect event
The SDK reconnects automatically. Repeated calls can create race conditions and confusing UI state.
Forgetting to remove handlers
Handler registries use string IDs. Remove the exact ID when the owner is disposed, or callbacks can be delivered to stale feature state.
Rendering a second outgoing message after the callback
Use the locally saved message ID to update the optimistic bubble. Do not insert a new list item for each callback stage.
Shipping test tokens
Tokens belong to individual users and should come from your authenticated backend. Do not commit sample tokens or App Secrets.
Production Checklist
Flutter and Dart versions satisfy the package requirements.
Development and Production App Keys are selected by environment.
App Secret and Server API calls stay on the backend.
Token endpoint verifies the current app session and returns a stable user mapping.
One app-level service owns initialization, handlers, connection, and destruction.
Handlers are registered before connecting and removed during disposal.
Connection success accepts NCError(code: 0) in the Flutter wrapper.
The UI supports loading, empty, reconnecting, token-expired, sending, sent, and failed states.
History and live events are merged by stable message ID.
Media permissions, upload progress, cancellation, and retry are tested on real devices.
APNs/FCM push and notification deep links are verified.
Offline, app restart, account switching, weak network, and multi-device behavior are tested.
Group/open/community permissions and moderation are enforced outside the UI.
Start the Flutter Integration
Create a free Nexconn application, add the Flutter package, implement one authenticated token endpoint, and test the first direct message between two users.
We'd love to discuss how Nexconn's real-time communication solutions can support your business. Request a demo, explore pricing, or get tailored onboarding guidance.