読者です 読者をやめる 読者になる 読者になる

【Android】Activityの内容を動的に生成する

こう、Activityのレイアウトとしては結構違うんだけど、まぁ大体要約すると「onCreateでProgressDialogを表示し、その裏でDBかWebから何らかのリソースをとってきて整形してListViewに表示する」みたいなのって、よくあるパターンじゃないですか。

こんだけパターン化されてるのになんで一々Acitivityを作らなきゃいけないのかと馬鹿らしくなってきたので、クロージャで動的に生成することにしました。

Activityを動的に生成するためのクラスを作る

上記の処理を行うために必要なものをまとめたクラスを作成します。

  • setContentViewで指定するリソースID
  • AsyncTaskのPreExecuteで行う処理
  • AsyncTaskのBackgroundで行う処理
  • AsyncTaskのonPostExecuteで行う処理
  • AsyncTaskのexecuteに渡すパラメータ

とりあえずこんだけあればいいでしょう。必要なものは後で増やせばいいし。

出来上がったものがこちらです。

public class EditableParams<Return, Param> implements Serializable {

    private static final long serialVersionUID = -4002959801411818492L;

    private int resourceId;
    private V1<ProgressDialog> onPreExecute;
    private R1<List<Return>, Param> onBackground;
    private V2<List<Return>, Activity> onPostExecute;
    private Param argument;

    public EditableParams(int resourceId) {
        this.resourceId = resourceId;
    }

    public int getResourceId() {
        return this.resourceId;
    }
    public V1<ProgressDialog> getOnPreExecute() {
        return this.onPreExecute;
    }
    public void setOnPreExecute(V1<ProgressDialog> onPreExecute) {
        this.onPreExecute = onPreExecute;
    }
    public R1<List<Return>, Param> getOnBackground() {
        return this.onBackground;
    }
    public void setOnBackground(R1<List<Return>, Param> onBackground) {
        this.onBackground = onBackground;
    }
    public V2<List<Return>, Activity> getOnPostExecute() {
        return this.onPostExecute;
    }
    public void setOnPostExecute(V2<List<Return>, Activity> onPostExecute) {
        this.onPostExecute = onPostExecute;
    }
    public Param getArgument() {
        return this.argument;
    }
    public void setArgument(Param argument) {
        this.argument = argument;
    }
}

Activityを動的に生成する際に最も注意しなければならないのは、Viewの生成とfindViewByIdです。

この二つに関しては該当Activity内(呼び出し側)でやらなければならないので、クロージャを作成する側ではどうすることも出来ません。なので、基本的に呼び出し側でやってもらうことになります。

EditableParamsにAcitivityのメンバとgetter/setterを持たせ、呼び出し側でsetterを呼び、クロージャ内でgetterを呼べば可能にはなるんですが、ちょっとキモすぎなのでやめました。

普通はPostExecuteでfindViewByIdするはずなので、そこは呼び出し側にthisを渡してもらって行います。

動的に生成するActivityの雛形を作る

言葉で説明してもよくわかんないですね。こんな感じにしました。

public class EditableActivity<Return, Param> extends FragmentActivity {

    public static final String KEY = "params";

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

        Intent i = getIntent();
        String holderKey = i.getStringExtra(KEY);
        final EditableParams<Return, Param> params = (EditableParams<Return, Param>) EditableParamsHolder.get(holderKey);
        setContentView(params.getResourceId());

        final Activity here = this;
        final ProgressDialog prog = new ProgressDialog(this);

        new ReactiveAsyncTask<Param, Void, List<Return>>(params.getOnBackground())
            .setOnPreExecute(new V1<Void>() {
                @Override
                public void call(Void arg0) {
                    params.getOnPreExecute().call(prog);
                }
            })
            .setOnPostExecute(new V1<List<Return>>() {
                @Override
                public void call(List<Return> x) {
                    if(prog != null) prog.dismiss();
                    params.getOnPostExecute().call(x, here);
                }
            }).execute(params.getArgument());
    }

}

素直にIntentでEditableParamsを渡そうとしても無理です。一応Serializableを実装させたものの、どうあがいても無理でした。

なので、本来はやっちゃいけないことなんですが、EditableParamsHolderと言うクラスを作ってそっちから持ってくるようにします。

public final class EditableParamsHolder {

    private static HashMap<String, EditableParams<?, ?>> _map = new HashMap<String, EditableParams<?, ?>>();

    public static void put(String key, EditableParams<?, ?> value) {
        if(_map == null) _map = new HashMap<String, EditableParams<?, ?>>();
        _map.put(key, value);
    }

    public static EditableParams<?, ?> get(String key) {
        return _map.get(key);
    }

}

Intentで渡すのはこのEditableParamsHolderからEditableParamsを取得するキーのみです。

GCが走ると容赦なくこのstaticなHashMapも死ぬので、再利用は考えません。使う時は必ず毎回putを呼ぶようにします。

実際にEditableParamsを作成する

EditableParams<TimelineData, Context> params
    = new EditableParams<TimelineData, Context>(R.layout.activity_timeline);
    
params.setArgument(getApplicationContext());

params.setOnPreExecute(new V1<ProgressDialog>() {
    @Override
    public void call(ProgressDialog prog) {
        prog.setTitle("test");
        prog.setMessage("test");
        prog.setProgressStyle(ProgressDialog.STYLE_SPINNER);
        prog.setCancelable(false);
        prog.show();
    }
});

params.setOnBackground(new R1<List<TimelineData>, Context>() {
    @Override
    public List<TimelineData> call(Context cont) {
        Timeline t = new Timeline(cont);
        TwitterResult<List<TimelineData>> r
            =  t.getUserTimeline("", PagingBuilder.getDefaultPage(cont));

        return r.getResult();
    }
});

params.setOnPostExecute(new V2<List<TimelineData>, Activity>() {
    @Override
    public void call(List<TimelineData> dataList, Activity activity) {
        ListView listview = (ListView) activity.findViewById(R.id.lsvTimeline);
        TimelineAdapter adapter = new TimelineAdapter(activity.getApplicationContext(), dataList);
        listview.setAdapter(adapter);
    }
});

EditableParamsHolder.put("test", params);
Intent intent = new Intent(getApplicationContext(), EditableActivity.class);
intent.putExtra(EditableActivity.KEY, "test");
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);

いかがでしょうか。中々いい感じにキモくなったと自負しています。

まとめ

応用するともっと色んなことが出来ます。オプションメニューだろうがKeyイベントのフックだろうが思いのままです。

でもそこまでやるなら普通にActivity作ったほうがいいと思います。