From bf782f9072e2fa39db582fbe5d03093b74d87904 Mon Sep 17 00:00:00 2001 From: Moe Hamade <69801237+moehamade@users.noreply.github.com> Date: Sat, 15 Aug 2026 01:35:16 +0300 Subject: [PATCH] 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) Claude-Session: https://claude.ai/code/session_01NaZivRfLu7FdKjzVwwRjcc (cherry picked from commit 33d6b36e220810c87595851ece7e9a63535c61b0) --- .../com/bitchat/android/BitchatApplication.kt | 2 ++ .../java/com/bitchat/android/MainActivity.kt | 14 +++----- .../java/com/bitchat/android/MainViewModel.kt | 5 ++- .../java/com/bitchat/android/di/MeshModule.kt | 36 +++++++++++++++++++ .../android/hotspot/HotspotActivity.kt | 2 ++ .../android/hotspot/HotspotViewModel.kt | 7 +++- .../java/com/bitchat/android/ui/AboutSheet.kt | 4 +-- .../android/ui/ApkDownloadViewModel.kt | 8 +++++ .../com/bitchat/android/ui/ChatViewModel.kt | 5 ++- 9 files changed, 68 insertions(+), 15 deletions(-) create mode 100644 app/src/main/java/com/bitchat/android/di/MeshModule.kt diff --git a/app/src/main/java/com/bitchat/android/BitchatApplication.kt b/app/src/main/java/com/bitchat/android/BitchatApplication.kt index 7b5926ef..b4107ba3 100644 --- a/app/src/main/java/com/bitchat/android/BitchatApplication.kt +++ b/app/src/main/java/com/bitchat/android/BitchatApplication.kt @@ -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() { diff --git a/app/src/main/java/com/bitchat/android/MainActivity.kt b/app/src/main/java/com/bitchat/android/MainActivity.kt index 3d752291..41d95a1e 100644 --- a/app/src/main/java/com/bitchat/android/MainActivity.kt +++ b/app/src/main/java/com/bitchat/android/MainActivity.kt @@ -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 create(modelClass: Class): 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) { diff --git a/app/src/main/java/com/bitchat/android/MainViewModel.kt b/app/src/main/java/com/bitchat/android/MainViewModel.kt index 15ec6fda..bd55ce02 100644 --- a/app/src/main/java/com/bitchat/android/MainViewModel.kt +++ b/app/src/main/java/com/bitchat/android/MainViewModel.kt @@ -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.asStateFlow() diff --git a/app/src/main/java/com/bitchat/android/di/MeshModule.kt b/app/src/main/java/com/bitchat/android/di/MeshModule.kt new file mode 100644 index 00000000..2cb8453c --- /dev/null +++ b/app/src/main/java/com/bitchat/android/di/MeshModule.kt @@ -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) +} diff --git a/app/src/main/java/com/bitchat/android/hotspot/HotspotActivity.kt b/app/src/main/java/com/bitchat/android/hotspot/HotspotActivity.kt index 35d0f4ec..8f894f2d 100644 --- a/app/src/main/java/com/bitchat/android/hotspot/HotspotActivity.kt +++ b/app/src/main/java/com/bitchat/android/hotspot/HotspotActivity.kt @@ -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 { diff --git a/app/src/main/java/com/bitchat/android/hotspot/HotspotViewModel.kt b/app/src/main/java/com/bitchat/android/hotspot/HotspotViewModel.kt index 73f9faa4..d012ccbb 100644 --- a/app/src/main/java/com/bitchat/android/hotspot/HotspotViewModel.kt +++ b/app/src/main/java/com/bitchat/android/hotspot/HotspotViewModel.kt @@ -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" diff --git a/app/src/main/java/com/bitchat/android/ui/AboutSheet.kt b/app/src/main/java/com/bitchat/android/ui/AboutSheet.kt index 54ab0318..0fa6bd72 100644 --- a/app/src/main/java/com/bitchat/android/ui/AboutSheet.kt +++ b/app/src/main/java/com/bitchat/android/ui/AboutSheet.kt @@ -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 diff --git a/app/src/main/java/com/bitchat/android/ui/ApkDownloadViewModel.kt b/app/src/main/java/com/bitchat/android/ui/ApkDownloadViewModel.kt index 4d0c2d5b..dcc6d4af 100644 --- a/app/src/main/java/com/bitchat/android/ui/ApkDownloadViewModel.kt +++ b/app/src/main/java/com/bitchat/android/ui/ApkDownloadViewModel.kt @@ -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), diff --git a/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt b/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt index 76e784bc..ed1b4ec3 100644 --- a/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt +++ b/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt @@ -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