kopia lustrzana https://github.com/meshtastic/Meshtastic-Android
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
rodzic
1f177dc63e
commit
654a32c01c
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
|
@ -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>>
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) }
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Ładowanie…
Reference in New Issue