برنامه نویسی

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

از این مقاله چه انتظاری دارید؟

ما اجرای انتقال محو را در مقاله قبلی به پایان رساندیم و از آن برای پایان دادن به صفحه چت خانگی خود استفاده خواهیم کرد.
این یک سناریوی خاص است که ما در حال تلاش برای ساختن آن هستیم ، بنابراین این یک عمل خوب برای بلوک با استفاده از قلاب است. با این حال ، ممکن است خیلی خاص باشد ، بنابراین احساس راحتی کنید که از این مقاله استفاده کنید.
ps. ما در حال کار بر روی پایه کد قبلاً ساخته شده خود هستیم.

سناریویی که ما در حال تلاش برای ساختن آن هستیم

  1. کاربر روی قسمت متن کلیک می کند
  2. برنامه صفحه کلید را می بندد (برای اولین بار)
  3. برنامه درخواست مکالمه را به هوش مصنوعی ارسال می کند
  4. برنامه در پاسخ سرور محو می شود
  5. کاربر برای شروع نوشتن پاسخ روی قسمت متن کلیک می کند
  6. برنامه پاسخ سرور را محو می کند
  7. کاربر بر روی دکمه ارسال از صفحه کلید کلیک می کند
  8. برنامه پیام را به AI ارسال می کند
  9. برنامه متن کاربر را محو می کند و در پاسخ سرور محو می شود

برنامه باید پاسخ سرور را بخواند و کاربر می تواند به جای نوشتن از 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

این برای این مقاله است. من می دانم که رسیدگی بیش از حد است ، و جزئیات زیادی را برای دنبال کردن دنبال می کنید ، اگر موارد قبلی را دنبال نکردید ، آن را به عنوان اختیاری رفتار کنید.

این برنامه اکنون به پایان رسیده است ، ما ویژگی اصلی را با این مقاله به طور کامل پیاده سازی کرده ایم. من شروع به کار بر روی برنامه هایی می کنم که می خواهم در فروشگاه بعدی منتشر کنم ، بنابراین ، مثل همیشه

با ما همراه باشید

نوشته های مشابه

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

دکمه بازگشت به بالا