AndroidとかiOSとかモバイル多め。その他技術的なことも書いていきます。

マルチモジュール時代のAdmob設定

広告を表示したいなと思いAdmobを調べていたんですが、せっかくならちゃんとDIしてマルチモジュールで利用できるようにしようと思いやってみました。この記事ではAdmobの設定方法などは省略します。 目標としてはappモジュール(分割済みなら各featureモジュール)からはAdmobを意識せず広告が読み込める(=広告ライブラリが変わっても影響がない)ことです。

環境

  • Dagger 2.23.2
  • firebase-ads 18.1.1
  • googler-services 4.3.0
  • appcomapt 1.0.2
  • recyclerview 1.0.0
  • Kotlin 1.3.41
  • Android Studio 3.4.2

モジュール構成

今回は広告のインターフェースとして advertisementモジュールとその実装として admobモジュールを用意しました。feature_hogeモジュールからはadvertisementモジュールのみが見えており、DIでその実装であるadmobモジュールの中身が渡されるというフローです。appは全てを知っており、DIを解決します。

f:id:phicdy:20190731225247p:plain

advertisementモジュール

advertisementモジュールはインターフェースです。そのためDaggerに依存しません。今回はRecyclerViewの中で使いたかったのでViewHolderを用意し、汎用的にFragmentもabstract classとして用意します。onBindViewHolder()内でAdViewHolder#bind()が呼ばれる想定です。

abstract class AdViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
    abstract fun bind()
}

abstract class AdFragment : Fragment()

これらを動的に利用するために AdProvider インターフェースを提供します。

interface AdProvider {
    fun init(context: Context)
    fun newViewHolderInstance(parent: ViewGroup): AdViewHolder
    fun newFragmentInstance(): AdFragment
}

admobモジュール

admobモジュールはadvertisementモジュールで定義されたインターフェースを実装しDIで提供します。

class AdmobViewHolder(
        parent: ViewGroup,
        itemView: View = LayoutInflater.from(parent.context).inflate(R.layout.item_admob, parent, false)
) : AdViewHolder(itemView) {

    private val adView: AdView = itemView.findViewById(R.id.adView)

    override fun bind() {
        adView.loadAd(AdRequest.Builder().build())
    }
}

class AdmobFragment : AdFragment() {

    override fun onCreateView(
            inflater: LayoutInflater,
            container: ViewGroup?,
            savedInstanceState: Bundle?
    ): View? {
        val rootView = inflater.inflate(R.layout.fragment_admob, container, false)

        val adView = rootView.findViewById<AdView>(R.id.adView)
        adView.loadAd(AdRequest.Builder().build())
        return rootView
    }

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

class AdmobProvider : AdProvider {

    override fun init(context: Context) {
        MobileAds.initialize(context, BuildConfig.AD_APP_ID)
    }

    override fun newViewHolderInstance(parent: ViewGroup): AdViewHolder = AdmobViewHolder(parent)

    override fun newFragmentInstance(): AdFragment = AdmobFragment.newInstance()
}

Admobの設定でApp IDと各広告のUnit IDが必要になります。これらはGitHubに入れたくないため、Googleで用意されているテスト用のデータをプロジェクトの gradle.properties で設定し、本番用のデータを ~/.gradle/gradle.properties に設定することで上書きます。

ADMOB_ID=admob_id
# Test ID
ADMOB_UNIT_ID_MAIN=ca-app-pub-3940256099942544/6300978111
ADMOB_UNIT_ID_SUB=ca-app-pub-3940256099942544/6300978111

この設定をbuild.gradleで読み込み、AndroidManifet/BuildConfig/string resourceに埋め込みます。

android {
    compileSdkVersion 28

    defaultConfig {
        minSdkVersion 21
        targetSdkVersion 28
        versionCode 1
        versionName "1.0"

        manifestPlaceholders = [
                ad_app_id: "$ADMOB_ID"
        ]

        buildConfigField "String", "AD_APP_ID", "\"$ADMOB_ID\""
        resValue "string", "ad_unit_id_main", "$ADMOB_UNIT_ID_MAIN"
        resValue "string", "ad_unit_id_sub", "$ADMOB_UNIT_ID_SUB"
...

}
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.phicdy.sample.admob">

    <uses-permission android:name="android.permission.INTERNET" />

    <application>
        <meta-data
            android:name="com.google.android.gms.ads.APPLICATION_ID"
            android:value="${ad_app_id}" />
    </application>
</manifest>
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <com.google.android.gms.ads.AdView xmlns:ads="http://schemas.android.com/apk/res-auto"
        android:id="@+id/adView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        ads:adSize="BANNER"
        ads:adUnitId="@string/ad_unit_id_main"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

次にDIの設定です。 AdmobProviderAdProvider の実装として提供します。

@Singleton
@Component(
        modules = [AdModule::class]
)
interface AdComponent {
    fun adProvider(): AdProvider

    @Component.Factory
    interface Factory {
        fun create(): AdComponent
    }
}

@Module
class AdModule {

    @Provides
    fun provideAdProvider(): AdProvider = AdmobProvider()
}

appモジュール

appモジュールでは AdComponent を利用して AdProvider を提供します。これでfeature_hogeモジュールでは @Inject するだけでAdProviderが使えるようになります。

@Singleton
@Component(
        modules = [
            AndroidInjectionModule::class,
            AppModule::class,
            ActivityModule::class,
            AdComponentModule::class
        ]
)
interface AppComponent : AndroidInjector<SampleApplication> {
    @Component.Factory
    abstract class Factory : AndroidInjector.Factory<SampleApplication>
}

@Module
object AdComponentModule {
    @JvmStatic
    @Provides
    @Singleton
    fun provideAdProvider(
            adComponent: AdComponent
    ): AdProvider {
        return adComponent.adProvider()
    }

    @JvmStatic
    @Provides
    @Singleton
    fun provideAdComponent() = DaggerAdComponent.factory().create()
}

おわりに

書き終わって思いましたが、これはAdmobに限らずTrackerなど使う側が実装を知らずに利用したい多くのケースで流用できると思います。マルチモジュールにすることでfeatureモジュールがfirebase-adsなどに依存しなくて済むし、差分ビルドが早くなる恩恵も受けられます。