Creare una chat in Flutter
In un precedente articolo abbiamo visto come creare una chat in Flutter usando un package apposito.
In questo articolo, invece, costruiremo una chat senza usare package appositi.
Abbiamo comunque bisogno di installare un pò di roba:
flutter pub add provider flutter_chat_bubble intl
flutter_chat_bubble è un widget per creare le classiche "bubble" dei messaggi.
Cominciamo con messaggio.dart che contiene i dati del messaggio e crea una lista iniziale:
enum TipoMessaggio {
inviato,
ricevuto,
}
class Messaggio {
final String utente;
final String messaggio;
final DateTime time;
final TipoMessaggio tipo;
Messaggio({
required this.utente,
required this.messaggio,
required this.time,
required this.tipo,
});
factory Messaggio.add({required String utente, required String messaggio}) {
return Messaggio(
utente: utente,
messaggio: messaggio,
time: DateTime.now(),
tipo: TipoMessaggio.inviato,
);
}
static List<Messaggio> creaLista() {
return [
Messaggio(
utente: 'Matteo',
messaggio: 'Ciao',
time: DateTime.now().subtract(const Duration(minutes: 15)),
tipo: TipoMessaggio.inviato,
),
Messaggio(
utente: 'Federica',
messaggio: 'Ciao',
time: DateTime.now().subtract(const Duration(minutes: 10)),
tipo: TipoMessaggio.ricevuto,
),
Messaggio(
utente: 'Matteo',
messaggio: 'Come stai?',
time: DateTime.now().subtract(const Duration(minutes: 5)),
tipo: TipoMessaggio.inviato,
),
Messaggio(
utente: 'Federica',
messaggio: 'Bene tu?',
time: DateTime.now(),
tipo: TipoMessaggio.ricevuto,
),
Messaggio(
utente: 'Matteo',
messaggio: 'Bene grazie!',
time: DateTime.now(),
tipo: TipoMessaggio.inviato,
),
];
}
}
Proseguiamo con controller_chat.dart, che si preoccupa di intercettare nuovi messaggi:
import 'package:flutter/material.dart';
import 'messaggio.dart';
class ControllerChat extends ChangeNotifier {
List<Messaggio> chat = Messaggio.creaLista();
late final ScrollController scrollController = ScrollController();
late final TextEditingController textEditingController =
TextEditingController();
late final FocusNode focusNode = FocusNode();
Future<void> onSubmit() async {
chat = [
...chat,
Messaggio.add(utente: 'Matteo', messaggio: textEditingController.text),
];
scrollController.animateTo(
0,
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
textEditingController.text = '';
notifyListeners();
}
void onChanged(String term) {
notifyListeners();
}
}
Poi abbiamo input_widget.dart, che ha una input text con un bottone:
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'controller_chat.dart';
class InputWidget extends StatelessWidget {
const InputWidget({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return SafeArea(
bottom: true,
child: Container(
constraints: const BoxConstraints(minHeight: 48),
width: double.infinity,
decoration: const BoxDecoration(
border: Border(
top: BorderSide(
color: Color(0xFFE5E5EA),
),
),
),
child: Stack(
children: [
TextField(
focusNode: context.read<ControllerChat>().focusNode,
onChanged: context.read<ControllerChat>().onChanged,
controller: context.read<ControllerChat>().textEditingController,
maxLines: null,
textAlignVertical: TextAlignVertical.top,
decoration: InputDecoration(
border: InputBorder.none,
contentPadding: const EdgeInsets.only(
right: 42,
left: 16,
top: 18,
),
hintText: 'messaggio',
enabledBorder: OutlineInputBorder(
borderSide: BorderSide.none,
borderRadius: BorderRadius.circular(8.0),
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide.none,
borderRadius: BorderRadius.circular(8.0),
),
),
),
Positioned(
bottom: 0,
right: 0,
child: IconButton(
icon: const Icon(Icons.send),
onPressed: context.read<ControllerChat>().onSubmit,
),
),
],
),
),
);
}
}
L'ultimo widget è bubble_widget.dart, che visualizza la chat:
import 'package:flutter/material.dart';
import 'package:flutter_chat_bubble/bubble_type.dart';
import 'package:flutter_chat_bubble/clippers/chat_bubble_clipper_1.dart';
import 'package:intl/intl.dart';
import 'messaggio.dart';
final DateFormat dateFormat = DateFormat('hh:mm a');
class BubbleWidget extends StatelessWidget {
final EdgeInsetsGeometry? margin;
final Messaggio messaggio;
const BubbleWidget({
super.key,
this.margin,
required this.messaggio,
});
Color get textColor {
switch (messaggio.tipo) {
case TipoMessaggio.inviato:
return Colors.white;
case TipoMessaggio.ricevuto:
return const Color(0xFF0F0F0F);
}
}
Color get bgColor {
switch (messaggio.tipo) {
case TipoMessaggio.ricevuto:
return const Color(0xFFE7E7ED);
case TipoMessaggio.inviato:
return const Color(0xFF007AFF);
}
}
CustomClipper<Path> get clipperType {
switch (messaggio.tipo) {
case TipoMessaggio.inviato:
return ChatBubbleClipper1(type: BubbleType.sendBubble);
case TipoMessaggio.ricevuto:
return ChatBubbleClipper1(type: BubbleType.receiverBubble);
}
}
CrossAxisAlignment get crossAlignment {
switch (messaggio.tipo) {
case TipoMessaggio.inviato:
return CrossAxisAlignment.end;
case TipoMessaggio.ricevuto:
return CrossAxisAlignment.start;
}
}
MainAxisAlignment get alignmentType {
switch (messaggio.tipo) {
case TipoMessaggio.ricevuto:
return MainAxisAlignment.start;
case TipoMessaggio.inviato:
return MainAxisAlignment.end;
}
}
EdgeInsets get paddingType {
switch (messaggio.tipo) {
case TipoMessaggio.inviato:
return const EdgeInsets.only(
top: 10,
bottom: 10,
left: 10,
right: 24,
);
case TipoMessaggio.ricevuto:
return const EdgeInsets.only(
top: 10,
bottom: 10,
left: 24,
right: 10,
);
}
}
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: alignmentType,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
margin: margin ?? EdgeInsets.zero,
child: PhysicalShape(
clipper: clipperType,
elevation: 2,
color: bgColor,
shadowColor: Colors.grey.shade200,
child: Container(
constraints: BoxConstraints(
maxWidth: MediaQuery.of(context).size.width * 0.8,
),
padding: paddingType,
child: Column(
crossAxisAlignment: crossAlignment,
children: [
Text(
messaggio.utente,
style: TextStyle(color: textColor),
),
Text(
messaggio.messaggio,
style: TextStyle(color: textColor),
),
const SizedBox(
height: 8,
),
Text(
dateFormat.format(messaggio.time),
style: TextStyle(color: textColor, fontSize: 12),
),
],
),
),
),
),
],
);
}
}
Richiamiamo il tutto così:
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'bubble_widget.dart';
import 'controller_chat.dart';
import 'messaggio.dart';
import 'input_widget.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Test',
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: ChangeNotifierProvider(
create: (_) => ControllerChat(),
child: const MyHomePage(title: 'Flutter Test'),
),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
resizeToAvoidBottomInset: true,
body: Column(
children: [
Expanded(
child: GestureDetector(
onTap: () {
context.read<ControllerChat>().focusNode.unfocus();
},
child: Align(
alignment: Alignment.topCenter,
child: Selector<ControllerChat, List<Messaggio>>(
selector: (context, controller) =>
controller.chat.reversed.toList(),
builder: (context, chatList, child) {
return ListView.separated(
shrinkWrap: true,
reverse: true,
padding: const EdgeInsets.only(top: 12, bottom: 20) +
const EdgeInsets.symmetric(horizontal: 12),
separatorBuilder: (_, __) => const SizedBox(
height: 12,
),
controller:
context.read<ControllerChat>().scrollController,
itemCount: chatList.length,
itemBuilder: (context, index) {
return BubbleWidget(messaggio: chatList[index]);
},
);
},
),
),
),
),
const InputWidget(),
],
),
);
}
}
Enjoy!
dart flutter provider intl
Commentami!