サンプルコードはhttps://github.com/sys1yagi/TransfuseSampleで公開してます。Android Studio用です。
宣伝
本出たり出そうだったりしてます。どっちもいい感じです。年末年始に是非!
Androidオープンソースライブラリ徹底活用 | Effective Android |
---|---|
|
結論
TransfuseはPOJOにアノテーションベースでDIするので設計がだいぶ縛られるけど、これこそDIの正しい設計なのかもしれないと思いました。Transfuseすごい。こわい。ただアノテーションと普通のクラス名が被るのがめんどい。 @Activityとか@Fragmentとか@Viewとか。どっちかをFQDNにしないといけない。だるい。あと他のライブラリとのインテグレーションが出来るのかよくわからない。多分問題なく出来ると思うが・・・。ボイラープレートなコードは大分減るので楽。
環境構築
Transfuseは元々はmaven用の様です。Android Studioで使う場合はいくつか手順が必要です。Android Studioで新規プロジェクトを作成した状態とします。
build.gradleの設定
まずbuild.gradleを色々いじります。Transfuseに必要な依存性を管理するために、configurationsブロックにtransfuseを追加します。
configurations { compile transfuse.extendsFrom(compile) }
次にdependenciesを追加します。
dependencies { compile 'com.android.support:appcompat-v7:+' transfuse 'org.androidtransfuse:transfuse:0.2.2' transfuse 'com.google.android:android:2.1_r1' transfuse 'com.google.android:support-v4:r7' transfuse 'org.parceler:parceler:0.1.1' compile 'org.androidtransfuse:transfuse-api:0.2.2' compile 'org.parceler:parceler-api:0.1.1' }
androidブロックにsourceSets.mainブロックを追加してAndroidManifest.xmlの場所を指定します。
android { sourceSets.main { manifest.srcFile 'AndroidManifest.xml' } }
build.gradleの最下部辺りに全variantでtransfuseを実行する設定を書きます。
android.applicationVariants.all { variant -> def aptOutput = file("${project.buildDir}/source/r/${variant.dirName}") aptOutput.mkdirs() aptOutput.eachFileRecurse groovy.io.FileType.FILES, { if (it.name.equals('R.java')) { return } it.delete() } variant.javaCompile.options.compilerArgs += [ '-processorpath', configurations.transfuse.asPath, '-s', aptOutput ] }
全体はコチラで参照できます。→TransfuseSample/TransfuseSample/build.gradle
AndroidManifest.xmlを移動する
Transfuseのタスクを実行する際にAndroidManifest.xmlを読みにいくのですが、以下のようにデフォルトの場所に置いているとうまく読んでくれません。
$MODULE_NAME/src/main/AndroidManifest.xml
以下のパスに移動して下さい。
$MODULE_NAME/src/AndroidManifest.xml
これで利用可能になるはずです。
基本的な使い方
TransfuseはPOJOに対して必要なアノテーションを付加していくスタイルになります。例えば簡単なActivityだと以下の様になります。
@Activity(label = "@string/app_name") @Layout(R.layout.activity_main) @IntentFilter({ @Intent(type = IntentType.ACTION, name = android.content.Intent.ACTION_MAIN), @Intent(type = IntentType.CATEGORY, name = android.content.Intent.CATEGORY_LAUNCHER) }) public class Main { @OnCreate public void create(Bundle saved) { // initialize } }
このコードをコンパイルすると、MainActivity.javaやインジェクタなどが生成されます。また、AndroidManifest.xmlを自動生成してくれます。@Activityや@IntentFilterなどAndroidManifest.xmlに関連するアノテーションを付加しておくとAndroidManifest.xmlに反映されます。
最初AndroidManifest.xmlを自動生成する機能が動きませんでした。色々いじっていたら出来るようになったんですが条件がいまいちわかりません。自動生成が動かない場合は手でAndroidManifest.xmlにActivity等の定義を追加しましょう。
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="jp.mydns.sys1yagi.android.transfuse" android:versionCode="1" android:versionName="1.0"> <uses-sdk android:minSdkVersion="7" android:targetSdkVersion="19"/> <application android:name="android.app.Application"> <activity android:label="@string/app_name" android:name=".MainActivity"> <intent-filter> <action android:name="android.intent.action.MAIN"/> <category android:name="android.intent.category.LAUNCHER"/> <category android:name="android.intent.category.DEFAULT"/> </intent-filter> </activity> </application> </manifest>
あとはビルド&実行。
./gradlew --daemon installDebug adb shell am start -n jp.mydns.sys1yagi.android.transfuse/.MainActivity --activity-clear-top
Transfuseの機能
Transfuseは沢山のアノテーションや機能を提供しています。ざっくり各種機能を説明します。
Activity Lifecycle Method
POJOでActivityを実現するので、ライフサイクルメソッドはすべてアノテーションで指定します。
@Activity @Layout(R.layout.activity_lifecycle_methods) public class ActivityLifecycleMethods { @OnCreate public void create() { } @OnResume public void resume() { } @OnPause public void pause() { } }
大体以下のアノテーションが使えます。@OnNewIntentが無いのが気になります。
@OnCreate @OnStart @OnPause @OnResume @OnStop @OnDestroy @OnBackPressed @OnSaveInstanceState @OnRestoreInstanceState @OnConfigurationChanged
View Injection
Viewのインジェクションは@Viewを使います。RoboGuiceとかだと@InjectViewですが、Transfuseだと@Injectと@Viewの組み合わせになります。
@Activity @Layout(R.layout.fragment_view_injection) public class ViewInjection { @Inject @View(R.id.text) TextView mTextView; @Inject @View(tag="hello_button") Button mButton; @OnCreate private void init(){ mTextView.setText("Hello Transfuse!!"); mButton.setText("Tag Injection!!"); } }
面白いのはViewのID指定だけでなく、tagでの指定もできる点でしょうか。上記コードで利用しているレイアウトは以下の通りです。
<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/text" android:text="@string/hello_world" android:layout_width="wrap_content" android:layout_height="wrap_content"/> <Button android:id="@+id/button" android:tag="hello_button" android:text="@string/hello_world" android:layout_width="wrap_content" android:layout_height="wrap_content"/> </LinearLayout>
Extra Injection
Activityで受け取るIntentのパラメータもインジェクションできます。@Extraでキー名を指定できるほか、パラメータがオプショナルかどうかも指定できます。
@Activity public class ExtraInjection { @Inject @Extra("title") String mTitle; @Inject @Extra(value = "message") String mMessage; @Inject @Extra(value = "count", optional = false) Integer mCount; }
上記コードをコンパイルすると、「ExtraInjectionActivityStrategy.java」が生成されます。このクラスは対象のActivityを起動するIntentを生成するクラスです。ExtraInjectionActivityStrategyはコンストラクタで必須パラメータを受け取り、オプショナルなパラメータについてはsetterメソッドが生やされます。
@Inject IntentFactory mIntentFactory; private void start(){ mIntentFactory.start( new ExtraInjectionActivityStrategy("Transfuse!", "hello") .setCount(10) ); }
Transfuseが提供するIntentFactoryクラスのstartメソッドにインスタンスを渡すことでActivityを起動できます。
Resource Injection
リソースのインジェクションは @Resourceを使います。対応する型が間違っているとコンパイル時にエラーになるか、運良く通っても実行時にクラッシュします。
@Activity public class ResourceInjection { @Inject @Resource(R.string.injection_text) String mInjectionText; @Inject @Resource(R.drawable.ic_launcher) Drawable mIcLauncher; }
Preference Injection
@Preferenceを使ってSharedPreferenceの値をフィールドにインジェクションできます。
@Fragment public class PreferenceInjection { @Inject @Preference(value = "name") String mName; @Inject @Preference(value = "is_notification", defaultValue = "false") Boolean mIsNotification; }
@Preferenceを持つクラスをビルドすると以下の様なコードで値が解決されます。任意に名前を付けたSharedPreferenceを利用する事はできないようです。
PreferenceManager.getDefaultSharedPreferences(this).getString("name")
SystemService Injection
SystemServiceは@SystemServiceを使います。アノテーションにSystemServiceの名前をセットすれば対応したクラスがインジェクションされます。
@Activity public class SystemServiceInjection { @Inject @SystemService(Context.LAYOUT_INFLATER_SERVICE) LayoutInflater mLayoutInflater; }
Application Lifecycle Method
ApplicationクラスもPOJOで書けちゃいます。
@Application(name = "TransfuseApplication") public class TransfuseApplicationBase { @OnCreate public void create() { } }
以下のアノテーションが使えます。
@OnCreate @OnTerminate @OnLowMemory @OnConfigurationChanged
Fragment Lifecycle Method
FragmentももちろんPOJOです。Activityと同じ様に、@Fragmentで作成できます。
@Fragment @Layout(R.layout.fragment_fragment_lifecycle_method) public class FragmentLifecycleMethod { @OnActivityCreated void activityCreated() { } @OnDestroyView void destroyView(){ } }
以下のLifecycle Methodのアノテーションが使えます。@OnCreateは何故かonCreateView()にバインドされます。onAttachやonDetachのアノテーションもありません。まぁあんまり使わないしいいんじゃないでしょうか。
@OnCreate @OnActivityCreated @OnStart @OnResume @OnPause @OnStop @OnDestroyView @OnDestroy @OnLowMemory @OnConfigurationChanged
@Fragmentにはnameとtypeの属性をセットできます。name属性は生成するFragmentの名前を指定できます。以下の例だとLifecycleMethodFragment.javaが生成されます。デフォルトではクラス名のSuffixに"Fragment"が付きます。
@Fragment(name="LifecycleMethodFragment") public class FragmentLifecycleMethod { }
type属性を設定するとextendsするクラスを指定できます。但しClass<? extends android.support.v4.app.Fragment>である必要があります。
import android.support.v4.app.ListFragment; @Fragment(type = ListFragment.class) public class FragmentList{ }
Fragmentを使う場合、Activity側はFragmentActivityを利用する必要があります。Activity側でもtype属性でFragmentActivityを指定しておきましょう。
@Activity(type = FragmentActivity.class) public class FragmentContainer { }
<fragment>でFragmentを埋める分には特に問題出ないですが、動的にFragmentを切り替える場合ちょっと面倒です。サンプルではこうしてますがもうちょいいい感じに出来るんじゃないかと思っています。
BroadcastReceiver Lifecycle Method
BroadcastReceiverもPOJO(ry。@BroadcastReceiverと@Intentで定義できます。
@BroadcastReceiver @Intent(type = IntentType.ACTION, name = Action.ACTION) public class Action { public final static String ACTION = "jp.mydns.sys1yagi.android.transfuse.ACTION"; @OnReceive public void boot(Context context, android.content.Intent intent) { //do something } }
投げる方はこんな感じ。
@Fragment @Layout(R.layout.fragment_broadcast) public class Broadcast { @Inject Context mContext; @RegisterListener(R.id.button) View.OnClickListener onClickListener = new View.OnClickListener() { @Override public void onClick(View v) { Intent intent = new Intent(Action.ACTION); mContext.sendBroadcast(intent); } }; }
ActivityやFragmentとなるクラスはPOJOなのでsendBroadcast()とか持ってません。なのでフィールドにContextを@Injectしておいて、それを使います。以下の場合だとmContextにはgetActivity()の値がインジェクションされます。
Service Lifecycle Method
ServiceもPOJO(ry。ただOnBindが無かったりでまだ未完成感が・・・。こういうIssueもありますが進んでない感。一応@Serviceで定義できます。でも@OnCreateと@OnDestroyしかサポートしてません。
@Service @IntentFilter(@Intent(type= IntentType.ACTION, name="transfuse")) public class Calc{ @OnCreate public void create(){ Log.d(TAG, "onCreate"); } @OnDestroy public void destroy(){ Log.d(TAG, "onDestroy"); } }
Listener Registration
BroadcastReceiver Lifecycle Methodの所のコードに出てますが、@RegisterListenerでリスナをViewにバインドできます。
@RegisterListener(R.id.button) View.OnClickListener mListener = new View.OnClickListener() { @Override public void onClick(View v) { mTextView.setText("Pushed!!!!!!!"); } }; @RegisterListener(value = R.id.long_click_text) View.OnLongClickListener mLongClickListener = new View.OnLongClickListener() { @Override public boolean onLongClick(View v) { mTextView.setText("Long Pushed!!!!!!!"); return false; } };
以下のViewクラスの持つリスナをサポートしています。API Level 11以降に追加されたものはサポートしてません。
View.OnLongClickListener View.OnClickListener View.OnCreateContextMenuListener View.OnFocusChangeListener View.OnKeyListener View.OnTouchListener
Call-Through Events
Call-Through EventsはPOJO側で定義したメソッドを生成コード側に生やす機能です。イベント用のインタフェースがあるので、それをPOJO側で実装します。例えばActivityのonKeyDownを実装するには、POJO側でActivityOnKeyDownListenerインタフェースを実装しておきます。
@Activity @Layout(R.layout.fragment_call_through_events) @RegisterListener public class CallThroughEvents implements ActivityOnKeyDownListener { public boolean onKeyDown(int keyCode, android.view.KeyEvent event) { return false; } }
上記コードをコンパイルすると、以下のコードが生成されます。CallThroughEventsActivityされたCallThroughEventsActivity側にonKeyDownメソッドが生え、CallThroughEventsのonKeyDown()を呼び出している事がわかります。
public class CallThroughEventsActivity extends Activity implements ContextScopeHolder { private jp.mydns.sys1yagi.android.transfuse.CallThroughEvents callThroughEvents$0; //略 @Override public boolean onKeyDown(int int$1, KeyEvent keyEvent$0) { return callThroughEvents$0 .onKeyDown(int$1, keyEvent$0); } //略 }
以下のCall-Through用のインタフェースが用意されています。Service用のもありましたがうまく動きませんでした。
ActivityMenuComponent ActivityOnKeyDownListener ActivityOnKeyLongPressListener ActivityOnKeyMultipleListener ActivityOnKeyUpListener ActivityOnTouchEventListener ActivityOnTrackballEventListener
Method Intercepters
@Asynchronous、@UIThreadをメソッドに付加する事で、メソッドを実行するスレッドを設定できます。以下の様に通信処理などをするメソッドに@Asynchronousを付けておき、結果を反映するメソッドに@UIThreadを付けておけば、非同期処理とUIスレッドの連携を簡単に書けます。
@Fragment public class MethodInterceptors { @Asynchronous public void loadAsynchronous() { try { URL url = new URL(LOAD_URL); String result = IOUtils.toString(url); setResult(result); } catch (Exception e) { e.printStackTrace(); setResult(e.getMessage()); } } @UIThread public void setResult(String text) { mTextView.setText(text); } }
Events Bus
TransfuseはEvent Busの機能も提供しています。以下の様に任意のイベントクラスを定義しておきます。
public class MessageEvent { private String mMessage; public MessageEvent(String message) { mMessage = message; } public String getMessage() { return mMessage; } }
イベントの送信はEventManagerを使います。以下の様に@InjectでEventManagerのインスタンスを持っておき、任意のタイミングでtriggerメソッドでイベントを送信します。
@Inject EventManager mEventManager; @RegisterListener(R.id.button) View.OnClickListener mListener = new View.OnClickListener() { @Override public void onClick(android.view.View v) { mEventManager.trigger(new MessageEvent("send event!!")); } };
イベントを受け取りたいクラスで@Observesを付加してメソッドを定義しておきます。EventManagerのtriggerメソッドでイベントを送信すると、@Observesが付加されていて引数がイベントの種類に一致するメソッドが呼び出されます。
@Observes public void onMessage(MessageEvent event) { mTextView.setText("receive event = " + event.getMessage()); }
この仕組みを使うとリスナなどの登録無しでイベントのやりとりが出来ます。例えばログイン状態が変化した時や、Push通知を受け取った時などにEvent Busを使ってイベントを投げる事で、関連した画面やクラスで処理を行えます。
Parcel
モデルクラスに@Parcelを付加するとParcelableになります。IntelliJ Plugin for Android Parcelable boilerplate code generationなんかいらんかったんや!
@Parcel public class SampleData { private int mId; private String mName; private String mDescription; }
ImplementedBy
@ImplementedByはインタフェースのデフォルト実装を宣言するアノテーションです。以下のインタフェースがあるとします。
public interface ICalculator { public int add(int a, int b); }
これを実装したCalculatorImplクラスがあるとします。
public class CalculatorImpl implements ICalculator { @Override public int add(int a, int b) { return a + b; } }
以下の様にICalculatorをインジェクションしたいクラスがあった時、Transfuse側にこの依存性を解決する為の情報を教えてあげる必要があります。依存性を解決する為の情報を教える方法の1つが@ImplementedByです。
@Fragment public class ImplementedBy { @Inject ICalculator mCalculator; }
インタフェース側で以下の様に@ImplementedByでデフォルトの実装クラスを定義しておくと、@Inject ICalculatorに指定したクラスがインジェクションされます。
@ImplementedBy(CalculatorImpl.class) public interface ICalculator { public int add(int a, int b); }
Module
@TransfuseModuleで依存性を解決するモジュールを定義できます。モジュールに@Injectを解決する際に利用する情報を定義していきます。
@TransfuseModule public class ApplicationModules { }
@Provider
モジュール内で@Providesを付加したメソッドを定義しておくと、@Injectを解決する際に利用されます。以下の例だとArrayAdapterを解決できます。
@TransfuseModule public class ApplicationModules { @Provides public ArrayAdapter getAdapter(Context context) { return new ArrayAdapter<String>(context, android.R.layout.simple_list_item_1); } }
ArrayAdapterなど汎用的なクラスの場合、@Inject ArrayAdapterと書いてある全ての部分に上記のプロバイダが適用されます。@Namedを使うと特定のプロバイダを指定できます。
@TransfuseModule public class ApplicationModules { @Provides @Named("TopMenuList") public ArrayAdapter getAdapter(Context context) { return new ArrayAdapter<String>(context, android.R.layout.simple_list_item_1); } @Provides @Named("SubMenuList") public ArrayAdapter getSubMenuAdapter(Context context) { return new ArrayAdapter<String>(context, R.layout.sub_menu); } }
以下の様に@Inject側でインジェクションするプロバイダの名前を指定します。
@Inject @Named("TopMenuList") ArrayAdapter mAdapter;
Provider
プロバイダはProvider
public class SampleDataProvider implements Provider<SampleData>{ @Override public SampleData get() { SampleData sampleData = new SampleData(); sampleData.setId(-1); sampleData.setName("provided data"); sampleData.setDescription("provided data"); return sampleData; } }
定義したプロバイダはモジュール側の@BindProvidersで設定できます。
@TransfuseModule @BindProviders({ @BindProvider(type = SampleData.class, provider = SampleDataProvider.class) }) public class ApplicationModules { }
Bind
直接モジュールに解決するクラスを定義する場合は@Bindingsを使います。@ImplementedByに似ていますが、@ImplementedByはインタフェース側に書くのに対し、@Bindingsはモジュール側に書きます。モジュール側でデフォルトの実装を任意に切り替えられるわけです。
@TransfuseModule @Bindings({ @Bind(type = ICalculator.class, to = CalculatorImpl.class) }) public class ApplicationModules { }
Factory
@Injectはしないけど色々解決したい場合は@Factoryを使います。以下の様に@Factoryを付加したクラスを定義します。
@Factory public interface SampleDataFactory { SampleData getSampleData(); }
以下の様にFactories.get()でFactoryクラスを指定して取り出せます。getSampleData()の戻り値は、コンパイル時にモジュール内の定義に基いて解決されます(@BindProviderや@Bindや@ImplementedByや@Provides)。
public class UseFactory { public void getData() { SampleData sampleData = Factories.get(SampleDataFactory.class).getSampleData(); //do something. } }
Scope
スコープは依存性を解決する時にたどるオブジェクトマップの範囲を定義できます。例えば@Singletonをクラス宣言に付けておくと、そのクラスはシングルトンになります。
@Singleton public class SingletonCalculator { public int multi(int a, int b) { return a * b; } }
例えば以下の様に二つのフィールドに@Injectを書くと、通常は二つのインスタンスがそれぞれのフィールドにインジェクションされます。@Singletonが付いている場合、両方同じインスタンスがインジェクションされます。
@Fragment public class Scope { @Inject SingletonCalculator mSingletonCalculator1; @Inject SingletonCalculator mSingletonCalculator2; }
デフォルトでは@Singletonの他にも@ContextScopeがあります。@ContextScopeはActivity単位でインスタンスが分かれます。この他カスタムスコープを自分で定義したり、@Injectをどのスコープで解決するかを指定できる@ScopeReferenceなどがあります。
おわりに
Transfuseすごい。一個これでアプリ作ってみたい。テスト周りには手が回らなかったのでテストも書きたい。本家ではRobolectricでやっているみたい。
0 件のコメント:
コメントを投稿