Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion .github/workflows/android.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name: Android Debug CI/CD Rho.Studio®
on:
workflow_dispatch:
push:
branches: [ " " ]
branches: [ "37-data-layer-and-dagger-di" ]
pull_request:
branches: [ "dev" , "pre-release" ]

Expand All @@ -23,6 +23,12 @@ jobs:
distribution: 'temurin'
cache: gradle

# Add this step here
- name: Decode Google Services JSON
env:
GOOGLE_SERVICES_JSON: ${{ secrets.GOOGLE_SERVICES_JSON }}
run: echo $GOOGLE_SERVICES_JSON | base64 --decode > app/google-services.json

- name: Grant execute permission for gradlew
run: chmod +x gradlew

Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,4 @@ replay_pid*
.idea/
.gradle/
build/
/app/google-services.json
20 changes: 13 additions & 7 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.compose)
alias(libs.plugins.kotlin.parcelize)
alias(libs.plugins.ksp)
alias(libs.plugins.googleServices)
}

android {
Expand Down Expand Up @@ -37,13 +39,6 @@ android {
}
}

// Add the new DSL here
kotlin {
compilerOptions {
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
}
}

dependencies {
implementation(project(path = ":core:domain"))
implementation(project(path = ":core:data"))
Expand All @@ -62,9 +57,20 @@ dependencies {
implementation(libs.androidx.material3)
implementation(libs.androidx.compose.material.icons.extended)
implementation(libs.androidx.navigation.compose)

// Dagger
implementation(libs.dagger)
ksp(libs.dagger.compiler)

// Firebase
implementation(platform(libs.firebase.bom))
implementation(libs.firebase.auth)
implementation(libs.firebase.analytics)

implementation(libs.gson)
implementation(libs.material)
testImplementation(libs.junit)
testImplementation(libs.mockk)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
androidTestImplementation(platform(libs.androidx.compose.bom))
Expand Down
1 change: 1 addition & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
xmlns:tools="http://schemas.android.com/tools">

<application
android:name=".RhoStudioUIApp"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
Expand Down
95 changes: 69 additions & 26 deletions app/src/main/java/com/rho/studio/ui/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,17 @@
* File: MainActivity.kt
* Author: Alexis Tercero
* Email: alexis.tercero@rho.studio
* Date: 2026-08-04
* Date: 2026-08-17
* ==========================================================================
* Description:
* The primary entry point for the RHO Studio application, migrated to
* pure Jetpack Compose.
* Screen Assembly: Built LoginScreen and HomeScreen to unify the components.
* Main Entry Point: Migrated MainActivity to ComponentActivity.
* Compose Navigation: Implemented a NavHost in MainActivity to handle routing
* based on SessionManager state, replacing nav_graph.xml.
* State Management: Switched state observation from LiveData to StateFlow
* using collectAsState() for better compatibility with Compose.
* Navigation: Implemented NavHost for routing based on SessionManager state.
* Dagger Scoping: Orchestrates UserComponent lifecycle, ensuring data cleanup
* on logout via ComponentManager.
* State Management: Uses StateFlow with collectAsState() for Compose compatibility.
* ==========================================================================
*/
package com.rho.studio.ui
Expand All @@ -38,41 +38,42 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import com.rho.studio.ui.core.data.manager.SessionManager
import com.rho.studio.ui.core.domain.model.SessionState
import com.rho.studio.ui.core.ui.common.HeaderViewModel
import com.rho.studio.ui.features.auth.LoginScreen
import com.rho.studio.ui.features.auth.LoginViewModel
import com.rho.studio.ui.features.home.HomeScreen
import com.rho.studio.ui.features.home.HomeViewModel
import com.rho.studio.ui.core.ui.theme.UITheme
import com.rho.studio.ui.di.ComponentManager
import kotlinx.coroutines.launch
import javax.inject.Inject

class MainActivity : ComponentActivity() {

private lateinit var sessionManager: SessionManager
private lateinit var loginViewModel: LoginViewModel
private lateinit var homeViewModel: HomeViewModel

/**
* # Dependency Injection Integration
* Field injection of the SessionManager SSOT. This removes manual singleton access
* and ensures the class is provisioned by the Dagger CoreComponent.
*/
@Inject
lateinit var sessionManager: SessionManager

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
initializeManagers()
setContent {
UITheme {
MainContent()
}
}
}

private fun initializeManagers() {
SessionManager.init(applicationContext)
sessionManager = SessionManager.getInstance()
loginViewModel = ViewModelProvider(this)[LoginViewModel::class.java]
homeViewModel = ViewModelProvider(this)[HomeViewModel::class.java]
// Bootstrapping: Connects the Activity to the Dagger dependency graph.
ComponentManager.getAppComponent().inject(this)

/**
* # Reactive Error Feedback
* Globally collects infrastructure errors and displays them as Toasts,
* regardless of the current navigation destination.
*/
lifecycleScope.launch {
sessionManager.error.collect { error ->
error?.let {
Expand All @@ -81,27 +82,57 @@ class MainActivity : ComponentActivity() {
}
}
}

setContent {
UITheme {
MainContent()
}
}
}

@Composable
private fun MainContent() {
val navController = rememberNavController()
val isSessionChecked by sessionManager.isSessionChecked.collectAsState()
val isAuthenticated by sessionManager.isAuthenticated.collectAsState()

/**
* # Sealed State Management
* Uses a sealed class (SessionState) instead of simple booleans to prevent
* illegal UI states and ensure the UI is always a reflection of the session truth.
*/
val sessionState by sessionManager.sessionState.collectAsState()
val isLoading by sessionManager.isLoading.collectAsState()

val isSessionChecked = sessionState !is SessionState.Uninitialized && sessionState !is SessionState.Checking
val isAuthenticated = sessionState is SessionState.Authenticated

if (!isSessionChecked) {
LoadingScreen()
return
}

/**
* # Reactive ViewModel Provisioning
* Utilizes Compose-native viewModel() pattern to avoid race conditions.
* LoginViewModel is sourced from the persistent AppScope.
*/
val appFactory = ComponentManager.getAppComponent().viewModelFactory()
val loginViewModel: LoginViewModel = androidx.lifecycle.viewmodel.compose.viewModel(factory = appFactory)

LaunchedEffect(isAuthenticated) {
if (isAuthenticated) {
navController.navigate("home") {
popUpTo("login") { inclusive = true }
}
} else {
loginViewModel.resetForm()

/**
* # Secure Session Isolation
* Atomically destroys the authenticated dependency graph on logout,
* ensuring PII (Personally Identifiable Information) is binary-purged from memory.
*/
ComponentManager.destroyUserComponent()

navController.navigate("login") {
popUpTo("home") { inclusive = true }
}
Expand All @@ -117,7 +148,19 @@ class MainActivity : ComponentActivity() {
LoginScreen(viewModel = loginViewModel)
}
composable("home") {
HomeScreen(homeViewModel = homeViewModel)
/**
* # Tiered DI Scoping
* Home and Header ViewModels are provided by the dynamic UserComponent,
* which only exists while the user is actively authenticated.
*/
val userFactory = ComponentManager.createUserComponent().viewModelFactory()
val homeViewModel: HomeViewModel = androidx.lifecycle.viewmodel.compose.viewModel(factory = userFactory)
val headerViewModel: HeaderViewModel = androidx.lifecycle.viewmodel.compose.viewModel(factory = userFactory)

HomeScreen(
homeViewModel = homeViewModel,
headerViewModel = headerViewModel
)
}
}

Expand Down
33 changes: 33 additions & 0 deletions app/src/main/java/com/rho/studio/ui/RhoStudioUIApp.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/**
* ██████╗ ██╗ ██╗ ██████╗ ███████╗████████╗██╗ ██╗██████╗ ██╗ ██████╗
* ██╔══██╗██║ ██║██╔═══██╗ ██╔════╝╚══██╔══╝██║ ██║██╔══██╗██║██╔═══██╗
* ██████╔╝███████║██║ ██║ ███████╗ ██║ ██║ ██║██║ ██║██║██║ ██║
* ██╔══██╗██╔══██║██║ ██║ ╚════██║ ██║ ██║ ██║██║ ██║██║██║ ██║
* ██║ ██║██║ ██║╚██████╔╝ ███████║ ██║ ╚██████╔╝██████╔╝██║╚██████╔╝
* ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝
*
* ==========================================================================
* File: RhoStudioUIApp.kt
* Author: Alexis Tercero
* Email: alexis.tercero@rho.studio
* Date: 2026-08-17
* ==========================================================================
* Description:
* Main Application class for RhoStudio UI. Responsible for initializing
* core services including Firebase and the dependency injection framework.
* ==========================================================================
*/
package com.rho.studio.ui

import android.app.Application
import com.google.firebase.FirebaseApp
import com.rho.studio.ui.di.ComponentManager

class RhoStudioUIApp : Application() {

override fun onCreate() {
super.onCreate()
FirebaseApp.initializeApp(this)
ComponentManager.init(this)
}
}
45 changes: 45 additions & 0 deletions app/src/main/java/com/rho/studio/ui/di/AppComponent.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/**
* ██████╗ ██╗ ██╗ ██████╗ ███████╗████████╗██╗ ██╗██████╗ ██╗ ██████╗
* ██╔══██╗██║ ██║██╔═══██╗ ██╔════╝╚══██╔══╝██║ ██║██╔══██╗██║██╔═══██╗
* ██████╔╝███████║██║ ██║ ███████╗ ██║ ██║ ██║██║ ██║██║██║ ██║
* ██╔══██╗██╔══██║██║ ██║ ╚════██║ ██║ ██║ ██║██║ ██║██║██║ ██║
* ██║ ██║██║ ██║╚██████╔╝ ███████║ ██║ ╚██████╔╝██████╔╝██║╚██████╔╝
* ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝
*
* ==============================================================================================
* File: AppComponent.kt
* Author: Alexis Tercero
* Email: alexis.tercero@rho.studio
* Date: 2026-08-14
* ==============================================================================================
* Description: Root Dagger component for the application.
* Responsible for bridging the core data layers with the UI layer and
* managing the application-wide dependency graph.
* ==============================================================================================
*/
package com.rho.studio.ui.di

import com.rho.studio.ui.MainActivity
import com.rho.studio.ui.core.data.di.CoreComponent
import com.rho.studio.ui.core.ui.di.DaggerViewModelFactory
import com.rho.studio.ui.features.auth.di.AuthModule
import dagger.Component
import javax.inject.Scope

@Scope
@Retention(AnnotationRetention.RUNTIME)
annotation class AppScope

@AppScope
@Component(
dependencies = [CoreComponent::class],
modules = [AuthModule::class, com.rho.studio.ui.core.ui.di.UIModule::class]
)
interface AppComponent {
fun inject(activity: MainActivity)

// Exposed for UserComponent
fun coreComponent(): CoreComponent

fun viewModelFactory(): DaggerViewModelFactory
}
60 changes: 60 additions & 0 deletions app/src/main/java/com/rho/studio/ui/di/ComponentManager.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/**
* ██████╗ ██╗ ██╗ ██████╗ ███████╗████████╗██╗ ██╗██████╗ ██╗ ██████╗
* ██╔══██╗██║ ██║██╔═══██╗ ██╔════╝╚══██╔══╝██║ ██║██╔══██╗██║██╔═══██╗
* ██████╔╝███████║██║ ██║ ███████╗ ██║ ██║ ██║██║ ██║██║██║ ██║
* ██╔══██╗██╔══██║██║ ██║ ╚════██║ ██║ ██║ ██║██║ ██║██║██║ ██║
* ██║ ██║██║ ██║╚██████╔╝ ███████║ ██║ ╚██████╔╝██████╔╝██║╚██████╔╝
* ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝
*
* ==============================================================================================
* File: ComponentManager.kt
* Author: Alexis Tercero
* Email: alexis.tercero@rho.studio
* Date: 2026-08-14
* ==============================================================================================
* Description: Centralized manager for the Dagger component hierarchy.
* Handles the lifecycle of global (App) and scoped (User) components.
* Enables session-based dependency injection by providing mechanisms to
* initialize and tear down the UserComponent upon login/logout.
* ==============================================================================================
*/
package com.rho.studio.ui.di

import android.content.Context
import com.rho.studio.ui.core.data.di.CoreComponent
import com.rho.studio.ui.core.data.di.CoreModule
import com.rho.studio.ui.core.data.di.DaggerCoreComponent

object ComponentManager {

private lateinit var coreComponent: CoreComponent
private lateinit var appComponent: AppComponent
private var userComponent: UserComponent? = null

fun init(context: Context) {
CoreModule.init(context)
coreComponent = DaggerCoreComponent.builder()
.build()

appComponent = DaggerAppComponent.builder()
.coreComponent(coreComponent)
.build()
}

fun getAppComponent(): AppComponent = appComponent

fun createUserComponent(): UserComponent {
if (userComponent == null) {
userComponent = DaggerUserComponent.builder()
.coreComponent(coreComponent)
.build()
}
return userComponent!!
}

fun destroyUserComponent() {
userComponent = null
}

fun getUserComponent(): UserComponent? = userComponent
}
Loading