2011年11月18日金曜日

RoboGuiceでAndroidアプリのDI開発

今更ながらRoboGuiceを使ってみたので忘れずメモ。
RoboGuiceについて簡単に触れておくとGoogle GuiceベースのAndroid用のDIコンテナである。
roboguice - Project Hosting on Google Code

細かいViewの制御は抜きにして以下のようなアプリを例としてRoboGuiceを使用して作成してみる。
  • トップ画面より入力された名前を次の画面で文字列を付け足して表示する。




  • まず前エントリーを参考にMavenプロジェクトからAndroidプロジェクトを作成。
    pom.xmlを追記
    <dependency>
      <groupId>org.roboguice</groupId>
      <artifactId>roboguice</artifactId>
      <version>1.1.2</version>
    </dependency>
    

    TopActivity
    public class TopActivity extends RoboActivity {
    
        @InjectResource(R.string.message)
        String message;
    
        @InjectView(R.id.txt_msg)
        TextView mTextView;
    
        @InjectView(R.id.edt_name)
        EditText mEditText;
    
        @InjectView(R.id.btn_decision)
        Button mButton;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
    
    //        mTextView.setText(message); // ここでViewにアクセスするとエラー
            setContentView(R.layout.top); // ここでViewがInjectされる
            mTextView.setText(message);
            mButton.setOnClickListener(new OnClickListener() {
    
                public void onClick(View v) {
                    Intent intent = new Intent(TopActivity.this, HelloActivity.class);
                    intent.putExtra("name", mEditText.getText().toString());
                    startActivity(intent);
                }
            });
        }
    
    }
    
    
    RoboActivityを継承してアクティビティを作成する。※1.1からGuiceActivityからRoboActivityに変更された
    ListActivityの場合はRoboListActivity、TabActivityの場合はRoboTabActivityなどそれぞれ対応したもの
    が存在する。
    ソースを見るとわかる通り、@InjectView(resouceId)でViewをInjectしてくれる。
    リソースを参照したい場合は@InjectResource(resourceId)を使用すればいい。
    他にもこんなことが可能である。
    Drawable icon = getResources().getDrawable(R.drawable.icon);
    ↓
    @InjectResource(R.drawable.icon)
    Drawable icon;
    
    LocationManager loc = (LocationManager) getSystemService(Activity.LOCATION_SERVICE);
    ↓
    @Inject
    LocationManager loc;
    

    HelloActivityを実装する前にインターフェースと実装クラスを使用して
    少しDIっぽい処理を入れてみる。
    RoboSampleService
    public interface RoboSampleService {
    
        public String hello(String name);
    }
    
    

    RoboSampleServiceImpl
    public class RoboSampleServiceImpl implements RoboSampleService {
    
        public String hello(String name) {
            return "Hello " + name;
        }
    
    }
    
    

    続いて上記サービスをバインド
    MyApplication
    public class MyApplication extends RoboApplication {
    
        @Override
        protected void addApplicationModules(List<Module> modules) {
            modules.add(new MyModule());
        }
    
        static class MyModule extends AbstractAndroidModule {
    
            @Override
            protected void configure() {
                // RoboSampleServiceのInject要求に対してRoboSampleServiceImplを返すようバインドする
                bind(RoboSampleService.class).to(RoboSampleServiceImpl.class);
            }
    
        }
    }
    
    ※こちらもGuiceApplicationからRoboApplicationに変更されているので注意

    アプリケーションのエントリポイントとして上記クラスを呼び出す必要があるので
    AndroidManifest.xmlに以下を追加
    <application...
        android:name=".MyApplication"
        ...>
    

    最後にHelloActivityの実装
    HelloActivity
    public class HelloActivity extends RoboActivity {
    
        @InjectExtra("name")
        String name;
    
        @InjectView(R.id.hello_msg)
        TextView textView;
    
        @Inject
        RoboSampleService service;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.hello);
            textView.setText(service.hello(name));
        }
    
    }
    

    @InjectExtra("name")でTopActivityでputExtraした値が入る。
    また、RoboSampleServiceに@Injectアノテーションをつけることで上記でバインドした
    RoboSampleServiceImplをInjectしてくれるのでインスタンスを生成せず使用することが出来る。

    続いてDIの長所を活かしてHelloActivityのテストを書いてみる。
    テストプロジェクトを作成する前にテスト対象のプロジェクトのクラスパスをエクスポート。

    通常通りAndroidテストプロジェクトを作成する。
    今回作成したサンプルプログラムのhelloメソッドは静的な文字列を付加するだけの
    シンプルな作りであるが、実際は動的であったり、バグが混入していたり、
    決まっていなかったりするので依存するクラスの振る舞いを固定化するモックを作成する。

    MockServiceImpl
    public class MockServiceImpl implements RoboSampleService {
    
        @Override
        public String hello(String name) {
            return "test";
        }
    
    }
    

    続いてテストコードの実装
    HelloActivityTest
    public class HelloActivityTest extends RoboActivityUnitTestCase<HelloActivity> {
    
        private Context mContext;
    
        public HelloActivityTest() {
            super(HelloActivity.class);
        }
    
        class MockApplication extends RoboApplication {
    
            public MockApplication(Context context) {
                super();
                attachBaseContext(context);
            }
    
            @Override
            protected void addApplicationModules(List<Module> modules) {
                modules.add(new MockModule());
            }
    
        }
    
        class MockModule extends AbstractAndroidModule {
    
            @Override
            protected void configure() {
                // RoboSampleServiceのInject要求に対してMockServiceImplを返すようバインドする
                bind(RoboSampleService.class).to(MockServiceImpl.class);
            }
    
        }
    
        @Override
        protected void setUp() throws Exception {
            super.setUp();
            mContext = getInstrumentation().getContext();
            setApplication(new MockApplication(mContext));
        }
    
        @MediumTest
        public void testShouldBeHelloMsg() {
            Intent intent = new Intent(mContext, HelloActivity.class);
            intent.putExtra("name", "name");
            HelloActivity a = startActivity(intent, null, null);
            assertNotNull(a);
    
            // Mockで返す文字列を"test"にしているため
            assertEquals(((TextView)a.findViewById(R.id.hello_msg)).getText(), "test");
        }
    }
    

    RoboActivityUnitTestCase<対象アクティビティ>を継承してテストコードを作成する。
    RoboSampleServiceのInject要求に対してモックを返すようにバインドさせ、
    setApplicationでセットすることで依存したオブジェクトの振る舞いを固定化させることが出来る。

    Run As -> Android JUnit Testで...
    これで一通り完了。案外使いやすいかも?

    0 件のコメント:

    コメントを投稿