Replace Koin with Dagger Hilt

This commit is contained in:
William Brawner 2021-06-03 19:47:06 -06:00
parent bdacaaae34
commit 3f98551b9d
15 changed files with 102 additions and 70 deletions

View file

@ -5,6 +5,8 @@ import java.io.FileNotFoundException
plugins { plugins {
id("com.android.application") id("com.android.application")
id("kotlin-android") id("kotlin-android")
id("kotlin-kapt")
id("dagger.hilt.android.plugin")
} }
val keystoreProperties = Properties() val keystoreProperties = Properties()
@ -64,7 +66,8 @@ android {
dependencies { dependencies {
implementation(project(":piholeclient")) implementation(project(":piholeclient"))
implementation(libs.bundles.coroutines) implementation(libs.bundles.coroutines)
implementation("io.insert-koin:koin-androidx-viewmodel:${libs.versions.koin.get()}") implementation(libs.hilt.android.core)
kapt(libs.hilt.android.kapt)
implementation(libs.androidx.core) implementation(libs.androidx.core)
implementation(libs.androidx.appcompat) implementation(libs.androidx.appcompat)
implementation(libs.material) implementation(libs.material)

View file

@ -7,17 +7,20 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import com.wbrawner.piholeclient.PiHoleApiService import com.wbrawner.piholeclient.PiHoleApiService
import com.wbrawner.piholeclient.VersionResponse import com.wbrawner.piholeclient.VersionResponse
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.net.ConnectException import java.net.ConnectException
import java.net.SocketTimeoutException import java.net.SocketTimeoutException
import javax.inject.Inject
const val KEY_BASE_URL = "baseUrl" const val KEY_BASE_URL = "baseUrl"
const val KEY_API_KEY = "apiKey" const val KEY_API_KEY = "apiKey"
const val IP_MIN = 0 const val IP_MIN = 0
const val IP_MAX = 255 const val IP_MAX = 255
class AddPiHelperViewModel( @HiltViewModel
class AddPiHelperViewModel @Inject constructor(
private val sharedPreferences: SharedPreferences, private val sharedPreferences: SharedPreferences,
private val apiService: PiHoleApiService private val apiService: PiHoleApiService
) : ViewModel() { ) : ViewModel() {

View file

@ -10,15 +10,17 @@ import android.view.animation.LinearInterpolator
import android.view.animation.RotateAnimation import android.view.animation.RotateAnimation
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.FragmentNavigatorExtras import androidx.navigation.fragment.FragmentNavigatorExtras
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import com.wbrawner.pihelper.databinding.FragmentAddPiHoleBinding import com.wbrawner.pihelper.databinding.FragmentAddPiHoleBinding
import org.koin.android.ext.android.inject import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class AddPiHoleFragment : Fragment() { class AddPiHoleFragment : Fragment() {
private val viewModel: AddPiHelperViewModel by inject() private val viewModel: AddPiHelperViewModel by activityViewModels()
private var _binding: FragmentAddPiHoleBinding? = null private var _binding: FragmentAddPiHoleBinding? = null
private val binding get() = _binding!! private val binding get() = _binding!!

View file

@ -13,13 +13,15 @@ import android.view.ViewGroup
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import com.wbrawner.pihelper.MainActivity.Companion.ACTION_FORGET_PIHOLE import com.wbrawner.pihelper.MainActivity.Companion.ACTION_FORGET_PIHOLE
import com.wbrawner.pihelper.databinding.FragmentInfoBinding import com.wbrawner.pihelper.databinding.FragmentInfoBinding
import org.koin.android.ext.android.inject import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class InfoFragment : Fragment() { class InfoFragment : Fragment() {
private val viewModel: AddPiHelperViewModel by inject() private val viewModel: AddPiHelperViewModel by activityViewModels()
private var _binding: FragmentInfoBinding? = null private var _binding: FragmentInfoBinding? = null
private val binding get() = _binding!! private val binding get() = _binding!!

View file

@ -3,17 +3,20 @@ package com.wbrawner.pihelper
import android.graphics.drawable.ColorDrawable import android.graphics.drawable.ColorDrawable
import android.os.Bundle import android.os.Bundle
import android.widget.Toast import android.widget.Toast
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.navigation.NavController import androidx.navigation.NavController
import androidx.navigation.findNavController import androidx.navigation.findNavController
import com.wbrawner.pihelper.MainFragment.Companion.ACTION_DISABLE import com.wbrawner.pihelper.MainFragment.Companion.ACTION_DISABLE
import com.wbrawner.pihelper.MainFragment.Companion.ACTION_ENABLE import com.wbrawner.pihelper.MainFragment.Companion.ACTION_ENABLE
import com.wbrawner.pihelper.MainFragment.Companion.EXTRA_DURATION import com.wbrawner.pihelper.MainFragment.Companion.EXTRA_DURATION
import com.wbrawner.pihelper.databinding.ActivityMainBinding import com.wbrawner.pihelper.databinding.ActivityMainBinding
import org.koin.android.ext.android.inject import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class MainActivity : AppCompatActivity() { class MainActivity : AppCompatActivity() {
private val addPiHoleViewModel: AddPiHelperViewModel by inject() private val addPiHoleViewModel: AddPiHelperViewModel by viewModels()
private val navController: NavController by lazy { private val navController: NavController by lazy {
findNavController(R.id.content_main) findNavController(R.id.content_main)
} }
@ -22,7 +25,14 @@ class MainActivity : AppCompatActivity() {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val binding = ActivityMainBinding.inflate(layoutInflater) val binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
window.setBackgroundDrawable(ColorDrawable(getColor(R.color.colorSurface))) window.setBackgroundDrawable(
ColorDrawable(
ContextCompat.getColor(
this,
R.color.colorSurface
)
)
)
val args = when (intent.action) { val args = when (intent.action) {
ACTION_ENABLE -> { ACTION_ENABLE -> {
if (addPiHoleViewModel.apiKey == null) { if (addPiHoleViewModel.apiKey == null) {

View file

@ -16,16 +16,18 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat.getColor import androidx.core.content.ContextCompat.getColor
import androidx.core.text.set import androidx.core.text.set
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import com.wbrawner.pihelper.databinding.DialogDisableCustomTimeBinding import com.wbrawner.pihelper.databinding.DialogDisableCustomTimeBinding
import com.wbrawner.pihelper.databinding.FragmentMainBinding import com.wbrawner.pihelper.databinding.FragmentMainBinding
import com.wbrawner.piholeclient.Status import com.wbrawner.piholeclient.Status
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koin.android.ext.android.inject
@AndroidEntryPoint
class MainFragment : Fragment() { class MainFragment : Fragment() {
private val viewModel: PiHelperViewModel by inject() private val viewModel: PiHelperViewModel by activityViewModels()
private var _binding: FragmentMainBinding? = null private var _binding: FragmentMainBinding? = null
private val binding get() = _binding!! private val binding get() = _binding!!

View file

@ -1,23 +1,7 @@
package com.wbrawner.pihelper package com.wbrawner.pihelper
import android.app.Application import android.app.Application
import android.content.Context import dagger.hilt.android.HiltAndroidApp
import com.wbrawner.piholeclient.piHoleClientModule
import org.koin.android.ext.koin.androidContext
import org.koin.android.ext.koin.androidLogger
import org.koin.core.context.startKoin
@Suppress("unused") @HiltAndroidApp
class PiHelperApplication: Application() { class PiHelperApplication : Application()
override fun onCreate() {
super.onCreate()
startKoin{
androidLogger()
androidContext(this@PiHelperApplication)
modules(listOf(
piHoleClientModule,
piHelperModule
))
}
}
}

View file

@ -1,33 +1,36 @@
package com.wbrawner.pihelper package com.wbrawner.pihelper
import android.content.Context
import android.content.SharedPreferences
import androidx.security.crypto.EncryptedSharedPreferences import androidx.security.crypto.EncryptedSharedPreferences
import com.wbrawner.piholeclient.NAME_BASE_URL import com.wbrawner.piholeclient.NAME_BASE_URL
import org.koin.androidx.viewmodel.dsl.viewModel import dagger.Module
import org.koin.core.qualifier.named import dagger.Provides
import org.koin.dsl.module import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Named
import javax.inject.Singleton
const val ENCRYPTED_SHARED_PREFS_FILE_NAME = "pihelper.prefs" const val ENCRYPTED_SHARED_PREFS_FILE_NAME = "pihelper.prefs"
val piHelperModule = module { @Module
single { @InstallIn(SingletonComponent::class)
object PiHelperModule {
@Provides
@Singleton
fun providesSharedPreferences(@ApplicationContext context: Context): SharedPreferences =
EncryptedSharedPreferences.create( EncryptedSharedPreferences.create(
ENCRYPTED_SHARED_PREFS_FILE_NAME, ENCRYPTED_SHARED_PREFS_FILE_NAME,
"pihelper", "pihelper",
get(), context,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
) )
}
viewModel { @Provides
AddPiHelperViewModel(get(), get()) @Singleton
} @Named(NAME_BASE_URL)
fun providesBaseUrl(sharedPreferences: SharedPreferences) = sharedPreferences
viewModel { .getString(KEY_BASE_URL, "")
PiHelperViewModel(get())
}
single(named(NAME_BASE_URL)) {
get<EncryptedSharedPreferences>().getString(KEY_BASE_URL, "")
}
} }

View file

@ -5,11 +5,14 @@ import androidx.lifecycle.ViewModel
import com.wbrawner.piholeclient.PiHoleApiService import com.wbrawner.piholeclient.PiHoleApiService
import com.wbrawner.piholeclient.Status import com.wbrawner.piholeclient.Status
import com.wbrawner.piholeclient.StatusProvider import com.wbrawner.piholeclient.StatusProvider
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
import javax.inject.Inject
import kotlin.coroutines.coroutineContext import kotlin.coroutines.coroutineContext
class PiHelperViewModel( @HiltViewModel
class PiHelperViewModel @Inject constructor(
private val apiService: PiHoleApiService private val apiService: PiHoleApiService
) : ViewModel() { ) : ViewModel() {
val status = MutableLiveData<Status>() val status = MutableLiveData<Status>()

View file

@ -10,15 +10,17 @@ import android.view.animation.Animation
import android.view.animation.LinearInterpolator import android.view.animation.LinearInterpolator
import android.view.animation.RotateAnimation import android.view.animation.RotateAnimation
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.transition.TransitionInflater import androidx.transition.TransitionInflater
import com.wbrawner.pihelper.databinding.FragmentRetrieveApiKeyBinding import com.wbrawner.pihelper.databinding.FragmentRetrieveApiKeyBinding
import org.koin.android.ext.android.inject import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class RetrieveApiKeyFragment : Fragment() { class RetrieveApiKeyFragment : Fragment() {
private val viewModel: AddPiHelperViewModel by inject() private val viewModel: AddPiHelperViewModel by activityViewModels()
private var _binding: FragmentRetrieveApiKeyBinding? = null private var _binding: FragmentRetrieveApiKeyBinding? = null
private val binding get() = _binding!! private val binding get() = _binding!!
@ -50,8 +52,9 @@ class RetrieveApiKeyFragment : Fragment() {
viewModel.authenticateWithPassword(binding.password.text.toString()) viewModel.authenticateWithPassword(binding.password.text.toString())
} catch (ignored: Exception) { } catch (ignored: Exception) {
Log.e("Pi-helper", "Failed to authenticate with password", ignored) Log.e("Pi-helper", "Failed to authenticate with password", ignored)
binding.password.error = "Failed to authenticate with given password. Please verify " + binding.password.error =
"you've entered it correctly and try again." "Failed to authenticate with given password. Please verify " +
"you've entered it correctly and try again."
showProgress(false) showProgress(false)
} }
} }

View file

@ -15,6 +15,7 @@ import android.view.animation.LinearInterpolator
import android.view.animation.RotateAnimation import android.view.animation.RotateAnimation
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.FragmentNavigatorExtras import androidx.navigation.fragment.FragmentNavigatorExtras
@ -22,15 +23,16 @@ import androidx.navigation.fragment.findNavController
import androidx.transition.Transition import androidx.transition.Transition
import androidx.transition.TransitionInflater import androidx.transition.TransitionInflater
import com.wbrawner.pihelper.databinding.FragmentScanNetworkBinding import com.wbrawner.pihelper.databinding.FragmentScanNetworkBinding
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koin.android.ext.android.inject
import java.net.Inet4Address import java.net.Inet4Address
@AndroidEntryPoint
class ScanNetworkFragment : Fragment() { class ScanNetworkFragment : Fragment() {
private val viewModel: AddPiHelperViewModel by inject() private val viewModel: AddPiHelperViewModel by activityViewModels()
private var _binding: FragmentScanNetworkBinding? = null private var _binding: FragmentScanNetworkBinding? = null
private val binding get() = _binding!! private val binding get() = _binding!!

View file

@ -3,9 +3,11 @@ buildscript {
google() google()
mavenCentral() mavenCentral()
} }
val hiltVersion by extra("2.36")
val kotlinVersion by extra("1.4.32") val kotlinVersion by extra("1.4.32")
dependencies { dependencies {
classpath("com.android.tools.build:gradle:7.0.0-beta03") classpath("com.android.tools.build:gradle:7.0.0-beta03")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlinVersion}") classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlinVersion}")
classpath("com.google.dagger:hilt-android-gradle-plugin:$hiltVersion")
} }
} }

View file

@ -3,13 +3,13 @@ androidx-core = "1.3.2"
androidx-appcompat = "1.2.0" androidx-appcompat = "1.2.0"
coroutines = "1.4.3" coroutines = "1.4.3"
espresso = "3.3.0" espresso = "3.3.0"
koin = "2.2.3" hilt-android = "2.36"
kotlin = "1.4.32" kotlin = "1.4.32"
lifecycle = "2.2.0" lifecycle = "2.2.0"
material = "1.3.0" material = "1.3.0"
maxSdk = "30" maxSdk = "30"
moshi = "1.9.2" moshi = "1.9.2"
minSdk = "21" minSdk = "23"
navigation = "2.3.2" navigation = "2.3.2"
okhttp = "4.2.2" okhttp = "4.2.2"
versionCode = "1" versionCode = "1"
@ -18,6 +18,8 @@ versionName = "1.0"
[libraries] [libraries]
androidx-core = { module = "androidx.core:core-ktx", version.ref = "androidx-core" } androidx-core = { module = "androidx.core:core-ktx", version.ref = "androidx-core" }
androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidx-appcompat" } androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidx-appcompat" }
hilt-android-core = { module = "com.google.dagger:hilt-android", version.ref = "hilt-android" }
hilt-android-kapt = { module = "com.google.dagger:hilt-compiler", version.ref = "hilt-android" }
material = { module = "com.google.android.material:material", version.ref = "material" } material = { module = "com.google.android.material:material", version.ref = "material" }
moshi-core = { module = "com.squareup.moshi:moshi", version.ref = "moshi" } moshi-core = { module = "com.squareup.moshi:moshi", version.ref = "moshi" }
moshi-codegen = { module = "com.squareup.moshi:moshi-kotlin-codegen", version.ref = "moshi" } moshi-codegen = { module = "com.squareup.moshi:moshi-kotlin-codegen", version.ref = "moshi" }

View file

@ -2,6 +2,7 @@ plugins {
id("com.android.library") id("com.android.library")
id("kotlin-android") id("kotlin-android")
id("kotlin-kapt") id("kotlin-kapt")
id("dagger.hilt.android.plugin")
} }
android { android {
@ -30,14 +31,15 @@ android {
} }
dependencies { dependencies {
implementation("io.insert-koin:koin-core:${libs.versions.koin.get()}") api("com.squareup.okhttp3:okhttp:${libs.versions.okhttp.get()}")
implementation("com.squareup.okhttp3:okhttp:${libs.versions.okhttp.get()}")
implementation("com.squareup.okhttp3:logging-interceptor:${libs.versions.okhttp.get()}") implementation("com.squareup.okhttp3:logging-interceptor:${libs.versions.okhttp.get()}")
implementation(libs.androidx.core) implementation(libs.androidx.core)
implementation(libs.androidx.appcompat) implementation(libs.androidx.appcompat)
implementation(libs.bundles.coroutines) implementation(libs.bundles.coroutines)
implementation(libs.hilt.android.core)
kapt(libs.hilt.android.kapt)
implementation(libs.kotlin.reflect) implementation(libs.kotlin.reflect)
implementation(libs.moshi.core) api(libs.moshi.core)
kapt(libs.moshi.codegen) kapt(libs.moshi.codegen)
testImplementation(libs.junit) testImplementation(libs.junit)
} }

View file

@ -2,23 +2,31 @@ package com.wbrawner.piholeclient
import com.squareup.moshi.JsonReader import com.squareup.moshi.JsonReader
import com.squareup.moshi.Moshi import com.squareup.moshi.Moshi
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import okhttp3.Cookie import okhttp3.Cookie
import okhttp3.CookieJar import okhttp3.CookieJar
import okhttp3.HttpUrl import okhttp3.HttpUrl
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor import okhttp3.logging.HttpLoggingInterceptor
import okio.Buffer import okio.Buffer
import org.koin.dsl.module
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import javax.inject.Singleton
const val NAME_BASE_URL = "baseUrl" const val NAME_BASE_URL = "baseUrl"
val piHoleClientModule = module { @Module
single { @InstallIn(SingletonComponent::class)
Moshi.Builder().build() object PiHoleClientModule {
} @Provides
@Singleton
fun providesMoshi(): Moshi = Moshi.Builder().build()
single { @Provides
@Singleton
fun providesOkHttpClient(): OkHttpClient {
val client = OkHttpClient.Builder() val client = OkHttpClient.Builder()
.connectTimeout(500, TimeUnit.MILLISECONDS) .connectTimeout(500, TimeUnit.MILLISECONDS)
.cookieJar(object : CookieJar { .cookieJar(object : CookieJar {
@ -53,10 +61,11 @@ val piHoleClientModule = module {
} }
) )
} }
client.build() return client.build()
} }
single<PiHoleApiService> { @Provides
OkHttpPiHoleApiService(get(), get()) @Singleton
} fun providesPiHoleApiService(okHttpClient: OkHttpClient, moshi: Moshi): PiHoleApiService =
OkHttpPiHoleApiService(okHttpClient, moshi)
} }