【Android】Twitter4JのUserStreamのイベント時の動作をクロージャで指定する

JavaTwitter関連の処理をしたいならTwitter4Jを使わない理由がない、と思っています。本当によく出来たライブラリです。

当然このライブラリでUserStreamを処理する事もできます。ありがたいことに全部非同期で処理してくれます。

詳しいやり方については先人たちが山ほどサンプルを残してくれているのでここでは書きませんが、Android、っつーか、GUIがシングルスレッドで動いているようなやつだと、結構面倒くさいです。

[20140219追記]結局詳しいやり方も書きました。

イベントが多すぎる

これはGUIどうこうじゃなくて、もう単純に、JavaのObserverパターンの問題なんですが、イベント多すぎて実装がだるい。

ちょっとUserStreamの一機能を使おうと思ったらいきなりこんなんなりますからね。

UserStreamListener streamListener = new UserStreamListener() {
    @Override
    public void onException(Exception arg0) {}
    
    @Override
    public void onTrackLimitationNotice(int arg0) {}
    
    @Override
    public void onStatus(Status arg0) {}
    
    @Override
    public void onStallWarning(StallWarning arg0) {}
    
    @Override
    public void onScrubGeo(long arg0, long arg1) {}
    
    @Override
    public void onDeletionNotice(StatusDeletionNotice arg0) {}
    
    @Override
    public void onUserProfileUpdate(User arg0) {}
    
    @Override
    public void onUserListUpdate(User arg0, UserList arg1) {}
    
    @Override
    public void onUserListUnsubscription(User arg0, User arg1, UserList arg2) {}
    
    @Override
    public void onUserListSubscription(User arg0, User arg1, UserList arg2) {}
    
    @Override
    public void onUserListMemberDeletion(User arg0, User arg1, UserList arg2) {}
    
    @Override
    public void onUserListMemberAddition(User arg0, User arg1, UserList arg2) {}
    
    @Override
    public void onUserListDeletion(User arg0, UserList arg1) {}
    
    @Override
    public void onUserListCreation(User arg0, UserList arg1) {}
    
    @Override
    public void onUnfavorite(User arg0, User arg1, Status arg2) {}
    
    @Override
    public void onUnblock(User arg0, User arg1) {}
    
    @Override
    public void onFriendList(long[] arg0) {}
    
    @Override
    public void onFollow(User arg0, User arg1) {}
    
    @Override
    public void onFavorite(User arg0, User arg1, Status arg2) {}
    
    @Override
    public void onDirectMessage(DirectMessage arg0) {}
    
    @Override
    public void onDeletionNotice(long arg0, long arg1) {}
    
    @Override
    public void onBlock(User arg0, User arg1) {}
};

これら全部を実装しないといけないんですが、ぶっちゃけこんなにいらないです。まぁそりゃ何も記述しなきゃいいんですけど、なんかこう、見た瞬間イライラするじゃないですかこういうの。しませんか?しないならいいんじゃないですかね。

UIスレッドに戻すのが面倒くさい

これらのイベントは全部別スレッドで行われるので、UIに反映させる場合はHandler#post(Runnable)を使ってあげないといけないです。

ToastやAlertDialogぐらいならいいんですが、例えばStatusを拾ってArrayAdapterに詰め込んでListView更新して…といった場合だと、どうしてもActivity内でListenerを宣言しないと、更に面倒なことになります。

一つの対策としてはUserStreamListenerを実装したクラスを作り、そこで既定の動作を定義してしまい、UIに深く関わる処理はListener作成時にオーバーライドするというのが考えられます。

public class StreamListener implements UserStreamListener {

    private Context _cont;
    private Handler _handler = new Handler();

    public StreamListener(Context cont) {
        this._cont = cont;
    }

    //エラー
    @Override
    public void onException(Exception e) {
        e.printStackTrace();
        _handler.post(new Runnable() {
            @Override
            public void run() {
                Toast.makeText(_cont,
                        "ヒャアアアアアアアアアア",
                        Toast.LENGTH_SHORT).show();
            }
        });
    }

    //ステータス取得
    @Override
    public void onStatus(Status status) {}
    
    //以下省略
}
public class MainActivity extends Activity {
    
    private static Handler _handler = new Handler();
    
    private StreamListener _listener = new StreamListener(getApplicationContext()) {
        @Override
        public void onStatus(Status status) {
            _handler.post(new Runnable() {
                @Override
                public void run() {
                    // UIに関係する処理
                }
            });
        }
    };
    
    //以下省略
}

まぁ、要はテンプレートパターンの亜種みたいなもんです。UserStreamを沢山作る必要があり、なおかつ微妙に処理を変えたいならこれでもいいでしょう。ただ、UserStreamを複数作りたくなることはまずないです。一個で十分です。むしろシングルトンであって欲しいぐらいです。

このやり方の何がダメって、どうせ一箇所でしか使わないのに、ソースが分散してしまうんですよ。何処で何やってるのか非常にわかりにくい。出来ればUserStreamを使うActivity内に必要なイベントだけを必要なだけ書いておきたい。

クロージャクロージャを使う

それなら各種イベントにクロージャを入れちゃえばいいんじゃないかなと。こんな感じに。

public final class UserStream {

    private static UserStream _thisInstance;
    private UserStreamListener _streamListener;
    private TwitterStream _twitterStream;
    private static Handler _handler = new Handler();
    private boolean _isConnect;

    public boolean isConnect() {
        return _isConnect;
    }

    private UserStream() {
        streamListenerInit();
    }

    public static UserStream getInstance() {
        if(_thisInstance == null) _thisInstance = new UserStream();
        return _thisInstance;
    }
    
    private V1<Exception> _onError;
    //あまりにも多いので省略
    
    private void streamListenerInit() {
        _streamListener = new UserStreamListener() {
            public void onException(Exception arg0) {
                if(_onError != null) _onError.call(arg0);
            }
            //あまりにも多いので省略
        };
    }
    
    public void start() {
        if(!_isConnect) {
            if(_twitterStream == null) {
                ConfigurationBuilder conf  = new ConfigurationBuilder()
                                                .setOAuthAccessTokenSecret("")
                                                .setOAuthConsumerKey("")
                                                .setOAuthConsumerSecret("")
                                                .setOAuthAccessToken("")
                                                .setUserStreamRepliesAllEnabled(false);
                _twitterStream = new TwitterStreamFactory(conf.build()).getInstance();
                _twitterStream.addListener(_streamListener);
            }
            _twitterStream.user();
            _isConnect = true;
        }
    }

    public void close() {
        if(_isConnect) {
            _twitterStream.cleanUp();
            _isConnect = false;
        }
    }

    @Override
    protected void finalize() throws Throwable {
        try {
            super.finalize();
        } finally {
            if(_twitterStream != null) {
                 _twitterStream.cleanUp();
            }
        }
    }
}

当然このままだと動かないのでsetterを作ってあげます。

public UserStream setOnError(V1<Exception> onError) {
    this._onError = onError;
    return this;
}

public UserStream setOnErrorUI(final V1<Exception> onError) {
    this._onError = new V1<Exception>() {
        public void call(Exception arg0) {
            runMainThread(arg0, onError);
        }
    };
    return this;
}

public <T> UserStream setOnError(final R1<T, Exception> onError, final V1<T> onUIThread) {
    this._onError = new V1<Exception>() {
        public void call(Exception arg0) {
            runMainThread(onError.call(arg0), onUIThread);
        }
    };
    return this;
}

private <T> void runMainThread(final T arg, final V1<T> action) {
    _handler.post(new Runnable() {
        public void run() {
            action.call(arg);
        }
    });
}

同じように全イベントのクロージャを作ってあげます。載せる意味はあんまりないので割愛します。

で、UserStreamを使うActivityでこんな風に呼び出します。

private void startUserStream() {
    UserStream.getInstance()
      .setOnErrorUI(new V1<Exception>() {
        public void call(Exception arg0) {
            //即UIスレッドで処理
            Toast.makeText(getApplicationContext()
                        , "ヒャアアアアアアアアアア"
                        , Toast.LENGTH_SHORT).show();
        }
      }).setOnStatus(new R1<BeansTimeline, Status>(){
        public BeansTimeline call(Status arg0) {
            //別スレッドでの処理
            return new BeansTimeline();
        }
      }, new V1<BeansTimeline>(){
        public void call(BeansTimeline arg0) {
            //別スレッドで処理した結果をUIスレッドで処理
        }
      }).start(getApplicationContext());
}

private void endUserStream() {
    UserStream.getInstance().close();
}

シングルトンなので特にActivity側のメンバ変数で持たなくても…いい…はずです…。(自信がない)

まとめ

一応動作確認ぐらいはしましたが、やっぱり多少ぬるぬる感が損なわれてるような気がします。これは私のListViewがらみの処理がアレなだけな気がしますが。