При реализации пользовательского интерфейса чата необходимо учитывать множество сложных деталей. Чтобы создать чат-приложение, которое мы используем десятки раз в день, нам нужно рассмотреть, казалось бы, очевидные детали и реализовать высококачественную функцию чата, учитывающую пользовательский опыт (UX).

В этом посте объясняется, как разработать приложение для чата, использующее логику пользовательского интерфейса interaction, используемую в типичных приложениях для чата, таких как WhatsApp, KakaoTalk и Line.

Базовая структура

Во-первых, давайте рассмотрим базовую структуру экрана чата.

    Scaffold(
      appBar: AppBar(
        title: const Text("Chat"),
        backgroundColor: const Color(0xFF007AFF),
      ), // <-- App bar
      body: Column(
        children: [
          Expanded(
            child: ListView.separated(...), // <- Chat list view
          ), 
           _BottomInputField(), // <- Fixed bottom TextField widget
        ],
      ),
    );

Как правило, экран чата имеет простую структуру. Он состоит из AppBar, Chat ListView и TextField, закрепленных внизу.

Важным моментом здесь является то, что представление списка чатов и текстовое поле должны быть заключены в виджет Column, а раздел представления списка чатов должен быть заключен в виджет Expanded.

chat list view и input field, обернутые в виджет Column, располагаются вертикально, и, поскольку chat list view section обернуты в Expanded, представление input field естественным образом фиксируется внизу. Преимущество этого заключается в том, что нет необходимости исправлять виджет input field внизу с помощью виджетов Stack & Positioned.

Обратите внимание, что примеры, которые я буду продолжать показывать, также расположены в этой структуре.

1. Взаимодействие, при котором поле ввода и раздел просмотра списка чатов реагируют на изменения при обнаружении области виртуальной клавиатуры.

Первое взаимодействие в чате, которое следует рассмотреть, — это то, как input field и chat list view section реагируют на изменения, когда появляется virtual keyboard. Для удобства пользователя важно, чтобы при появлении виртуальной клавиатуры поле ввода и список чатов естественным образом следовали за движением.

Для этого вам нужно установить следующие два файла properties.

свойство resizeToAvoidBottomInset

    return Scaffold(
      resizeToAvoidBottomInset: true, // assign true
      appBar: AppBar(
        title: const Text("Ximya"),
        backgroundColor: const Color(0xFF007AFF),
      ),

Во-первых, вам нужно установить для свойства resizeToAvoidBottomInset виджета Scaffold значение true. Когда для этого свойства установлено значение true, виджет Scaffold автоматически регулирует свой размер, чтобы избежать перекрытия с virtual keyboard при появлении виртуальной клавиатуры.

обратная собственность

ListView.separated(
 reverse: true,
    itemCount: chatList.length,
    ...
 )

Во-вторых, вам нужно установить для свойства reversed виджета ListView значение true. Это свойство указывает, следует ли упорядочивать элементы списка в обратном порядке. Если задать для параметра reversed значение true, элементы располагаются снизу вверх, и можно обнаружить изменение размера виртуальной клавиатуры.

ПРИМЕЧАНИЕ. Индекс и положение Если для параметра reverse
установлено значение true, элементы в ListView располагаются снизу вверх. В результате индекс и положение элементов на экране меняются местами. Это необходимо учитывать при манипулировании данными, передаваемыми в ListView. Если необходимы манипуляции с данными, решением может быть еще одно изменение значений перед передачей данных в ListView.
Например, controller.chatList.reversed.toList().

2. Взаимодействие при добавлении чата и прокрутке вниз

Когда сообщение добавляется в список чата, оно должно располагаться внизу и прокручиваться естественным образом. Для этого вам нужно установить для свойства reversed ListView значение true. Если задать для reversed значение true, элементы располагаются снизу вверх. Поэтому при добавлении сообщения область ListView расширяется, а положение прокрутки изменяется.

3. Выравнивание сообщений чата вверху

До сих пор я говорил вам, что вам нужно установить для свойства reversed виджета ListView значение true. Однако это приводит к тому, что раздел списка чатов помещается в самый низ экрана.

 Align(
 alignment: Alignment.topCenter,
 child: ListView.separated(
 shrinkWrap: true,
 reverse: true,
    itemCount: chatList.length,
    itemBuilder: (context, index) {
    return Bubble(chat: chatList[index]);
       },
    );
   ),

Поскольку установка свойства reversed в значение true помещает раздел списка чатов в самый низ экрана, вам необходимо внести некоторые изменения, чтобы сообщения чата отображались в верхней части экрана. Оберните виджет ListView с помощью Align и задайте для свойства выравнивания значение Alignment.topCenter, чтобы разместить его вверху. Кроме того, вам необходимо установить свойство shrinkWrap: true в ListView. Таким образом, ListView регулирует свой размер, чтобы соответствовать своему внутреннему содержимому, и размещается вверху под влиянием виджета «Выравнивание».

4. Оптимизация положения прокрутки после отправки сообщений:

При отправке сообщения чата позиция прокрутки должна измениться на самый низ, независимо от того, где находится текущая позиция прокрутки. Для этого вы можете управлять поведением прокрутки ListView с помощью ScrollController.

final scrollController = ScrollController()

...

ListView.separated(
 shrinkWrap: true,
 reverse: true,
    controller: scrollController                                  
    itemCount: chatList.length,
    itemBuilder: (context, index) {
  return Bubble(chat: chatList[index]);
     },
 );            

Сначала инициализируйте переменную ScrollController. Затем передайте эту переменную в свойство контроллера ListView. Теперь вы можете управлять поведением прокрутки ListView.

Future<void> onFieldSubmitted() async {
  addMessage();
   
  // Move the scroll position to the bottom
  scrollController.animateTo(
    0,
    duration: const Duration(milliseconds: 300),
    curve: Curves.easeInOut,
  );

  textEditingController.text = '';
}

Затем примените событие scrollController.animatedTo к методу, возникающему при добавлении чата, чтобы добавить анимацию, которая прокручивается до самого низа. Причина, по которой мы передали значение смещения 0 методуanimatedTo, заключается в том, что если для listview.buidler задано значение reversed:true, позиция 0 фактически означает самый низ списка.

5. Отключение виртуальной клавиатуры в области чата Нажмите

Наконец, в типичном приложении для чата есть interaction where the virtual keyboard hides down, когда нажимается область общего списка чата, когда виртуальная клавиатура поднята. Чтобы реализовать это, вам просто нужно добавить простой фрагмент кода.

          Expanded(
            child: GestureDetector(
              onTap: () {
                FocusScope.of(context).unfocus(); // <-- Hide virtual keyboard
              },
              child: Align(
                alignment: Alignment.topCenter,
                child: Selector<ChatController, List<Chat>>(
                  selector: (context, controller) =>
                      controller.chatList.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<ChatController>().scrollController,
                      itemCount: chatList.length,
                      itemBuilder: (context, index) {
                        return Bubble(chat: chatList[index]);
                      },
                    );
                  },
                ),
              ),
            ),
          ),

Оберните раздел списка чатов виджетом `GestureDetector` и передайте событие `FocusScope.of(context).unfocus()` в функцию onTap.

// 1. Initialization
final focusNode = FocusNode();


// 2. Passing the focusNode object
TextField(
 focusNode :  focusNode,
...
),


// 3. When the chat section is tapped
onChatListSectinoTapped() {
 focusNode.unfocus()

Другой способ — использовать объект FocusNode, чтобы скрыть виртуальную клавиатуру. Инициализируйте объект FocusNode и установите атрибут focusNode в текстовом поле. Затем, при нажатии на раздел списка чатов, вызовите focusNode.unfocus(), чтобы скрыть виртуальную клавиатуру.

Заключение

В этом посте мы рассмотрели взаимодействия, которые необходимо учитывать при создании приложения для чата. Хотя это может показаться незначительными деталями, я считаю, что учет этих взаимодействий значительно улучшает полноту функциональности чата. Если вам интересна общая структура, а не только упомянутые выше взаимодействия, я рекомендую клонировать репозиторий GitHub с кодом примера.😀

В следующем посте мы рассмотрим, как получать ответы в режиме реального времени с помощью API ChatGPT на основе пользовательского интерфейса функции чата, который мы рассмотрели до сих пор.