初心者が流し読みで理解できるAndroidアプリのDI(勉強会資料)


DIとは

Dependency Injectionの略。日本語で「依存性の注入」

コンポーネント間の依存関係を外部から注入するデザインパターン(と言う一文は理解できなくてOK)


DIしたいアプリの例

UrlProviderは、通信先URLを生成するクラス

3つのクラスがUrlProviderを使っている

  1. WebView URLをアプリ内部で表示する
  2. OpenBrowser URLをアプリ外部のブラウザで表示する
  3. HttpClient URLとバックグラウンドで通信する

実装はこんな感じ(関係ない部分は省略)

class WebView {
    private val urlProvider = UrlProvider()
    ・・・
}
class OpenBrowser {
    private val urlProvider = UrlProvider()
    ・・・
}
class HttpClient {
    private val urlProvider = UrlProvider()
    ・・・
}

懸念

UrlProviderを別々に初期化するので、

  1. メモリを2倍、3倍食う
  2. どれかが変更されたら、他2箇所と内容が変わる
  3. UrlProviderが完成するまで、動作確認もテストも出来ない

改善すると、こうなる

class WebView(urlProvider: UrlProvider) { ・・・ }
class OpenBrowser(urlProvider: UrlProvider) { ・・・ }
class HttpClient(urlProvider: UrlProvider) { ・・・ }

class MainActivity {
    private val urlProvider = UrlProvider()
    // 完成までは以下を使う
    // private val urlProvider = MockUrlProvider()
    override fun onCreate(savedInstanceState: Bundle?) {
        val webView = WebView(urlProvider)
        val openBrowser = OpenBrowser(urlProvider)
        val httpClient = HttpClient(urlProvider)
        ・・・
    }
}

先述の一文に則れば、一応この時点でDIできたと言える

[NOTE]オブジェクト生成時に外部から必要なものを注入しているから、DIの条件を満たす


でも、アプリの開発が進んでこうなったら?


悪夢!!


その時の実装はこんな感じ(めっちゃ省略)

class LoginFragment(urlProvider: UrlProvider) { ・・・ }
class WebViewFragment(urlProvider: UrlProvider) { ・・・ }
class HomeFragment(urlProvider: UrlProvider) { ・・・ }
class LoginViewModel(urlProvider: UrlProvider) { ・・・ }
class FaqViewModel(urlProvider: UrlProvider) { ・・・ }
class BannerViewModel(urlProvider: UrlProvider) { ・・・ }
class Login(urlProvider: UrlProvider) { ・・・ }
class LoginRepository(urlProvider: UrlProvider) { ・・・ }
class LoginService(urlProvider: UrlProvider) { ・・・ }

ダサいのは一目瞭然だが、具体的にどうダメなのか

UrlProviderの引数を変えたり、UrlProvider自体を別のものに変えた時、ほぼ全部のクラスを直さなきゃならなくなる


そこでDIツール登場!


あっ、UrlProviderは全クラスに渡さなくてもいいね!(うっかり)

これが最小限のDI実装


DIツールの役割(重要!!)

  1. インスタンスを1個作る(UrlProvider)
  2. 作ったインスタンスを必要としているクラスに配布する(To: FaqViewModel, BannerViewModel, LoginService)
  3. 引数を使わず初期化できるようサポートする(To: HomeFragment, WebViewFragment, LoginRepository)

ちなみに1は、1個と言わず複数作ることも可能(Singletonパターン/Factoryパターンを選べる)


DIツール適用手順

1. DIツールの設定(前頁の役割1,2)

Koinの例

class KoinApp : Application() {
    override fun onCreate() {
        super.onCreate()
        startKoin {
            modules(module)
        }
    }
    val module = module {
        single { UrlProvider() } // インスタンス作成
        single { LoginService(get()) } // 1つ目のクラスにインスタンス注入
        viewModel { FaqViewModel(get()) } // 2つ目のクラスにインスタンス注入
        viewModel { BannerViewModel(get()) } // 3つ目のクラスにインスタンス注入
    }
}

ちなみにインターフェースと実体がある場合はこう書く

single { UrlProviderImpl() as UrlProvider }  

2. 各クラスでの初期化処理(前々頁の役割3)

class LoginRepository() {
    private val loginService: LoginService by inject() // DIツールからインスタンスを受け取る
    ・・・
}

class WebViewFragment : Fragment() {
    private val faqViewModel: FaqViewModel by viewModel() // DIツールからインスタンスを受け取る
    ・・・
}

class HomeFragment : Fragment() {
    private val bannerViewModel: BannerViewModel by viewModel() // DIツールからインスタンスを受け取る
    ・・・
}

このように、DIツールのサポートを受けて初期化する


以上!

DI初心者が流し読みしても理解できそうなテンポで書いたつもりですが、おわかりいただけたでしょうか。

夜中に眠れず勢いで書いた記事なので、ここがわかりづらい!とか間違ってる!とかあればコメントください。


最後にDIのメリット/デメリット

  • メリット
    • クラス同士の依存が減り、クラスの差し替えが容易になる
    • インスタンスをあちこちで使いまわせる
    • 通信処理をダミーに差し替えるなどすれば、単体テストが容易になる
    • クリーンアーキテクチャーを構築できる
  • デメリット
    • 最初から全体を考えて複数クラスを同時実装するので、初期開発コストがかかる
    • 学習コストがかかる
    • 処理速度が多少伸びる
    • DIツール自体を差し替えるのが難しい

最後に

実際にDIを使ったサンプルアプリUnusedAppFinderもあるので、半年間メンテを怠っていますが参考にして下さい。


余談:DIを適用した車!?