Introduce Hilt dependency injection

Uses Hilt to get the database initialization off of the
main thread.

The initial introduction always has a disproportionate
fan-out of boilerplate. In this case, all entry points which
were using UIViewModel needed to be annotated in order to let
the code gen know that they needed to support it.

The PacketRepository is injected into things via the main
thread (e.g., the MeshService) but due to the lazy declaration,
the database isn't hydrated until the DAO is access while on an
IO thread.
pull/360/head
Mike Cumings 2022-02-08 13:50:21 -08:00
rodzic 1f177dc63e
commit 654a32c01c
18 zmienionych plików z 131 dodań i 66 usunięć

Wyświetl plik

@ -2,6 +2,7 @@ apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-parcelize'
apply plugin: 'kotlinx-serialization'
apply plugin: 'dagger.hilt.android.plugin'
apply plugin: 'com.google.gms.google-services'
apply plugin: 'com.github.triplet.play'
apply plugin: 'de.mobilej.unmock'
@ -135,7 +136,9 @@ dependencies {
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.4.0'
implementation "androidx.room:room-runtime:$room_version"
implementation "com.google.dagger:hilt-android:$hilt_version"
kapt "androidx.room:room-compiler:$room_version"
kapt "com.google.dagger:hilt-compiler:$hilt_version"
// optional - Kotlin Extensions and Coroutines support for Room
@ -200,3 +203,7 @@ dependencies {
implementation project(':geeksville-androidlib')
}
kapt {
correctErrorTypes true
}

Wyświetl plik

@ -0,0 +1,18 @@
package com.geeksville.mesh
import android.app.Application
import android.content.Context
import android.content.SharedPreferences
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
@InstallIn(SingletonComponent::class)
@Module
object ApplicationModule {
@Provides
fun provideSharedPreferences(application: Application): SharedPreferences {
return application.getSharedPreferences("ui-prefs", Context.MODE_PRIVATE)
}
}

Wyświetl plik

@ -62,6 +62,7 @@ import com.google.android.material.snackbar.Snackbar
import com.google.android.material.tabs.TabLayoutMediator
import com.vorlonsoft.android.rate.AppRate
import com.vorlonsoft.android.rate.StoreType
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
@ -123,6 +124,7 @@ eventually:
val utf8 = Charset.forName("UTF-8")
@AndroidEntryPoint
class MainActivity : AppCompatActivity(), Logging,
ActivityCompat.OnRequestPermissionsResultCallback {

Wyświetl plik

@ -7,8 +7,13 @@ import com.geeksville.android.GeeksvilleApplication
import com.geeksville.android.Logging
import com.geeksville.util.Exceptions
import com.google.firebase.crashlytics.FirebaseCrashlytics
import dagger.hilt.android.HiltAndroidApp
class MeshUtilApplication : GeeksvilleApplication() {
// NOTE: This is a workaround since the Hilt Gradle plugin doesn't support constructors with default parameters
open class GeeksvilleApplicationWrapper : GeeksvilleApplication()
@HiltAndroidApp
class MeshUtilApplication : GeeksvilleApplicationWrapper() {
override fun onCreate() {
super.onCreate()

Wyświetl plik

@ -0,0 +1,29 @@
package com.geeksville.mesh.database
import android.app.Application
import androidx.room.Room
import com.geeksville.mesh.database.dao.PacketDao
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
@InstallIn(SingletonComponent::class)
@Module
class DatabaseModule {
@Provides
fun provideDatabase(application: Application): MeshtasticDatabase {
return Room.databaseBuilder(
application.applicationContext,
MeshtasticDatabase::class.java,
"meshtastic_database"
)
.fallbackToDestructiveMigration()
.build()
}
@Provides
fun providePacketDao(database: MeshtasticDatabase): PacketDao {
return database.packetDao()
}
}

Wyświetl plik

@ -1,8 +1,6 @@
package com.geeksville.mesh.database
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import com.geeksville.mesh.database.dao.PacketDao
import com.geeksville.mesh.database.entity.Packet
@ -10,27 +8,4 @@ import com.geeksville.mesh.database.entity.Packet
@Database(entities = [Packet::class], version = 1, exportSchema = false)
abstract class MeshtasticDatabase : RoomDatabase() {
abstract fun packetDao(): PacketDao
companion object {
@Volatile
private var INSTANCE: MeshtasticDatabase? = null
fun getDatabase(
context: Context
): MeshtasticDatabase {
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
MeshtasticDatabase::class.java,
"meshtastic_database"
)
.fallbackToDestructiveMigration()
.build()
INSTANCE = instance
instance
}
}
}
}

Wyświetl plik

@ -1,24 +1,34 @@
package com.geeksville.mesh.database
import androidx.lifecycle.LiveData
import com.geeksville.mesh.database.dao.PacketDao
import com.geeksville.mesh.database.entity.Packet
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.withContext
import javax.inject.Inject
class PacketRepository(private val packetDao : PacketDao) {
val allPackets : LiveData<List<Packet>> = packetDao.getAllPacket(MAX_ITEMS)
val allPacketsInReceiveOrder : Flow<List<Packet>> = packetDao.getAllPacketsInReceiveOrder(MAX_ITEMS)
class PacketRepository @Inject constructor(private val packetDaoLazy: dagger.Lazy<PacketDao>) {
private val packetDao by lazy {
packetDaoLazy.get()
}
suspend fun insert(packet: Packet) {
suspend fun getAllPackets(): Flow<List<Packet>> = withContext(Dispatchers.IO) {
packetDao.getAllPacket(MAX_ITEMS)
}
suspend fun getAllPacketsInReceiveOrder(): Flow<List<Packet>> = withContext(Dispatchers.IO) {
packetDao.getAllPacketsInReceiveOrder(MAX_ITEMS)
}
suspend fun insert(packet: Packet) = withContext(Dispatchers.IO) {
packetDao.insert(packet)
}
suspend fun deleteAll() {
suspend fun deleteAll() = withContext(Dispatchers.IO) {
packetDao.deleteAll()
}
companion object {
private const val MAX_ITEMS = 500
}
}

Wyświetl plik

@ -1,6 +1,5 @@
package com.geeksville.mesh.database.dao
import androidx.lifecycle.LiveData
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.Query
@ -11,7 +10,7 @@ import kotlinx.coroutines.flow.Flow
interface PacketDao {
@Query("Select * from packet order by received_date desc limit 0,:maxItem")
fun getAllPacket(maxItem: Int): LiveData<List<Packet>>
fun getAllPacket(maxItem: Int): Flow<List<Packet>>
@Query("Select * from packet order by received_date asc limit 0,:maxItem")
fun getAllPacketsInReceiveOrder(maxItem: Int): Flow<List<Packet>>

Wyświetl plik

@ -7,18 +7,20 @@ import android.net.Uri
import android.os.RemoteException
import android.view.Menu
import androidx.core.content.edit
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.geeksville.android.Logging
import com.geeksville.mesh.*
import com.geeksville.mesh.database.MeshtasticDatabase
import com.geeksville.mesh.database.PacketRepository
import com.geeksville.mesh.database.entity.Packet
import com.geeksville.mesh.service.MeshService
import com.geeksville.mesh.ui.positionToMeter
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@ -26,6 +28,7 @@ import java.io.BufferedWriter
import java.io.FileWriter
import java.text.SimpleDateFormat
import java.util.*
import javax.inject.Inject
import kotlin.math.roundToInt
/// Given a human name, strip out the first letter of the first three words and return that as the initials for
@ -51,20 +54,25 @@ fun getInitials(nameIn: String): String {
return initials.take(nchars)
}
class UIViewModel(private val app: Application) : AndroidViewModel(app), Logging {
@HiltViewModel
class UIViewModel @Inject constructor(
private val app: Application,
private val repository: PacketRepository,
private val preferences: SharedPreferences
) : ViewModel(), Logging {
private val repository: PacketRepository
val allPackets: LiveData<List<Packet>>
private val _allPacketState = MutableStateFlow<List<Packet>>(emptyList())
val allPackets: StateFlow<List<Packet>> = _allPacketState
init {
val packetsDao = MeshtasticDatabase.getDatabase(app).packetDao()
repository = PacketRepository(packetsDao)
allPackets = repository.allPackets
viewModelScope.launch {
repository.getAllPackets().collect { packets ->
_allPacketState.value = packets
}
}
debug("ViewModel created")
}
fun insertPacket(packet: Packet) = viewModelScope.launch(Dispatchers.IO) {
repository.insert(packet)
}
@ -78,8 +86,6 @@ class UIViewModel(private val app: Application) : AndroidViewModel(app), Logging
context.getSharedPreferences("ui-prefs", Context.MODE_PRIVATE)
}
private val context: Context get() = app.applicationContext
var actionBarMenu: Menu? = null
var meshService: IMeshService? = null
@ -208,7 +214,7 @@ class UIViewModel(private val app: Application) : AndroidViewModel(app), Logging
channels.value =
c // Must be done after calling the service, so we will will properly throw if the service failed (and therefore not cache invalid new settings)
getPreferences(context).edit(commit = true) {
preferences.edit(commit = true) {
this.putString("channel-url", c.getChannelUrl().toString())
}
}
@ -226,11 +232,11 @@ class UIViewModel(private val app: Application) : AndroidViewModel(app), Logging
val bluetoothEnabled = object : MutableLiveData<Boolean>(false) {
}
val provideLocation = object : MutableLiveData<Boolean>(getPreferences(context).getBoolean(MyPreferences.provideLocationKey, false)) {
val provideLocation = object : MutableLiveData<Boolean>(preferences.getBoolean(MyPreferences.provideLocationKey, false)) {
override fun setValue(value: Boolean) {
super.setValue(value)
getPreferences(context).edit(commit = true) {
preferences.edit(commit = true) {
this.putBoolean(MyPreferences.provideLocationKey, value)
}
}
@ -246,7 +252,7 @@ class UIViewModel(private val app: Application) : AndroidViewModel(app), Logging
ownerName.value = s
// note: we allow an empty userstring to be written to prefs
getPreferences(context).edit(commit = true) {
preferences.edit(commit = true) {
putString("owner", s)
}
}
@ -286,7 +292,7 @@ class UIViewModel(private val app: Application) : AndroidViewModel(app), Logging
// our device in localNodePosition.
var localNodePosition: MeshProtos.Position? = null
val dateFormat = SimpleDateFormat("yyyy-MM-dd,HH:mm:ss", Locale.getDefault())
repository.allPacketsInReceiveOrder.first().forEach { packet ->
repository.getAllPacketsInReceiveOrder().first().forEach { packet ->
packet.proto?.let { proto ->
packet.position?.let { position ->
if (proto.from == myNodeNum) {

Wyświetl plik

@ -22,7 +22,6 @@ import com.geeksville.mesh.*
import com.geeksville.mesh.MeshProtos.MeshPacket
import com.geeksville.mesh.MeshProtos.ToRadio
import com.geeksville.mesh.android.hasBackgroundPermission
import com.geeksville.mesh.database.MeshtasticDatabase
import com.geeksville.mesh.database.PacketRepository
import com.geeksville.mesh.database.entity.Packet
import com.geeksville.mesh.model.DeviceVersion
@ -36,9 +35,12 @@ import com.google.android.gms.location.LocationServices
import com.google.android.gms.location.LocationSettingsRequest
import com.google.protobuf.ByteString
import com.google.protobuf.InvalidProtocolBufferException
import dagger.Lazy
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.*
import kotlinx.serialization.json.Json
import java.util.*
import javax.inject.Inject
import kotlin.math.absoluteValue
import kotlin.math.max
@ -49,7 +51,10 @@ import kotlin.math.max
* Note: this service will go away once all clients are unbound from it.
* Warning: do not override toString, it causes infinite recursion on some androids (because contextWrapper.getResources calls to string
*/
@AndroidEntryPoint
class MeshService : Service(), Logging {
@Inject
lateinit var packetRepository: Lazy<PacketRepository>
companion object : Logging {
@ -119,9 +124,6 @@ class MeshService : Service(), Logging {
private val serviceScope = CoroutineScope(Dispatchers.IO + serviceJob)
private var connectionState = ConnectionState.DISCONNECTED
/// A database of received packets - used only for debug log
private var packetRepo: PacketRepository? = null
private var fusedLocationClient: FusedLocationProviderClient? = null
// If we've ever read a valid region code from our device it will be here
@ -326,9 +328,6 @@ class MeshService : Service(), Logging {
info("Creating mesh service")
val packetsDao = MeshtasticDatabase.getDatabase(applicationContext).packetDao()
packetRepo = PacketRepository(packetsDao)
// Switch to the IO thread
serviceScope.handledLaunch {
loadSettings() // Load our last known node DB
@ -994,7 +993,7 @@ class MeshService : Service(), Logging {
serviceScope.handledLaunch {
// Do not log, because might contain PII
// info("insert: ${packetToSave.message_type} = ${packetToSave.raw_message.toOneLineString()}")
packetRepo!!.insert(packetToSave)
packetRepository.get().insert(packetToSave)
}
}

Wyświetl plik

@ -6,7 +6,6 @@ import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.EditorInfo
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.Observer
import com.geeksville.android.Logging
import com.geeksville.android.hideKeyboard
import com.geeksville.mesh.R
@ -16,7 +15,9 @@ import com.geeksville.mesh.model.UIViewModel
import com.geeksville.mesh.service.MeshService
import com.geeksville.util.exceptionToSnackbar
import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class AdvancedSettingsFragment : ScreenFragment("Advanced Settings"), Logging {
private val MAX_INT_DEVICE = 0xFFFFFFFF
private var _binding: AdvancedSettingsBinding? = null

Wyświetl plik

@ -33,6 +33,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import com.google.protobuf.ByteString
import com.google.zxing.integration.android.IntentIntegrator
import dagger.hilt.android.AndroidEntryPoint
import java.security.SecureRandom
@ -51,6 +52,7 @@ fun ImageView.setOpaque() {
imageAlpha = 255
}
@AndroidEntryPoint
class ChannelFragment : ScreenFragment("Channel"), Logging {
private var _binding: ChannelFragmentBinding? = null

Wyświetl plik

@ -6,13 +6,15 @@ import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.Observer
import androidx.lifecycle.asLiveData
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.geeksville.mesh.R
import com.geeksville.mesh.databinding.DebugFragmentBinding
import com.geeksville.mesh.model.UIViewModel
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class DebugFragment : Fragment() {
private var _binding: DebugFragmentBinding? = null
@ -48,8 +50,8 @@ class DebugFragment : Fragment() {
binding.closeButton.setOnClickListener {
parentFragmentManager.popBackStack()
}
model.allPackets.observe(viewLifecycleOwner, Observer { packets ->
model.allPackets.asLiveData().observe(viewLifecycleOwner) { packets ->
packets?.let { adapter.setPackets(it) }
})
}
}
}

Wyświetl plik

@ -32,8 +32,10 @@ import com.mapbox.maps.extension.style.sources.generated.GeoJsonSource
import com.mapbox.maps.plugin.animation.MapAnimationOptions
import com.mapbox.maps.plugin.animation.flyTo
import com.mapbox.maps.plugin.gestures.gestures
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class MapFragment : ScreenFragment("Map"), Logging {
private val model: UIViewModel by activityViewModels()

Wyświetl plik

@ -26,6 +26,7 @@ import com.geeksville.mesh.databinding.MessagesFragmentBinding
import com.geeksville.mesh.model.UIViewModel
import com.geeksville.mesh.service.MeshService
import com.google.android.material.chip.Chip
import dagger.hilt.android.AndroidEntryPoint
import java.text.DateFormat
import java.util.*
@ -41,6 +42,7 @@ fun EditText.on(actionId: Int, func: () -> Unit) {
}
}
@AndroidEntryPoint
class MessagesFragment : ScreenFragment("Messages"), Logging {
private var _binding: MessagesFragmentBinding? = null

Wyświetl plik

@ -49,6 +49,7 @@ import com.google.android.gms.location.LocationSettingsRequest
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import com.hoho.android.usbserial.driver.UsbSerialDriver
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
@ -439,6 +440,7 @@ class BTScanModel(app: Application) : AndroidViewModel(app), Logging {
}
@SuppressLint("NewApi")
@AndroidEntryPoint
class SettingsFragment : ScreenFragment("Settings"), Logging {
private var _binding: SettingsFragmentBinding? = null

Wyświetl plik

@ -9,7 +9,6 @@ import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.core.text.HtmlCompat
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.Observer
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.geeksville.android.Logging
@ -19,9 +18,11 @@ import com.geeksville.mesh.databinding.AdapterNodeLayoutBinding
import com.geeksville.mesh.databinding.NodelistFragmentBinding
import com.geeksville.mesh.model.UIViewModel
import com.geeksville.util.formatAgo
import dagger.hilt.android.AndroidEntryPoint
import java.net.URLEncoder
import kotlin.math.roundToInt
@AndroidEntryPoint
class UsersFragment : ScreenFragment("Users"), Logging {
private var _binding: NodelistFragmentBinding? = null

Wyświetl plik

@ -3,6 +3,7 @@
buildscript {
ext.kotlin_version = '1.6.10'
ext.coroutines_version = "1.5.2"
ext.hilt_version = '2.40.5'
repositories {
google()
@ -30,6 +31,8 @@ buildscript {
// for unit testing https://github.com/bjoernQ/unmock-plugin
classpath 'com.github.bjoernq:unmockplugin:0.7.9'
classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version"
}
}