マルチモジュール時代の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を解決します。
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の設定です。 AdmobProvider
を AdProvider
の実装として提供します。
@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などに依存しなくて済むし、差分ビルドが早くなる恩恵も受けられます。
Chrome Custom TabでデフォルトブラウザがChrome以外の場合の対応
デフォルトブラウザがChrome以外のとき、FirefoxはChrome Custom Tabで開いてくれるがドルフィンブラウザなどは非対応のブラウザではそちらが開いてしまう。
Chrome Custom TabのIntentに com.android.chrome
を指定してあげればChrome Custom Tabで開けるようになる。万が一Chromeがないまたは無効化されている場合に備えて ActivityNotFoundException
をハンドリングしておく。
今回はSnackbarを指定しているが、もっと丁寧にやるならWebViewで開くようにしてあげるともっといいかもしれない。
try { val customTabsIntent = CustomTabsIntent.Builder() .setShowTitle(true) .setToolbarColor(ContextCompat.getColor(activity, R.color.background_toolbar)) .build() customTabsIntent.intent.setPackage("com.android.chrome") // Chromeを指定 customTabsIntent.launchUrl(context, Uri.parse(url)) } catch (e: ActivityNotFoundException) { // Chromeがない、または無効にされている Snackbar.make(findViewById(R.id.fab), R.string.open_internal_browser_error, Snackbar.LENGTH_SHORT).show() }
Androidのマルチモジュールでのjacoco設定
ハマったのでまとめておく。結論から言うとjacocoにアップロードしてGitHubのPR連携をする場合はレポートをマージする必要はない。マージしてしまうとパスを判別できなくなるせいかエラーになってしまう。
CircleCIで bash <(curl -s https://codecov.io/bash)
としていれば勝手に複数のレポートをアップロードしてくれる。
一方で自分でJenkinsなどを立てている場合はマージしてファイルを1つにしないとグラフ表示などの設定がうまくいかない。
マージする場合
build.gradle
buildscript { ... ext.jacoco_version = "0.8.2" ... } apply plugin: 'jacoco' jacoco { toolVersion = jacoco_version } task jacocoMerge(type: JacocoMerge) { gradle.afterProject { p, state -> if (p.rootProject != p && p.plugins.hasPlugin('jacoco')) { executionData file("${p.buildDir}/jacoco").listFiles().findAll { it.name.endsWith(".exec") } } } } def coverageExcludeFiles = ['**/R.class', '**/R$*.class', '**/com/android/**/*.*', '**/BuildConfig.class', '**/*Activity*.class', '**/*Fragment*.class', '**/*Receiver.class', '**/*Manifest*.class', '**/*Application*.class', ....] task jacocoMergedReport(type: JacocoReport, dependsOn: [tasks.jacocoMerge]) { executionData jacocoMerge.destinationFile def sources = [] subprojects.forEach { sources += "${it.projectDir.path}/src/main/java" } sourceDirectories = files(sources) classDirectories = fileTree( dir: ".", includes: ["**/build/intermediates/classes/debug/**", "**/build/tmp/kotlin-classes/debug/**"], excludes: coverageExcludeFiles ) reports { xml.enabled = true html.enabled true csv.enabled false xml.destination file("${buildDir}/reports/jacoco/report.xml") html.destination file("${buildDir}/reports/jacoco/html") } } task testDebugUnitTest(dependsOn: [tasks.jacocoMergedReport])
マージしない場合
phicdy.hatenablog.com phicdy.hatenablog.com
辺りの設定を各モジュールに設定すればOK。
具体的には↓みたいな設定を jacoco.gradle
としてプロジェクトのルートに置く。
apply plugin: 'jacoco' jacoco { toolVersion = jacoco_version } // A list of directories which should be included in coverage report def coverageSourceDirs = ['src/main/java', 'src/main/kotlin'] // A list of files which should be excluded from coverage report since they are generated and/or framework code def coverageExcludeFiles = ['**/R.class', '**/R$*.class', '**/com/android/**/*.*', '**/BuildConfig.class', '**/*Activity*.class', '**/*Fragment*.class', '**/*Receiver.class', '**/*Manifest*.class', '**/*Application*.class', ...] task jacocoTestReport(type: JacocoReport, dependsOn: ['testDebugUnitTest']) { group = "Reporting" description = "Generate Jacoco coverage reports after running tests." reports { xml.enabled true html.enabled true csv.enabled false xml.destination new File("${buildDir}/reports/jacoco/jacocoTestReport.xml") html.destination new File("${buildDir}/reports/jacoco/html") classDirectories = fileTree(dir: "${buildDir}/intermediates/classes/debug", exclude: coverageExcludeFiles) + fileTree(dir: "$buildDir/tmp/kotlin-classes/debug", excludes: coverageExcludeFiles) } sourceDirectories = files(coverageSourceDirs) executionData = files "${buildDir}/jacoco/testDebugUnitTest.exec" doLast { println "jacoco xml report has been generated to file://${buildDir}/reports/jacoco/report.xml" println "jacoco html report has been generated to file://${reports.html.destination}/index.html" } }
そしてjacocoを使いたいモジュールで apply from: "$rootDir/jacoco.gradle"
するかもしくはルートのbuild.gradleで適用させてしまうと楽。
subprojects { if (name == 'app') { apply plugin: 'com.android.application' } else { apply plugin: 'com.android.library' } apply plugin: 'kotlin-android' apply plugin: 'kotlin-kapt' apply from: "$rootDir/jacoco.gradle" ... }
参考
Koin 2.0へのマイグレーション
先日Koin 2.0が出たのでアップデートしてみました。
エントリポイントの変更
1.0まではApplicationクラスでstartKoin()に自身とモジュールのリストを渡していましたが、DSLになり、個別に指定するようになりました。
- startKoin(this, listOf(appModule)) + startKoin { + androidLogger() + androidContext(this@MyApplication) + modules(listOf(appModule)) + }
スコープの変更
1.0までは自分で名前をつけており、非常に不便でした。2.0からはActivity/Fragmentを型として指定できるので便利です。
- scope("main") { (view: MainActivityView) -> - MainActivityPresenter( - view = view, - hogeRepository = get(), - fugaUseCase = get() - ) + scope(named<MainActivity>()) { + scoped { (view: MainActivityView) -> + MainActivityPresenter( + view = view, + hogeRepository = get(), + fugaUseCase = get() + ) + } }
Activity/Fragment側では1.0までは bindScope(getOrCreateScope("main"))
と書いていましたが不要になりました。そしてinject()の代わりにcurrentScope.inject()を使います。これで自動的にスコープが紐づくようです。
- private val presenter: MainActivityPresenter by inject { parametersOf(this) } + private val presenter: MainActivityPresenter by currentScope.inject { parametersOf(this) }
私の環境ではこれだけでマイグレーションできました。もっといろんな機能を使っている方は公式サイトも2.0のドキュメントになっているのでそちらを参照すれば対応可能かと思います。
おわりに
Annotation Processorを使っているDaggerほどではないもののKoin 2.0ではかなり起動時のパフォーマンスが改善されました。今後にも期待です。
参考
Macbook Pro(15-inch, 2016)のキーボードが2回押されるので修理に出した
これです。
実は買ってから結構序盤の段階でbが2回押されたり1回も押されなかったりする問題が起きてました。上のプログラムが発表されてからいつか修理に出したいなーと思いつつも1週間くらいPCないのはなーと思いとどまってました。このゴールデンウィークで1週間海外旅行に行くことになったのでその間に出すことにしました。
流れ
まずバックアップを取ります。Apple Storeでバックアップ取ったか聞かれます。結局データは消えずに戻ってきました。
バックアップを取ったらApple Storeに行きます(ちゃんと調べてないけどWebから直接修理に出せる気もする)。職場が近いので表参道店に行きました。平日11:30から昼休みを取って行ったんですが、12:40からしか空いてないと言われました。平日でも混んでいる・・・。1時間後にもう1度行きました。
まず症状を伝えたところキーボード内部の掃除をしてくれました。この段階でbが1回も押されない現象は直りました。ただ2回押される問題は直らなかったので結局修理に出すことに。キーボード下の基盤を全交換なので本来ならば88000円のようでしたが上記プログラムがあるので無料でした。
配送と店舗受け取りか選べます。私は店舗受け取りにしました。
4/24に修理に出したのですが4/26にはもう受取可能のメールが来ました。これだったら旅行とかなくても出せばよかった・・・。むしろ到着が早すぎて5/3受け取り以降は要連絡(日程調整が要る?)となってしまったので帰国後すぐ受け取らなければならなかったです。
あとは受け取って終わりです。受け渡しのときにMacbook Proだけ渡されたので何か袋とか大きいバッグを持っていったほうがよいです。
終わりに
2~3日で修理が終わって受け取れるのでいつ出してもよかったなと思いました。キーボードが直っただけでなくディスプレイなども綺麗にしてくれたのでありがたかったです。
Ultimate Hacking Keyboardを買いました
会社ではErgoDoxを使っていますが、リモートワークするときに毎回持って帰るのがめんどくさいなーと思って会社の自作キーボードチャンネルを見ていたところ良さげな分離キーボードだったので買ってみました。去年の11/3に注文して3/21にようやく到着した。
かっちょいい。
箱を開けるとこんな感じ
パームレストも買いました。
値段
計40533円($324.93+2380円)
- キーボード本体: $240
- パームレスト: $55
- 送料: $29.93
- 関税: 2380円
組み立て
半田とかのスキルはないのでキーボードが完成済みなのは助かります。↓に従って組み立てます。
3種類組み立て方があって、今回はTentingを選択。
まずパームレストをつけます。パームレストにネジが入っています。
左右2箇所ずつネジ止めします。
足をつけます。上下に3個ずつのネジをつけます。
足をはめます。
完成。
キーマップ
繋いでみると現在のキーマップが表示されます。MacのQWERTYなのでFn+4で切り替える。
Keymap Name | Keymap Abbreviation | Keymap Switch Shortcut |
---|---|---|
QWERTY for PC | QWR | Fn + 1 |
Dvorak for PC | DVO | Fn + 2 |
Colemak for PC | COL | Fn + 3 |
QWERTY for Mac | QWM | Fn + 4 |
Dvorak for Mac | DVM | Fn + 5 |
Colemak for Mac | COM | Fn + 6 |
Mouseキー
Macの左Ctrlの位置にMouseキーがあります。これでマウスを操作したりクリックしたり画面をスクロールしたりできる模様。
Modキー
矢印キーを打つときはModキーを押しながらjkliで打つ。他にもDeleteキーやHomeキーもModキーを押しながらで打てるようになる。
Agentがあってこれでキーをいじれるようです。 github.com
終わりに
まだまだ操作に慣れないですが、慣れると使いやすそうです。ErgoDoxほど自由ではないかなと思ってましたが以外にカスタマイズできそう。キーボード自体の品質も良くて満足です。ただ来るまで半年近くかかったので今から買う人はそのくらいかかること前提で注文したほうがいいと思います。
追記(2019/3/23)
公式アカウントからリプライが来ました。どうやら4月中旬からは改善されて1週間以内に発送できるようになるようです。
Thank you very much for sharing! The wait time will be much less in the future. It'll take less than a week for us to ship new orders starting from about the middle of April.
— Ult. Hack. Keyboard (@UltHackKeyboard) March 22, 2019
Android Gradle plugin 3.2.0からjacocoでJavaファイルのカバレッジが取れない問題の修正
通常はKotlinだけで書くと思うので関係ないです。Java -> Kotlin移行中のプロジェクトだと起きる。
↓以前の記事
原因
Product Falvorを free
としたとき、Javaのクラスファイルの出力先がAndroid Gradle plugin 3.2.0で ${buildDir}/intermediates/classes/free/debug/
から ${buildDir}/intermediates/javac/freeDebug/compileFreeDebugJavaWithJavac/classes/
に変わった。
修正
あとGradleのアップデートでパスの指定がdeprecatedになったのでそれも修正
task jacocoTestReport(type: JacocoReport, dependsOn: ['testFreeDebugUnitTest']) { group = "Reporting" description = "Generate Jacoco coverage reports after running tests." reports { xml.enabled true html.enabled true csv.enabled false xml.destination new File("${buildDir}/reports/jacoco/jacocoTestReport.xml") html.destination new File("${buildDir}/reports/jacoco/html") classDirectories.setFrom( files( fileTree( dir: "${buildDir}/intermediates/javac/freeDebug/compileFreeDebugJavaWithJavac/classes/", exclude: coverageExcludeFiles ) + fileTree( dir: "$buildDir/tmp/kotlin-classes/freeDebug", excludes: coverageExcludeFiles ) ) ) } sourceDirectories.setFrom(files(coverageSourceDirs)) executionData.setFrom(new File("${buildDir}/jacoco/testFreeDebugUnitTest.exec")) doLast { println "jacoco xml report has been generated to file://${buildDir}/reports/jacoco/jacocoTestReport.xml" println "jacoco html report has been generated to file://${reports.html.destination}/index.html" } }