2013/11/29

ExecutorServiceでマルチスレッドで実行可能なタスクキューを使う

マルチスレッドシリーズその2。
その1はこちら→[Java]マルチスレッドでの排他処理を行うsynchronizedを理解する
前回と同じく理解するためにサンプルを Android で作ってみました。

参考
ExecutorService の使い方 - Java 入門(図や概念がわかりやすい!)
Java - ExecutorService の復習 - Qiita [キータ](動作の違いがわかりやすい!)
死刑執行中 ExecutorService 終了中 - 倭マン's BLOG
[Java]ExecutorServiceの正しい終了(shutdown)の仕方 | 大発見 | 大発見

サンプルコードは以下にあります。
ExecutorActivity.java | wada811/AndroidLibrary@wada811
AndroidLibrary ってなってるけどやっぱりコレは Android 関係ないです。

実行するタスク

名前と実行時間(sleep時間)を貰ってログを出力するだけのクラス。以後、共通で使います。
public class ExecutorRunnable implements Runnable {
    private String mName;
    private int mSeconds;

    public ExecutorRunnable(String name, int seconds) {
        mName = name;
        mSeconds = seconds;
    }

    @Override
    public void run(){
        LogUtils.d(mName + ": ThreadId: " + Thread.currentThread().getId());
        LogUtils.d(mName + ": start");
        try{
            TimeUnit.SECONDS.sleep(mSeconds);
        }catch(InterruptedException e){
            e.printStackTrace();
        }
        LogUtils.d(mName + ": end");
    }
}

Executors.newSingleThreadExecutor();

public void newSingleThreadExecutorTest(){
    LogUtils.d();
    ExecutorService executorService = Executors.newSingleThreadExecutor();
    executorService.submit(new ExecutorRunnable("A", 1));
    executorService.submit(new ExecutorRunnable("B", 1));
    executorService.submit(new ExecutorRunnable("C", 1));
    executorService.submit(new ExecutorRunnable("D", 1));
}
SingleThread なのでタスクをいくつ追加しても一つのスレッドで実行します。追加した順に実行されます。
実行中だったら待たされます。実行ログからもそのことがわかります。
11-27 23:50:49.697: [ExecutorActivity#newSingleThreadExecutorTest:108]
11-27 23:50:49.707: [ExecutorActivity$ExecutorRunnable#run:299]A: ThreadId: 27824
11-27 23:50:49.707: [ExecutorActivity$ExecutorRunnable#run:300]A: start
11-27 23:50:50.708: [ExecutorActivity$ExecutorRunnable#run:306]A: end
11-27 23:50:50.708: [ExecutorActivity$ExecutorRunnable#run:299]B: ThreadId: 27824
11-27 23:50:50.718: [ExecutorActivity$ExecutorRunnable#run:300]B: start
11-27 23:50:51.719: [ExecutorActivity$ExecutorRunnable#run:306]B: end
11-27 23:50:51.719: [ExecutorActivity$ExecutorRunnable#run:299]C: ThreadId: 27824
11-27 23:50:51.719: [ExecutorActivity$ExecutorRunnable#run:300]C: start
11-27 23:50:52.720: [ExecutorActivity$ExecutorRunnable#run:306]C: end
11-27 23:50:52.720: [ExecutorActivity$ExecutorRunnable#run:299]D: ThreadId: 27824
11-27 23:50:52.730: [ExecutorActivity$ExecutorRunnable#run:300]D: start
11-27 23:50:53.731: [ExecutorActivity$ExecutorRunnable#run:306]D: end

Executors.newFixedThreadPool(int);

public void newFixedThreadPoolTest(){
    LogUtils.d();
    ExecutorService executorService = Executors.newFixedThreadPool(2);
    executorService.submit(new ExecutorRunnable("A", 1));
    executorService.submit(new ExecutorRunnable("B", 1));
    executorService.submit(new ExecutorRunnable("C", 1));
    executorService.submit(new ExecutorRunnable("D", 1));
}
引数のスレッド数で実行します。今回は2スレッドで実行します。追加した順に実行されます。
実行ログからも A, B と実行され、 A が終わったら C、B が終わったら D と実行されていることがわかります。
11-27 23:56:04.974: [ExecutorActivity#newFixedThreadPoolTest:124]
11-27 23:56:04.984: [ExecutorActivity$ExecutorRunnable#run:299]A: ThreadId: 27825
11-27 23:56:04.994: [ExecutorActivity$ExecutorRunnable#run:300]A: start
11-27 23:56:05.024: [ExecutorActivity$ExecutorRunnable#run:299]B: ThreadId: 27826
11-27 23:56:05.034: [ExecutorActivity$ExecutorRunnable#run:300]B: start
11-27 23:56:05.995: [ExecutorActivity$ExecutorRunnable#run:306]A: end
11-27 23:56:05.995: [ExecutorActivity$ExecutorRunnable#run:299]C: ThreadId: 27825
11-27 23:56:05.995: [ExecutorActivity$ExecutorRunnable#run:300]C: start
11-27 23:56:06.035: [ExecutorActivity$ExecutorRunnable#run:306]B: end
11-27 23:56:06.035: [ExecutorActivity$ExecutorRunnable#run:299]D: ThreadId: 27826
11-27 23:56:06.045: [ExecutorActivity$ExecutorRunnable#run:300]D: start
11-27 23:56:07.006: [ExecutorActivity$ExecutorRunnable#run:306]C: end
11-27 23:56:07.046: [ExecutorActivity$ExecutorRunnable#run:306]D: end

Executors.newCachedThreadPool();

public void newCachedThreadPoolTest(){
    LogUtils.d();
    ExecutorService executorService = Executors.newCachedThreadPool();
    executorService.submit(new ExecutorRunnable("A", 1));
    executorService.submit(new ExecutorRunnable("B", 1));
    executorService.submit(new ExecutorRunnable("C", 1));
    executorService.submit(new ExecutorRunnable("D", 1));
}
必要な分だけ(※)スレッドを生成して実行します。
(※ 内部の処理を見ると Integer.MAX_VALUE とあるので限界は int の最大値?)
タスクが何個 submit されようと必要に応じてスレッドを生成して同時に実行していることがわかります。
11-28 00:00:09.593: [ExecutorActivity#newCachedThreadPoolTest:140]
11-28 00:00:09.603: [ExecutorActivity$ExecutorRunnable#run:299]A: ThreadId: 27829
11-28 00:00:09.623: [ExecutorActivity$ExecutorRunnable#run:300]A: start
11-28 00:00:09.623: [ExecutorActivity$ExecutorRunnable#run:299]B: ThreadId: 27830
11-28 00:00:09.633: [ExecutorActivity$ExecutorRunnable#run:300]B: start
11-28 00:00:09.643: [ExecutorActivity$ExecutorRunnable#run:299]C: ThreadId: 27831
11-28 00:00:09.643: [ExecutorActivity$ExecutorRunnable#run:300]C: start
11-28 00:00:09.653: [ExecutorActivity$ExecutorRunnable#run:299]D: ThreadId: 27832
11-28 00:00:09.653: [ExecutorActivity$ExecutorRunnable#run:300]D: start
11-28 00:00:10.614: [ExecutorActivity$ExecutorRunnable#run:306]A: end
11-28 00:00:10.634: [ExecutorActivity$ExecutorRunnable#run:306]B: end
11-28 00:00:10.634: [ExecutorActivity$ExecutorRunnable#run:306]C: end
11-28 00:00:10.654: [ExecutorActivity$ExecutorRunnable#run:306]D: end

Executors.newScheduledThreadPool(int);

public void newScheduledThreadPoolTest(){
    LogUtils.d();
    ScheduledExecutorService executorService = Executors.newScheduledThreadPool(2);
    executorService.schedule(new ExecutorRunnable("A", 1), 1, TimeUnit.SECONDS);
    executorService.schedule(new ExecutorRunnable("B", 1), 1, TimeUnit.SECONDS);
    executorService.schedule(new ExecutorRunnable("C", 1), 1, TimeUnit.SECONDS);
    executorService.schedule(new ExecutorRunnable("D", 1), 1, TimeUnit.SECONDS);
}
引数のスレッド数で実行します。今回は2スレッドで実行します。
実行タイミングを設定できますが同じタイミングを設定した場合は実行順が前後することがあります。
というか実行タイミング依存なので実行順は追加順に依存しないということだと思います。
設定のスレッド数に達したら待たされていることがわかります。
11-28 00:04:23.701: [ExecutorActivity#newScheduledThreadPoolTest:156]
11-28 00:04:24.712: [ExecutorActivity$ExecutorRunnable#run:299]A: ThreadId: 27833
11-28 00:04:24.722: [ExecutorActivity$ExecutorRunnable#run:299]B: ThreadId: 27834
11-28 00:04:24.722: [ExecutorActivity$ExecutorRunnable#run:300]A: start
11-28 00:04:24.722: [ExecutorActivity$ExecutorRunnable#run:300]B: start
11-28 00:04:25.733: [ExecutorActivity$ExecutorRunnable#run:306]B: end
11-28 00:04:25.733: [ExecutorActivity$ExecutorRunnable#run:306]A: end
11-28 00:04:25.743: [ExecutorActivity$ExecutorRunnable#run:299]D: ThreadId: 27834
11-28 00:04:25.753: [ExecutorActivity$ExecutorRunnable#run:299]C: ThreadId: 27833
11-28 00:04:25.753: [ExecutorActivity$ExecutorRunnable#run:300]C: start
11-28 00:04:25.753: [ExecutorActivity$ExecutorRunnable#run:300]D: start
11-28 00:04:26.754: [ExecutorActivity$ExecutorRunnable#run:306]D: end
11-28 00:04:26.754: [ExecutorActivity$ExecutorRunnable#run:306]C: end

Executors.newSingleThreadScheduledExecutor(); で ScheduledExecutorService#schedule(Runnable, long, TimeUnit) の実行タイミングが被らないパターン

public void newSingleThreadScheduledExecutorTest(){
    LogUtils.d();
    ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor();
    executorService.schedule(new ExecutorRunnable("A", 1), 8, TimeUnit.SECONDS);
    executorService.schedule(new ExecutorRunnable("B", 1), 4, TimeUnit.SECONDS);
    executorService.schedule(new ExecutorRunnable("C", 1), 0, TimeUnit.SECONDS);
    executorService.schedule(new ExecutorRunnable("D", 1), 12, TimeUnit.SECONDS);
}
SingleThread なのでタスクをいくつ追加しても一つのスレッドで実行します。
設定した実行タイミングの順で実行されていることがわかるようにしました。
11-28 00:10:40.248: [ExecutorActivity#newSingleThreadScheduledExecutorTest:172]
11-28 00:10:40.278: [ExecutorActivity$ExecutorRunnable#run:299]C: ThreadId: 27847
11-28 00:10:40.278: [ExecutorActivity$ExecutorRunnable#run:300]C: start
11-28 00:10:41.289: [ExecutorActivity$ExecutorRunnable#run:306]C: end
11-28 00:10:44.262: [ExecutorActivity$ExecutorRunnable#run:299]B: ThreadId: 27847
11-28 00:10:44.262: [ExecutorActivity$ExecutorRunnable#run:300]B: start
11-28 00:10:45.273: [ExecutorActivity$ExecutorRunnable#run:306]B: end
11-28 00:10:48.256: [ExecutorActivity$ExecutorRunnable#run:299]A: ThreadId: 27847
11-28 00:10:48.266: [ExecutorActivity$ExecutorRunnable#run:300]A: start
11-28 00:10:49.267: [ExecutorActivity$ExecutorRunnable#run:306]A: end
11-28 00:10:52.260: [ExecutorActivity$ExecutorRunnable#run:299]D: ThreadId: 27847
11-28 00:10:52.270: [ExecutorActivity$ExecutorRunnable#run:300]D: start
11-28 00:10:53.271: [ExecutorActivity$ExecutorRunnable#run:306]D: end

Executors.newSingleThreadScheduledExecutor(); で ScheduledExecutorService#schedule(Runnable, long, TimeUnit) の実行タイミングが被るパターン

public void newSingleThreadScheduledExecutorDuringExecutionTest(){
    LogUtils.d();
    ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor();
    executorService.schedule(new ExecutorRunnable("A", 1), 1, TimeUnit.SECONDS);
    executorService.schedule(new ExecutorRunnable("B", 1), 1, TimeUnit.SECONDS);
    executorService.schedule(new ExecutorRunnable("C", 1), 1, TimeUnit.SECONDS);
    executorService.schedule(new ExecutorRunnable("D", 1), 1, TimeUnit.SECONDS);
}
SingleThread なのでタスクをいくつ追加しても一つのスレッドで実行します。
実行タイミングがかぶったら待たされます。
11-28 00:14:52.637: [ExecutorActivity#newSingleThreadScheduledExecutorDuringExecutionTest:188]
11-28 00:14:53.648: [ExecutorActivity$ExecutorRunnable#run:299]A: ThreadId: 27848
11-28 00:14:53.648: [ExecutorActivity$ExecutorRunnable#run:300]A: start
11-28 00:14:54.659: [ExecutorActivity$ExecutorRunnable#run:306]A: end
11-28 00:14:54.669: [ExecutorActivity$ExecutorRunnable#run:299]B: ThreadId: 27848
11-28 00:14:54.669: [ExecutorActivity$ExecutorRunnable#run:300]B: start
11-28 00:14:55.680: [ExecutorActivity$ExecutorRunnable#run:306]B: end
11-28 00:14:55.690: [ExecutorActivity$ExecutorRunnable#run:299]C: ThreadId: 27848
11-28 00:14:55.690: [ExecutorActivity$ExecutorRunnable#run:300]C: start
11-28 00:14:56.701: [ExecutorActivity$ExecutorRunnable#run:306]C: end
11-28 00:14:56.701: [ExecutorActivity$ExecutorRunnable#run:299]D: ThreadId: 27848
11-28 00:14:56.711: [ExecutorActivity$ExecutorRunnable#run:300]D: start
11-28 00:14:57.712: [ExecutorActivity$ExecutorRunnable#run:306]D: end

Executors.newSingleThreadScheduledExecutor(); で ScheduledExecutorService#scheduleAtFixedRate(Runnable, long, long, TimeUnit) の実行タイミングを過ぎないパターン

public void newSingleThreadScheduledExecutorAtFixedRateTest(){
    LogUtils.d();
    ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor();
    executorService.scheduleAtFixedRate(new ExecutorRunnable("A", 1), 1, 2, TimeUnit.SECONDS);
    shutdown(executorService); // 8秒後に終了します
}
一つのスレッドで実行します。実行タイミングを設定できます。繰り返し実行でき、実行間隔を設定できます。
実行間隔は実行開始から次の実行開始までの時間です。
実行間隔 が 2 秒で、実行 1 秒なので終了してから 1 秒で実行されます。
11-28 00:19:44.952: [ExecutorActivity#newSingleThreadScheduledExecutorAtFixedRateTest:205]
11-28 00:19:45.963: [ExecutorActivity$ExecutorRunnable#run:299]A: ThreadId: 27850
11-28 00:19:45.963: [ExecutorActivity$ExecutorRunnable#run:300]A: start
11-28 00:19:46.974: [ExecutorActivity$ExecutorRunnable#run:306]A: end
11-28 00:19:47.965: [ExecutorActivity$ExecutorRunnable#run:299]A: ThreadId: 27850
11-28 00:19:47.965: [ExecutorActivity$ExecutorRunnable#run:300]A: start
11-28 00:19:48.966: [ExecutorActivity$ExecutorRunnable#run:306]A: end
11-28 00:19:49.967: [ExecutorActivity$ExecutorRunnable#run:299]A: ThreadId: 27850
11-28 00:19:49.967: [ExecutorActivity$ExecutorRunnable#run:300]A: start
11-28 00:19:50.968: [ExecutorActivity$ExecutorRunnable#run:306]A: end
11-28 00:19:51.969: [ExecutorActivity$ExecutorRunnable#run:299]A: ThreadId: 27850
11-28 00:19:51.969: [ExecutorActivity$ExecutorRunnable#run:300]A: start
11-28 00:19:52.970: [ExecutorActivity$1#run:273]shutdown
11-28 00:19:52.980: [ExecutorActivity$ExecutorRunnable#run:306]A: end

Executors.newSingleThreadScheduledExecutor(); で ScheduledExecutorService#scheduleAtFixedRate(Runnable, long, long, TimeUnit) の実行タイミングを過ぎるパターン

public void newSingleThreadScheduledExecutorAtFixedRateDuringExecutionTest(){
    LogUtils.d();
    ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor();
    executorService.scheduleAtFixedRate(new ExecutorRunnable("A", 3), 1, 2, TimeUnit.SECONDS);
    shutdown(executorService); // 8秒後に終了します
}
一つのスレッドで実行します。実行タイミングを設定できます。繰り返し実行でき、実行間隔を設定できます。
実行間隔は実行開始から次の実行開始までの時間です。
実行に 3 秒かかるのに実行間隔を 2 秒にした場合、次の実行開始タイミングは過ぎているのですぐに実行します。
11-28 00:23:59.431: [ExecutorActivity#newSingleThreadScheduledExecutorAtFixedRateDuringExecutionTest:221]
11-28 00:24:00.442: [ExecutorActivity$ExecutorRunnable#run:299]A: ThreadId: 27851
11-28 00:24:00.442: [ExecutorActivity$ExecutorRunnable#run:300]A: start
11-28 00:24:03.444: [ExecutorActivity$ExecutorRunnable#run:306]A: end
11-28 00:24:03.454: [ExecutorActivity$ExecutorRunnable#run:299]A: ThreadId: 27851
11-28 00:24:03.454: [ExecutorActivity$ExecutorRunnable#run:300]A: start
11-28 00:24:06.467: [ExecutorActivity$ExecutorRunnable#run:306]A: end
11-28 00:24:06.477: [ExecutorActivity$ExecutorRunnable#run:299]A: ThreadId: 27851
11-28 00:24:06.477: [ExecutorActivity$ExecutorRunnable#run:300]A: start
11-28 00:24:07.438: [ExecutorActivity$1#run:273]shutdown
11-28 00:24:08.449: [ExecutorActivity$1#run:276]shutdownNow
11-28 00:24:08.499: [ExecutorActivity$ExecutorRunnable#run:306]A: end

Executors.newSingleThreadScheduledExecutor(); で ScheduledExecutorService#scheduleWithFixedDelay(Runnable, long, long, TimeUnit) の実行タイミングを被らないパターン

public void newSingleThreadScheduledExecutorWithFixedDelayTest(){
    LogUtils.d();
    ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor();
    executorService.scheduleWithFixedDelay(new ExecutorRunnable("A", 1), 1, 2, TimeUnit.SECONDS);
    shutdown(executorService); // 8秒後に終了します
}
一つのスレッドで実行します。実行タイミングを設定できます。繰り返し実行でき、実行間隔を設定できます。
実行間隔は実行終了から次の実行開始までの時間です。
1 秒のタスクを実行終了してから 2 秒後に次の実行が開始されていることがわかります。
11-28 00:31:25.436: [ExecutorActivity#newSingleThreadScheduledExecutorWithFixedDelayTest:236]
11-28 00:31:26.447: [ExecutorActivity$ExecutorRunnable#run:299]A: ThreadId: 27852
11-28 00:31:26.447: [ExecutorActivity$ExecutorRunnable#run:300]A: start
11-28 00:31:27.458: [ExecutorActivity$ExecutorRunnable#run:306]A: end
11-28 00:31:29.470: [ExecutorActivity$ExecutorRunnable#run:299]A: ThreadId: 27852
11-28 00:31:29.470: [ExecutorActivity$ExecutorRunnable#run:300]A: start
11-28 00:31:30.481: [ExecutorActivity$ExecutorRunnable#run:306]A: end
11-28 00:31:32.483: [ExecutorActivity$ExecutorRunnable#run:299]A: ThreadId: 27852
11-28 00:31:32.483: [ExecutorActivity$ExecutorRunnable#run:300]A: start
11-28 00:31:33.443: [ExecutorActivity$1#run:273]shutdown
11-28 00:31:33.494: [ExecutorActivity$ExecutorRunnable#run:306]A: end

Executors.newSingleThreadScheduledExecutor(); で ScheduledExecutorService#scheduleWithFixedDelay(Runnable, long, long, TimeUnit) の実行タイミングが被るパターン

public void newSingleThreadScheduledExecutorWithFixedDelayDuringExecutionTest(){
    LogUtils.d();
    ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor();
    executorService.scheduleWithFixedDelay(new ExecutorRunnable("A", 3), 1, 2, TimeUnit.SECONDS);
    shutdown(executorService);
}
一つのスレッドで実行します。実行タイミングを設定できます。繰り返し実行でき、実行間隔を設定できます。
実行間隔は実行終了から次の実行開始までの時間なので実行タイミングが被ることがないです。
3 秒のタスクを実行終了してから 2 秒後に次の実行が開始されていることがわかります。
11-28 00:34:05.822: [ExecutorActivity#newSingleThreadScheduledExecutorWithFixedDelayDuringExecutionTest:252]
11-28 00:34:06.833: [ExecutorActivity$ExecutorRunnable#run:299]A: ThreadId: 27853
11-28 00:34:06.833: [ExecutorActivity$ExecutorRunnable#run:300]A: start
11-28 00:34:09.846: [ExecutorActivity$ExecutorRunnable#run:306]A: end
11-28 00:34:11.848: [ExecutorActivity$ExecutorRunnable#run:299]A: ThreadId: 27853
11-28 00:34:11.858: [ExecutorActivity$ExecutorRunnable#run:300]A: start
11-28 00:34:13.840: [ExecutorActivity$1#run:273]shutdown
11-28 00:34:14.851: [ExecutorActivity$1#run:276]shutdownNow
11-28 00:34:14.901: [ExecutorActivity$ExecutorRunnable#run:306]A: end

おまけ

上の ScheduledExecutorService#scheduleAtFixedRate と
ScheduledExecutorService#scheduleWithFixedDelay ではタスクを一つしか与えていないが、
これをそれぞれもう一つ initialDelay を1秒ずらして追加すると
どちらも実行終了を待たされて次の実行タイミングが来てすぐに実行するようになってしまいます。

Executors.newScheduledThreadPool(int); ではどうなる?

上の ScheduledExecutorService#scheduleAtFixedRate と
ScheduledExecutorService#scheduleWithFixedDelay で ExecutorService の生成を
Executors.newScheduledThreadPool(int); で行った場合は、
繰り返される同一のタスクは一つのスレッドで固定で、タスクの追加ごとにスレッドが分かれるようになります。
実行タイミングが過ぎたり、被ったりすると実行タイミングが来てすぐに実行するようになってしまいます。
ちゃんと等間隔で実行したかったら一つの ExecutorService に一つのタスクが良いかと思います。

ExecutorService の終了の仕方

try{
    LogUtils.d("shutdown");
    executorService.shutdown();
    if(!executorService.awaitTermination(1, TimeUnit.SECONDS)){
        LogUtils.d("shutdownNow");
        executorService.shutdownNow();
    }
}catch(InterruptedException e){
    e.printStackTrace();
    LogUtils.d("shutdownNow: " + e.getMessage());
    executorService.shutdownNow();
}
ExecutorService#shutdown で終了命令を出して新規タスクの追加の禁止して、
実行中のタスクが終了せずに ExecutorService#awaitTermination でタイムアウト時間を過ぎたら
ExecutorService#shutdownNow で実行中のタスクをキャンセルするといった流れです。
shutdownNow が呼ばれると ExecutorRunnable の sleep で InterruptedException が投げられますが
ExecutorService#shutdownNow が直接的にタスクの実行を強制終了するわけではないようです。

まとめ

長くなりましたが ExecutorService の使い方をまとめました。
ExecutorService はかなり強力なマルチスレッドの実行キューの便利クラスなのでぜひ使いこなしたいですね!
2013/11/28

[HomeBrew]gitのバージョンアップをするためにやったこと

Git の diff を美しく表示するために必要なたった 1 つの設定 #gitという記事を見て、
やってみたくなったので設定してみたら diff-highlight が見つからないと言われました。

git のバージョンも古かったので git 環境のアップデートを行おうと思いました。
git は HomeBrew で入れていたので、まずは HomeBrew のアップデートを行います。

brew doctor を実行すると
You may wish to `brew unlink` these brews:

    openssl
のように言われたのですがいらないのがいくつかあったので
brew remove openssl などのように色々削除しました。

もう一度 brew doctor を実行すると
You should `brew install` the missing dependencies:

    brew install readline
のように言われたので brew install readline を実行しました。

更に再び brew doctor を実行すると
Error: Homebrew doesn't know what compiler versions ship with your version of
Xcode. Please file an issue with the output of `brew --config`:
  https://github.com/mxcl/homebrew/issues

Thanks!
のように言われていたので Xcode と Command Line Tools をアップデートしました。

ダメ押しで brew doctor を実行しましたが状況は変わらず絶望しつつも
brew update を実行すると成功したので
brew upgrade で git を含め全てバージョンアップしました。
git version 1.8.4.3

ところで目的の diff-highlight はというとパスの通ってないところにいたので
以下のコマンドでシンボリックリンクを作成して実行できるようにしました。
ln -s /usr/local/opt/git/share/git-core/contrib/diff-highlight/diff-highlight /usr/local/bin
~/.gitconfig に以下を追加しました。
[pager]
 log = diff-highlight | less
 show = diff-highlight | less
 diff = diff-highlight | less
文字列単位の差分がある場合は差分にハイライトされたように背景がつきます。
2013/11/24

[Android]Property AnimationでViewの背景色を変更する

View の背景色をアニメーションしながら変更したい場合があって
調べてみたら Property Animation | Android Developers というのがあった。
そしてモロそのままの質問が Stack Overflow にあった。
Animate change of view background color in Android - Stack Overflow

背景色が黒から透明になって消えていくアニメーションをするサンプル。
さり気なく API Level 11 から。2.x 系をサポートするなら NineOldAndroids を使おう。
final Integer black = getResources().getColor(R.color.black);
final Integer transparent = getResources().getColor(R.color.transparent);
ValueAnimator colorAnimation = ValueAnimator.ofObject(new ArgbEvaluator(), black, transparent);
colorAnimation.addUpdateListener(new AnimatorUpdateListener(){
    @Override
    public void onAnimationUpdate(ValueAnimator animator){
        mView.setBackgroundColor((Integer)animator.getAnimatedValue());
    }
});
colorAnimation.setDuration(1000);
colorAnimation.setInterpolator(new LinearInterpolator());
colorAnimation.start();

指定するアニメーション補完クラスは以下がわかりやすい。
[Android] アニメーション補間クラスをグラフ化してみました - adakoda
式さえわかれば移植できそうだなー。これ↓っぽい?
Equations.as - tweener - A class for creating tweens in actionscript 2 and 3 - because there's infinity between 0 and 1. - Google Project Hosting
あとはこの手のアニメーション系の Web 版があったからそこから持ってきて実装というのも良さそう。
2013/11/23

[Android]機種依存でMediaRecorderで動画撮影に失敗する

来たぜ!Android の機種依存の闇に!

Android の機種依存の特徴

  • まともに組んでも何故か動かない
  • 動かない原因がわからない
  • 機種依存の実機がないと何が起きてるのかわからない
Xperia 系端末は適当に組んでも良い感じに動いてくれるけど
他の機種で動かないっていうのは、まぁあることだ。
まとも組めばいいのだけど Android API で対処できる範囲でまともに組んでも動かないやつがいて、
そういうのが本当の機種依存なわけだ。
そういうやつはたいていエラーログにもろくな情報がない。

MediaRecorder は機種依存でなくてもろくなエラーログを出力しなくて、
start failed とか stop failed とだけ言ってくるので失敗してるのはわかってるよ!と言い返したくなる。
MediaRecorder なんかは Java 層がただのラッパーだから仕方ないのかもしれないが直せなくて辛い。
Android API を眺めて設定項目を変えて色々試してみるしかなくて機種依存の実機がないと修正は困難になる。

愚痴はココらへんにして、Samsung Galaxy S2 SC-02C と HUAWEI M886 で起きたバグを確認したい。

Samsung Galaxy S2 SC-02C 動画撮影が開始できない機種依存バグ

まずはこいつを見てくれ。見事に MediaRecorder#start() で RuntimeException で落ちてるだろう?
しかもメッセージは start failed. と来た。Caused by もないとはいよいよもって死ぬがよい。
{
  "Exception": {
    "name": "java.lang.RuntimeException",
    "message": "start failed.",
    "ExceptionStacktrace": [
      "at [MediaRecorder#start:-2]",
      "at [aa#b:-1]",
      "at [VideoCameraActivity#f:-1]",
      "at [e#onClick:-1]",
      "at [View#performClick:3620]",
      "at [View$PerformClick#run:14292]",
      "at [Handler#handleCallback:605]",
      "at [Handler#dispatchMessage:92]",
      "at [Looper#loop:137]",
      "at [ActivityThread#main:4507]",
      "at [Method#invokeNative:-2]",
      "at [Method#invoke:511]",
      "at [ZygoteInit$MethodAndArgsCaller#run:980]",
      "at [ZygoteInit#main:747]",
      "at [NativeStart#main:-2]"
    ]
  }
}
この JSON は独自クラッシュレポートによって手に入れたものだ。こいつがあると色々便利だ。
[Android]独自クラッシュレポート送信機能を実装する レポート作成編 | DevAchieve
カメラパラメータをこいつで送信してわかったことがある。
DisplaySize(800x480)、PreviewSize(800x480) に対して VideoSize(720x480) で
CamcorderProfile は "videoFrameWidth: ": 1920, "videoFrameHeight: ": 1080 という設定になっていた。
こいつが怪しい。
試しに Xperia A の値を確認すると、
DispalySize(1184x720)、PreviewSize(1280x720) に対して VideoSize(1920x1080) で
CamcorderProfile は "videoFrameWidth: ": 1920, "videoFrameHeight: ": 1080 という設定になっていた。
VideoSize と CamcorderProfile の videoFrame(Width|Height) が一致していない。

とここまで書いて気がついたのだが、 VideoSize は MediaRecorder#setVideoSize で設定している。
MediaRecorder#setVideoSize(int width, int height) | Android Developers
071 プレビューが見えるように撮影ができるように修正 refs #6 #11 · c2b99d2 · gabu/gabubon2
ちょうど↑こんなかんじで PreviewSize を指定していたのだけど
VideoSize(720x480) って PreviewSize(800x480) の近似サイズなような気がする。
Galaxy S2 のサポートしているビデオサイズのリストは以下のとおり。
"video-size-values": "720x480,176x144,320x240,352x288,640x480,1280x720,1920x1080"

この線が濃厚なので調べてみると Stack Overflow にそれっぽい回答があった。
video - How to use getSupportedVideoSizes() on Android? - Stack Overflow
CamcorderProfile camcorderProfile = CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_HIGH);
mMediaRecorder.setVideoSize(camcorderProfile.videoFrameWidth, camcorderProfile.videoFrameHeight);
上記のように CamcorderProfile の videoFrame(Width|Height) を使えば確実みたい。
このへんの指定の情報が少なすぎて泣ける。
過去にきっと泣かされてきたエンジニアたち↓
ビデオアプリが難なワケ: hikariのブログ
071 動画撮影が行えない · Issue #6 · gabu/gabubon2
希望を信じた Androider を、わたしは泣かせたくない。ということで多分解決。
機種依存バグじゃなくて Android API の使い方を間違えた僕が悪い。

HUAWEI M886 動画撮影が開始できない機種依存バグ

さてもう一つ。こっちはかなり高い確率で本当に機種依存バグ。だって中華端末だもん。
以下はレポートの設定値の一部抜粋。
{
    "preview-size": "1280x720",
    "preview-size-values": "1280x720,800x480,768x432,720x480,640x480,576x432,480x320,384x288,352x288,320x240,240x160,176x144",
    "record-size": "",
    "videoFrameWidth: ": 1280,
    "videoFrameHeight: ": 720
}
DisplaySize(854x480), PreviewSize(1280x720) で VideoSize がなくて
なぜか "record-size" で、しかも空という状況。
Android の Camera.Parameters を確認すると以下の定義は見つかるが、
"record-size" という設定値のキーはない。
private static final String KEY_VIDEO_SIZE = "video-size";
レポートくれたユーザーには思わず俺が直せることを祈ってくれって返した!
さすがに設定キーが違うと設定メソッド使えないから上の修正で奇跡的に直ることを祈ってくれ!

このように動画撮影には色々ノウハウが必要だから気をつけるんだぞ!
DevCamera はそのへん頑張ってるから応援よろしくお願いします。
2013/11/18

[Android]加速度センサーのノイズ除去とSensorFragment

直線加速度センサーを使うことがあったので SensorFragment を作ってみました。
参考
Sensors Overview | Android Developers
SensorEvent | Android Developers
ノイズ除去にはローパスフィルタというものを使いました。
加速度センサの値のノイズ除去 - Javaな日々
加速度センサ と ローパスフィルタ (波形) | アンドロイドな日々
加速度センサ と ローパスフィルタ (周波数特性) | アンドロイドな日々
public class SensorFragment extends Fragment implements SensorEventListener {

    public static final String  TAG              = SensorFragment.class.getSimpleName();
    private static final String KEY_SENSOR_TYPES = "KEY_SENSOR_TYPES";
    private SensorCallback      mCallback;
    private SensorManager       mSensorManager;
    private float               x, y, z = 0;

    public static interface SensorCallbackProvider {
        public SensorCallback getSensorCallback();
    }

    public static interface SensorCallback {

        public void onLinearAccelerationSensorChanged(float x, float y, float z);

    }

    public static SensorFragment newInstance(ArrayList<Integer> sensorTypes){
        SensorFragment fragment = new SensorFragment();
        Bundle args = new Bundle();
        args.putIntegerArrayList(KEY_SENSOR_TYPES, sensorTypes);
        fragment.setArguments(args);
        return fragment;
    }

    @Override
    public void onAttach(Activity activity){
        super.onAttach(activity);

        if(activity instanceof SensorCallbackProvider == false){
            throw new ClassCastException(activity.getLocalClassName() + " must implements " + SensorCallbackProvider.class.getSimpleName());
        }
        SensorCallbackProvider picker = (SensorCallbackProvider)activity;
        mCallback = picker.getSensorCallback();
        mSensorManager = (SensorManager)activity.getSystemService(Context.SENSOR_SERVICE);
        registerListener();
    }

    @Override
    public void onDetach(){
        super.onDetach();
        unregisterListener();
    }

    private void registerListener(){
        if(mSensorManager != null){
            ArrayList<Integer> sensorTypes = getArguments().getIntegerArrayList(KEY_SENSOR_TYPES);
            for(int sensorType : sensorTypes){
                Sensor sensor = mSensorManager.getDefaultSensor(sensorType);
                mSensorManager.registerListener(this, sensor, SensorManager.SENSOR_DELAY_NORMAL);
            }
        }
    }

    public void registerListener(Integer sensorType){
        ArrayList<Integer> sensorTypes = getArguments().getIntegerArrayList(KEY_SENSOR_TYPES);
        sensorTypes.add(sensorType);
        getArguments().putIntegerArrayList(KEY_SENSOR_TYPES, sensorTypes);
        if(mSensorManager != null){
            Sensor sensor = mSensorManager.getDefaultSensor(sensorType);
            mSensorManager.registerListener(this, sensor, SensorManager.SENSOR_DELAY_NORMAL);
        }
    }

    private void unregisterListener(){
        if(mSensorManager != null){
            ArrayList<Integer> sensorTypes = getArguments().getIntegerArrayList(KEY_SENSOR_TYPES);
            for(int sensorType : sensorTypes){
                Sensor sensor = mSensorManager.getDefaultSensor(sensorType);
                mSensorManager.unregisterListener(this, sensor);
            }
        }
    }

    public void unregisterListener(Integer sensorType){
        ArrayList<Integer> sensorTypes = getArguments().getIntegerArrayList(KEY_SENSOR_TYPES);
        sensorTypes.remove(sensorType);
        getArguments().putIntegerArrayList(KEY_SENSOR_TYPES, sensorTypes);
        if(mSensorManager != null){
            Sensor sensor = mSensorManager.getDefaultSensor(sensorType);
            mSensorManager.unregisterListener(this, sensor);
        }
    }

    @Override
    public void onSensorChanged(SensorEvent event){
        if(event.sensor.getType() == Sensor.TYPE_LINEAR_ACCELERATION){
            x = (float)(x * 0.9 + event.values[0] * 0.1);
            y = (float)(y * 0.9 + event.values[1] * 0.1);
            z = (float)(z * 0.9 + event.values[2] * 0.1);
            mCallback.onLinearAccelerationSensorChanged(x, y, z);
        }
    }

    @Override
    public void onAccuracyChanged(Sensor sensor, int accuracy){
        // do nothing
    }

}
まだ他のセンサー系を使っていないのでコレで大丈夫なのか気になります。
2013/11/17

[Android]R.idの追加方法

通常レイアウトで R.id を追加するのだけど
動的に生成した View に id を設定したいときは
R.id も動的に生成しなければいけません。
方法は以下の2つです。

リソースから生成する

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <item name="INTERNAL_EMPTY_ID" type="id"/>
    <item name="INTERNAL_PROGRESS_CONTAINER_ID" type="id"/>
    <item name="INTERNAL_LIST_CONTAINER_ID" type="id"/>
</resources>

プログラムから生成する

int viewId = View.generateViewId();
ただし、 API Level 17 (Android4.2) からです。

コレで、以下の ExpandableListFragment の内部の id を固定値から動的に生成した id に変更しました。
ExpandableListFragment - OrangeSignal ブログ
動的に生成しないとライブラリプロジェクトとかで id 衝突してバグる可能性ありますからね。
2013/11/15

[Java]マルチスレッドでの排他処理を行うsynchronizedを理解する

今までマルチスレッドプログラミングというものをしてこなかったので
排他処理とか synchronized とかなにそれオイシイの状態だったので
理解するために簡単なサンプルを Android で作ってみました。

参考
Javaスレッドメモ(Hishidama's Java thread Memo)

サンプルコードは以下にあります。
SynchronizedActivity.java | wada811/AndroidLibrary@wada811
AndroidLibrary ってなってるけどコレは Android 関係ないです。

メソッド修飾の synchronized

クラスメソッド

/**
 * クラスメソッドを synchronized にした場合のテスト実行メソッド
 */
public void syncClassMethodExecute(){
    PreferenceUtils.putString(this, SyncClassMethodExecuter.KEY, null);
    new Thread(new SyncClassMethodExecutorRunnable("A")).start();
    new Thread(new SyncClassMethodExecutorRunnable("B")).start();
    new Thread(new SyncClassMethodExecutorRunnable("C")).start();
}

public class SyncClassMethodExecutorRunnable implements Runnable {
    String threadName;

    public SyncClassMethodExecutorRunnable(String threadName) {
        this.threadName = threadName;
    }

    @Override
    public void run(){
        SyncClassMethodExecuter.execute(self, threadName);
    }
}

/**
 * クラスメソッドを synchronized にした場合のテストクラス
 */
public static class SyncClassMethodExecuter {

    public static final String KEY = SyncClassMethodExecuter.class.getSimpleName();

    public static final synchronized void execute(Context context, String threadName){
        String lastThreadName = PreferenceUtils.getString(context, KEY, null);
        if(lastThreadName != null){
            LogUtils.d(threadName + ": do nothing. " + lastThreadName + " is executed.");
        }else{
            try{
                TimeUnit.SECONDS.sleep(1);
            }catch(InterruptedException e){
                e.printStackTrace();
            }
            LogUtils.d(threadName + ": execute");
            PreferenceUtils.putString(context, KEY, threadName);
            try{
                TimeUnit.SECONDS.sleep(1);
            }catch(InterruptedException e){
                e.printStackTrace();
            }
        }
    }
}
synchronized にしてるから1秒待って A が実行されたら1秒後に B, C と実行されていて
ブロックされているのが実行ログからわかります。
11-14 22:57:39.801: [SynchronizedActivity#syncClassMethodExecute:115]
11-14 22:57:40.872: [SynchronizedActivity$SyncClassMethodExecutor#execute:152]A: execute
11-14 22:57:41.933: [SynchronizedActivity$SyncClassMethodExecutor#execute:145]B: do nothing. A is executed.
11-14 22:57:41.943: [SynchronizedActivity$SyncClassMethodExecutor#execute:145]C: do nothing. A is executed.
synchronized をとれば A, B, C 全部が execute を通ることも確認できます。

インスタンスメソッド

/**
 * インスタンスメソッドを synchronized にした場合のテスト実行メソッド
 */
public void syncThisInstanceMethodExecute(){
    LogUtils.d();
    SyncThisInstanceMethodExecutor executor = new SyncThisInstanceMethodExecutor();
    new Thread(new SyncThisInstanceMethodExecutorRunnable(executor, "A")).start();
    new Thread(new SyncThisInstanceMethodExecutorRunnable(executor, "B")).start();
    new Thread(new SyncThisInstanceMethodExecutorRunnable(executor, "C")).start();
}

public class SyncThisInstanceMethodExecutorRunnable implements Runnable {
    private SyncThisInstanceMethodExecutor mExecutor;
    private String                         mThreadNmae;

    public SyncThisInstanceMethodExecutorRunnable(SyncThisInstanceMethodExecutor executor, String threadNmae) {
        mExecutor = executor;
        mThreadNmae = threadNmae;
    }

    @Override
    public void run(){
        mExecutor.execute(mThreadNmae);
    }

}

/**
 * インスタンスメソッドを synchronized にした場合のテストクラス
 */
public class SyncThisInstanceMethodExecutor {
    private int mCount = 0;

    public synchronized void execute(String threadName){
        if(mCount == N){
            LogUtils.d(threadName + ": do nothing");
        }else{
            for(int i = 0; i < N; i++){
                LogUtils.d(threadName + ": " + ++mCount);
            }
            try{
                TimeUnit.SECONDS.sleep(1);
            }catch(InterruptedException e){
                e.printStackTrace();
            }
        }
    }
}
インスタンスメソッドの synchronized はメソッドボディを synchronized(this){ 〜 } で囲ったのと
同じ動きをするらしいので SyncThisInstanceMethodExecutor という名前にしました。
参考: Javaスレッドメモ(Hishidama's Java thread Memo)
コレもバッチリ A の実行が終わるのを待たされているのが実行ログから分かります。
[SynchronizedActivity#syncThisInstanceMethodExecute:219]
[SynchronizedActivity$SyncThisInstanceMethodExecutor#execute:253]A: 1
[SynchronizedActivity$SyncThisInstanceMethodExecutor#execute:253]A: 2
[SynchronizedActivity$SyncThisInstanceMethodExecutor#execute:253]A: 3
[SynchronizedActivity$SyncThisInstanceMethodExecutor#execute:253]A: 4
[SynchronizedActivity$SyncThisInstanceMethodExecutor#execute:253]A: 5
[SynchronizedActivity$SyncThisInstanceMethodExecutor#execute:253]A: 6
[SynchronizedActivity$SyncThisInstanceMethodExecutor#execute:253]A: 7
[SynchronizedActivity$SyncThisInstanceMethodExecutor#execute:253]A: 8
[SynchronizedActivity$SyncThisInstanceMethodExecutor#execute:253]A: 9
[SynchronizedActivity$SyncThisInstanceMethodExecutor#execute:253]A: 10
[SynchronizedActivity$SyncThisInstanceMethodExecutor#execute:250]B: do nothing
[SynchronizedActivity$SyncThisInstanceMethodExecutor#execute:250]C: do nothing

ロックオブジェクトを synchronized

/**
 * インスタンスメソッドでロックオブジェクトを synchronized した場合のテスト実行メソッド
 */
public void syncLockInstanceMethodExecute(){
    LogUtils.d();
    SyncLockInstanceMethodExecutor executor = new SyncLockInstanceMethodExecutor();
    new Thread(new SyncLockInstanceMethodExecutorRunnable(executor, "A")).start();
    new Thread(new SyncLockInstanceMethodExecutorRunnable(executor, "B")).start();
    new Thread(new SyncLockInstanceMethodExecutorRunnable(executor, "C")).start();
}

public class SyncLockInstanceMethodExecutorRunnable implements Runnable {
    private SyncLockInstanceMethodExecutor mExecutor;
    private String                         mThreadNmae;

    public SyncLockInstanceMethodExecutorRunnable(SyncLockInstanceMethodExecutor executor, String threadNmae) {
        mExecutor = executor;
        mThreadNmae = threadNmae;
    }

    @Override
    public void run(){
        mExecutor.execute(mThreadNmae);
    }

}

/**
 * インスタンスメソッドでロックオブジェクトを synchronized した場合のテストクラス
 */
public class SyncLockInstanceMethodExecutor {
    private final Object lock   = new Object();
    private int          mCount = 0;

    public void execute(String threadName){
        synchronized(lock){
            if(mCount == N){
                LogUtils.d(threadName + ": do nothing");
            }else{
                for(int i = 0; i < N; i++){
                    LogUtils.d(threadName + ": " + ++mCount);
                }
                try{
                    TimeUnit.SECONDS.sleep(1);
                }catch(InterruptedException e){
                    e.printStackTrace();
                }
            }
        }
    }
}
SyncLockInstanceMethodExecutor は SyncThisInstanceMethodExecutor とほとんど同じだけど
メソッド修飾の synchronized じゃなくて synchronized 文になってオブジェクトをロックしています。
SyncThis と同じなので実行ログは省略します。
StackOverflow でこっちの方法を推奨している回答がありました。
synchronization - How to synchronize or lock upon variables in Java? - Stack Overflow
blog.pasberth.com: Javaのsynchronizedってなにをブロックしているの?のコメントでもあるように
synchronized(this) は同一インスタンスでしか排他処理ができないので
SyncLockInstanceMethodExecutor を複数インスタンス生成されたら排他制御できなくなります。
なので以下のようにグローバルなオブジェクトを synchronized すると別インスタンスでも排他制御できます。
public static final Object LOCK = new Object();

public void syncGrobalLockExecute(){
    LogUtils.d();
    new Thread(new SyncGrobalLockExecutorRunnable(new SyncGrobalLockExecutor(), "A")).start();
    new Thread(new SyncGrobalLockExecutorRunnable(new SyncGrobalLockExecutor(), "B")).start();
    new Thread(new SyncGrobalLockExecutorRunnable(new SyncGrobalLockExecutor(), "C")).start();
}

public class SyncGrobalLockExecutorRunnable implements Runnable {
    private SyncGrobalLockExecutor mExecutor;
    private String                 mThreadNmae;

    public SyncGrobalLockExecutorRunnable(SyncGrobalLockExecutor executor, String threadNmae) {
        mExecutor = executor;
        mThreadNmae = threadNmae;
    }

    @Override
    public void run(){
        mExecutor.execute1(mThreadNmae);
        mExecutor.execute2(mThreadNmae);
        mExecutor.execute3(mThreadNmae);
    }

}

public class SyncGrobalLockExecutor {
    private int mCount = 0;

    public void execute1(String threadName){
        synchronized(LOCK){
            for(int i = 0; i < N; i++){
                LogUtils.d(threadName + "[1]: " + ++mCount);
            }
        }
    }

    public void execute2(String threadName){
        synchronized(LOCK){
            for(int i = 0; i < N; i++){
                LogUtils.d(threadName + "[2]: " + ++mCount);
            }
        }
    }

    public void execute3(String threadName){
        synchronized(LOCK){
            for(int i = 0; i < N; i++){
                LogUtils.d(threadName + "[3]: " + ++mCount);
            }
        }
    }
}
同じ LOCK に対する排他制御なので execute1, execute2, execute3 同士も排他制御されます。
インスタンスごとに呼ぶメソッドを変えてあげればわかりやすいです。
SyncGrobalLockExecutor はサンプルに入ってないので適当に書き換えて試してみて下さい。

synchronized を使いこなせればプログラミングの幅が広がりそうですね。
長くなったのでこのへんで。
2013/11/14

[Android]FragmentからActivityへのイベントの通知方法

Fragment から Activity へのイベントの通知方法のベストな方法を紹介します。
@vvakameさんの CallbackPicker のことなんですが、
文書化されていないようなので記事を書いておきます。
CallbackPicker よりも CallbackProvider の方が
なんとなくしっくり来たので勝手に名前を変えました。

とりあえず以下の方法を理解していることが前提です。
Y.A.M の 雑記帳: Fragment から Activity にコールバックする方法
リンク先の内容を簡単に説明すると、
onAttach で activity がリスナーを実装しているかチェックしてコールバックメソッドを呼ぼう!ってことです。

今回の方法はその方法を1段深くしたような形になっています。
public class MainActivity extends FragmentActivity implements MainFragmnetCallbackProvider {

    @Override
    public void onCreate(Bundle savedInstanceState) {  
        super.onCreate(savedInstanceState);

        MainFragment fragment = MainFragment.newInstance();
        getFragmentManager().beginTransaction().add(android.R.id.content, fragment, MainFragment.TAG).commit();
    }

    @Override
    public MainFragmnetCallback getMainFragmnetCallback(){
        return new MainFragmnetCallback(){
            @Override
            public void onAttach(MainFragmnet mainFragmnet){

            }
        }
    }
}
public class MainFragment extends Fragment {

    public static final String   TAG = MainFragment.class.getSimpleName();
    private MainFragmentCallback mCallback;

    public static interface MainFragmentCallbackProvider {

        public MainFragmentCallback getMainCallback();

    }

    public static interface MainFragmentCallback {

        public void onAttach(MainFragment mainFragment);

    }

    public static MainFragment newInstance(){
        MainFragment fragment = new MainFragment();
        Bundle args = new Bundle();
        fragment.setArguments(args);
        return fragment;
    }

    @Override
    public void onAttach(Activity activity){
        super.onAttach(activity);

        if(activity instanceof MainFragmentCallbackProvider == false){
            throw new ClassCastException(activity.getLocalClassName() + " must implements " + MainFragmentCallbackProvider.class.getSimpleName());
        }
        MainFragmentCallbackProvider provider = (MainFragmentCallbackProvider)activity;
        mCallback = provider.getMainCallback();
        mCallback.onAttach(this);
    }

}
MainFragment に2つの interface を用意して Activity で MainFragmnetCallbackProvider を実装します。
MainFragmnetCallbackProvider は MainFragmentCallback を返す getMainCallback() のみを提供します。
getMainCallback() で MainFragmentCallback を 匿名インナークラスとして書いてあげれば
MainFragment に関するイベントはその匿名インナークラス内に集約されます(←重要!※)。
あとは MainFragment で MainFragmentCallback のメソッド宣言に好きなコールバックを書いてあげるだけ!

※ MainFragmnetCallbackProvider を使うことでコールバックメソッドのスコープが小さくなるから便利ですよ!
MainFragment のちょっとしたリスナーだけだったらいいかもしれないけど
MainActivity で 他にも Fragment を扱ったりするとコールバックメソッドが入り乱れて見難くなります。
7, 8個くらい Fragment を Activity にくっつけたときはコレじゃなかったら可読性低くて大変だったかも。

interface の名前的に自由にコールバックメソッドを追加しやすかったのも良いと思いました。
Fragment 同士の連携も Activity に一旦コールバックして
Activity から Fragment のメソッド実行すれば良いので
お互いの実装に関わりを持たずにコールバックを投げておけば良いというのも開発しやすいポイントです。

この MainFragment は Fragment クラスの拡張を作る際のテンプレにしておきたいので記事を書きました。

タグ(RSS)