У меня довольно сложное приложение Flutter, и в моем приложении есть homepage.dart, где у меня также есть ящик, внутри этого ящика у меня есть список страниц, включая «чат», когда пользователи нажимают на чат, и у меня есть аналогичный интерфейс, например обычное приложение WhatsApp обеспечивает более естественное ощущение при использовании этой функции чата в приложении. моя цель - когда пользователь получает уведомление, я бы хотел отправить его прямо на экран чата с подробностями, которые я передаю через уведомление, но я не могу этого добиться, поэтому теперь я хотел сосредоточиться на деле где пользователь получает уведомление при использовании приложения. в этом случае я показываю диалоговое окно оповещения с информацией об уведомлении, а также у меня есть кнопка, которая отправляет их непосредственно на чатscreen.dart.
Теперь я хочу, чтобы когда пользователи находились на странице чата или на странице экрана чата, я не хочу отображать диалоговое окно с предупреждением, потому что они уже находятся в интерфейсе чата, в моем приложении я все еще использую пользовательские настройки для сохранения последней страницы. пользователь был таким, когда последней страницей была страница Chatpage.dart или Chatscreen.dart, он осознавал, что диалог не должен отображаться, но когда я перехожу на домашнюю страницу или другую страницу, я вижу уведомление и когда я перемещаюсь по пользователь на экране чата, и собеседник, с которым он разговаривает, пишет сообщение, оно не только отображается в чате, но и запускает диалоговое окно с предупреждением, и оттуда, независимо от страницы, оно постоянно показывает диалоговое окно с предупреждением. это как если бы диалоговое окно оповещения не распознавало страницу, на которой находится пользователь. Я собираюсь опубликовать код страниц, которые связаны с правильной функциональностью этого:chatscreen.dart, main.dart, и из-за ограничений по длине я не могу добавить homepage.dart.
чатScreen.dart:
class ChatScreen extends StatefulWidget {
final String peerId;
final String peerAvatar;
final String peerName;
final String myId;
final String name;
static final String routeName = "/chatScreen";
final String company;
final String selectedBranch;
final String myPhoto;
//ChatScreen({}) : super(key: key);
//static final String routeName = "home";
ChatScreen({Key? key,required this.peerId, required this.peerAvatar, required this.company, required this.myPhoto, required this.selectedBranch,required this.peerName, required this.myId, required this.name}) : super(key: key);
@override
_ChatScreenState createState() => _ChatScreenState();
}
class _ChatScreenState extends State<ChatScreen> {
TextEditingController messageController = TextEditingController();
ScrollController listScrollController = ScrollController();
final prefs = new PreferenciasUsuario();
bool isTyping = false;
File? selectedImage; // This will hold the selected image file
ImagePicker picker = ImagePicker(); // Instance of ImagePicker
//ImageProvider<Object>? myPhoto;
String? fileName;
String company = "";
String selectedBranch = "";
String id = "";
String peerId= "";
String peerAvatar= "";
String peerName= "";
String myPhoto= "";
String name= "";
//AudioRecorder record = AudioRecorder();
late bool _isRecording;
bool showPlayer = false;
late String _audioPath;
String? currentRoute;
@override
void initState() {
company = widget.company;
selectedBranch = widget.selectedBranch;
id = widget.myId;
_isRecording = false;
_audioPath = '';
peerId= widget.peerId;
peerAvatar= widget.peerAvatar;
peerName= widget.peerName;
myPhoto= widget.myPhoto;
name= widget.name;
currentRoute = ModalRoute.of(context)?.settings.name;
super.initState();
}
@override
void dispose() {
super.dispose();
}
Future<void> sendMessage() async {
final String messageText = messageController.text.trim();
if (messageText.isNotEmpty) {
messageController.clear();
print(" name ${name}");
print("widget.myId ${widget.myId}");
print("peerId ${peerId}");
print("messageText $messageText");
// Add message to messages subcollection in the chat document
final HttpsCallable callable = FirebaseFunctions.instance.httpsCallable('sendChat');
final result = await callable.call(<String, dynamic>{
"myPhoto": myPhoto,
"name": name,
"senderId": widget.myId, // Assume currentUser is available
"receiverId": peerId,
"text": messageText,
"type": "text", // For text messages. You can define other types as needed.
"company": widget.company,
"selectedBranch": widget.selectedBranch,
"peerPhoto": peerAvatar,
"peerName": peerName,
});
if (result.data['success']) {
if (mounted){
}
}
}
}
// Function to call a Firebase Cloud Function
Future<void> uploadMyPhoto({required Map<String, dynamic> photoData,}) async {
HttpsCallable callable = FirebaseFunctions.instance.httpsCallable('uploadChatPhoto');
try {
final HttpsCallableResult result = await callable.call({
'id': id,
"name": name,
"senderId": widget.myId, // Assume currentUser is available
"receiverId": peerId,
'company': company,
'selectedBranch': selectedBranch,
'photo': photoData,
"type": "img",
});
print("gcame down");
final String response = result.data['message'];
print("message : $response");
setState(() {
});
Navigator.of(context).pop();
Navigator.of(context).pop();
} catch (e) {
print("Failed to call cloud function: $e");
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Color.fromRGBO(20, 33, 61, 1),
title: Text(peerName),
actions: [
CircleAvatar(
backgroundImage: NetworkImage(peerAvatar),
),
SizedBox(width: 10),
],
),
body: Container(
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage('assets/ChatWatermark.jpg'), // Path to your asset image
fit: BoxFit.cover, // Adjust the image size to cover the entire container
),
),
child: Column(
children: <Widget>[
// Expanded to fill available space, leaving room for the message input
Expanded(
child: buildMessagesList(),
),
// Message input area
buildInputArea(),
],
),
),
);
}
// Updated to fetch messages from Firestore
Widget buildMessagesList() {
final chatId = getChatId(widget.myId, peerId);
return StreamBuilder<QuerySnapshot>(
stream: FirebaseFirestore.instance
.collection('users')
.doc(widget.myId)
.collection('chats')
.doc(chatId)
.collection('messages')
.orderBy('timestamp', descending: true)
.snapshots(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return Center(child: CircularProgressIndicator());
} else {
final documents = snapshot.data!.docs;
// Group messages by date
print("about to fuck up");
Map<String, List<DocumentSnapshot>> groupedMessages = {};
documents.forEach((doc) {
final timestamp = doc['timestamp'] as Timestamp;
print("timestamp $timestamp");
// var timestamp = Timestamp(timestamp['_seconds'], timestamp['_nanoseconds']);
final DateTime messageDate = timestamp.toDate().toLocal();
final String formattedDate = formatDate(messageDate);
print("went th");
if (groupedMessages.containsKey(formattedDate)) {
groupedMessages[formattedDate]!.add(doc);
} else {
groupedMessages[formattedDate] = [doc];
}
});
print("groupedMessages $groupedMessages");
return ListView.builder(
padding: EdgeInsets.all(10.0),
itemCount: groupedMessages.length,
itemBuilder: (context, index) {
final date = groupedMessages.keys.toList()[index];
final dateFormat = DateFormat('dd/MM/yyyy');
DateTime dateShow = dateFormat.parse(date);
print("datetum : $date");
List<DocumentSnapshot<Object?>> messages = groupedMessages[date]!;
messages = messages.reversed.toList();
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: EdgeInsets.symmetric(vertical: 10, horizontal: 16),
child: Center(
child: ClipRRect(
borderRadius: BorderRadius.circular(10),
child: Container(
padding: EdgeInsets.all(5),
color: Color.fromRGBO(40, 175, 138, 0.2),
child: Text(
formatDate(dateShow),
style: TextStyle(fontSize: 15, fontWeight: FontWeight.bold, color: Color.fromARGB(255, 4, 75, 36)),
),
),
),
),
),
...messages.map((message) => buildMessageItem(message.data() as Map<String, dynamic>)).toList(),
],
);
},
reverse: true,
controller: listScrollController,
);
}
},
);
}
return Padding(
padding: EdgeInsets.symmetric(vertical: 8),
child: Row(
mainAxisAlignment: isOwnMessage ? MainAxisAlignment.end : MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Flexible(
child: GestureDetector(
onTap: () {
if (type == "img") {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ImagePage(
imgList: ImageConfig(
source: "http", // Assuming the source is always "http" for network images
path: message["photo"]["url"],
ref: null, // Pass reference if applicable
),
),
),
);
}
},
child: Container(
padding: EdgeInsets.all(12),
constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * 0.7),
decoration: BoxDecoration(
color: type == "not" ? Colors.purple : (isOwnMessage ? Colors.blue[400] : Colors.grey[200]),
borderRadius: BorderRadius.only(
topLeft: Radius.circular(isOwnMessage ? 20 : 4),
topRight: Radius.circular(isOwnMessage ? 4 : 20),
bottomLeft: Radius.circular(20),
bottomRight: Radius.circular(20),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (type == "text" )
Text(
message['text'],
style: TextStyle(color: isOwnMessage ? Colors.white : Colors.black),
)
else if (type == "img")
Image.network(
message["photo"]["url"],
height: 150, // Adjust height as needed
width: double.infinity,
fit: BoxFit.cover,
) else if (type == "not")
Text(
message['text'],
style: TextStyle(color: Colors.white),
),
SizedBox(height: 4),
if (type == "not")
Row(
children: [
Text(
formattedTime,
style: TextStyle(color: Colors.white, fontSize: 12),
),
Spacer(),
Icon(Icons.spatial_audio_off_rounded, color: Colors.white,)
],
)
else if (type != "not")
Text(
formattedTime,
style: TextStyle(color: isOwnMessage? Colors.white70 : Colors.black54, fontSize: 12),
),
],
),
),
),
),
],
),
);
}
Widget buildInputArea() {
return Container(
padding: EdgeInsets.all(8.0),
color: Colors.white,
child: Row(
children: <Widget>[
// Text input
Expanded(
child: Container(
decoration: BoxDecoration(
color: Colors.grey[200],
borderRadius: BorderRadius.circular(20),
),
padding: EdgeInsets.symmetric(horizontal: 12),
child: TextField(
controller: messageController,
decoration: InputDecoration(
hintText: 'Nachricht',
hintStyle: TextStyle(color: Colors.grey),
border: InputBorder.none,
),
onChanged: (text) {
setState(() {
isTyping = text.isNotEmpty;
});
},
),
),
),
SizedBox(width: 8),
// Send button
Material(
color: Colors.transparent,
borderRadius: BorderRadius.circular(20),
child: Row(
children: [
Visibility(
visible: !isTyping,
child: IconButton(
onPressed: ()=>_showImageOptions(context),
icon: Icon(Icons.camera_alt,) ,color: Color.fromRGBO(20, 33, 61, 1)),
),
isTyping ? IconButton(
icon: Icon(Icons.send, color: Color.fromRGBO(20, 33, 61, 1)),
onPressed: () => sendMessage(),
): Container(),
],
),
),
],
),
);
}
void _showImageOptions(BuildContext context) {
showModalBottomSheet(
context: context,
builder: (context) {
return Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
ListTile(
leading: Icon(Icons.camera,color: Color.fromRGBO(151, 195, 184, 1),),
title: Text('Kamera'),
onTap: () {
Navigator.of(context).pop();
_processImage(ImageSource.camera);
},
),
ListTile(
leading: Icon(Icons.image,color: Color.fromRGBO(151, 195, 184, 1),),
title: Text('Gallerie'),
onTap: () {
Navigator.of(context).pop();
_processImage(ImageSource.gallery);
},
),
],
);
},
);
}
Future<void> _processImage(ImageSource source) async {
try {
final pickedFile = await picker.pickImage(source: source);
if (pickedFile != null) {
setState(() {
selectedImage = File(pickedFile.path);
});
uploadAndCallFunction(selectedImage!);
}
} catch (e) {
// Handle any exceptions
}
}
Future<void> uploadAndCallFunction(File imageFile) async {
try {
String fileName = path.basename(imageFile.path);
String destination = "$company/$selectedBranch/chat/images/$id/$fileName";
// Upload file
UploadTask task = FirebaseApi.uploadFile(destination, imageFile, context);
// When the upload task is complete, get the download URL
TaskSnapshot snapshot = await task.whenComplete(() {});
String urlDownload = await snapshot.ref.getDownloadURL();
// Prepare data to send to Cloud Function
Map<String, dynamic> photoData = {
'url': urlDownload,
'ref': destination,
};
print("Upload complete: $urlDownload");
// Call the Cloud Function and pass 'photoData' as a parameter
await uploadMyPhoto(photoData:photoData);
} catch (e) {
print("An error occurred during upload or function call: $e");
}
}
// Helper method to determine the chatId based on userIds
String getChatId(String userId, String peerId) {
// Simple rule: smallerId_biggerId
return peerId;
}
}
основной.дарт:
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
final GlobalKey<NavigatorState> chatNavigatorKey = GlobalKey<NavigatorState>();
final GlobalKey<NavigatorState> chatScreenNavigatorKey = GlobalKey<NavigatorState>();
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(
);
await FirebaseApiNotification().initNotification();
// Pass data to MyApp widget and runApp
runApp(MyApp(
));
}
} catch (e) {
print("Error reading cache: $e");
// If there's an error reading cache, proceed without cached data
runApp(MyApp(navigationService: navigationService));
}
configLoading();
} else {
runApp(MyApp(navigationService: navigationService));
configLoading();
}
}
Future<void> enableFirestorePersistence() async {
await FirebaseFirestore.instance.enablePersistence();
}
class MyApp extends StatelessWidget {
final String? userId;
final String? company;
final String? selectedBranch;
final List<String>? groups;
final String? role;
final String? name;
final NavigationService? navigationService;
MyApp({Key? key, this.userId, this.selectedBranch,this.navigationService, this.company, this.groups, this.role, this.name}) : super(key: key);
final prefs = new PreferenciasUsuario();
@override
Widget build(BuildContext context) {
final prefs = new PreferenciasUsuario();
print(prefs.token);
initNotificationListener(context);
// Define the base text style
final baseTextStyle = const TextStyle(
fontFamily: 'RobotoCondensed',
fontStyle: FontStyle.normal,
);
return Provider(
child: GetMaterialApp(
navigatorKey: navigatorKey,
debugShowCheckedModeBanner: false,
title: "Agenda-App",
initialRoute: prefs.ultimaPagina,
routes: {
"register": (BuildContext context) => RegisterScreen(),
"home": (BuildContext context) => Homepage(),
"chat": (BuildContext context) => Chat(id: userId, company: company, name: name,role: role, selectedBranch: selectedBranch, groups:groups ),
"/chatScreen": (BuildContext context) => ChatScreen(company: "",selectedBranch: "",name:"",myId: "",peerId: "", peerName: "", peerAvatar: "", myPhoto: "",),
},
theme: ThemeData(
useMaterial3: true,
scaffoldBackgroundColor: light,
visualDensity: VisualDensity.adaptivePlatformDensity,
textTheme: textTheme,
appBarTheme: AppBarTheme(
titleTextStyle: TextStyle(color: Colors.white, fontFamily: 'RobotoCondensed',
fontStyle: FontStyle.normal,
fontWeight: FontWeight.bold,
fontSize: 20),
iconTheme: IconThemeData(color: Colors.white)
)
),
builder: EasyLoading.init(),
),
);
}
void initNotificationListener(BuildContext context) {
final firebaseApiNotification = FirebaseApiNotification();
firebaseApiNotification.onNotificationReceived = (data) {
NotificationHandler.handleNotification(context, data);
};
}
}
class NotificationHandler {
static final List<String> excludedRoutes = ['chat', '/chatScreen'];
static void handleNotification(BuildContext context, Map<String, dynamic> data) {
// Get the current route
String? currentRoute = ModalRoute.of(context)?.settings.name;
// Check if the current route is in the excluded routes list
if (!excludedRoutes.contains(currentRoute)) {
// Show the notification dialog
_showNotificationDialogRespond(context, data);
}
}
static void _showNotificationDialogRespond(BuildContext context, Map<String, dynamic> notificationData) {
// Your showDialog logic here
List<String> titleParts = notificationData['title']?.split('-') ?? "";
String senderName = titleParts.length > 1 ? titleParts[0].trim() : '';
String messageTitle = titleParts.length > 1 ? titleParts.sublist(1).join('-').trim() : titleParts[0];
double screenWidth = MediaQuery.of(context).size.width;
showDialog(
context: context,
barrierDismissible: false,
builder: (_) {
// Your AlertDialog widget here
return AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(20.0))),
titlePadding: EdgeInsets.fromLTRB(20.0, 20.0, 20.0, 20.0),
contentPadding: EdgeInsets.symmetric(horizontal: 20.0).copyWith(top: 0),
title: Align(
alignment: Alignment.topRight,
child: IconButton(
icon: Icon(Icons.close),
onPressed: () => Navigator.of(context).pop(),
),
),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: screenWidth - 40, // Subtracting horizontal padding
padding: EdgeInsets.fromLTRB(20, 10, 20, 10),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(20),
topRight: Radius.circular(20),
bottomLeft: Radius.circular(5),
bottomRight: Radius.circular(20),
),
boxShadow: [BoxShadow(blurRadius: 3, color: Colors.grey.shade300)],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
CircleAvatar(
backgroundImage: notificationData['imageUrl'] != null && notificationData['imageUrl'] ! = ""? NetworkImage(notificationData['imageUrl']) : Image.asset("assets/logoApp.png").image, // Replace with sender's photo URL
radius: 30,
),
SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (senderName.isNotEmpty) Text(senderName, style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18)),
Text(messageTitle, style: TextStyle(fontSize: 18, color: Colors.blueGrey)),
],
),
),
],
),
SizedBox(height: 10),
Text(notificationData['body'] ?? '....', style: TextStyle(fontSize: 16)),
SizedBox(height: 10),
//Text(notificationTime, style: TextStyle(fontSize: 12, color: Colors.grey)),
],
),
),
],
),
),
actions: [
ElevatedButton(
onPressed: () {
Navigator.pop(context);
// String response = responseController.text;
Navigator.of(context).pushNamed('/chatScreen',
arguments: {
"peerId": notificationData['id']?? "",
"peerAvatar": notificationData['ImageUrl']?? "",
"peerName": notificationData['title'] ?? "",
"myId": notificationData['receiverId'] ?? "",
"name": notificationData['peerName'] ?? "",
"company": notificationData['company'],
"selectedBranch": notificationData['branch']?? "",
"myPhoto": notificationData['peerPhoto'],
},
);
},
child: Text('zum Chat',style: TextStyle(color: Colors.white),),
style: ElevatedButton.styleFrom(
backgroundColor: Color.fromRGBO(40, 175, 138, 1),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
),
),
],
);
},
);
}
}
Вы можете создать класс модели, который может использоваться поставщиком. В этом классе модели вы можете создать StreamController, который прослушивает ваше уведомление и транслирует его всем своим слушателям. Все виджеты, которые хотят прослушивать ваше уведомление, могут подписаться на широковещательный поток (через StreamSubscription) и предпринять необходимые действия.
class MyCustomModel {
bool isDisposed = false;
StreamController controller = StreamController.broadcast();
final firebaseApiNotification = FirebaseApiNotification();
MyCustomModel() {
firebaseApiNotification.onNotificationReceived = (data) {
if (!isDisposed) {
controller.sink.add(data);
}
};
}
void dispose() {
isDisposed = true;
controller.close();
super.dispose();
}
}
Переменная isDispose помогает нам в случае, если Модель удалена и получено уведомление.
Теперь измените своего поставщика (над GetMaterialApp) следующим образом:
Provider(
create: (context) => MyCustomModel(),
dispose: (context, value) => value.dispose(),
child: const GetMaterialApp(),
),
Теперь, как вы упомянули, вы хотите переключиться на страницу чата, как только будет получено уведомление.
Сделайте классы маршрутов (виджеты) сохраняющими состояние (HomePage..etc), ЗА ИСКЛЮЧЕНИЕМ вашей страницы чата, поскольку вы не хотите выводить диалоговое окно оповещения или помещать другой экран виджета страницы чата в свой стек маршрутов. Теперь добавьте метод DidChangeDependities() (ссылка ) и переменную StreamSubscription в их объекты State. Используйте Provider.of ( ссылка)
так :
StreamSubscription<E> subscription;
bool disposed = false;
bool subscriptionInitialised = false;
@override
void didChangeDependencies() {
if (!subscriptionInitialised) {
subscription = Provider.of<MyCustomModel>(context, listen: false).controller.listen(triggerNavigation);
subscriptionInitialised = true;
}
}
void triggerNavigation(E data) {
if (!disposed) {
/*
In here, you can use Navigation Api to move directly to your chatpage screen
OR
you can use a alert dialog box which can handle navigation accordingly.
*/
}
}
@override
void dispose() {
disposed = true;
if (subscriptionInitialised) subscription.cancel();
super.dispose();
}
обязательно закройте потоки и подписки, чтобы избежать утечек памяти и обработки данных потока.
Для получения дополнительной информации о StreamController см.: StreamController
Для получения дополнительной информации о StreamSubscription см.: StreamSubscription