LoginSignup
27
34

More than 5 years have passed since last update.

CleanArchitecture + ModelView(MVVM) + DataBindingを試してみる

Last updated at Posted at 2016-11-25

基本的には、以前書いものにCleanArchitectureとModelView(MVVM)の構成を適用しただけになります。

MVP + Dagger2 + Retrofit2 + RxAndroidの構成で通信を試してみる
http://qiita.com/MuuKojima/items/8843c9451339a8b68f22

今回の内容としてはUSDからJPYに変換した為替レートを受け取りTextViewに表示するだけです。

使用API: http://fixer.io/

実行結果

スクリーンショット 2016-11-24 午後11.31.10.png

実際のリクエストです。

http://api.fixer.io/latest?base=USD&symbols=JPY

下記のように1件JPYのレートが入っています。

今回の例
{
base: "USD",
date: "2016-11-21",
rates: {
        JPY: 110.61
    }
}

パッケージの構成はdatauibindingdiに分割
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基本ルール(一方向からのアクセス)

clean_architecture_evolution.png

※参照 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の設定から

build.gradle
buildscript {
    dependencies {
       // 追加
       classpath 'com.uphyca.gradle:gradle-android-apt-plugin:0.9.4'
    }
}

appのbuild.gradle

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行追加

AndroidManifest.xml
<?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クラス作成

CountryCode.java

public class CountryCode {
    private float JPY;

    public float getJPY() {
        return JPY;
    }
}
ExchangeRateResponse.java

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しないように注意する

ExchangeRateApi.java
public interface ExchangeRateApi {
    String URL = "/latest";

    @GET(URL)
    Observable<ExchangeRateResponse> getExchangeRate(@Query("base") String base,
                                                     @Query("symbols") String symbols);
}

ExchangeRateApiを使うためのインターフェースExchangeRateRepositoryを作成

ExchangeRateRepository.java
public interface ExchangeRateRepository {
    /**
     * 為替レートを取得
     *
     * @param base
     * @param symbols
     * @return
     */
    Observable<ExchangeRateResponse> getExchangeRate(String base, String symbols);
}

ExchangeRateApiを実際に使う実体クラスのExchangeRateRepositoryImplを作成
今回はAPI通信だが、DBから値を取る場合もここに記述

ExchangeRateRepositoryImpl.java
@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パッケージは完成

ExchangeRepositoryModule.java
@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のスレッドを指定
こうすることで例えば、バックグラウンドスレッドで処理したものをメインスレッドで受け取る事が可能になる

UseCase.java
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しないように注意

PostExecutionThread.java
public interface PostExecutionThread {
    Scheduler getScheduler();
}

UseCase

exchangeパッケージ内にusecaseパッケージ作成
usecaseにはビジネスロジックを入れる(ex.お気に入り、ログイン、)
ExchangeRateRepositoryを使うためのユースケースGetExchangeRateを追加
ここまででdataパッケージ内はすべてが完成

GetExchangeRate.java
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を作成

UIThread.java
@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

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を作成

RepositoryModule.java
@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を作成

ApiModule.java
@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を作成

AppComponent.java
@Singleton
@Component(modules = {ApiModule.class})
public interface AppComponent {

    ExchangeRateRepository exchangeRateRepository();

    Executor executor();

    PostExecutionThread postExecutionThread();
}

Injectする際に使用するカスタムアノテーションを作成
スコープをActivityが生きてるまでとする
命名は特に決まっていないが、ググるとPerActivity等を推奨していたので、その通りに作成
diパッケージは完成

PerActivity.java
@Scope
@Retention(RetentionPolicy.RUNTIME)
public @interface PerActivity {}

ここでもう一度、実行もしくはrebuildする
(DaggerAppComponentが生成される)
Applicationクラスを継承したAppApplicationを作成
ActivityFragmentからgetAppComponent()AppComponentを取得し、inject()する

AppApplication.java
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" を一行追記

AndroidManifest.xml
<?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を追加
ActivityFragmentから使う
変数の型が例えばListの場合はOnListChangedCallbackを継承したり、型がbooleanの場合はOnBooleanChangedCallback使ったりして、臨機応変に対応する

OnFieldChangedCallback.java
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を作成

ExchangeRateView.java
/* package */  interface ExchangeRateView {
    void bindExchangeRate(ExchangeRateResponse exchangeRateResponse);
}

ViewModel

Viewの状態はすべて、このViewModelが管理する(変数を所持したりなど)
変数が変更された場合はビューへの通知はDataBindingがやってくれる

ExchangeRateViewをimplementsしたExchangeRateViewModelを作成

ExchangeRateViewModel.java
@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など作ってまとめた方がいいが、
一旦これだけなので、ここに書いておく

ExchangeRatePresenter.java
@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を作成

ExchangeRateComponent.java
@PerActivity
@Component(dependencies = {AppComponent.class})
/* package */ interface ExchangeRateComponent {
    void inject(ExchangeRateActivity activity);
}

ここでもう一度、実行もしくはrebuildする
(DaggerExchangeRateComponentが生成される)
java:ExchangeRateActivity作成

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を追加

activity_exchange_rate.xml
<?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>

実行してみる

スクリーンショット 2016-11-24 午後11.31.10.png

Githubにサンプルを置いておきます
https://github.com/MuuKojima/CleanArchitectureMVVMDataBinding

27
34
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
27
34