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.
This commit is contained in:
Mike Cumings 2022-02-08 13:50:21 -08:00
parent 1f177dc63e
commit 654a32c01c
18 changed files with 131 additions and 66 deletions

View file

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

View file

@ -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)
}
}

View file

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

View file

@ -7,8 +7,13 @@ import com.geeksville.android.GeeksvilleApplication
import com.geeksville.android.Logging import com.geeksville.android.Logging
import com.geeksville.util.Exceptions import com.geeksville.util.Exceptions
import com.google.firebase.crashlytics.FirebaseCrashlytics 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() { override fun onCreate() {
super.onCreate() super.onCreate()

View file

@ -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()
}
}

View file

@ -1,8 +1,6 @@
package com.geeksville.mesh.database package com.geeksville.mesh.database
import android.content.Context
import androidx.room.Database import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase import androidx.room.RoomDatabase
import com.geeksville.mesh.database.dao.PacketDao import com.geeksville.mesh.database.dao.PacketDao
import com.geeksville.mesh.database.entity.Packet 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) @Database(entities = [Packet::class], version = 1, exportSchema = false)
abstract class MeshtasticDatabase : RoomDatabase() { abstract class MeshtasticDatabase : RoomDatabase() {
abstract fun packetDao(): PacketDao 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
}
}
}
} }

View file

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

View file

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

View file

@ -7,18 +7,20 @@ import android.net.Uri
import android.os.RemoteException import android.os.RemoteException
import android.view.Menu import android.view.Menu
import androidx.core.content.edit import androidx.core.content.edit
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.geeksville.android.Logging import com.geeksville.android.Logging
import com.geeksville.mesh.* import com.geeksville.mesh.*
import com.geeksville.mesh.database.MeshtasticDatabase
import com.geeksville.mesh.database.PacketRepository import com.geeksville.mesh.database.PacketRepository
import com.geeksville.mesh.database.entity.Packet import com.geeksville.mesh.database.entity.Packet
import com.geeksville.mesh.service.MeshService import com.geeksville.mesh.service.MeshService
import com.geeksville.mesh.ui.positionToMeter import com.geeksville.mesh.ui.positionToMeter
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers 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.flow.first
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -26,6 +28,7 @@ import java.io.BufferedWriter
import java.io.FileWriter import java.io.FileWriter
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*
import javax.inject.Inject
import kotlin.math.roundToInt 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 /// 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) 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 private val _allPacketState = MutableStateFlow<List<Packet>>(emptyList())
val allPackets: StateFlow<List<Packet>> = _allPacketState
val allPackets: LiveData<List<Packet>>
init { init {
val packetsDao = MeshtasticDatabase.getDatabase(app).packetDao() viewModelScope.launch {
repository = PacketRepository(packetsDao) repository.getAllPackets().collect { packets ->
allPackets = repository.allPackets _allPacketState.value = packets
}
}
debug("ViewModel created") debug("ViewModel created")
} }
fun insertPacket(packet: Packet) = viewModelScope.launch(Dispatchers.IO) { fun insertPacket(packet: Packet) = viewModelScope.launch(Dispatchers.IO) {
repository.insert(packet) repository.insert(packet)
} }
@ -78,8 +86,6 @@ class UIViewModel(private val app: Application) : AndroidViewModel(app), Logging
context.getSharedPreferences("ui-prefs", Context.MODE_PRIVATE) context.getSharedPreferences("ui-prefs", Context.MODE_PRIVATE)
} }
private val context: Context get() = app.applicationContext
var actionBarMenu: Menu? = null var actionBarMenu: Menu? = null
var meshService: IMeshService? = null var meshService: IMeshService? = null
@ -208,7 +214,7 @@ class UIViewModel(private val app: Application) : AndroidViewModel(app), Logging
channels.value = 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) 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()) 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 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) { override fun setValue(value: Boolean) {
super.setValue(value) super.setValue(value)
getPreferences(context).edit(commit = true) { preferences.edit(commit = true) {
this.putBoolean(MyPreferences.provideLocationKey, value) this.putBoolean(MyPreferences.provideLocationKey, value)
} }
} }
@ -246,7 +252,7 @@ class UIViewModel(private val app: Application) : AndroidViewModel(app), Logging
ownerName.value = s ownerName.value = s
// note: we allow an empty userstring to be written to prefs // note: we allow an empty userstring to be written to prefs
getPreferences(context).edit(commit = true) { preferences.edit(commit = true) {
putString("owner", s) putString("owner", s)
} }
} }
@ -286,7 +292,7 @@ class UIViewModel(private val app: Application) : AndroidViewModel(app), Logging
// our device in localNodePosition. // our device in localNodePosition.
var localNodePosition: MeshProtos.Position? = null var localNodePosition: MeshProtos.Position? = null
val dateFormat = SimpleDateFormat("yyyy-MM-dd,HH:mm:ss", Locale.getDefault()) 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.proto?.let { proto ->
packet.position?.let { position -> packet.position?.let { position ->
if (proto.from == myNodeNum) { if (proto.from == myNodeNum) {

View file

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

View file

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

View file

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

View file

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

View file

@ -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.MapAnimationOptions
import com.mapbox.maps.plugin.animation.flyTo import com.mapbox.maps.plugin.animation.flyTo
import com.mapbox.maps.plugin.gestures.gestures import com.mapbox.maps.plugin.gestures.gestures
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class MapFragment : ScreenFragment("Map"), Logging { class MapFragment : ScreenFragment("Map"), Logging {
private val model: UIViewModel by activityViewModels() private val model: UIViewModel by activityViewModels()

View file

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

View file

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

View file

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

View file

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