flutter_message_composer 0.3.1
flutter_message_composer: ^0.3.1 copied to clipboard
An adaptive Flutter message composer with multiline text, attachments, audio previews, and hold-to-record gestures with slide-to-cancel and locking.
example/lib/main.dart
import 'package:flutter/material.dart';
import 'package:flutter_message_composer/flutter_message_composer.dart';
void main() => runApp(const ExampleApp());
class ExampleApp extends StatelessWidget {
const ExampleApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.teal),
),
home: const ComposerExamplePage(),
);
}
}
class ComposerExamplePage extends StatefulWidget {
const ComposerExamplePage({super.key});
@override
State<ComposerExamplePage> createState() => _ComposerExamplePageState();
}
class _ComposerExamplePageState extends State<ComposerExamplePage> {
final controller = TextEditingController();
final focusNode = FocusNode();
String latestSubmission = 'Nothing sent yet';
static const quickReplies = ['Thanks!', 'Sounds good', 'I will check'];
@override
void dispose() {
controller.dispose();
focusNode.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Message composer')),
body: Center(
child: Padding(
padding: const EdgeInsets.all(24),
child: Text(
latestSubmission,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.titleMedium,
),
),
),
bottomNavigationBar: MessageComposer(
controller: controller,
focusNode: focusNode,
hintText: 'Message',
sendTooltip: 'Send',
attachTooltip: 'Attachments',
recordTooltip: 'Record audio',
cancelRecordingTooltip: 'Discard audio',
stopRecordingTooltip: 'Stop recording',
playRecordingTooltip: 'Play audio',
pauseRecordingTooltip: 'Pause audio',
sendRecordingTooltip: 'Send audio',
lockRecordingLabel: 'Lock recording',
recordingLockedLabel: 'Recording locked',
recordingTimerStyle: const TextStyle(fontWeight: FontWeight.w600),
suggestions: SizedBox(
height: 42,
child: ListView.separated(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 4),
itemCount: quickReplies.length,
separatorBuilder: (_, _) => const SizedBox(width: 8),
itemBuilder: (context, index) {
final reply = quickReplies[index];
return ActionChip(
label: Text(reply),
onPressed: () {
controller.value = TextEditingValue(
text: reply,
selection: TextSelection.collapsed(offset: reply.length),
);
focusNode.requestFocus();
},
);
},
),
),
menuActions: [
ChatInputMenuAction(
id: 'photo',
label: 'Photo',
icon: const Icon(Icons.image_outlined),
onPressed: () => showNotice('Connect an image picker here.'),
),
ChatInputMenuAction(
id: 'document',
label: 'Document',
icon: const Icon(Icons.description_outlined),
onPressed: () => showNotice('Connect a file picker here.'),
),
],
onRecordingPermissionDenied: () {
showNotice('Microphone permission is required.');
},
onRecordingError: (_) {
showNotice('The audio recording could not be completed.');
},
onAudioPreviewError: (_) {
showNotice('The audio preview could not be played.');
},
onSend: handleSubmission,
),
);
}
void handleSubmission(ChatInputSubmission submission) {
setState(() {
if (submission.hasAudio) {
latestSubmission =
'Audio: ${submission.audio!.name} '
'(${submission.audio!.duration.inSeconds}s)';
} else {
latestSubmission = submission.text;
}
});
if (!submission.hasAudio) controller.clear();
}
void showNotice(String message) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(message)));
}
}