言葉はまるで雪の結晶、溶けていって消えてしまう、Androidチャットアプリ

[クソアプリ Advent Calendar 2022](https://qiita.com/advent-calendar/2022/kuso-app) 4日目の記事です。

Official髭男dismの「Subtitle」という曲が心にとても沁みます。
歌詞の一部を引用します。

>言葉はまるで雪の結晶
君にプレゼントしたとして
時間が経ってしまえば大抵
記憶から<ruby>溢<rt>こぼ</rt></ruby>れ落ちて溶けていって
消えてしまう

うっかりしたことが、フジテレビ系ドラマ「silent」を見逃してしまったこと。なんでも「泣けるドラマ」だそうで話題ですね。このドラマの主題歌でもあります。

何度も聴いているうちに、思い付いたのがこんなAndroidアプリです。

![animation (2)-min.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100612/c41a0beb-dca4-9681-7ec2-15df14caacdc.gif)

相手のメッセージが溶けて消えるチャットアプリです。相手の発言は、表示されるや否や、溶けて消えます。

# 開発環境

Windows10で、Android Studioはバージョン2021.2.1です。

会話する「相手」は、株式会社リクルートが提供するAI・機械学習ソリューション「[A3RT](https://a3rt.recruit.co.jp/)」のプロダクトの一つ「Talk API」です。
https://a3rt.recruit.co.jp/product/talkAPI/

ライブラリー「converter-moshi」を利用しました。
このライブラリーには、JSONパーサー「[Moshi](https://github.com/square/moshi)」と、HTTPクライアント「[Retrofit](https://square.github.io/retrofit/)」を含んでいるので、以下の一行をbuild.gradleファイルに記述してsync nowすれば済みます。

```groovy:build.gradle
implementation 'com.squareup.retrofit2:converter-moshi:2.9.0'
```

ありがとうございます、Square様！

# Talk API利用方法

メールアドレスを登録して、APIのキーを発行してもらいます。
そのAPIキーを使って仕様の通りにリクエストを送信すれば、日常会話で応答してくれます。
「最近めっきり寒くなってきましたね」に対して「好きですよね」というように、
時折、頓珍漢な返答をしてくれるので、クスッとなります。

メールアドレス１つでAPIキーは貰えますし、APIキーさえあればcurlコマンドでも会話を楽しめますので、簡単ですよ。
ありがとうございます、株式会社リクルート様！

# 参考にしたQiita記事

[湯婆婆Androidアプリを作ってみました](https://qiita.com/wakwak/items/b00372ab0eddeeac3e4c)という自分の記事を参考にしました。
字がふわ～っと消えるアニメーションを今回も適用したかったので。

この湯婆婆Androidアプリは、字（正確に言えば、`String`を1個ずつ`android.graphics`パッケージの`Canvas`や`Paint`を駆使して`Bitmap`オブジェクトに変換したもの）を、`android.animation`パッケージのAPIを使って「上へ向かって」ふわ～っと浮かび上がらせるアニメーションですが、今回の溶けて消えるチャットアプリは、向きが「右斜め下へ向かって」なので、X軸Y軸のパラメータをいじればいいだけですので、当記事では詳しい説明は割愛します（湯婆婆Androidアプリの記事を参照してください）。

# プログラム

Activityクラス1個と、リスト表示するのでRecyclerViewを使うことにしたので、それのAdapterクラスとViewHolderクラスも作りました。

以下、コードをすべて掲載すると長くなってしまうので、ポイントだけ残して、割愛できるところは省略しています。

## RetrofitのAPIインタフェース

```java:TalkApi.java
public interface TalkApi {
    @FormUrlEncoded
    @POST("/talk/v1/smalltalk")
    Call<TalkApiResponse> postRequest(@Field("apikey") String apikey, @Field("query") String query);
}
```

[Talk APIのリクエストパラメータ](https://a3rt.recruit.co.jp/product/talkAPI/#reference)の説明によると、POSTリクエストであること、「apikey」と「query」というパラメータが必要だとのことなので、このようなメソッド定義となりました。

## JSONデータクラス

Talk APIが返却してくれる雑談応答は、例えば以下のようなJSONです。

```json:Talk APIが返却してくれる雑談応答の例
{"status": 0, "message": "ok", "results": [{"perplexity": 4.643262344645122, "reply": "趣味を楽しんでいる人は素敵ですよね"}]}
```

なので、以下のようなデータクラスを作りました。

```java:TalkApiResponse.java
public class TalkApiResponse {
    public int status;
    public String message;
    public List<Result> results;

    public class Result {
        public double perplexity;
        public String reply;
    }
}
```

getter/setterメソッドを作るのが面倒くさくて`public`まみれです。Moshiに甘え切って完全に委ねます。
しかも、欲しい情報はただ一つ、`reply`だけなんですけどね。

## Activityクラスの概観

このアプリの挙動としては、自分のメッセージを入力して送信ボタンをクリックしたら、自分のメッセージを表示させたそばから、Talk APIにリクエストを送信し、レスポンスされたメッセージを表示します。そんなActivityです。

```java:MainActivity.java
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        EditText inputEditText = findViewById(R.id.inputEditText);
        Button sendButton = findViewById(R.id.sendButton);

        // RecyclerView関連
        RecyclerView recyclerView = findViewById(R.id.listView);
        ChatAdapter adapter = new ChatAdapter(this);
        recyclerView.setAdapter(adapter);
        recyclerView.setLayoutManager(new LinearLayoutManager(this));

        // ライブラリー「Retrofit」と「Moshi」を使います
        TalkApi api = new Retrofit.Builder()
                .client(new OkHttpClient.Builder().build())
                .baseUrl("https://api.a3rt.recruit.co.jp")
                .addConverterFactory(MoshiConverterFactory.create())
                .build()
                .create(TalkApi.class);

        // ボタン押下
        sendButton.setOnClickListener(v -> {
            String inputStr = inputEditText.getText().toString();

            // 自分のメッセージを表示
            adapter.add(inputStr);

            // Talk APIへPOSTリクエストして、レスポンスを受信する
            Call<TalkApiResponse> talkApiResponseCall = api.postRequest("Talk APIからもらったAPIキー", inputStr);
            talkApiResponseCall.enqueue(new Callback<TalkApiResponse>() {
                @Override
                public void onResponse(Call<TalkApiResponse> call, Response<TalkApiResponse> response) {
                    String reply = response.body().results.get(0).reply;
                    // 相手のメッセージを表示
                    adapter.add(reply);
                }

                @Override
                public void onFailure(Call<TalkApiResponse> call, Throwable throwable) {
                    // エラー処理
                }
            });
        });
    }
}
```

`Callback`インタフェースの匿名オブジェクトの`onResponse`メソッド内で、レスポンスのステータスで成功したか否かチェックを怠っていたり、`onFailure`メソッドも空っぽというように、いろいろとちょっとお行儀が悪い箇所が多いのですが、勘弁してください。

このActivityのレイアウトリソースファイルには`<RecyclerView>`を配置しておきます。

## RecyclerView.Adapterクラスの概観

```java
public class ChatAdapter extends RecyclerView.Adapter<ChatAdapter.FukidashiViewHolder> {
    private final Context context;
    private final List<String> msgList;

    public ChatAdapter(Context context) {
        this.context = context;
        this.msgList = new ArrayList<>();
    }

    @Override
    public int getItemCount() {
        return msgList.size();
    }

    @NonNull
    @Override
    public FukidashiViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        return new FukidashiViewHolder(
                LayoutInflater.from(parent.getContext()).inflate(R.layout.fukidashi, parent, false)
        );
    }

    @Override
    public void onBindViewHolder(@NonNull FukidashiViewHolder holder, int position) {
        if (自分のメッセージ) {
            // 自分の吹き出しに普通に表示
        } else {
            // 相手（Talk API）のメッセージ

            // 文字列を１字ずつバラにする
            String[] strings = new String[message.length()];
            for (int i = 0; i < message.length(); i++) {
                strings[i] = String.valueOf(message[1].charAt(i));
            }

            // １字ずつをBitmapにして、ImageViewに貼り付けて、GridLayputに配置する
            ImageView[] imageViews = new ImageView[strings.length];
            for (int i = 0; i < strings.length; i++) {
                imageViews[i] = new ImageView(context);
                imageViews[i].setImageBitmap(convertTextToBitmap(strings[i]));
                holder.otherMsgArea.addView(imageViews[i]);
            }

            // 溶かす
            for (ImageView imageView : imageViews) {
                meltingAnimation(imageView);
            }
        }
    }

    // Activityから貰う新メッセージ
    public void add(String newMsg) {
        msgList.add(newMsg);
    }
}
```

一行分のレイアウトリソースファイルは「fukidashi.xml」と名付けました。そこに吹き出しを配置しています。吹き出しの画像は9-patch画像です。黒い吹き出しが自分のメッセージ用、緑の吹き出しが相手のメッセージ用です。

![fukidashi_other.9.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100612/8647d1f5-7def-53c6-5392-dd2171fefead.png)　![fukidashi_me.9.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100612/321c5bd4-fae7-8379-c58a-baf88050d408.png)

コード中にある、以下のメソッドは、

- `convertTextToBitmap`メソッドは、`String`を`Bitmap`に変換して返すメソッドです。
- `meltingAnimation`メソッドは、`ImageView`を右斜め下にふわ～っと消していくアニメーションをするメソッドです。

これらのメソッドは[湯婆婆Androidアプリを作ってみました](https://qiita.com/wakwak/items/b00372ab0eddeeac3e4c)という自分の記事にあるのとほぼ同じなので、ここでは説明は割愛します。

- 自分のメッセージであれば、普通に出す。
- 相手（Talk API）のメッセージであれば、表示したそばから溶けるように消す。

そんな処理の振り分けのif構文を、`RecyclerView.Adapter`の`onBindViewHolder`メソッドの中で処理しています。

# それでは最後にお聴きください

湯婆婆Androidアプリと同様に、「表示させた文字を、消す」シリーズのアプリを作ってみました。
まるで役に立ちません。古今東西のアプリとかシステムとかソフトウェアで、表示させた文字がふわ～っと消えるだなんてものは、見たことがありません。

でも、こうして『消えていくもの』を、冒頭で紹介したOfficial髭男dismの「Subtitle」だけでなく、多くのアーティストたちが歌い上げています。

> 手を伸ばし抱き止めた激しい光の束
輝いて消えてった　未来のために

LiSAの「<ruby>炎<rt>ほむら</rt><ruby>」です。鬼も滅しないといけないですし。

> 大切な人も恋も愛も
性も、どうしよう
いつまでたっても守りきれないよ
いつかは消えてしまう

あいみょんの「あした世界が終わるとしても」です。世界が終わるのなら、そりゃ消えますよね。

> 悲しみっていつかは　消えてしまうものなのかなぁ…
タメ息は少しだけ　白く残ってすぐ消えた

SMAPの「夜空ノムコウ」です。全ては思うほどうまくいかないようです。

> 砂まじりの茅ケ崎
人も波も消えて

サザンオールスターズの「勝手にシンドバッド」です。
砂や波、といえば、もう一曲。

> 愛はまるで砂の城ね
出来た<ruby>瞬間<rt>とたん</rt><ruby>波がさらう

斉藤由貴の「砂の城」です。おっとこれは、城は消えても、光る砂が残るわ。光る砂が残るわ。

以上、記憶に消えない、心に残る名曲集でした。