TRY ANDROID DEV

Android アプリ開発のコーディングネタ。あとエンジニアとしての活動

Interfaceの実体クラスにまたInterfaceがある場合のDI(Dagger2)

背景

以下の構成を考える。 f:id:off2white:20190103095800j:plain

ドメイン層は他の層と依存関係を持たないが、ドメインのメソッドで通信を行いたい。 そのためにinterfaceを設定してリポジトリ層と疎結合にする。 ただしリポジトリ層でも通信をする場合とテスト用にローカルファイルを返却する場合で切り替えたい。
そのためInterfaceの実体クラス内にまたInterfaceがある状況になっている。

この構成時にDagger2でDIしたい。

環境

実装

以下の4クラスを実装する

RepositoryComponent

package c.offwhite.novel.di

import c.offwhite.novel.domain.ISearchRepository
import dagger.Component

@Component(modules = [RepositoryModule::class, ApiModule::class])
interface RepositoryComponent {
    fun inject(): ISearchRepository
}

RepositoryModule

@Module
class RepositoryModule {

    @Provides
    fun provideNarouRepository(narouApi: INarouOpenApi): ISearchRepository {
        return NarouRepository(narouApi)
    }
}

ApiModule

@Module
class ApiModule {
    @Provides
    fun provideNarouApi(narouOpenApi: NarouOpenApi): INarouOpenApi {
        return narouOpenApi
    }
}

WebSite

class WebSite (val domain: String) {

    @Inject lateinit var searchRepository: ISearchRepository

    fun search(word: String): List<NovelIntroduction> {
        searchRepository = DaggerRepositoryComponent.create().inject()
        return searchRepository.search(word)
    }
}

解説

Dagger2が依存性を解決できないパターンが複数存在する - Interface(何が具体化されるかわからないため) - サードパーティ製のクラス(@Injectが書いてないため)

これらのDagger2だけで自動的に解決できない依存性を満たすために、こちらから供給するクラス(Factoryクラス)を指定する必要がある。
図のように「もしISearchRepositoryというinterfaceが登場したら、NarouRepositoryを注入する」といった形である。

f:id:off2white:20190103172408j:plain

供給するクラスを指定するクラスをmoduleと呼び、以下のように定義する。

@Module
class RepositoryModule {

    @Provides
    fun provideNarouRepository(narouApi: INarouOpenApi): ISearchRepository {
        return NarouRepository(narouApi)
    }
}

モジュールクラスには@Moduleを記述し、供給メソッドには@Providesを記述する。 @Providesで指定するメソッドは対象のinterfaceを返却するメソッドとし、注入する実体クラスを指定する。
もし供給するRepositoryを切り替えたくなった場合はここのreturnの値を切り替えれば良い。

今回のケースでは実体クラスNarouRepositoryはApIインスタンスを注入する必要がある。 もちろんここで以下のように実体クラスを指定しても良い。

実体クラスを指定した場合

@Module
class RepositoryModule {

    @Provides
    fun provideNarouRepository(): ISearchRepository {
        return NarouRepository(NarouOpenApi())
    }
}

しかし今回はApiインスタンスも条件によって切り替えたいため、このモジュール一つにまとめてしまうと責務が分割できない。 よってここのパラメータをInterface指定としている。 こうなるとまたこのInterfaceの依存を指定する必要があるので、Apiクラスのモジュールも作成する。

ApiModule

@Module
class ApiModule {
    @Provides
    fun provideNarouApi(narouOpenApi: NarouOpenApi): INarouOpenApi {
        return narouOpenApi
    }
}

あとはComponentクラスに「依存注入がわからなくなったらこのmoduleを参照するように」と指定するだけで良い。

RepositoryComponent

package c.offwhite.novel.di

import c.offwhite.novel.domain.ISearchRepository
import dagger.Component

@Component(modules = [RepositoryModule::class, ApiModule::class])
interface RepositoryComponent {
    fun inject(): ISearchRepository
}

以下の@Coomponent(modules = ...)が該当箇所にあたる。ここはリストでの指定が必要なので一つでもリスト形式で記述する。 この指定によって、Daggerが依存性を解決できなかった時に参照する先を指定できる。

@Component(modules = [RepositoryModule::class, ApiModule::class])

あとは以下のドメインクラスにて指定して利用するだけになる。

WebSite

class WebSite (val domain: String) {

    @Inject lateinit var searchRepository: ISearchRepository

    fun search(word: String): List<NovelIntroduction> {
        searchRepository = DaggerRepositoryComponent.create().inject()
        return searchRepository.search(word)
    }
}

これでInterfaceの実体クラス内にまたinterfaceがある場合でもDIできた。

個人的に詰まったところ

  • kolint-kapt がないとkotlinがjavaのannotation processingをサポートしてくれないので、DaggerComponentが作成されない。 そのためgradleに以下の記述が必須。
apply plugin: 'kotlin-kapt'

Android開発のマージリクエストで最低限確認するべき項目まとめ

このドキュメントについて

本ドキュメントはAndroid開発において最低限確認するべき項目をまとめたものです。

作成理由

マージリクエスト時のレビューは基本的にはレビューアの技術力に左右されるものだと思いますが、 よく忙しさやモチベーションに比例して指摘できる事項が異なってきたりしてしまいます。

そのため毎回必ず確認すべき項目についてチェックリストを作成することにしました。

コードレビューでチェックする観点は多数存在するのですが、 あまりにチェック項目が多すぎるとレビューコストが高くなり、おざなりになりがちです。 そのため今回は必ずチェックすべき項目のみピックアップしたつもりです。

レビュー項目

  • 設計確認

    • [ ] 設計(要件)を満たすように実装されていること
    • [ ] 設計や要件に記載がない箇所についてはTODOが残されていること
  • UI

    • [ ] 各解像度でレイアウトが確認されていること(スクショが上がってなかったら貼ってもらう)
  • コーディング

    • [ ] 重複したコードが存在しないこと
    • [ ] クラスの責務が明確になっていること(メソッドが存在するクラスは適切に選択されていること)
    • [ ] メソッド名から想定できない処理をメソッド内で実施していないこと
    • [ ] 一目でわからない処理をする場合は"目的(理由)"や"思想"についてのコメントが書かれていること
    • [ ] 不要なコード/リソースが残っていないこと
  • 命名

    • [ ] クラス名・変数名・定数名・メソッド名が適切に命名されていること

RecyclerViewにLiveDataをDataBindingしたい

状況

  • List型のデータを取得し、画面にRecyclerViewで表示させたい。
  • だけどobserve書きたくないし、RecyclerViewのAdapterクラスをViewModelで持ちたくないし。
  • LiveDataに値をpostしたらいい感じに紐づいてくれないだろうか

実装

以下のように実装した。 主に4つのクラスで構成される。

  • ViewModel
  • Fragment
  • RecyclerView.Adapter
  • Fragmentのxml

ViewModel

/**
 * メイン画面のViewModel
 */
class MainViewModel constructor(private val showNovelListUseCase: ShowNovelListUseCase) : ViewModel() {

    // 小説リスト
    val novelList = MutableLiveData<List<NovelIntroduction>>()

    // onCreate時に呼び出す想定
    fun onCreate() = GlobalScope.launch {
        val novel = showNovelListUseCase.getNovelList("")
        novelList.postValue(novel)
    }

    // ViewModel()にDIする場合はFactoryクラスを作成する必要がある。
    class Factory(private val showNovelListUseCase: ShowNovelListUseCase) : ViewModelProvider.NewInstanceFactory() {
        override fun <T : ViewModel?> create(modelClass: Class<T>): T {
            return MainViewModel(showNovelListUseCase) as T
        }
    }
}

Fragment

class MainFragment : Fragment() {

    companion object {
        fun newInstance() = MainFragment()
    }

    // ViewModel
    private lateinit var viewModel: MainViewModel

    // binding
    private lateinit var binding: MainFragmentBinding

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
        // レイアウトをinflateする
        binding = DataBindingUtil.inflate(inflater, R.layout.main_fragment, container, false)

        // LiveDataとxmlをbindする
        binding.setLifecycleOwner(this)

        // inflateしたViewを返却する
        return binding.root
    }

    override fun onActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)

        // viewModel作成
        viewModel = ViewModelProviders
            .of(this, MainViewModel.Factory(ShowNovelListUseCase()))
            .get(MainViewModel::class.java)

        // 生成したviewModelをbindする
        binding.viewModel = viewModel

        // 横向き
        val manager = LinearLayoutManager(context)
        manager.orientation = LinearLayoutManager.HORIZONTAL
        binding.recyclerView.layoutManager = manager
        binding.recyclerView.adapter = NovelListAdapter()


        viewModel.onCreate()
    }

}

RecyclerView.Adapter

class NovelListAdapter : RecyclerView.Adapter<NovelListAdapter.ViewHolder>() {

    // 表示アイテム
    private var novelList: List<NovelIntroduction> = emptyList()

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        if (holder is ItemViewHolder && novelList.size > position) {
            holder.bind(novelList[position])
        }
    }

    override fun getItemCount(): Int {
        return novelList.size
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NovelListAdapter.ViewHolder {
        return ItemViewHolder(parent)
    }

    /**
     * データをセットしてアップデートする
     */
    fun update(novelList: List<NovelIntroduction>) {
        this.novelList = novelList
        notifyDataSetChanged()
    }

    abstract class ViewHolder(view: View) : RecyclerView.ViewHolder(view)
    class ItemViewHolder(
        private val parent: ViewGroup,
        private val binding: MainItemBinding = DataBindingUtil.inflate(
            LayoutInflater.from(parent.context),
            R.layout.main_item,
            parent,
            false
        )
    ) : ViewHolder(binding.root) {

        fun bind(item: NovelIntroduction) {
            binding.item = item
        }
    }

    companion object {
        @JvmStatic
        @BindingAdapter("items")
        fun RecyclerView.bindItems(items: List<NovelIntroduction>?) {

            // まだ情報が取得できていない場合はitemsがnullになる可能性があるため、nullチェック必須。
            if (items == null) {
                return
            }

            //  RecyclerView.Adapterを継承しているので、RecyclerViewに設定されているadapterを取得できる
            val adapter = adapter as NovelListAdapter
            adapter.update(items)
        }
    }
}

main_fragment.xml

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools">

    <data>

        <variable
                name="viewModel"
                type="c.offwhite.sampledi.ui.main.MainViewModel"/>
    </data>

    <android.support.constraint.ConstraintLayout
            android:id="@+id/main"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            tools:context=".ui.main.MainFragment">

        <android.support.v7.widget.RecyclerView
                android:id="@+id/recyclerView"
                android:orientation="horizontal"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                app:items="@{viewModel.novelList}"/>

    </android.support.constraint.ConstraintLayout>
</layout>

解説

まずはLiveDataの設定。ここは通常のLiveDataのコーディングと同じである。 showNovelListUseCase.getNoveList()は通信が発生する可能性があるのでバックグラウンドでの実行とし、LiveDataはpostValueで値を設定する

ViewModel

    // 小説リスト
    val novelList = MutableLiveData<List<NovelIntroduction>>()

    // onCreate時に呼び出す想定
    fun onCreate() = GlobalScope.launch {
        val novel = showNovelListUseCase.getNovelList("")
        novelList.postValue(novel)
    }

次にxmlのRecyclerViewにDataBindingを追加する。app:items="@{viewModel.novelList}"の行がそれにあたる。

main_fragment.xml

        <android.support.v7.widget.RecyclerView
                android:id="@+id/recyclerView"
                android:orientation="horizontal"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                app:items="@{viewModel.novelList}"/>

しかし、本来RecyclerViewにListを受けるセッターが用意されていないため、自作する必要がある。
自作セッターの定義方法は@BindingAdapter("対象のattribute")を指定することで可能。今回はapp:itemsに対する自作セッターのため、@BindingAdapter("items")となる。

なお、staticメソッドでないとダメな模様。

RecyclerView.Adapter

    companion object {
        @JvmStatic
        @BindingAdapter("items")
        fun RecyclerView.bindItems(items: List<NovelIntroduction>?) {

            // まだ情報が取得できていない場合はitemsがnullになる可能性があるため、nullチェック必須。
            if (items == null) {
                return
            }

            //  RecyclerView.Adapterを継承しているので、RecyclerViewに設定されているadapterを取得できる
            val adapter = adapter as NovelListAdapter
            adapter.update(items)
        }
    }

これでViewModel.novelListが更新されるたびに adapter.update()が呼び出されるため、adapter内のデータ変更が通知される。

RecyclerView.Adapter

    /**
     * データをセットしてアップデートする
     */
    fun update(novelList: List<NovelIntroduction>) {
        this.novelList = novelList
        notifyDataSetChanged()
    }

はまったところ

  • Android DataBinding error. Could not find accessorが発生

    • apply plugin: 'kotlin-kapt'がないと@BindingAdapterが動かない。しかしDataBindingはなくても大丈夫という罠。
  • Parameter specified as non-null is null: method kotlin.jvm.internal.Intrinsics.checkParameterIsNotNull, parameter itemsが発生

    • @BindingAdapter(items)を指定していたRecyclerView.bindItemsのパラメータをnon-nullにしていたが、非同期処理中にnullのLiveDataをbindしてしまい、non-nullを違反してしまっていた。nullableにしてnullチェックを導入することで回避している。

kotlinでSmart cast to xxx is impossible because response.body() is a complex expressionが発生した場合

状況

Retrofitを使った以下のソースコードでスマートキャストできないとエラーが発生した。

    override fun search(word: String): List<NovelIntroduction> {
        val response = searchService.getNovelList(word).execute()
        if (response.isSuccessful && response.body()!=null) {
            return response.body().mapNotNull { r -> NovelIntroductionTranslator().toNovelIntroduction(r) } // <-- Smart cast impossible
        }
    }

発生しているエラー

Smart cast to xxx is impossible because response.body() is a complex expression

response.body()はList? を返却するメソッドだが、if文でnullチェックしてもListにキャストできない。

対応方法

とりあえず!!をつければエラーは消えるのだが、せっかくのkotlinなのにnull pointer exceptionが発生しうる状況は避けたい。
smat castは同じプロパティに対して何回アクセスしても同じ値を返すことが保証されている場合可能らしい。
つまりvarであったらそもそもsmart castできない。

RetrofitのResponseクラスを覗いてみると以下のようにコーディングされていた。

private final @Nullable T body;
・
・
・
  public @Nullable T body() {
    return body;
  }

よく考えたらRetrofitはjavaでコーディングされているのでvalでない。 (javaでいくらfinalがついていてもListであればimmutableにしない限り内容書き換えられるのでたぶんダメ)

ということは一旦response.body()をvalに移し変えてからnullチェックを行えばよい。

if (response.isSuccessful) {
            val body = response.body()
            if (body!= null) return body.mapNotNull { r -> NovelIntroductionTranslator().toNovelIntroduction(r) }
}

これでSmart Castできた。

APIレスポンス(json)が一項目目と二項目目以降でレスポンス項目が異なって困った話

状況

  • 利用しようとしたAPIレスポンス(json)が一項目目と二項目目以降でレスポンス項目が異なるという鬼畜仕様。
  • そのため全てのレスポンス項目を含むレスポンスクラスを作成し、一旦レスポンスを受け取る。
  • Translatorクラスを作成し、レスポンスをドメインオブジェクトに変換する際に一項目目を取り除きたい。

レスポンスJSONはこんな感じ

[{"allcount":617021},{"title":"\u5143\u30fb\u9b54\u738b\u8ecd\u306e\u7adc\u9a0e\u58eb\u304c\u7d4c\u55b6\u3059\u308b\u731f\u5175\u56e3\u3002","ncode":"N6147EU"

コーディング

一項目目はallcountしか返却されず、その後のレスポンスでは各項目のデータが返却されるようなので、 いったん一項目目の場合はnullを返すTranslatorクラスとする。

    fun toNovelIntroduction(response : NarouNovelIntroductionResponse) : NovelIntroduction? {

        if (response.allcount == null) {
            return null
        }

        return NovelIntroduction(
            response.title ?: "",
            response.ncode?: "",
            response.userid?: "",
            response.writer?: "",
            response.story?: "",
            response.biggenre?: 0,
            response.genre?: 0,
            response.gennsaku?: "",
            response.keyword?: "",
            response.general_firstup?: "",
            response.general_lastup?: "",
            response.novel_type?: 0,
            response.end?: 0,
            response.general_all_no?: 0,
            response.length?: 0,
            response.time?: 0,
            response.isstop?: 0,
            response.isbl?: 0,
            response.isgl?: 0,
            response.iszankoku?: 0,
            response.istensei?: 0,
            response.pc_or_k?: 0,
            response.global_point?: 0,
            response.fav_novel_cnt?: 0,
            response.review_cnt?: 0,
            response.all_point?: 0,
            response.all_hyoka_cnt?: 0,
            response.sasie_cnt?: 0,
            response.kaiwaritu ?: 0,
            response.novelupdated_at?: "",
            response.updated_at?:"")
    }

返却データをfilterかけて対応。

    override fun search(word : String) : List<NovelIntroduction> {
        return searchService.getNovelList(word).map{response -> NovelIntroductionTranslator().toNovelIntroduction(response)}.filterNotNull()
    }

これで対応できた。

追記

kotlinにはmapNotNullというものがあるらしい。これを使うと以下のようにコーディングできる。便利。

    override fun search(word : String) : List<NovelIntroduction> {
        return searchService.getNovelList(word).mapNotNull{response -> NovelIntroductionTranslator().toNovelIntroduction(response)}
    }

最小構成でDagger2の使い方の初歩を理解する with kotlin

対象者

  • DIはもうわかったけどDagger2の使い方がわからない人
  • javaのサンプルは多いけどkotlinのサンプルが少なくて困惑している人
  • @Moduleとか@Provideとか@Componentとか@Injectってあるけど最小で動かすとしたらどれが必要なんだと困惑している人

最小構成(パターン①)

以下の3クラスで構成する

CoffeeMaker.kt

import javax.inject.Inject

class CoffeeMaker @Inject constructor(private val heater: Heater) {

    fun getTemperature() : String {
        return heater.getTemperature()
    }
}

Heater.kt

import javax.inject.Inject

class Heater @Inject constructor() {

    fun getTemperature() : String {
        return "38度"
    }
}

MainActivity.kt

import dagger.Component

class MainActivity : AppCompatActivity() {

    private lateinit var coffeeShop :CoffeeShop

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val dataBinding = DataBindingUtil.setContentView<MainActivityBinding>(this, R.layout.main_activity)

        // DaggerCoffeeShopはComponent interfaceを継承したクラス。
        // Dagger2によってビルド時に自動生成される。
        coffeeShop = DaggerCoffeeShop.create()

        // ビルド時に自動生成されたクラスがDIしてくれているので、
        // maker()実行だけで、メンバにインジェクト済みのインスタンスが取得できる
        val coffeeMaker = coffeeShop.maker()

        // 38度が返ってくる
        dataBinding.textView.text = coffeeMaker.getTemperature()
    }

}

@Component
interface CoffeeShop {
    fun maker(): CoffeeMaker
}

解説:パターン①

Dagger2は一行で書くと
"@Componentが記載されているinterfaceに対する、実体Factoryクラスを自動生成してくれる"
ツールだと理解した。

今回自動生成されたDaggerCoffeeShopは以下のような形になる。

public final class DaggerCoffeeShop implements CoffeeShop {
  private DaggerCoffeeShop(Builder builder) {}

  public static Builder builder() {
    return new Builder();
  }

  public static CoffeeShop create() {
    return new Builder().build();
  }

  @Override
  public CoffeeMaker maker() {
    return new CoffeeMaker(new Heater());
  }

  public static final class Builder {
    private Builder() {}

    public CoffeeShop build() {
      return new DaggerCoffeeShop(this);
    }
  }
}

以下行でわかるとおり、@Componentを付けたinterfaceに対する実体クラスが自動生成されている。

public final class DaggerCoffeeShop implements CoffeeShop {

また、以下の行ではmaker()の返却インスタンスであるCoffeeMakerインスタンスを生成する コードが自動生成されている。

  @Override
  public CoffeeMaker maker() {
    return new CoffeeMaker(new Heater());
  }

これによってDIを考慮して設計されたクラスをインスタンス化したいときに、クラス生成地獄を隠蔽することが可能になっている。

最小構成(パターン②)

"@Componentが記載されているinterfaceに対する、実体Factoryクラスを自動生成してくれる" のはわかった。 しかしActivityが複数のメンバ変数を持つような場合は一つ一つ上記のような書き方で注入していく必要があるのだろうか。

Dagger2ではもうひとつ書き方が用意されている。 "自分自身の@Injectが記載されているメンバ変数全てにインスタンスを設定してくれる"方法である。

以下に対象コードを記載する。

class OtherActivity : AppCompatActivity() {

    @Inject lateinit var coffeeMaker : CoffeeMaker

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val binding = DataBindingUtil.setContentView<OtherActivityBinding>(this,R.layout.other_activity)

        DaggerActivityComponent.create().inject(this)

        binding.textView.text = coffeeMaker.getTemperature()
    }
}

@Component
interface ActivityComponent {
    fun inject(activity:OtherActivity)
}

解説:パターン②

先ほどと異なるのは @Componentが記載されているインターフェースのメソッドに自分自身が設定されていることである。 このような場合、以下のようなクラスが自動生成される。

public final class DaggerActivityComponent implements ActivityComponent {
  private DaggerActivityComponent(Builder builder) {}

  public static Builder builder() {
    return new Builder();
  }

  public static ActivityComponent create() {
    return new Builder().build();
  }

  private CoffeeMaker getCoffeeMaker() {
    return new CoffeeMaker(new Heater());
  }

  @Override
  public void inject(OtherActivity activity) {
    injectOtherActivity(activity);
  }

  private OtherActivity injectOtherActivity(OtherActivity instance) {
    OtherActivity_MembersInjector.injectCoffeeMaker(instance, getCoffeeMaker());
    return instance;
  }

  public static final class Builder {
    private Builder() {}

    public ActivityComponent build() {
      return new DaggerActivityComponent(this);
    }
  }
}

injectメソッドを実行すると、injectOtherActivityが呼ばれている。 ここでは以下の処理が行われている。

OtherActivity_MembersInjector.injectCoffeeMaker(instance, getCoffeeMaker());

コードを追うと以下のようになっている。

public final class OtherActivity_MembersInjector implements MembersInjector<OtherActivity> {
  private final Provider<CoffeeMaker> coffeeMakerProvider;

  public OtherActivity_MembersInjector(Provider<CoffeeMaker> coffeeMakerProvider) {
    this.coffeeMakerProvider = coffeeMakerProvider;
  }

  public static MembersInjector<OtherActivity> create(Provider<CoffeeMaker> coffeeMakerProvider) {
    return new OtherActivity_MembersInjector(coffeeMakerProvider);
  }

  @Override
  public void injectMembers(OtherActivity instance) {
    injectCoffeeMaker(instance, coffeeMakerProvider.get());
  }

  public static void injectCoffeeMaker(OtherActivity instance, CoffeeMaker coffeeMaker) {
    instance.coffeeMaker = coffeeMaker;
  }
}

呼び出されている以下のメソッドを見ると、指定したインスタンスに生成されたインスタンスを設定している。

public static void injectCoffeeMaker(OtherActivity instance, CoffeeMaker coffeeMaker) {
    instance.coffeeMaker = coffeeMaker;
  }

このような仕組みで自身のメンバ変数にDIする書き方も可能である。

まとめ

・ ひとまずDagger2を利用してインスタンス生成までは実行できた。

・ しかしDagger2を利用して本来やりたいことは自動テスト時に対象クラス以外をモッククラスに置き換えることである。 そのためにはModuleとprovideを理解する必要がある。

・ 全ソースコードココ参照

おまけ:自分が理解するうえで詰まった(困惑した)ポイントまとめ

1. なんかいろいろ書き方があってわからない
→指定したクラスの依存性を満たしたインスタンスを返却する方法と、自分自身を指定してメンバに注入する方法が混ざって混乱していた。

2. 注入される側(今回の例で言えばHeaterクラス)のコンストラクタには@Injectいらなくない?注入されるだけだし。
→記入しないと注入するクラスだと認識してくれなくてコードが自動生成されないみたい。

3. RebuildしてもDaggerXXXクラスが自動生成されない....
→MakeProjectしたら動作するようになった。謎。

4. なぜかフィールドインジェクション作成時ビルドが成功しなかった。
→メンバ変数がprivateだとinjectするタイミングで参照できないために、自動生成に失敗していたみたい。privateメンバ変数に値を入れたい場合はコンストラクタインジェクションしかないかも。

Clean ArchitectureでAndroidアプリのクラス設計をしてみる

結論

こんな感じになった。

f:id:off2white:20181213145952p:plain
クラス図

こんなシステムを想定した

  • Webサイトで公開されている小説情報を表示したい
  • Webサイトの検索機能を使えば、小説情報一覧を取得できる
  • 小説情報一覧に含まれるコードを利用すれば、小説の中身が取得できる

Clean Architectureとは

ネットにいくらでも転がっているので割愛。
今回はRobert C. MartinさんのClean Architecture本を参考にした。

クラス設計をしていく上で気になったこと

疑問その1

Webから取得したレスポンスデータを処理してドメインモデルに変換するクラスはどこにあるべきなのか

結論

円の内側のレイヤーは外側レイヤーを知るべきではない。 内側のレイヤーは一番変えてはいけない(変わらない)レイヤー= domain層 のため、externalはdomainを知っていてよいが、domainはexternalを知ってはいけない。そのためexternal層でレスポンスデータを処理し、ドメインモデルを生成する。   

疑問その2

リポジトリを知ってはいけないとすると、ドメインで通信処理がしたくなったときどうすればよいのか

結論

インタフェースをかませるしかない。インターフェース自体の置き場所は、「ドメインは外側を知らない」ルールのため、ドメイン内に置く。

疑問その3

genreは101,102,201などが返却される。
この値は101=異世界[恋愛], 102=現実世界[恋愛]などと対応している。101の値などは基本利用しないため、レスポンスデータからドメインモデルに変換する際に置換して保持すべきではないか

結論

実際の業務に合わせた方がよいと思う。今回の場合は101で実際に管理しているため、ドメイン側でgenreName()を用意し、そこで対応表に合わせた名前を返すようにする。

感想

下位レイヤーと上位レイヤーの考え方が今までと異なっていた(業務ロジックが上で、通信周りは下のイメージだった)ので、そこの切り替えに時間がかかった気がする。

完成してみるとドメインがきれいに切り離されていていい感じ。