mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-09-19 04:59:59 +00:00
feat(di): inject the phone client's ViewModels with Hilt
Replaces the hand-rolled ViewModelProvider.Factory in MainActivity with constructor injection. No singleton is converted and no behaviour changes; this only moves ViewModel construction into the graph so feature modules can obtain their own ViewModels later. MeshModule bridges the existing process-wide mesh instances into the graph. Its bindings are deliberately unscoped: MeshServiceHolder is the source of truth, it is shared with the foreground service, and it supports replacing the mesh service after a panic clear. A @Singleton binding would pin the first instance for the process lifetime and hand out a stale service after a replacement. ChatViewModel still resolves to a single activity-scoped instance, so the unifiedMeshService.delegate assignment continues to reference the same object it did before. ApkDownloadViewModel is injected through its secondary constructor. Its collaborators are internal types, and routing those through Dagger's generated Java buys nothing; the primary constructor stays available so tests can keep substituting fakes. Verified: :app:assembleDebug, :wear:assembleDebug, and 591 unit tests pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NaZivRfLu7FdKjzVwwRjcc (cherry picked from commit 33d6b36e220810c87595851ece7e9a63535c61b0)
This commit is contained in:
parent
8b7af243b3
commit
bf782f9072
@ -4,10 +4,12 @@ import android.app.Application
|
||||
import com.bitchat.android.nostr.RelayDirectory
|
||||
import com.bitchat.android.ui.theme.ThemePreferenceManager
|
||||
import com.bitchat.android.net.ArtiTorManager
|
||||
import dagger.hilt.android.HiltAndroidApp
|
||||
|
||||
/**
|
||||
* Main application class for bitchat Android
|
||||
*/
|
||||
@HiltAndroidApp
|
||||
class BitchatApplication : Application() {
|
||||
|
||||
override fun onCreate() {
|
||||
|
||||
@ -16,7 +16,6 @@ import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import com.bitchat.android.mesh.BluetoothMeshService
|
||||
@ -46,9 +45,11 @@ import com.bitchat.android.ui.theme.BitchatTheme
|
||||
import com.bitchat.android.wifiaware.WifiAwareController
|
||||
import com.bitchat.android.nostr.PoWPreferenceManager
|
||||
import com.bitchat.android.services.VerificationService
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@AndroidEntryPoint
|
||||
class MainActivity : OrientationAwareActivity() {
|
||||
|
||||
private lateinit var permissionManager: PermissionManager
|
||||
@ -62,15 +63,8 @@ class MainActivity : OrientationAwareActivity() {
|
||||
private lateinit var unifiedMeshService: MeshService
|
||||
private val mainViewModel: MainViewModel by viewModels()
|
||||
private var pendingMeshForegroundServiceStart = false
|
||||
private val chatViewModel: ChatViewModel by viewModels {
|
||||
object : ViewModelProvider.Factory {
|
||||
override fun <T : androidx.lifecycle.ViewModel> create(modelClass: Class<T>): T {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return ChatViewModel(application, meshService, unifiedMeshService) as T
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val chatViewModel: ChatViewModel by viewModels()
|
||||
|
||||
private val forceFinishReceiver = object : android.content.BroadcastReceiver() {
|
||||
override fun onReceive(context: android.content.Context, intent: android.content.Intent) {
|
||||
if (intent.action == com.bitchat.android.util.AppConstants.UI.ACTION_FORCE_FINISH) {
|
||||
|
||||
@ -7,9 +7,12 @@ import com.bitchat.android.onboarding.OnboardingState
|
||||
import com.bitchat.android.onboarding.BatteryOptimizationStatus
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import javax.inject.Inject
|
||||
|
||||
class MainViewModel : ViewModel() {
|
||||
@HiltViewModel
|
||||
class MainViewModel @Inject constructor() : ViewModel() {
|
||||
|
||||
private val _onboardingState = MutableStateFlow(OnboardingState.CHECKING)
|
||||
val onboardingState: StateFlow<OnboardingState> = _onboardingState.asStateFlow()
|
||||
|
||||
36
app/src/main/java/com/bitchat/android/di/MeshModule.kt
Normal file
36
app/src/main/java/com/bitchat/android/di/MeshModule.kt
Normal file
@ -0,0 +1,36 @@
|
||||
package com.bitchat.android.di
|
||||
|
||||
import android.content.Context
|
||||
import com.bitchat.android.mesh.BluetoothMeshService
|
||||
import com.bitchat.android.mesh.MeshService
|
||||
import com.bitchat.android.service.MeshServiceHolder
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
|
||||
/**
|
||||
* Bridges the process-wide mesh instances into the injection graph.
|
||||
*
|
||||
* [MeshServiceHolder] remains the single source of truth: it is shared with the
|
||||
* foreground service, it synchronises creation, and it supports replacing the
|
||||
* mesh service after a panic clear. These bindings are therefore deliberately
|
||||
* unscoped — each injection re-reads the holder. Caching them with @Singleton
|
||||
* would pin the first instance for the process lifetime and hand out a stale
|
||||
* mesh service after a replacement.
|
||||
*/
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object MeshModule {
|
||||
|
||||
@Provides
|
||||
fun provideBluetoothMeshService(
|
||||
@ApplicationContext context: Context
|
||||
): BluetoothMeshService = MeshServiceHolder.getOrCreate(context)
|
||||
|
||||
@Provides
|
||||
fun provideMeshService(
|
||||
@ApplicationContext context: Context
|
||||
): MeshService = MeshServiceHolder.getUnifiedOrCreate(context)
|
||||
}
|
||||
@ -7,6 +7,7 @@ import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.viewModels
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
@ -47,6 +48,7 @@ import java.io.File
|
||||
* Activity for managing Wi-Fi P2P hotspot for offline APK sharing.
|
||||
* Pure Compose implementation, no fragments.
|
||||
*/
|
||||
@AndroidEntryPoint
|
||||
class HotspotActivity : ComponentActivity() {
|
||||
|
||||
companion object {
|
||||
|
||||
@ -7,6 +7,8 @@ import android.util.Log
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.bitchat.android.wifiaware.WifiAwareController
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
@ -16,7 +18,10 @@ import java.io.File
|
||||
/**
|
||||
* ViewModel for managing hotspot state and lifecycle.
|
||||
*/
|
||||
class HotspotViewModel(application: Application) : AndroidViewModel(application) {
|
||||
@HiltViewModel
|
||||
class HotspotViewModel @Inject constructor(
|
||||
application: Application
|
||||
) : AndroidViewModel(application) {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "HotspotViewModel"
|
||||
|
||||
@ -63,8 +63,8 @@ import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.bitchat.android.ui.theme.BitchatFontFamily
|
||||
import com.bitchat.android.R
|
||||
import com.bitchat.android.core.ui.component.button.CloseButton
|
||||
@ -617,7 +617,7 @@ fun AboutSheet(
|
||||
)
|
||||
|
||||
// === Prepare App for Sharing Section ===
|
||||
val apkViewModel: ApkDownloadViewModel = viewModel()
|
||||
val apkViewModel: ApkDownloadViewModel = hiltViewModel()
|
||||
val apkUiState by apkViewModel.state.collectAsStateWithLifecycle()
|
||||
val apkStatus = apkUiState.apkStatus
|
||||
val releaseStatus = apkUiState.releaseStatus
|
||||
|
||||
@ -15,6 +15,8 @@ import com.bitchat.android.util.LatestReleaseProvider
|
||||
import com.bitchat.android.util.ShareableApkVariant
|
||||
import com.bitchat.android.util.UniversalApkManager
|
||||
import com.bitchat.android.util.WorkManagerApkDownloader
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
@ -151,6 +153,7 @@ sealed class ApkUiEffect {
|
||||
* ViewModel for APK download/status/share logic following MVI pattern.
|
||||
* UI sends [ApkUiEvent], observes [ApkUiState], and collects [ApkUiEffect].
|
||||
*/
|
||||
@HiltViewModel
|
||||
class ApkDownloadViewModel internal constructor(
|
||||
application: Application,
|
||||
private val apkManager: UniversalApkManager,
|
||||
@ -158,6 +161,11 @@ class ApkDownloadViewModel internal constructor(
|
||||
private val latestReleaseProvider: LatestReleaseProvider
|
||||
) : AndroidViewModel(application) {
|
||||
|
||||
// Hilt injects through this constructor rather than the primary one:
|
||||
// UniversalApkManager's collaborators are internal, and routing internal
|
||||
// Kotlin types through Dagger's generated Java buys nothing here. Tests
|
||||
// keep using the primary constructor to substitute fakes.
|
||||
@Inject
|
||||
constructor(application: Application) : this(
|
||||
application = application,
|
||||
apkManager = UniversalApkManager(application),
|
||||
|
||||
@ -7,6 +7,8 @@ import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.bitchat.android.favorites.FavoritesChangeListener
|
||||
import com.bitchat.android.favorites.FavoritesPersistenceService
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
@ -53,7 +55,8 @@ private data class ConversationLiveIdentityState(
|
||||
* Refactored ChatViewModel - Main coordinator for bitchat functionality
|
||||
* Delegates specific responsibilities to specialized managers while maintaining 100% iOS compatibility
|
||||
*/
|
||||
class ChatViewModel(
|
||||
@HiltViewModel
|
||||
class ChatViewModel @Inject constructor(
|
||||
application: Application,
|
||||
initialMeshService: BluetoothMeshService,
|
||||
initialUnifiedMeshService: MeshService
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user