Screens / messaging
Conversations
Lists the conversations of a messaging app, pinned first, with search, pull to refresh, a new conversation, archive, mute and delete, and its empty, loading, error and offline states.
Import
Conversations is part of Fliin Pro. With a license key, sign in once with fliin login, then add it in a Flutter app where fliin init has run:
fliin login
fliin add conversationsAlso installs: alert, app-bar, avatar, confirm-delete, conversation-row, core, empty-state, icons, page, search-field, separator, skeleton, text-field
Examples
Loaded
The list of conversations, sorted with the pinned ones first. standalone holds its own controller over the service you pass; give it yours to reach your backend.
import 'package:your_app/ui/fliin/screens/conversations/conversations_controller.dart';
import 'package:your_app/ui/fliin/screens/conversations/conversations_data.dart';
import 'package:your_app/ui/fliin/screens/conversations/conversations_screen.dart';
import 'package:flutter/widgets.dart';
class ConversationsLoadedExample extends StatelessWidget {
const ConversationsLoadedExample({super.key});
@override
Widget build(BuildContext context) {
return ConversationsScreen.standalone(
service: SampleConversationsService(latency: const Duration(milliseconds: 600)),
initial: ConversationsLoaded(sortConversations(sampleConversations)),
onNewConversation: () {},
onOpen: (_) {},
clock: () => sampleConversationsNow,
);
}
}Loading
While the service answers, placeholders keep the shape of the rows.
import 'package:your_app/ui/fliin/screens/conversations/conversations_controller.dart';
import 'package:your_app/ui/fliin/screens/conversations/conversations_data.dart';
import 'package:your_app/ui/fliin/screens/conversations/conversations_screen.dart';
import 'package:flutter/widgets.dart';
class ConversationsLoadingExample extends StatelessWidget {
const ConversationsLoadingExample({super.key});
@override
Widget build(BuildContext context) {
return ConversationsScreen.standalone(
service: SampleConversationsService(latency: const Duration(milliseconds: 600)),
initial: const ConversationsLoading(),
onNewConversation: () {},
onOpen: (_) {},
clock: () => sampleConversationsNow,
);
}
}Empty
No conversation yet: the screen invites the user to start one.
import 'package:your_app/ui/fliin/screens/conversations/conversations_controller.dart';
import 'package:your_app/ui/fliin/screens/conversations/conversations_data.dart';
import 'package:your_app/ui/fliin/screens/conversations/conversations_screen.dart';
import 'package:flutter/widgets.dart';
class ConversationsEmptyExample extends StatelessWidget {
const ConversationsEmptyExample({super.key});
@override
Widget build(BuildContext context) {
return ConversationsScreen.standalone(
service: SampleConversationsService(latency: const Duration(milliseconds: 600)),
initial: const ConversationsEmpty(),
onNewConversation: () {},
onOpen: (_) {},
clock: () => sampleConversationsNow,
);
}
}Error
When loading fails, the screen says so and offers to try again.
import 'package:your_app/ui/fliin/screens/conversations/conversations_controller.dart';
import 'package:your_app/ui/fliin/screens/conversations/conversations_data.dart';
import 'package:your_app/ui/fliin/screens/conversations/conversations_screen.dart';
import 'package:flutter/widgets.dart';
class ConversationsErrorExample extends StatelessWidget {
const ConversationsErrorExample({super.key});
@override
Widget build(BuildContext context) {
return ConversationsScreen.standalone(
service: SampleConversationsService(latency: const Duration(milliseconds: 600)),
initial: const ConversationsFailed(),
onNewConversation: () {},
onOpen: (_) {},
clock: () => sampleConversationsNow,
);
}
}Offline
Without a connection, the screen asks to reconnect rather than showing an error.
import 'package:your_app/ui/fliin/screens/conversations/conversations_controller.dart';
import 'package:your_app/ui/fliin/screens/conversations/conversations_data.dart';
import 'package:your_app/ui/fliin/screens/conversations/conversations_screen.dart';
import 'package:flutter/widgets.dart';
class ConversationsOfflineExample extends StatelessWidget {
const ConversationsOfflineExample({super.key});
@override
Widget build(BuildContext context) {
return ConversationsScreen.standalone(
service: SampleConversationsService(latency: const Duration(milliseconds: 600)),
initial: const ConversationsOffline(),
onNewConversation: () {},
onOpen: (_) {},
clock: () => sampleConversationsNow,
);
}
}Photos
Each person with a photo (ConversationPerson.avatar), the initials standing in while it loads, and no search above a short inbox (showSearch: false).
import 'package:your_app/ui/fliin/screens/conversations/conversations_controller.dart';
import 'package:your_app/ui/fliin/screens/conversations/conversations_data.dart';
import 'package:your_app/ui/fliin/screens/conversations/conversations_screen.dart';
import 'package:flutter/widgets.dart';
class ConversationsPhotosExample extends StatelessWidget {
const ConversationsPhotosExample({super.key});
@override
Widget build(BuildContext context) {
var n = 0;
ConversationPerson withPhoto(ConversationPerson person) => ConversationPerson(
id: person.id,
name: person.name,
initials: person.initials,
color: person.color,
avatar: AssetImage('images/people/${n++ % 6 + 1}.jpg'),
);
final conversations = [
for (final c in sortConversations(sampleConversations))
Conversation(
id: c.id,
title: c.title,
people: [for (final person in c.people) withPhoto(person)],
lastMessage: c.lastMessage,
lastAt: c.lastAt,
unread: c.unread,
muted: c.muted,
pinned: c.pinned,
),
];
return ConversationsScreen.standalone(
service: SampleConversationsService(latency: const Duration(milliseconds: 600)),
initial: ConversationsLoaded(conversations),
showSearch: false,
onNewConversation: () {},
onOpen: (_) {},
clock: () => sampleConversationsNow,
);
}
}Routing
fliin add conversations prints the route for the router it finds in your pubspec, a file next to the page that wires its navigation. Each of them:
go_router
// lib/conversations_route.dart, next to conversations_page.dart.
import 'package:go_router/go_router.dart';
import 'conversations_page.dart';
/// Add conversationsRoute to the routes of your GoRouter.
final conversationsRoute = GoRoute(
path: '/conversations',
builder: (context, state) => ConversationsPage(
// The path of a chat in your app.
onOpen: (conversation) => context.push('/conversations/${Uri.encodeComponent(conversation.id)}'),
// The path of a new conversation in your app, outside /conversations/:id.
onNewConversation: () => context.push('/compose'),
),
);navigator
// lib/conversations_route.dart, next to conversations_page.dart.
import 'package:flutter/material.dart';
import 'conversations_page.dart';
/// Opens “Conversations”: `Navigator.of(context).push(conversationsRoute(conversationPage: …, newConversationPage: …))`.
/// [conversationPage] builds the chat of a conversation in your app.
/// [newConversationPage] builds the new conversation of your app.
Route<void> conversationsRoute({
required Widget Function(String conversationId) conversationPage,
required WidgetBuilder newConversationPage,
}) => MaterialPageRoute<void>(
builder: (context) => ConversationsPage(
onOpen: (conversation) => Navigator.of(context).push(MaterialPageRoute<void>(builder: (_) => conversationPage(conversation.id))),
onNewConversation: () => Navigator.of(context).push(MaterialPageRoute<void>(builder: newConversationPage)),
),
);State
The controller is a ChangeNotifier from widgets.dart, with no package. fliin add conversations prints the page that holds it with the state management it finds, its imports written for your package; below they read your_app. Your backend implements the Service interface of the screen's _data.dart file:
none
// lib/conversations_page.dart
// Your backend: implement ConversationsService (conversations_data.dart) and pass it instead of the sample service.
import 'package:your_app/ui/fliin/screens/conversations/conversations_data.dart';
import 'package:your_app/ui/fliin/screens/conversations/conversations_screen.dart';
import 'package:flutter/widgets.dart';
class ConversationsPage extends StatelessWidget {
const ConversationsPage({
super.key,
required this.onOpen,
required this.onNewConversation,
});
final ValueChanged<Conversation> onOpen;
final VoidCallback onNewConversation;
@override
Widget build(BuildContext context) => ConversationsScreen.standalone(
service: SampleConversationsService(),
onOpen: onOpen,
onNewConversation: onNewConversation,
);
}provider
// lib/conversations_page.dart
// Your backend: implement ConversationsService (conversations_data.dart) and pass it instead of the sample service.
import 'package:your_app/ui/fliin/screens/conversations/conversations_controller.dart';
import 'package:your_app/ui/fliin/screens/conversations/conversations_data.dart';
import 'package:your_app/ui/fliin/screens/conversations/conversations_screen.dart';
import 'package:flutter/widgets.dart';
import 'package:provider/provider.dart';
class ConversationsPage extends StatelessWidget {
const ConversationsPage({
super.key,
required this.onOpen,
required this.onNewConversation,
});
final ValueChanged<Conversation> onOpen;
final VoidCallback onNewConversation;
@override
Widget build(BuildContext context) => ChangeNotifierProvider(
create: (_) => ConversationsController(service: SampleConversationsService())..load(),
child: Builder(
builder: (context) => ConversationsScreen(
controller: context.read<ConversationsController>(),
onOpen: onOpen,
onNewConversation: onNewConversation,
),
),
);
}riverpod
// lib/conversations_page.dart
// Your backend: implement ConversationsService (conversations_data.dart) and pass it instead of the sample service.
import 'package:your_app/ui/fliin/screens/conversations/conversations_controller.dart';
import 'package:your_app/ui/fliin/screens/conversations/conversations_data.dart';
import 'package:your_app/ui/fliin/screens/conversations/conversations_screen.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
final conversationsProvider = Provider.autoDispose<ConversationsController>((ref) {
final controller = ConversationsController(service: SampleConversationsService())..load();
ref.onDispose(controller.dispose);
return controller;
});
class ConversationsPage extends ConsumerWidget {
const ConversationsPage({
super.key,
required this.onOpen,
required this.onNewConversation,
});
final ValueChanged<Conversation> onOpen;
final VoidCallback onNewConversation;
@override
Widget build(BuildContext context, WidgetRef ref) => ConversationsScreen(
controller: ref.watch(conversationsProvider),
onOpen: onOpen,
onNewConversation: onNewConversation,
);
}bloc
// lib/conversations_page.dart
// Your backend: implement ConversationsService (conversations_data.dart) and pass it instead of the sample service.
import 'package:your_app/ui/fliin/screens/conversations/conversations_controller.dart';
import 'package:your_app/ui/fliin/screens/conversations/conversations_data.dart';
import 'package:your_app/ui/fliin/screens/conversations/conversations_screen.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
/// The screen's controller as a bloc, for your BlocListener and BlocBuilder:
/// one emission per controller notification. `version` tells them apart
/// when `state` does not change (mode, filters, text…, read them on
/// `controller`). The screen listens to the controller itself.
class ConversationsCubit extends Cubit<({ConversationsState state, int version})> {
ConversationsCubit(this.controller) : super((state: controller.state, version: 0)) {
controller.addListener(_changed);
}
final ConversationsController controller;
void _changed() => emit((state: controller.state, version: state.version + 1));
@override
Future<void> close() {
controller
..removeListener(_changed)
..dispose();
return super.close();
}
}
class ConversationsPage extends StatelessWidget {
const ConversationsPage({
super.key,
required this.onOpen,
required this.onNewConversation,
});
final ValueChanged<Conversation> onOpen;
final VoidCallback onNewConversation;
@override
Widget build(BuildContext context) => BlocProvider(
create: (_) => ConversationsCubit(ConversationsController(service: SampleConversationsService())..load()),
child: Builder(
builder: (context) => ConversationsScreen(
controller: context.read<ConversationsCubit>().controller,
onOpen: onOpen,
onNewConversation: onNewConversation,
),
),
);
}API
ConversationsScreen: class extends StatefulWidget
ConversationsScreen.new: const new({super.key, required ConversationsController controller, VoidCallback? onBack, ValueChanged<Conversation>? onOpen, VoidCallback? onNewConversation, DateTime Function() clock, bool showSearch})
ConversationsScreen.standalone: const new({super.key, required ConversationsService service, ConversationsState? initial, VoidCallback? onBack, ValueChanged<Conversation>? onOpen, VoidCallback? onNewConversation, DateTime Function() clock, bool showSearch})
ConversationsScreen.controller: get ConversationsController?
ConversationsScreen.service: get ConversationsService?
ConversationsScreen.initial: get ConversationsState?
ConversationsScreen.onBack: get VoidCallback?
ConversationsScreen.onOpen: get ValueChanged<Conversation>?
ConversationsScreen.onNewConversation: get VoidCallback?
ConversationsScreen.clock: get DateTime Function()
ConversationsScreen.showSearch: get bool
ConversationsScreen.createState: State<ConversationsScreen>()
ConversationsState: sealed class permits {ConversationsEmpty, ConversationsFailed, ConversationsLoaded, ConversationsLoading, ConversationsOffline}
ConversationsState.new: const new()
ConversationsLoading: final class extends ConversationsState
ConversationsLoading.new: const new()
ConversationsNotice: enum {refreshFailed, refreshOffline, actionFailed}
ConversationsNotice.refreshFailed: static final ConversationsNotice
ConversationsNotice.refreshOffline: static final ConversationsNotice
ConversationsNotice.actionFailed: static final ConversationsNotice
ConversationsLoaded: final class extends ConversationsState
ConversationsLoaded.new: new(List<Conversation> conversations, {ConversationsNotice? notice})
ConversationsLoaded.conversations: get List<Conversation>
ConversationsLoaded.notice: get ConversationsNotice?
ConversationsEmpty: final class extends ConversationsState
ConversationsEmpty.new: const new({ConversationsNotice? notice})
ConversationsEmpty.notice: get ConversationsNotice?
ConversationsFailed: final class extends ConversationsState
ConversationsFailed.new: const new()
ConversationsOffline: final class extends ConversationsState
ConversationsOffline.new: const new()
ConversationsController: class extends ChangeNotifier
ConversationsController.new: new({required ConversationsService service, ConversationsState initial})
ConversationsController.service: get ConversationsService
ConversationsController.state: get ConversationsState
ConversationsController.load: Future<void>()
ConversationsController.refresh: Future<bool>()
ConversationsController.archive: Future<bool>(String id)
ConversationsController.delete: Future<bool>(String id)
ConversationsController.setMuted: Future<bool>(String id, bool muted)
ConversationsController.dispose: void()
ConversationPerson: final class
ConversationPerson.new: const new({required String id, required String name, required String initials, FlAvatarColor color, ImageProvider? avatar})
ConversationPerson.id: get String
ConversationPerson.name: get String
ConversationPerson.initials: get String
ConversationPerson.color: get FlAvatarColor
ConversationPerson.avatar: get ImageProvider?
Conversation: final class
Conversation.new: const new({required String id, required String title, required List<ConversationPerson> people, required String lastMessage, required DateTime lastAt, int unread, bool muted, bool pinned})
Conversation.id: get String
Conversation.title: get String
Conversation.people: get List<ConversationPerson>
Conversation.lastMessage: get String
Conversation.lastAt: get DateTime
Conversation.unread: get int
Conversation.muted: get bool
Conversation.pinned: get bool
Conversation.isGroup: get bool
Conversation.withMuted: Conversation(bool muted)
sortConversations: List<Conversation>(Iterable<Conversation> conversations)
searchConversations: List<Conversation>(List<Conversation> conversations, String query)
ConversationMoment: sealed class permits {MomentDate, MomentMinutes, MomentNow, MomentToday, MomentWeekday, MomentYesterday}
ConversationMoment.new: const new()
MomentNow: final class extends ConversationMoment
MomentNow.new: const new()
MomentMinutes: final class extends ConversationMoment
MomentMinutes.new: const new(int minutes)
MomentMinutes.minutes: get int
MomentToday: final class extends ConversationMoment
MomentToday.new: const new(DateTime at)
MomentToday.at: get DateTime
MomentYesterday: final class extends ConversationMoment
MomentYesterday.new: const new()
MomentWeekday: final class extends ConversationMoment
MomentWeekday.new: const new(int weekday)
MomentWeekday.weekday: get int
MomentDate: final class extends ConversationMoment
MomentDate.new: const new(DateTime at)
MomentDate.at: get DateTime
conversationMoment: ConversationMoment(DateTime at, DateTime now)
ConversationsNoNetwork: final class implements Exception
ConversationsNoNetwork.new: const new()
ConversationsService: abstract interface class
ConversationsService.new: new()
ConversationsService.load: abstract Future<List<Conversation>>()
ConversationsService.archive: abstract Future<void>(String id)
ConversationsService.setMuted: abstract Future<void>(String id, bool muted)
ConversationsService.delete: abstract Future<void>(String id)
sampleConversationsNow: get dynamic
sampleConversations: get dynamic
SampleConversationsService: final class implements ConversationsService
SampleConversationsService.new: new({List<Conversation>? conversations, Duration latency})
SampleConversationsService.latency: get Duration
SampleConversationsService.load: Future<List<Conversation>>()
SampleConversationsService.archive: Future<void>(String id)
SampleConversationsService.delete: Future<void>(String id)
SampleConversationsService.setMuted: Future<void>(String id, bool muted)
conversationsL10n: const Map<String, Map<String, String>>
ConversationsStrings: final class
ConversationsStrings.new: const new(String language)
ConversationsStrings.of: factory new(BuildContext context)
ConversationsStrings.language: get String
ConversationsStrings.title: get String
ConversationsStrings.back: get String
ConversationsStrings.refresh: get String
ConversationsStrings.newShort: get String
ConversationsStrings.newConversation: get String
ConversationsStrings.search: get String
ConversationsStrings.clearSearch: get String
ConversationsStrings.loading: get String
ConversationsStrings.unread: String(int count)
ConversationsStrings.muted: get String
ConversationsStrings.pinned: get String
ConversationsStrings.actions: get String
ConversationsStrings.archive: get String
ConversationsStrings.mute: get String
ConversationsStrings.unmute: get String
ConversationsStrings.delete: get String
ConversationsStrings.deleteTitle: get String
ConversationsStrings.deleteMessage: String(String name)
ConversationsStrings.cancel: get String
ConversationsStrings.archived: get String
ConversationsStrings.deleted: get String
ConversationsStrings.emptyTitle: get String
ConversationsStrings.emptyMessage: get String
ConversationsStrings.noResultsTitle: get String
ConversationsStrings.noResultsMessage: String(String query)
ConversationsStrings.errorTitle: get String
ConversationsStrings.errorMessage: get String
ConversationsStrings.offlineTitle: get String
ConversationsStrings.offlineMessage: get String
ConversationsStrings.retry: get String
ConversationsStrings.refreshFailed: get String
ConversationsStrings.refreshOffline: get String
ConversationsStrings.actionFailed: get String
ConversationsStrings.moment: String(ConversationMoment moment)Strings
Every label in English, French and Arabic. The screen follows the closest Localizations; the same table is in the manifest, ready for your ARB files.
| Key | en | fr | ar |
|---|---|---|---|
conversationsTitle | Messages | Messages | الرسائل |
conversationsBack | Back | Retour | رجوع |
conversationsRefresh | Refresh | Actualiser | تحديث |
conversationsNew | New | Nouveau | جديدة |
conversationsNewConversation | New conversation | Nouvelle conversation | محادثة جديدة |
conversationsSearch | Search conversations | Rechercher une conversation | البحث في المحادثات |
conversationsClearSearch | Clear search | Effacer la recherche | مسح البحث |
conversationsLoading | Loading conversations | Chargement des conversations | جارٍ تحميل المحادثات |
conversationsUnread | Unread: {count} | Non lus : {count} | غير مقروءة: {count} |
conversationsMuted | Muted | En sourdine | مكتومة |
conversationsPinned | Pinned | Épinglée | مثبّتة |
conversationsActions | Actions | Actions | إجراءات |
conversationsArchive | Archive | Archiver | أرشفة |
conversationsMute | Mute | Mettre en sourdine | كتم |
conversationsUnmute | Unmute | Réactiver les notifications | إلغاء الكتم |
conversationsDelete | Delete | Supprimer | حذف |
conversationsDeleteTitle | Delete this conversation? | Supprimer la conversation ? | حذف هذه المحادثة؟ |
conversationsDeleteMessage | All messages with {name} will be erased. This can’t be undone. | Tous les messages avec {name} seront effacés. Cette action est définitive. | سيتم مسح كل الرسائل مع {name}. لا يمكن التراجع عن ذلك. |
conversationsCancel | Cancel | Annuler | إلغاء |
conversationsArchived | Conversation archived | Conversation archivée | تمت أرشفة المحادثة |
conversationsDeleted | Conversation deleted | Conversation supprimée | تم حذف المحادثة |
conversationsNow | Now | À l’instant | الآن |
conversationsMinutes | {minutes} min | {minutes} min | {minutes} د |
conversationsYesterday | Yesterday | Hier | أمس |
conversationsMonday | Monday | lundi | الاثنين |
conversationsTuesday | Tuesday | mardi | الثلاثاء |
conversationsWednesday | Wednesday | mercredi | الأربعاء |
conversationsThursday | Thursday | jeudi | الخميس |
conversationsFriday | Friday | vendredi | الجمعة |
conversationsSaturday | Saturday | samedi | السبت |
conversationsSunday | Sunday | dimanche | الأحد |
conversationsDate | {month}/{day}/{year} | {day}/{month}/{year} | {day}/{month}/{year} |
conversationsEmptyTitle | No conversations yet | Aucune conversation | لا توجد محادثات |
conversationsEmptyMessage | Write to someone to start a conversation. | Écrivez à quelqu’un pour commencer une conversation. | راسل أحدهم لبدء محادثة. |
conversationsNoResultsTitle | No results | Aucun résultat | لا توجد نتائج |
conversationsNoResultsMessage | No conversation matches “{query}”. | Aucune conversation ne correspond à « {query} ». | لا توجد محادثة تطابق «{query}». |
conversationsErrorTitle | Conversations could not load | Impossible de charger les conversations | تعذّر تحميل المحادثات |
conversationsErrorMessage | Something went wrong on our side. | Un problème est survenu de notre côté. | حدثت مشكلة من جهتنا. |
conversationsOfflineTitle | You are offline | Vous êtes hors ligne | أنت غير متصل |
conversationsOfflineMessage | Check your connection, then try again. | Vérifiez votre connexion, puis réessayez. | تحقق من اتصالك ثم أعد المحاولة. |
conversationsRetry | Try again | Réessayer | أعد المحاولة |
conversationsRefreshFailed | Couldn’t refresh. The list may be out of date. | Actualisation impossible. La liste n’est peut-être plus à jour. | تعذّر التحديث. قد لا تكون القائمة محدّثة. |
conversationsRefreshOffline | You are offline. The list may be out of date. | Vous êtes hors ligne. La liste n’est peut-être plus à jour. | أنت غير متصل. قد لا تكون القائمة محدّثة. |
conversationsActionFailed | That didn’t work. The conversation is back as it was. | L’action n’a pas abouti. La conversation est revenue comme avant. | لم يكتمل الإجراء. عادت المحادثة كما كانت. |
Reference renders
The 16 images published with version 0.2.4, rendered by the shared harness on macOS with Flutter 3.47.7. Each shows every state side by side, at the size of an iPhone (390 × 844) or of an Android phone (412 × 915). fliin verify conversations compares your copy to them.
















Use it to
- show the messaging inbox, newest chat first
- select chats whose name or message holds a typed word
- mute or archive a chat from the list
- delete a chat after confirmation
- open a new chat from the top bar
- Not for
- the thread of a single discussion: use the chat thread (chat)
- received notifications: use the notification center (notifications)
- States