برنامه Flutter ، محو شدن منطق چت با بلوک و قلاب

از این مقاله چه انتظاری دارید؟
ما اجرای انتقال محو را در مقاله قبلی به پایان رساندیم و از آن برای پایان دادن به صفحه چت خانگی خود استفاده خواهیم کرد.
این یک سناریوی خاص است که ما در حال تلاش برای ساختن آن هستیم ، بنابراین این یک عمل خوب برای بلوک با استفاده از قلاب است. با این حال ، ممکن است خیلی خاص باشد ، بنابراین احساس راحتی کنید که از این مقاله استفاده کنید.
ps. ما در حال کار بر روی پایه کد قبلاً ساخته شده خود هستیم.
سناریویی که ما در حال تلاش برای ساختن آن هستیم
- کاربر روی قسمت متن کلیک می کند
- برنامه صفحه کلید را می بندد (برای اولین بار)
- برنامه درخواست مکالمه را به هوش مصنوعی ارسال می کند
- برنامه در پاسخ سرور محو می شود
- کاربر برای شروع نوشتن پاسخ روی قسمت متن کلیک می کند
- برنامه پاسخ سرور را محو می کند
- کاربر بر روی دکمه ارسال از صفحه کلید کلیک می کند
- برنامه پیام را به AI ارسال می کند
- برنامه متن کاربر را محو می کند و در پاسخ سرور محو می شود
برنامه باید پاسخ سرور را بخواند و کاربر می تواند به جای نوشتن از STT (گفتار به متن) استفاده کند. لطفاً برای اجرای دقیق TTS و STT به برنامه Flutter ، گفتار به متن و متن به گفتار مراجعه کنید.
تماس ها و مدل های API (اختیاری)
ما سعی خواهیم کرد که روی UI صفحه اصلی تمرکز کنیم زیرا ما قبلاً نحوه اجرای هر API را در پروژه معماری تمیز خود به تفصیل شرح داده ایم ، اما تکرار آن ضرری ندارد. بنابراین اگر با این روند آشنا هستید ، از این قسمت از مقاله پرش کنید.
از لایه دامنه شروع کنید
ما همیشه با ایجاد مدل ها در لایه دامنه شروع می کنیم
class MessageModel {
MessageModel({
this.created,
this.active,
this.text,
this.isUser,
this.conversation,
this.id,});
MessageModel.fromJson(dynamic json) {
created = json['created'];
active = json['active'];
text = json['text'];
isUser = json['is_user'];
conversation = json['conversation'];
id = json['id'];
}
String? created;
bool? active;
String? text;
bool? isUser;
int? conversation;
int? id;
Map<String, dynamic> toJson() {
final map = <String, dynamic>{};
map['created'] = created;
map['active'] = active;
map['text'] = text;
map['is_user'] = isUser;
map['conversation'] = conversation;
map['id'] = id;
return map;
}
}
lib/دامنه/مدل ها/اشخاص/message_model.dart
بعدی به روزرسانی طرحواره مخزن در لایه دامنه است
import '../models/entities/message_model.dart';
...
abstract class RemoteRepository {
...
Future<DataState<GenericResponse<MessageModel>>> conversationStart();
Future<DataState<GenericResponse<MessageModel>>> conversationSend({
required String? text,
required int? conversation,
});
}
lib/دامنه/مخازن/remote_repository.dart
لایه داده ها
اولین قدم در لایه داده اضافه کردن درخواست ها به منبع داده است
...
@GET('/conversation/')
Future<HttpResponse<GenericResponse<MessageModel>>> conversationStart({
@Header("Authorization") String? token,
@Header("Accept-Language") String? lang,
});
@POST('/conversation/')
Future<HttpResponse<GenericResponse<MessageModel>>> conversationSend({
@Body() MessageRequest? request,
@Header("Authorization") String? token,
@Header("Accept-Language") String? lang,
});
...
lib/data/منابع/remote_datasource.dart
ps. مسئله را فراموش نکنید dart run build_runner build
برای تولید کد منبع داده.
بعدی اجرای طرح مخزن است
...
@override
Future<DataState<GenericResponse<MessageModel>>> conversationSend({
String? text,
int? conversation
}) => getStateOf(
request: () => remoteDatasource.conversationSend(
request: MessageRequest(
text: text,
conversation: conversation
),
lang: "en",
token: "Bearer ${preferencesRepository.getToken()}",
),
);
@override
Future<DataState<GenericResponse<MessageModel>>> conversationStart() => getStateOf(
request: () => remoteDatasource.conversationStart(
lang: "en",
token: "Bearer ${preferencesRepository.getToken()}",
),
);
...
lib/data/repositories/remote_repository_impl.dart
خوب و آسان ، بیایید با کار جدی شروع کنیم
رویدادها و ایالت های صفحه چت
بازگشت به صفحه اصلی ما ، بیایید شروع به کار بر روی منطق بلوک کنیم ، مثل همیشه ، هنگام کار با بلوک ، ما با وقایع ، سپس حالت ها و در نهایت منطق بلوک شروع می کنیم
وقایع
ما در حال حاضر دو رویداد داریم ، یکی برای TTS و دیگری برای STT ، ما به دو رویداد اضافی نیاز داریم ، یکی برای شروع مکالمه و دیگری برای ارسال متن.
part of 'home_bloc.dart';
@immutable
sealed class HomeEvent {
final String? text;
const HomeEvent({this.text});
}
class HomeSTTEvent extends HomeEvent {}
class HomeTTSEvent extends HomeEvent {
const HomeTTSEvent({super.text});
}
class HomeStartEvent extends HomeEvent {}
class HomeSendEvent extends HomeEvent {
const HomeSendEvent({super.text});
}
lib/ارائه/صفحه نمایش/خانه/home_event.dart
دولتها
با آماده شدن وقایع ، به ایالات متحده حرکت می کنیم ، ما در حال حاضر ایالات برای گوش دادن ، خواندن و خطاها داریم. اما ما همچنین برای بارگیری و دریافت متن به ایالات احتیاج داریم.
part of 'home_bloc.dart';
@immutable
sealed class HomeState {
final String? text;
final String? error;
const HomeState({
this.text,
this.error,
});
}
final class HomeInitial extends HomeState {}
final class HomeListeningState extends HomeState {}
final class HomeReadingState extends HomeState {}
final class HomeErrorState extends HomeState {
const HomeErrorState({super.error});
}
final class HomeSTTState extends HomeState {
const HomeSTTState({super.text});
}
final class HomeLoadingState extends HomeState {}
final class HomeReceiveState extends HomeState {
const HomeReceiveState({super.text});
}
lib/ارائه/صفحه نمایش/خانه/home_state.dart
با آماده شدن ایالات و رویدادهای ما ، زمان آن رسیده است که آنها را در پرونده بلوک وصل کنیم
منطق بلوک
برای اینکه درخواست های مکالمه را انجام دهیم ، ما نیاز به دسترسی به remote_repository
، بنابراین بیایید آن را به وابستگی های بلوک اضافه کنیم
...
final RemoteRepository repository;
HomeBloc(this.stt, this.tts, this.repository) : super(HomeInitial()) {
...
lib/ارائه/صفحه نمایش/خانه/home_bloc.dart
ما باید وابستگی جدید را از پرونده برنامه اصلی منتقل کنیم ، فقط ارائه دهنده Bloc فعلی را به روز کنید تا آن را تهیه کنید
...
- BlocProvider(create: (context)=>HomeBloc(locator(), locator())),
+ BlocProvider(create: (context)=>HomeBloc(locator(), locator(), locator())),
...
lib/main.dart
مرحله بعدی رسیدگی به رویدادهای تازه اضافه شده ما است ، بیایید با آن شروع کنیم HomeStartEvent
، وقتی آن را دریافت می کنیم ، باید درخواست مکالمه شروع را انجام دهیم
import '../../../utils/data_state.dart';
import '../../../utils/dio_exception_extension.dart';
...
HomeBloc(this.stt, this.tts, this.repository) : super(HomeInitial()) {
int? conversationId = 0;
...
HomeBloc(this.stt, this.tts, this.repository) : super(HomeInitial()) {
...
on<HomeStartEvent>(handleStartEvent);
}
...
FutureOr<void> handleStartEvent(
HomeStartEvent event,
Emitter<HomeState> emit,
) async {
emit(HomeLoadingState());
final response = await repository.conversationStart();
if (response is DataSuccess) {
conversationId = response.data?.data?.conversation;
emit(HomeReceiveState(
text: response.data?.data?.text,
));
} else {
emit(HomeErrorState(error: response.error?.getErrorMessage(),));
}
}
lib/ارائه/صفحه نمایش/خانه/home_bloc.dart
هنگام دریافت این رویداد ، ما درخواست مکالمه را شروع می کنیم ، شناسه مکالمه در نمونه بلوک ذخیره می شود ، سپس متن پاسخ را از طریق آن منتشر می کنیم HomeReceiveState
اکنون برای رسیدگی به رویداد ارسال
...
HomeBloc(this.stt, this.tts, this.repository) : super(HomeInitial()) {
...
on<HomeSendEvent>(handleSendEvent);
}
...
FutureOr<void> handleSendEvent(
HomeSendEvent event,
Emitter<HomeState> emit,
) async {
emit(HomeLoadingState());
recognizedText = "";
final response = await repository.conversationSend(
text: event.text ?? "",
conversation: conversationId ?? 0,
);
if (response is DataSuccess) {
emit(HomeReceiveState(
text: response.data?.data?.text,
));
} else if (response is DataFailed) {
emit(HomeErrorState(
error: response.error?.getErrorMessage(),
));
}
}
lib/ارائه/صفحه نمایش/خانه/home_bloc.dart
ما در حال تمیز کردن متن شناخته شده (در صورت استفاده از STT توسط کاربر) هستیم و سپس درخواست ارسال مکالمه را ارسال می کنیم و متن پاسخ را از طریق ارسال می کنیم HomeReceiveState
به UI
وقت آن است که خود صفحه اصلی را جابجا کنیم
صفحه اصلی
ما بعد از مقاله TTS و STT و مقاله Hooks روی آخرین نسخه صفحه اصلی کار می کنیم. ما قبلاً زمینه متن ، دکمه میکروفون و شنونده بلوک را داریم ، کد کامل به نظر می رسد
import 'package:alive_diary_app/config/dependencies.dart';
import 'package:alive_diary_app/config/router/app_router.dart';
import 'package:alive_diary_app/domain/repositories/preferences_repository.dart';
import 'package:alive_diary_app/presentation/widgets/layout_widget.dart';
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:google_fonts/google_fonts.dart';
import 'home_bloc.dart';
@RoutePage()
class HomeScreen extends HookWidget {
const HomeScreen({super.key, this.title});
final String? title;
@override
Widget build(BuildContext context) {
final bloc = BlocProvider.of<HomeBloc>(context);
final isLoading = useState(false);
final speechText = useState("");
final canWrite = useState<bool?>(null);
final textController = useTextEditingController();
AnimationController animationController = useAnimationController(
duration: const Duration(seconds: 4),
initialValue: 0,
);
void showText(String? text) async {
await Future.delayed(const Duration(milliseconds: 100));
SystemChannels.textInput.invokeMethod('TextInput.hide');
animationController.animateBack(0, duration: const Duration(seconds: 1));
await Future.delayed(const Duration(seconds: 2));
textController.text = text ?? "";
animationController.forward();
}
void clearText() async {
animationController.animateBack(0, duration: const Duration(seconds: 1));
await Future.delayed(const Duration(seconds: 2));
textController.text = "";
animationController.forward();
}
return BlocListener<HomeBloc, HomeState>(
listener: (context, state) {
isLoading.value = state is HomeListeningState;
if (state is HomeSTTState) {
speechText.value = state.text ?? "";
}
},
child: LayoutWidget(
title: 'home'.tr(),
actions: [
IconButton(
icon: const Icon(Icons.logout, color: Colors.black,),
onPressed: (){
locator<PreferencesRepository>().logout();
appRouter.replaceAll([LoginRoute()]);
},
)
],
floatingActionButton: FloatingActionButton(
tooltip: 'Listen',
child: Icon(
Icons.keyboard_voice_outlined,
color: isLoading.value ? Colors.green : Colors.blue,
),
onPressed: () => bloc.add(HomeSTTEvent()),
),
child:Container(
height: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 15),
decoration: const BoxDecoration(
image: DecorationImage(
image: AssetImage("assets/images/paper_bg.jpg"),
fit: BoxFit.cover,
),
),
child: FadeTransition(
opacity: animationController,
child: TextField(
decoration: const InputDecoration(border: InputBorder.none),
// focusNode: textNode,
cursorHeight: 35,
style: GoogleFonts.caveat(
fontSize: 30,
color: Colors.black,
),
keyboardType: TextInputType.multiline,
textInputAction: TextInputAction.send,
controller: textController,
maxLines: null,
onTap: () async {
if (canWrite.value == null) {
showText("How was your day?");
canWrite.value = true;
} else if (canWrite.value == true) {
clearText();
}
},
onSubmitted: (text) async {
showText("Tell me more");
},
),
),
),
),
);
}
}
lib/ارائه/صفحه نمایش/صفحه اصلی/صفحه اصلی .dart
برای اولین بار هنگام کلیک بر روی قسمت متن ، باید درخواست شروع مکالمه انجام شود و هنگام دریافت پاسخ ، باید با استفاده از آن نشان داده شود HomeReceiveState
...
return BlocListener<HomeBloc, HomeState>(
listener: (context, state) {
if (state is HomeReceiveState) {
showText(state.text);
canWrite.value = true;
}
...
child: FadeTransition(
opacity: animationController,
child: TextField(
...
onTap: () async {
if (canWrite.value == null) {
bloc.add(HomeStartEvent()); // new
} else if (canWrite.value == true) {
clearText();
}
},
onSubmitted: (text) async {
bloc.add(HomeSendEvent(text: text)); // new
},
),
),
lib/ارائه/صفحه نمایش/صفحه اصلی/صفحه اصلی .dart
ما باید اکنون بتوانیم آن را آزمایش کنیم ، با کلیک بر روی کادر متن باید پاسخ سرور را نشان دهد ، با کلیک دوباره روی آن باید متن را محو کند و به کاربر اجازه دهد پیام را تایپ کند ، ضربه زدن به صفحه کلید باید پیام را به سرور ارسال کند و نمایش دهد پاسخ آن
اولین پاسخ | پیام تایپ کردن | دریافت پاسخ |
---|---|---|
![]() |
![]() |
![]() |
اکنون برای خواننده ، از این پس ، هنگام دریافت پاسخ سرور ، بسیار آسان است ، تنها کاری که باید انجام دهیم اضافه کردن یک رویداد TTS است
...
void showText(String? text) async {
await Future.delayed(const Duration(milliseconds: 100));
SystemChannels.textInput.invokeMethod('TextInput.hide');
animationController.animateBack(0, duration: const Duration(seconds: 1));
await Future.delayed(const Duration(seconds: 2));
textController.text = text ?? "";
animationController.forward();
bloc.add(HomeTTSEvent(text: text));
}
...
lib/ارائه/صفحه نمایش/صفحه اصلی/صفحه اصلی .dart
این برای این مقاله است. من می دانم که رسیدگی بیش از حد است ، و جزئیات زیادی را برای دنبال کردن دنبال می کنید ، اگر موارد قبلی را دنبال نکردید ، آن را به عنوان اختیاری رفتار کنید.
این برنامه اکنون به پایان رسیده است ، ما ویژگی اصلی را با این مقاله به طور کامل پیاده سازی کرده ایم. من شروع به کار بر روی برنامه هایی می کنم که می خواهم در فروشگاه بعدی منتشر کنم ، بنابراین ، مثل همیشه
با ما همراه باشید