Android
Twitter
fabric

第三回 AndroidでTwitterクライアントの作成(リプライ機能)

第一回 AndroidでTwitterクライアントの作成(認証まで)

第二回 AndroidでTwitterクライアントの作成(タイムラインの取得)

上の二つの続きです。

前回まででタイムラインの表示までできたので、今回は返信機能をつけてみます。

ソースコードです
https://github.com/fungiy/MyTwitterApp

返信ボタンの追加

まずはじめに、個々のツイートが表示されるレイアウトを修正し、返信ボタンを追加しています。

tweet_row.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <TextView
        android:id="@+id/screen_name"
        android:layout_width="match_parent"
        android:layout_height="32dp"
        android:text="名前"
         />

    <TextView
        android:id="@+id/tweet_text"
        android:layout_width="match_parent"
        android:layout_height="32dp"
        android:text="ツイート本文"
         />

    <TextView
        android:id="@+id/favorite_count"
        android:layout_width="match_parent"
        android:layout_height="32dp"
        />

    <!-- 追加した部分 -->   
    <Button
        android:id="@+id/reply_button"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="返信する"
        />

</LinearLayout>

返信ボタンのクリックイベントを捕まえる

ListViewのボタンなどが押された場合、AdapterのgetViewメソッドの中でボタンに対してリスナーをセットしておくことでイベントをハンドリングできます。

前回までで、TweetAdapterは以下のようになっていると思います。
ここにボタンのイベント処理を追加していきます。

TweetAdapter.java(編集前)
package com.my_twitter_client_app.www.mytwitterapp;

import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.TextView;

import com.twitter.sdk.android.core.models.Tweet;

import java.util.List;

public class TweetAdapter extends BaseAdapter {

    private Context context;
    private LayoutInflater layoutInflater = null;
    private List<Tweet> tweetList;

    public TweetAdapter(Context context, List<Tweet> tweetList) {
        this.context = context;
        this.layoutInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        this.tweetList = tweetList;
    }

    @Override
    public int getCount() {
        return tweetList.size();
    }

    @Override
    public Object getItem(int position) {
        return tweetList.get(position);
    }

    @Override
    public long getItemId(int position) {
        return tweetList.get(position).getId();
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        convertView = layoutInflater.inflate(R.layout.tweet_row, parent, false);

        Tweet tweet = tweetList.get(position);

        TextView screenNameTextView = (TextView)convertView.findViewById(R.id.screen_name);
        TextView tweetTextTextView = (TextView)convertView.findViewById(R.id.tweet_text);
        TextView favoriteCountTextView = (TextView)convertView.findViewById(R.id.favorite_count);

        screenNameTextView.setText(tweet.user.name);
        tweetTextTextView.setText(tweet.text);
        favoriteCountTextView.setText(String.valueOf(tweet.favoriteCount));

        return convertView;
    }
}

以下のように、TextViewなどと同様にtweet_row.xmlのR.id.reply_buttonを取得してこれに対してボタンが押された時の処理を追加しています。

TweetAdapter.java(編集後)
package com.my_twitter_client_app.www.mytwitterapp;

import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.Button;
import android.widget.TextView;

import com.twitter.sdk.android.core.models.Tweet;

import java.util.List;

public class TweetAdapter extends BaseAdapter {

    private Context context;
    private LayoutInflater layoutInflater = null;
    private List<Tweet> tweetList;

    public TweetAdapter(Context context, List<Tweet> tweetList) {
        this.context = context;
        this.layoutInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        this.tweetList = tweetList;
    }

    @Override
    public int getCount() {
        return tweetList.size();
    }

    @Override
    public Object getItem(int position) {
        return tweetList.get(position);
    }

    @Override
    public long getItemId(int position) {
        return tweetList.get(position).getId();
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        convertView = layoutInflater.inflate(R.layout.tweet_row, parent, false);

        Tweet tweet = tweetList.get(position);

        TextView screenNameTextView = (TextView)convertView.findViewById(R.id.screen_name);
        TextView tweetTextTextView = (TextView)convertView.findViewById(R.id.tweet_text);
        TextView favoriteCountTextView = (TextView)convertView.findViewById(R.id.favorite_count);
        // 追加
        Button replyButton = (Button) convertView.findViewById(R.id.reply_button);

        screenNameTextView.setText(tweet.user.name);
        tweetTextTextView.setText(tweet.text);
        favoriteCountTextView.setText(String.valueOf(tweet.favoriteCount));

        // イベントを拾う
        replyButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {

            }
        });


        return convertView;
    }
}

いつも通り、replyButtonにOnClickListenerを追加してイベントを拾っています。
リスナーの中身についてはこれから書きます。

Adapterから親Activityのメソッドを呼ぶ

続いて、返信画面を表示するために一旦Adapter側から親のActivityに処理を戻す必要があります。
まずはじめに、適当な名前で新しくインターフェイスを作成します。
そしてリプライボタンが押された旨をAdapterからActivityに知らせるメソッドを定義します。

MyAdapterListener.java
package com.my_twitter_client_app.www.mytwitterapp;

import com.twitter.sdk.android.core.models.Tweet;

public interface MyAdapterListener {

    // リプライボタンが押されたことをActivityに知らせる
    public void onClickReplyButton(Tweet tweet);
}

このインターフェイスを、Adapter側でメンバとして持っておくことにします。
コンストラクタに引数を追加します。

ここでは、いくつか処理を追加しています。

  • 独自リスナーを受け取っておいてメンバでもつ
  • getViewの中では、クリックイベント発生時にリスナー(先ほど定義したインターフェイス)のメソッドを叩く
TweetAdapter.java
package com.my_twitter_client_app.www.mytwitterapp;

import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.Button;
import android.widget.TextView;

import com.twitter.sdk.android.core.models.Tweet;

import java.util.List;

public class TweetAdapter extends BaseAdapter {

    private Context context;
    private LayoutInflater layoutInflater = null;
    private List<Tweet> tweetList;
    // 追加
    private MyAdapterListener mListener;

    public TweetAdapter(Context context, List<Tweet> tweetList, MyAdapterListener mListener) {
        this.context = context;
        this.layoutInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        this.tweetList = tweetList;
        // 追加
        this.mListener = mListener;
    }

    @Override
    public int getCount() {
        return tweetList.size();
    }

    @Override
    public Object getItem(int position) {
        return tweetList.get(position);
    }

    @Override
    public long getItemId(int position) {
        return tweetList.get(position).getId();
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        convertView = layoutInflater.inflate(R.layout.tweet_row, parent, false);

        final Tweet tweet = tweetList.get(position);

        TextView screenNameTextView = (TextView)convertView.findViewById(R.id.screen_name);
        TextView tweetTextTextView = (TextView)convertView.findViewById(R.id.tweet_text);
        TextView favoriteCountTextView = (TextView)convertView.findViewById(R.id.favorite_count);
        Button replyButton = (Button) convertView.findViewById(R.id.reply_button);

        screenNameTextView.setText(tweet.user.name);
        tweetTextTextView.setText(tweet.text);
        favoriteCountTextView.setText(String.valueOf(tweet.favoriteCount));

        // イベントを拾う
        replyButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                // ボタンが押されたら、独自リスナーのメソッドを呼ぶ。
                // 引数にはリプライ対象のtweetを渡す
                mListener.onClickReplyButton(tweet);
            }
        });


        return convertView;
    }
}

最後に、このインターフェイスを親のActivity(今回の場合はTimelineActivity)はimplementsします。

以下のようになります。

TimelineActivity.java
package com.my_twitter_client_app.www.mytwitterapp;

import android.content.Intent;
import android.os.Bundle;
import android.support.design.widget.FloatingActionButton;
import android.support.design.widget.Snackbar;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.view.View;
import android.widget.ListView;
import android.widget.Toast;

import com.twitter.sdk.android.core.Callback;
import com.twitter.sdk.android.core.Result;
import com.twitter.sdk.android.core.TwitterApiClient;
import com.twitter.sdk.android.core.TwitterCore;
import com.twitter.sdk.android.core.TwitterException;
import com.twitter.sdk.android.core.models.Tweet;
import com.twitter.sdk.android.core.services.StatusesService;

import java.util.ArrayList;
import java.util.List;

import retrofit2.Call;

public class TimelineActivity extends AppCompatActivity implements MyAdapterListener {

    ListView listView;
    TweetAdapter adapter;
    List<Tweet> tweetList = new ArrayList<>();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_timeline);
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);

        FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
        fab.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Intent intent = new Intent(TimelineActivity.this, PostTweetActivity.class);
                startActivity(intent);
            }
        });

        listView = (ListView) findViewById(R.id.my_list_view);
        adapter = new TweetAdapter(this, tweetList, this);

        listView.setAdapter(adapter);

        getHomeTimeline();
    }

    private void getHomeTimeline() {
        TwitterApiClient twitterApiClient = TwitterCore.getInstance().getApiClient();

        StatusesService statusesService = twitterApiClient.getStatusesService();

        Call<List<Tweet>> call = statusesService.homeTimeline(20, null, null, false, false, false, false);
        call.enqueue(new Callback<List<Tweet>>() {
            @Override
            public void success(Result<List<Tweet>> result) {

                List<Tweet> tweets = result.data;

                // ListViewのListに取得したツイートのリストを追加
                tweetList.addAll(tweets);
                // ListViewの表示を更新
                adapter.notifyDataSetChanged();

                Toast toast = Toast.makeText(TimelineActivity.this, "タイムライン取得成功", Toast.LENGTH_LONG);
                toast.show();
            }

            @Override
            public void failure(TwitterException exception) {
                Toast toast = Toast.makeText(TimelineActivity.this, "タイムライン取得エラー", Toast.LENGTH_LONG);
                toast.show();
            }
        });
    }

    // 追加
    @Override
    public void onClickReplyButton(Tweet tweet) {

    }
}

新しく作成したMyAdapterListenerをimplementsしたため、onClickReplyButtonメソッドを作る必要があります。
onClickReplyButtonメソッドでは引数にTweetクラス(これはTwitter Kitにもともと入っている)を受け取ります。
ここにはリプライボタンが押されたら、Adapterからリプライ対象のTweetが渡ってくるようにします。

ここまでで、AdapterのイベントをActivityで受け取るところまで作り終えました。
次はonClickReplyButtonの中身を作って、実際にリプライボタンが押された時の動作を作ることにします。

ツイート投稿画面の作成

新しくツイート投稿用のActivityを作成します。
以下がそのコードです。

ツイートを入力しFloatingActionButtonを押すとEditTextの内容がツイートされます。

Intentでリプライ用のツイートのstatus_idが渡ってきているかどうかでリプライにするか普通のツイートにするか分岐しています。

PostTweetActivity.java
package com.my_twitter_client_app.www.mytwitterapp;

import android.os.Bundle;
import android.support.design.widget.FloatingActionButton;
import android.support.design.widget.Snackbar;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.util.Log;
import android.view.View;
import android.widget.EditText;
import android.widget.Toast;

import com.twitter.sdk.android.core.Callback;
import com.twitter.sdk.android.core.Result;
import com.twitter.sdk.android.core.Twitter;
import com.twitter.sdk.android.core.TwitterApiClient;
import com.twitter.sdk.android.core.TwitterCore;
import com.twitter.sdk.android.core.TwitterException;
import com.twitter.sdk.android.core.models.Tweet;
import com.twitter.sdk.android.core.services.StatusesService;

import retrofit2.Call;

public class PostTweetActivity extends AppCompatActivity {

    private EditText editText;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_post_tweet);
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);

        FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
        fab.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                String text = editText.getText().toString();

                long replyToStatusId = getIntent().getLongExtra("REPLY_TO_STATUS_ID", -1);

                // リプライかどうか
                if (replyToStatusId == -1) {
                    // 普通の投稿
                    post(text);
                } else {
                    // リプライとして投稿
                    postAsReply(text, replyToStatusId);
                }
            }
        });

        editText = (EditText) findViewById(R.id.edit_post_text);
    }

    private void post(String postText) {

        TwitterApiClient twitterApiClient = TwitterCore.getInstance().getApiClient();

        StatusesService statusesService = twitterApiClient.getStatusesService();
        Call<Tweet> call = statusesService.update(postText, null, null, null, null, null, null, null, null);

        call.enqueue(new Callback<Tweet>() {

            @Override
            public void success(Result<Tweet> result)
            {
                Toast toast = Toast.makeText(PostTweetActivity.this, "投稿成功", Toast.LENGTH_LONG);
                toast.show();
            }

            @Override
            public void failure(TwitterException exception) {
                Toast toast = Toast.makeText(PostTweetActivity.this, "投稿失敗", Toast.LENGTH_LONG);
                toast.show();
            }
        });
    }

    private void postAsReply(String postText, long replyToStatusId) {

        TwitterApiClient twitterApiClient = TwitterCore.getInstance().getApiClient();

        StatusesService statusesService = twitterApiClient.getStatusesService();
        Call<Tweet> call = statusesService.update(postText, replyToStatusId, null, null, null, null, null, null, null);

        call.enqueue(new Callback<Tweet>() {

            @Override
            public void success(Result<Tweet> result) {
                Toast toast = Toast.makeText(PostTweetActivity.this, "リプライ失敗", Toast.LENGTH_LONG);
                toast.show();
            }

            @Override
            public void failure(TwitterException exception) {
                Toast toast = Toast.makeText(PostTweetActivity.this, "リプライ失敗", Toast.LENGTH_LONG);
                toast.show();
            }
        });
    }

}

activity_post_tweet.xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.my_twitter_client_app.www.mytwitterapp.PostTweetActivity">

    <android.support.design.widget.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:theme="@style/AppTheme.AppBarOverlay">

        <android.support.v7.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"
            android:background="?attr/colorPrimary"
            app:popupTheme="@style/AppTheme.PopupOverlay" />

    </android.support.design.widget.AppBarLayout>

    <include layout="@layout/content_post_tweet" />

    <android.support.design.widget.FloatingActionButton
        android:id="@+id/fab"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom|end"
        android:layout_margin="@dimen/fab_margin"
        app:srcCompat="@android:drawable/ic_dialog_email" />

</android.support.design.widget.CoordinatorLayout>
content_post_tweet.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:layout_behavior="@string/appbar_scrolling_view_behavior"
    tools:context="com.my_twitter_client_app.www.mytwitterapp.PostTweetActivity"
    tools:showIn="@layout/activity_post_tweet"
    android:orientation="vertical"
    >

    <EditText
        android:id="@+id/edit_post_text"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:hint="今なにしてる?"
        android:inputType="textMultiLine"
        android:gravity="top"
        />

</LinearLayout>

リプライボタンを押したら画面遷移するようにする

onClickReplyButtonメソッドの中身を追加しています。

intent.putExtraでリプライ先のツイートのidをPostTweetActivityに渡しています。
PostTweetActivity側では、このExtraがIntentに付与されているかどうかでリプライか普通のツイートとして投稿するか分岐しています。

TimelineActivity.java
package com.my_twitter_client_app.www.mytwitterapp;

import android.content.Intent;
import android.os.Bundle;
import android.support.design.widget.FloatingActionButton;
import android.support.design.widget.Snackbar;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.view.View;
import android.widget.ListView;
import android.widget.Toast;

import com.twitter.sdk.android.core.Callback;
import com.twitter.sdk.android.core.Result;
import com.twitter.sdk.android.core.TwitterApiClient;
import com.twitter.sdk.android.core.TwitterCore;
import com.twitter.sdk.android.core.TwitterException;
import com.twitter.sdk.android.core.models.Tweet;
import com.twitter.sdk.android.core.services.StatusesService;

import java.util.ArrayList;
import java.util.List;

import retrofit2.Call;

public class TimelineActivity extends AppCompatActivity implements MyAdapterListener {

    ListView listView;
    TweetAdapter adapter;
    List<Tweet> tweetList = new ArrayList<>();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_timeline);
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);

        FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
        fab.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Intent intent = new Intent(TimelineActivity.this, PostTweetActivity.class);
                startActivity(intent);
            }
        });

        listView = (ListView) findViewById(R.id.my_list_view);
        adapter = new TweetAdapter(this, tweetList, this);

        listView.setAdapter(adapter);

        getHomeTimeline();
    }

    private void getHomeTimeline() {
        TwitterApiClient twitterApiClient = TwitterCore.getInstance().getApiClient();

        StatusesService statusesService = twitterApiClient.getStatusesService();

        Call<List<Tweet>> call = statusesService.homeTimeline(20, null, null, false, false, false, false);
        call.enqueue(new Callback<List<Tweet>>() {
            @Override
            public void success(Result<List<Tweet>> result) {

                List<Tweet> tweets = result.data;

                // ListViewのListに取得したツイートのリストを追加
                tweetList.addAll(tweets);
                // ListViewの表示を更新
                adapter.notifyDataSetChanged();

                Toast toast = Toast.makeText(TimelineActivity.this, "タイムライン取得成功", Toast.LENGTH_LONG);
                toast.show();
            }

            @Override
            public void failure(TwitterException exception) {
                Toast toast = Toast.makeText(TimelineActivity.this, "タイムライン取得エラー", Toast.LENGTH_LONG);
                toast.show();
            }
        });
    }

    @Override
    public void onClickReplyButton(Tweet tweet) {
        // PostTweetActivityに画面遷移
        Intent intent = new Intent(this, PostTweetActivity
                .class);
        intent.putExtra("REPLY_TO_STATUS_ID", tweet.id);
        startActivity(intent);
    }
}

ここまでの実装でうまくいけば、それぞれのツイートについているリプライボタンを押して投稿画面を開き、リプライを行う処理ができていると思います。
お疲れ様でした。