【Flutter】flutter_chat_uiを使ってチャット画面を実装する

先日仕事でLINEのようなチャット画面を作成する必要がありました。

調べてみるとflutter_chat_uiというドンピシャな機能があったので、こちらを使ってLINEのようなチャットのUIを作っていきたいと思います。

## バージョン情報

```
$ flutter --version
Flutter 3.0.4 • channel stable • https://github.com/flutter/flutter.git
Framework • revision 85684f9300 (6 weeks ago) • 2022-06-30 13:22:47 -0700
Engine • revision 6ba2af10bb
Tools • Dart 2.17.5 • DevTools 2.12.2
```

## パッケージの導入

まずはflutter_chat_uiを導入します。

https://pub.dev/packages/flutter_chat_ui



```
$ flutter pub add flutter_chat_ui
$ flutter pub get
```

```yaml:pubspec.yaml
dependencies:
 flutter_chat_ui: ^1.6.4
```

## 公式の通りに作っていく

パッケージの導入ができたらチャット画面を作っていきましょう。
実際のアプリではチャット画面に至るまでに誰とメッセージするかを選択する画面などが合ったりすると思いますが、今回はその部分は省略してあくまでチャットを実際に行う画面にフォーカスを当てていきたいと思います。

というわけで何も考えずに公式のコードをコピペしましょう。

```dart:chat_room.dart
import 'dart:convert';
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_chat_types/flutter_chat_types.dart' as types;
import 'package:flutter_chat_ui/flutter_chat_ui.dart';

String randomString() {
  final random = Random.secure();
  final values = List<int>.generate(16, (i) => random.nextInt(255));
  return base64UrlEncode(values);
}

class ChatRoom extends StatefulWidget {
  const ChatRoom({Key? key}) : super(key: key);

  @override
  ChatRoomState createState() => ChatRoomState();
}

class ChatRoomState extends State<ChatRoom> {
  final List<types.Message> _messages = [];
  final _user = const types.User(id: '82091008-a484-4a89-ae75-a22bf8d6f3ac');

  @override
  Widget build(BuildContext context) => Scaffold(
        body: Chat(
          user: _user,
          messages: _messages,
          onSendPressed: _handleSendPressed,
        ),
      );

  void _addMessage(types.Message message) {
    setState(() {
      _messages.insert(0, message);
    });
  }

  void _handleSendPressed(types.PartialText message) {
    final textMessage = types.TextMessage(
      author: _user,
      createdAt: DateTime.now().millisecondsSinceEpoch,
      id: randomString(),
      text: message.text,
    );

    _addMessage(textMessage);
  }
}
```

<img width="300px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2724713/9811f9c5-f6ba-3a35-462a-049509e99b5e.png">

え、完成したんだけど。すごい。

もう普通にメッセージを送れるし見た目も綺麗なので、あとはソースを見ながら進めていけば何とかなるとは思うのですが、この記事の存在価値がなくなるので一応ここではもう少しソースの中身を見ていくこととします。


## 詳しくコードの中身を見ていくよ

色々と書いてありますが、1番大事なところはbodyに記載されている下記部分でしょう。

```dart
body: Chat(
  user: _user, // ①
  messages: _messages,　　// ②
  onSendPressed: _handleSendPressed,
),
```

この①userと②messagesが分かれば色々と理解を深めれそうですね。と言うわけで見ていきます。


### user
userは`types.User`の値を設定することで、自分がどのユーザーかを判別するために使用されるようです。
後述する`types.Message`というメッセージのクラスにも`types.User`の値を格納する部分があるのですが、この値と比較して自分が作成したメッセージなのか自分宛てに送信されたメッセージなのか判別しているのだと思います。多分。

また`types.User`について中身を見ていくと


```dart:user.dart
enum Role { admin, agent, moderator, user }

@JsonSerializable()
@immutable
abstract class User extends Equatable {
  /// Creates a user.
  const User._({
    this.createdAt,
    this.firstName,
    required this.id,
    this.imageUrl,
    this.lastName,
    this.lastSeen,
    this.metadata,
    this.role,
    this.updatedAt,
  });

  const factory User({
    int? createdAt, // 作成日時
    String? firstName, // 性
    required String id, // ユニークなID
    String? imageUrl, // 画像URL
    String? lastName, // 名
    int? lastSeen, // 最後に表示した時のタイムスタンプ
    Map<String, dynamic>? metadata, // ユーザーに追加情報を付随させたいときに使う
    Role? role, // ロール
    int? updatedAt, // 更新日時
  }) = _User;
  ・・・
}
```

最低限必要なのはidだけなので先ほどコピペしたコードのように適当なidを指定するだけでもうまく描写されるようですが、名前や画像といった値も設定できるようです。

せっかくなのでやってみたいと思います。

```dart:chat_room.dart
class ChatRoomState extends State<ChatRoom> {
  final List<types.Message> _messages = [];
  final _user = const types.User(
    id: '82091008-a484-4a89-ae75-a22bf8d6f3ac',
  );
  
  // 追加
  final _other = const types.User(
      id: 'otheruser',
      firstName: "テスト",
      lastName: "太郎",
      imageUrl:
          "https://pbs.twimg.com/profile_images/1335856760972689408/Zeyo7jdq_bigger.jpg");

  // 追加
  @override
  void initState() {
    super.initState();
    _addMessage(types.TextMessage(
      author: _other,
      createdAt: DateTime.now().millisecondsSinceEpoch,
      id: randomString(),
      text: "テストです。",
    ));
  }

  @override
  Widget build(BuildContext context) => Scaffold(
        body: Chat(
          messages: _messages,
          onSendPressed: _handleSendPressed,
          user: _user,
          // ここを追加しないと表示されないので注意！！！！
          showUserAvatars: true,
          showUserNames: true,
        ),
      );

  void _addMessage(types.Message message) {
    setState(() {
      _messages.insert(0, message);
    });
  }

  void _handleSendPressed(types.PartialText message) {
    final textMessage = types.TextMessage(
      author: _user,
      createdAt: DateTime.now().millisecondsSinceEpoch,
      id: randomString(),
      text: message.text,
    );

    _addMessage(textMessage);
  }
}

```

<img width="300px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2724713/f522cf93-dc6b-747a-5ba1-42119087893f.png">

いい感じに名前と画像が表示されました！

`Chat()`の引数に`showUserAvatars:true`と`showUserNames:true`を指定しないと表示されないのでそこは注意してください。


### messages
次にmessagesです。
messagesは`List<types.Message>`の値を設定することで、格納したメッセージを画面に表示することができます。

`types.Message`の実装を見てみるとこんな感じ

```dart:message.dart

enum MessageType { custom, file, image, system, text, unsupported }
enum Status { delivered, error, seen, sending, sent }

@immutable
abstract class Message extends Equatable {
    ・・・　
    const Message({
      required this.author,
      this.createdAt,
      required this.id,
      this.metadata,
      this.remoteId,
      this.repliedMessage,
      this.roomId,
      this.showStatus,
      this.status,
      required this.type,
      this.updatedAt,
    });
    ・・・
    final User author; // メッセージ送信者
    final int? createdAt; // メッセージ作成時のタイムスタンプ
    final String id; // メッセージごとのユニークなID
    final Map<String, dynamic>? metadata; // メッセージに追加情報を付随させたいときに使う
    final String? remoteId; // バックエンドから来たメッセージのユニークID
    final Message? repliedMessage; // このメッセージにリプライしたメッセージ
    final String? roomId; // メッセージを送信したルームID
    final bool? showStatus; // 表示非表示のステータス
    final Status? status; // メッセージのステータス
    final MessageType type; // メッセージの種類
    final int? updatedAt; // 更新日時
    ・・・
}
```
色々と変数がありますが最低限必要なのは`author`,`id`,`type`だけで他の項目は必要に応じて使うといった形です。

メッセージアプリを作る上で必要そうな項目は大概揃っていますね。すごい。
最悪パラメーターが足りないときはmetadataに突っ込んでいい感じに加工すればいいんでしょうね。

パラメーターを見る限りUIに直結しそうなのは`status`だったのでこの部分を少し深ぼってみたいと思います。

#### statusについて

ソースを見ていくとstatusは`delivered`, `error`, `seen`, `sending`, `sent`び5種類の値が設定できるようなので、それぞれを設定したときにどのような見た目になるのか確認していきたいと思います。

```dart:chat_room.dart
final List<types.Message> _messages = [];
final _user = const types.User(id: '82091008-a484-4a89-ae75-a22bf8d6f3ac');
final _other = const types.User(id: '82091008-a484-4a89-ae7511111116f3ac');

// メッセージを直接作成
@override
void initState() {
  super.initState();
  for (types.User u in [_user, _other]) {
    _addMessage(types.TextMessage(
      author: u,
      createdAt: DateTime.now().millisecondsSinceEpoch,
      id: randomString(),
      text: "delivered",
      status: types.Status.delivered,
    ));
    _addMessage(types.TextMessage(
      author: u,
      createdAt: DateTime.now().millisecondsSinceEpoch,
      id: randomString(),
      text: "error",
      status: types.Status.error,
    ));
    _addMessage(types.TextMessage(
      author: u,
      createdAt: DateTime.now().millisecondsSinceEpoch,
      id: randomString(),
      text: "seen",
      status: types.Status.seen,
    ));
    _addMessage(types.TextMessage(
      author: u,
      createdAt: DateTime.now().millisecondsSinceEpoch,
      id: randomString(),
      text: "sending",
      status: types.Status.sending,
    ));
    _addMessage(types.TextMessage(
      author: u,
      createdAt: DateTime.now().millisecondsSinceEpoch,
      id: randomString(),
      text: "delivered",
      status: types.Status.delivered,
    ));
  }
}
```

<img width="300px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2724713/1637d094-62d6-ad8b-6b3f-33055d60e17e.png">

右側のアイコンが変わる感じですね。なるほど。
メッセージ送信する時などはまず`sending`で送信してあげて送信結果に応じて`error`と`sent`に振り分けるみたいなことをしてあげると親切かもしれませんね。

また自分宛ての送信結果についてはステータスは特に反映されないようです。

<font color="Crimson">※僕のミスでsentがなくdeliverdが2回出てきていますが、sentに設定した場合の表示はご自身で確認いただければと思います。。すみません。。。</font>


## 追加の設定

このままでも普通に動かせるのですが、細かいところで修正したい部分があったので修正内容を備忘録的に書きたいと思います。

### ユーザー名の色と送信したメッセージの背景色を変えたい

ユーザー名の色と送信したメッセージの背景色をアプリのトンナマに合うような形に変えました。

```dart:chat_room.dart
    ・・・
  @override
  Widget build(BuildContext context) => Scaffold(
        body: Chat(
          // 追加
          theme: const DefaultChatTheme(
            primaryColor: Colors.green,  // メッセージの背景色の変更
            userAvatarNameColors: [Colors.green],  // ユーザー名の文字色の変更
          ),
          messages: _messages,
          onSendPressed: _handleSendPressed,
          user: _user,
          showUserAvatars: true,
          showUserNames: true,
        ),
      );
    ・・・
```

ちなみにですが、どうやらデフォルトだとチャット相手のユーザー名の文字色は、ユーザーのidのハッシュ値から判別した10種類の色に自動で振り分けられるようです。

ユーザー名の文字色をListで渡しているのはそのためで、ここに複数の文字色を設定すればランダムで複数の文字色になるようです。


### 日本語対応したい

デフォルトだとメッセージを入力する箇所のプレイスホルダーが「Message」になっているなど、基本的に表示される文字の言語が英語になっています。
ここを日本語対応しました。

```dart:chat_room.dart
// 適当な場所に追加
  class ChatL10nJa extends ChatL10n {
    const ChatL10nJa({
      super.attachmentButtonAccessibilityLabel = '画像アップロード',
      super.emptyChatPlaceholder = 'メッセージがありません。',
      super.fileButtonAccessibilityLabel = 'ファイル',
      super.inputPlaceholder = 'メッセージを入力してください',
      super.sendButtonAccessibilityLabel = '送信',
    });
  }
    ・・・
  @override
  Widget build(BuildContext context) => Scaffold(
        body: Chat(
          theme: const DefaultChatTheme(
            primaryColor: Colors.green,
            userAvatarNameColors: [Colors.green],
          ),
          messages: _messages,
          onSendPressed: _handleSendPressed,
          user: _user,
          showUserAvatars: true,
          showUserNames: true,
          // 追加
        　　　　l10n: const ChatL10nJa(),
        ),
      );
    ・・・
```

ゴリ押し感が否めないので、もしかしたらもっとスマートなやり方があるかもですが、とりあえずこれで日本語化できました。


## おわりに

そんな感じで簡単にチャットのUIを作れるflutter_chat_uiについてでした。

誰かの役に立てばなと思います。ありがとうございました！