基本的には、以前書いものにCleanArchitectureとModelView(MVVM)の構成を適用しただけになります。
MVP + Dagger2 + Retrofit2 + RxAndroidの構成で通信を試してみる
http://qiita.com/MuuKojima/items/8843c9451339a8b68f22
今回の内容としてはUSDからJPYに変換した為替レートを受け取りTextViewに表示するだけです。
使用API: http://fixer.io/
実行結果
実際のリクエストです。
下記のように1件JPYのレートが入っています。
{
base: "USD",
date: "2016-11-21",
rates: {
JPY: 110.61
}
}
パッケージの構成はdata
、ui
、binding
、di
に分割
data
にmodelの部分をまとめる
ui
にactivity、 view、presenter、viewModelをまとめる
di
にDaggerで必要なmoduleをまとめる
binding
にDataBindingで使うCallBackを入れる
パッケージの切り方はレイヤー
ではなく、フィーチャー
で切り分けています。
APIから返ってきた値を一行表示するだけなのに、すごいクラス数になってますね...
どうしてもクラス数が多くなるのがCleanArchitectureのデメリットのようです...
|- binding
| |- OnFieldChangedCallback
|
|- data
| |- exchange
| | |- repository
| | | |- ExchangeRateApi
| | | |- ExchangeRateRepositoryImpl
| | | |- ExchangeRepositoryModule
| | | |
| | |- usecase
| | | |- GetExchangeRate
| | | |
| | |- CountryCode
| | |- ExchangeRateRepository
| | |- ExchangeRateResponse
| |
| |- PostExecutionThread
| |- UseCase
|
|- di
| |- ApiModule
| |- AppComponent
| |- JobExecutor
| |- PerActivity
| |- RepositoryModule
| |- UIThread
|
|- ui
| |- exchange
| |-ExchangeRateActivity
| |-ExchangeRateComponent
| |-ExchangeRatePresenter
| |-ExchangeRateView
| |-ExchangeRateViewModel
|
|- AppApplication
CleanArchitecture基本ルール(一方向からのアクセス)
※参照 http://fernandocejas.com/2015/07/18/architecting-android-the-evolution/
この基本ルールに則って、
今回はの順にアクセスしていきます。
ExchangeRateActivity(View)
↓
ExchangeRatePresenter(Presenter)
↓
GetExchangeRate(UseCase)
↓
ExchangeRateRepository(Repository)
↓
ExchangeRateApi(Cloud)
DB(Disk)の方は今回はやりませんが、Repositoryに追加すれば特に他を変えずに追加する事ができ、API経由から取ってくるか、DBから取ってくるかといった事は上の方のクラスは知らずに済みます
かなり長期戦ですが...
それでは、まずrootのbuild.gradle
の設定から
buildscript {
dependencies {
// 追加
classpath 'com.uphyca.gradle:gradle-android-apt-plugin:0.9.4'
}
}
appのbuild.gradle
apply plugin: 'com.android.application'
// 追加 ↑の下に入れる事
apply plugin: 'android-apt'
android {
// 追加 ここに追加することでDataBindingが使えます
dataBinding {
enabled = true
}
}
dependencies {
//////// 追加 ////////
// RxAndroid
compile 'io.reactivex:rxjava:1.1.0'
compile 'io.reactivex:rxandroid:1.1.0'
// Dagger
compile 'com.google.dagger:dagger:2.2'
apt 'com.google.dagger:dagger-compiler:2.2'
// Retrofit
compile 'com.squareup.retrofit2:retrofit:2.0.2'
compile 'com.squareup.retrofit2:converter-gson:2.0.2'
compile 'com.squareup.retrofit2:adapter-rxjava:2.0.2'
// OkHttp
compile 'com.squareup.okhttp3:okhttp:3.2.0'
compile 'com.squareup.okhttp3:logging-interceptor:3.2.0'
// Gson
compile 'com.google.code.gson:gson:2.6.2'
}
APIと通信するので、AndroidManifest.xml
にインターネットパーミッションを1行追加
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="kojimation.com.retrofitsample">
<!-- 追加 -->
<uses-permission android:name="android.permission.INTERNET" />
</manifest>
data
パッケージを作成し、その中にexchange
パッケージを作成。
exchange
パッケージ内にAPIから返って来るレスポンス用のオブジェクトを2クラス作成
public class CountryCode {
private float JPY;
public float getJPY() {
return JPY;
}
}
public class ExchangeRateResponse {
private String base;
private String date;
private CountryCode rates;
public String getBase() {
return base;
}
public String getDate() {
return date;
}
public CountryCode getRates() {
return rates;
}
}
Repository
exchange
パッケージ内にrepository
パッケージを作成
repository
はデータのCRUD操作を提供するクラス
今回使う為替レートを取得するExchangeRateApi
を作成
rxじゃないObservableをimportしないように注意する
public interface ExchangeRateApi {
String URL = "/latest";
@GET(URL)
Observable<ExchangeRateResponse> getExchangeRate(@Query("base") String base,
@Query("symbols") String symbols);
}
ExchangeRateApi
を使うためのインターフェースExchangeRateRepository
を作成
public interface ExchangeRateRepository {
/**
* 為替レートを取得
*
* @param base
* @param symbols
* @return
*/
Observable<ExchangeRateResponse> getExchangeRate(String base, String symbols);
}
ExchangeRateApi
を実際に使う実体クラスのExchangeRateRepositoryImpl
を作成
今回はAPI通信だが、DBから値を取る場合もここに記述
@Singleton
/* package */ class ExchangeRateRepositoryImpl implements ExchangeRateRepository {
private final ExchangeRateApi mExchangeRateApi;
@Inject
public ExchangeRateRepositoryImpl(ExchangeRateApi mExchangeRateApi) {
this.mExchangeRateApi = mExchangeRateApi;
}
@Override
public Observable<ExchangeRateResponse> getExchangeRate(String base, String symbols) {
return mExchangeRateApi.getExchangeRate(base, symbols);
}
}
ExchangeRateRepository
を提供するExchangeRepositoryModule
を作成
Moduleクラスは@Moduleを付け、クラス名の接尾辞をModuleとするのが決まりである
ここまででRepository
パッケージは完成
@Module
public class ExchangeRepositoryModule {
@Singleton
@Provides
public ExchangeRateRepository provideExchangeRateRepository(ExchangeRateRepositoryImpl exchangeRateRepository) {
return exchangeRateRepository;
}
@Provides
@Singleton
public ExchangeRateApi provideExchangeRateApi(Retrofit retrofit) {
return retrofit.create(ExchangeRateApi.class);
}
}
Repositryを使うためのUseCaseの基底クラスUseCase
を作成
subscribeOn()
Main以外のスレッドで実行
observeOn()
Mainのスレッドを指定
こうすることで例えば、バックグラウンドスレッドで処理したものをメインスレッドで受け取る事が可能になる
public abstract class UseCase<T> {
protected final Executor threadExecutor;
protected final PostExecutionThread postExecutionThread;
protected UseCase(Executor threadExecutor, PostExecutionThread postExecutionThread) {
this.threadExecutor = threadExecutor;
this.postExecutionThread = postExecutionThread;
}
protected Observable<T> bindUIThread(Observable<T> useCaseeObservable) {
return useCaseeObservable
.subscribeOn(Schedulers.from(threadExecutor))
.observeOn(postExecutionThread.getScheduler());
}
}
Schedulerのinterfaceを作成
rxじゃないSchedulerをimportしないように注意
public interface PostExecutionThread {
Scheduler getScheduler();
}
UseCase
exchange
パッケージ内にusecase
パッケージ作成
usecase
にはビジネスロジックを入れる(ex.お気に入り、ログイン、)
ExchangeRateRepository
を使うためのユースケースGetExchangeRate
を追加
ここまででdata
パッケージ内はすべてが完成
public class GetExchangeRate extends UseCase<ExchangeRateResponse> {
private final ExchangeRateRepository mExchangeRateRepository;
@Inject
protected GetExchangeRate(Executor threadExecutor, PostExecutionThread postExecutionThread, ExchangeRateRepository exchangeRateRepository) {
super(threadExecutor, postExecutionThread);
this.mExchangeRateRepository = exchangeRateRepository;
}
public Observable<ExchangeRateResponse> execute(String base, String symbols) {
return bindUIThread(mExchangeRateRepository.getExchangeRate(base, symbols));
}
}
通信結果をメインスレッドで受け取るようにdi
パッケージを作成しUIThread
を作成
@Singleton
/* package */ class UIThread implements PostExecutionThread {
@Inject
public UIThread() {
}
@Override
public Scheduler getScheduler() {
// RxのメソッドでMainスレッドを取得することができる
return AndroidSchedulers.mainThread();
}
}
Executor
を継承したJobExecutor
を作成
ココのExecutorクラスをそのままコピぺさせて頂きました
https://github.com/android10/Android-CleanArchitecture/blob/master/data/src/main/java/com/fernandocejas/android10/sample/data/executor/JobExecutor.java
@Singleton
public class JobExecutor implements Executor {
private static final int INITIAL_POOL_SIZE = 3;
private static final int MAX_POOL_SIZE = 5;
// Sets the amount of time an idle thread waits before terminating
private static final int KEEP_ALIVE_TIME = 10;
// Sets the Time Unit to seconds
private static final TimeUnit KEEP_ALIVE_TIME_UNIT = TimeUnit.SECONDS;
private final BlockingQueue<Runnable> workQueue;
private final ThreadPoolExecutor threadPoolExecutor;
private final ThreadFactory threadFactory;
@Inject
public JobExecutor() {
this.workQueue = new LinkedBlockingQueue<>();
this.threadFactory = new JobThreadFactory();
this.threadPoolExecutor = new ThreadPoolExecutor(INITIAL_POOL_SIZE, MAX_POOL_SIZE,
KEEP_ALIVE_TIME, KEEP_ALIVE_TIME_UNIT, this.workQueue, this.threadFactory);
}
@Override public void execute(Runnable runnable) {
if (runnable == null) {
throw new IllegalArgumentException("Runnable to execute cannot be null");
}
this.threadPoolExecutor.execute(runnable);
}
private static class JobThreadFactory implements ThreadFactory {
private static final String THREAD_NAME = "android_";
private int counter = 0;
@Override public Thread newThread(Runnable runnable) {
return new Thread(runnable, THREAD_NAME + counter++);
}
}
}
ExchangeRepositoryModule
を提供するRepositoryModule
を作成
@Module(includes = {ExchangeRepositoryModule.class})
public class RepositoryModule {
@Provides
@Singleton
public PostExecutionThread providePostExecutionThread(UIThread uiThread) {
return uiThread;
}
@Provides
@Singleton
public Executor provideExecutor(JobExecutor jobExecutor) {
return jobExecutor;
}
}
RepositoryModule
を使用して通信するApiModule
を作成
@Module(includes = {RepositoryModule.class})
public class ApiModule {
@Provides
@Singleton
Gson provideGson() {
GsonBuilder gsonBuilder = new GsonBuilder();
return gsonBuilder.create();
}
@Provides
@Singleton
OkHttpClient provideOkhttpClient() {
OkHttpClient.Builder client = new OkHttpClient.Builder();
client.addInterceptor(new HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY));
return client.build();
}
@Provides
@Singleton
Retrofit provideRetrofit(Gson gson, OkHttpClient okHttpClient) {
return new Retrofit.Builder()
.addConverterFactory(GsonConverterFactory.create(gson))
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
// ベースのURLの設定
.baseUrl("http://api.fixer.io")
.client(okHttpClient)
.build();
}
}
モジュールとinterfaceを紐付けるAppComponent
を作成
@Singleton
@Component(modules = {ApiModule.class})
public interface AppComponent {
ExchangeRateRepository exchangeRateRepository();
Executor executor();
PostExecutionThread postExecutionThread();
}
Injectする際に使用するカスタムアノテーションを作成
スコープをActivityが生きてるまでとする
命名は特に決まっていないが、ググるとPerActivity
等を推奨していたので、その通りに作成
di
パッケージは完成
@Scope
@Retention(RetentionPolicy.RUNTIME)
public @interface PerActivity {}
ここでもう一度、実行もしくはrebuildする
(DaggerAppComponentが生成される)
Application
クラスを継承したAppApplication
を作成
Activity
やFragment
からgetAppComponent()
でAppComponent
を取得し、inject()
する
public class AppApplication extends Application {
private AppComponent mAppComponent;
@Override
public void onCreate() {
super.onCreate();
mAppComponent = DaggerAppComponent.builder().build();
}
public AppComponent getAppComponent() {
return mAppComponent;
}
}
Applicationクラスを使うので、AndroidManifest.xmlに android:name=".AppApplication"
を一行追記
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="kojimation.com.daggerretrofitrxandorid">
<!-- 追加 -->
<uses-permission android:name="android.permission.INTERNET" />
<!-- このタグの中に追加 -->
<application
android:name=".AppApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
binding
パッケージを作成し、DataBindingで値の変更通知を受け取れるようにOnFieldChangedCallback
を追加
Activity
やFragment
から使う
変数の型が例えばListの場合はOnListChangedCallback
を継承したり、型がbooleanの場合はOnBooleanChangedCallback
使ったりして、臨機応変に対応する
public abstract class OnFieldChangedCallback<T> extends ObservableField.OnPropertyChangedCallback {
@Override
public void onPropertyChanged(Observable sender, int propertyId) {
onChanged(((ObservableField<T>)sender).get());
}
protected abstract void onChanged(T field);
}
View
ここからView側を作成していく
ui
パッケージを作成し、exchange
パッケージを中に作成(今はexchange
だけなので、ui
のパッケージはいらないですが..)
ViewのインターフェースとしてExchangeRateView
を作成
/* package */ interface ExchangeRateView {
void bindExchangeRate(ExchangeRateResponse exchangeRateResponse);
}
ViewModel
Viewの状態はすべて、このViewModelが管理する(変数を所持したりなど)
変数が変更された場合はビューへの通知はDataBindingがやってくれる
ExchangeRateView
をimplementsしたExchangeRateViewModel
を作成
@PerActivity
/* package */ class ExchangeRateViewModel implements ExchangeRateView {
private final ObservableField<ExchangeRateResponse> mExchangeRateResponse = new ObservableField<>();
@Inject
public ExchangeRateViewModel() {
}
@Override
public void bindExchangeRate(ExchangeRateResponse exchangeRateResponse) {
mExchangeRateResponse.set(exchangeRateResponse);
}
// observe
public void onExchangeRateChanged(OnFieldChangedCallback<ExchangeRateResponse> callback) {
addCallback(mExchangeRateResponse, callback);
}
protected final <T> void addCallback(final ObservableField<T> observable, final OnFieldChangedCallback<T> callback) {
observable.addOnPropertyChangedCallback(callback);
}
}
Presenter
PresenterにView側のロジックを集約し、
View側はPresenterを通して、Data側のUseCaseにアクセスする
Presenter役として、ExchangeRatePresenter
を作成
Viewのセットや、Subscriptionの登録など、上位クラスとしてBaseViewなど作ってまとめた方がいいが、
一旦これだけなので、ここに書いておく
@PerActivity
/* package */ class ExchangeRatePresenter {
private final CompositeSubscription mSubscriptions = new CompositeSubscription();
private final GetExchangeRate mGetExchangeRate;
private ExchangeRateView mView;
@Inject
public ExchangeRatePresenter(GetExchangeRate getExchangeRate) {
this.mGetExchangeRate = getExchangeRate;
}
public void getExchangeRate() {
// USDを基準にJPYのレートを取得
addSubscription(mGetExchangeRate.execute("USD", "JPY")
.subscribe(
new Observer<ExchangeRateResponse>() {
@Override
public void onCompleted() {
}
@Override
public void onError(Throwable e) {
Log.d("通信 -> ", "失敗" + e.toString());
}
@Override
public void onNext(ExchangeRateResponse exchangeRateResponse) {
mView.bindExchangeRate(exchangeRateResponse);
}
}
));
}
public void setView(ExchangeRateView view) {
mView = view;
}
private void addSubscription(Subscription s) {
mSubscriptions.add(s);
}
protected void unsubscribe() {
mSubscriptions.clear();
}
}
ExchangeRateComponent
を作成
@PerActivity
@Component(dependencies = {AppComponent.class})
/* package */ interface ExchangeRateComponent {
void inject(ExchangeRateActivity activity);
}
ここでもう一度、実行もしくはrebuildする
(DaggerExchangeRateComponentが生成される)
java:ExchangeRateActivity
作成
public class ExchangeRateActivity extends AppCompatActivity {
@Inject
ExchangeRatePresenter mPresenter;
@Inject
ExchangeRateViewModel mModel;
private TextView mTextView;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_exchange_rate);
mTextView = (TextView) findViewById(R.id.txt_jpy);
DaggerExchangeRateComponent.builder()
.appComponent(((AppApplication) getApplicationContext()).getAppComponent())
.build()
.inject(this);
// Presenter/ViewModelの設定
mPresenter.setView(mModel);
mModel.onExchangeRateChanged(new OnExchangeRateChanged());
mPresenter.getExchangeRate();
}
@Override
protected void onDestroy() {
super.onDestroy();
mPresenter.unsubscribe();
}
private class OnExchangeRateChanged extends OnFieldChangedCallback<ExchangeRateResponse> {
@Override
protected void onChanged(ExchangeRateResponse exchangeRateResponse) {
mTextView.setText("JPY: " + String.valueOf(exchangeRateResponse.getRates().getJPY()));
}
}
}
TextViewを追加
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/activity_exchange_rate"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin">
<!-- 追加 -->
<TextView
android:id="@+id/txt_jpy"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</RelativeLayout>
実行してみる
Githubにサンプルを置いておきます
https://github.com/MuuKojima/CleanArchitectureMVVMDataBinding