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
17 changes: 14 additions & 3 deletions .github/workflows/android.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
name: Android CI/CD Rho.Studio®
name: Android Debug CI/CD Rho.Studio®

on:
workflow_dispatch:
push:
branches: [ " " ]
branches: [ "27-plan-domain-layer-use-case-implementation" ]
pull_request:
branches: [ "dev" , "pre-release" , "main" ]

Expand All @@ -25,11 +25,22 @@ jobs:

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

- name: Run Unit Tests
run: ./gradlew test

- name: Build with Gradle
run: ./gradlew build
run: ./gradlew assembleDebug

- name: Upload APK
uses: actions/upload-artifact@v7.0.1
with:
name: app-debug
path: app/build/outputs/apk/debug/app-debug.apk

- name: Upload Test Reports
if: always()
uses: actions/upload-artifact@v7.0.1
with:
name: test-reports
path: app/build/reports/tests/
70 changes: 70 additions & 0 deletions CONTRIBUTION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# Architecture & Contribution Guide: Rho Studio UI

## 1. Project Vision & Architecture
Rho Studio UI is a modern Android application built with **Jetpack Compose** and **MVVM** following a **Single-Activity Architecture**.

To achieve enterprise-grade scalability, we strictly implement **Clean Architecture** principles. This ensures a clear separation of concerns, framework independence, and high testability.

---

## 2. Clean Architecture Layer Responsibilities
All contributions must respect the strict boundaries between the following three layers:

### 2.1 The UI Layer (`features/` & `ui/`)
* **Role**: Handles user interaction and data presentation.
* **Components**:
* **Compose Screens/Components**: Purely declarative and stateless. They observe state and emit events.
* **ViewModels**: Act as a bridge. They manage UI state (Loading, Error, Toast) and handle user intent by calling Use Cases.
* **Boundary Rule**: Never contains business logic. Never interacts directly with Repositories.

### 2.2 The Domain Layer (`core/domain/`)
* **Role**: The "Heart" of the application. Contains the essential business rules.
* **Components**:
* **Use Cases (Interactors)**: Classes like `LoginUseCase.kt` that encapsulate a single, atomic business transaction.
* **Domain Models**: Pure data entities (e.g., `User.kt`) that are framework-independent.
* **Boundary Rule**: **Pure Kotlin only**. Must not import `android.*` or depend on any external libraries/frameworks (except pure Kotlin ones). This layer is the "Single Source of Truth" for *logic*.

### 2.3 The Data Layer (`core/data/`)
* **Role**: Manages data acquisition and persistence.
* **Components**:
* **Repositories**: Implementation of data fetching (API, Room, Preferences).
* **Managers**: State holders like `SessionManager.kt` that coordinate global app state.
* **Boundary Rule**: Acts as the "Single Source of Truth" for *data state*. It implements the requirements defined by the Domain layer.

---

## 3. Core Requirements for Contributions

### 3.1 MVVM & UDF (Unidirectional Data Flow)
- **State flows down**: From ViewModel to Composables.
- **Events flow up**: From UI to ViewModel via lambdas.

### 3.2 Single-Activity & Reactive Navigation
- **MainActivity** is the sole navigation orchestrator.
- **ViewModels** and **Use Cases** must **never** hold a `NavController` or trigger navigation directly.
- **Logic**: Use Cases update the session/state in the Data layer. `MainActivity` observes this state and performs the transition (e.g., auto-routing to Login on session expiry).

---

## 4. Implementing New Features (Profile, Feed, Chat)

Every new feature should be built following the **Inside-Out** approach:

1. **Inside (Domain)**: Create the `UseCase` (e.g., `UpdateProfileUseCase`, `GetFeedUseCase`, `SendMessageUseCase`).
- Use the `Result<T>` wrapper for success/failure.
- Write a Unit Test for the logic.
2. **Middle (ViewModel)**: Create the bridge that transforms the Use Case `Result` into observable UI state.
3. **Outside (UI)**: Build the stateless Compose UI.
- **Feed UI**: Use `LazyColumn` for efficiency. Implement a stateless `PostItem.kt`.
- **Chat UI**: Implement specialized "Message Bubble" components. Input fields must update the ViewModel state immediately.

---

## 5. Technical Constraints
- **Atomic Transactions**: Multi-step actions (e.g., validate -> save -> sync) must be managed as a single atomic unit within a `UseCase`.
- **Framework Independence**: Keep the Domain layer free of Android dependencies to support future Gradle modularization.
- **Standardized Results**: Always return `Result.Success`, `Result.Error`, or `Result.Loading` from Use Cases.

---
**[Rho.Studio®](https://rho.studio/) - Engineering Department** - Contact [alexis.tercero@rho.studio](mailto:alexis.tercero@rho.studio)

4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ This report outlines the architecture design of the Rho Studio UI application.

<img width="480" height="940" alt="Image" src="https://github.com/user-attachments/assets/3ad79c42-4806-4dcd-8bb0-920b2587a86d" />

## Contributions

Must follow the next requirements [CONTRIBUTION.md](CONTRIBUTION.md)

---

## 1. Executive Summary
Expand Down
23 changes: 6 additions & 17 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -35,21 +35,6 @@ android {
buildFeatures {
compose = true
}

sourceSets {
getByName("main") {
// Java/Kotlin source directories
java.srcDirs(
"src/main/java" // This includes everything under java/
// No need to list each feature separately
)

// Resource directories - THIS IS KEY FOR FEATURE RESOURCES
res.srcDirs(
"src/main/res" // Global resources
)
}
}
}

// Add the new DSL here
Expand All @@ -60,6 +45,11 @@ kotlin {
}

dependencies {
implementation(project(path = ":core:domain"))
implementation(project(path = ":core:data"))
implementation(project(path = ":core:ui"))
implementation(project(path = ":features:auth"))
implementation(project(path = ":features:home"))

implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
Expand All @@ -71,7 +61,6 @@ dependencies {
implementation(libs.androidx.ui.tooling.preview)
implementation(libs.androidx.material3)
implementation(libs.androidx.compose.material.icons.extended)
implementation(libs.androidx.compose.runtime.livedata)
implementation(libs.androidx.navigation.compose)
implementation(libs.gson)
implementation(libs.material)
Expand All @@ -82,4 +71,4 @@ dependencies {
androidTestImplementation(libs.androidx.ui.test.junit4)
debugImplementation(libs.androidx.ui.tooling)
debugImplementation(libs.androidx.ui.test.manifest)
}
}
33 changes: 18 additions & 15 deletions app/src/main/java/com/rho/studio/ui/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
* File: MainActivity.kt
* Author: Alexis Tercero
* Email: alexis.tercero@rho.studio
* Date: 2026-07-29
* Date: 2026-08-04
* ==========================================================================
* Description:
* The primary entry point for the RHO Studio application, migrated to
Expand All @@ -19,6 +19,8 @@
* 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.
* ==========================================================================
*/
package com.rho.studio.ui
Expand All @@ -32,20 +34,22 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
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.manager.SessionManager
import com.rho.studio.ui.core.data.manager.SessionManager
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.ui.theme.UITheme
import com.rho.studio.ui.core.ui.theme.UITheme
import kotlinx.coroutines.launch

class MainActivity : ComponentActivity() {

Expand All @@ -55,9 +59,7 @@ class MainActivity : ComponentActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

initializeManagers()

setContent {
UITheme {
MainContent()
Expand All @@ -71,27 +73,28 @@ class MainActivity : ComponentActivity() {
loginViewModel = ViewModelProvider(this)[LoginViewModel::class.java]
homeViewModel = ViewModelProvider(this)[HomeViewModel::class.java]

sessionManager.error.observe(this) { error ->
error?.let {
Toast.makeText(this, it, Toast.LENGTH_LONG).show()
sessionManager.clearError()
lifecycleScope.launch {
sessionManager.error.collect { error ->
error?.let {
Toast.makeText(this@MainActivity, it, Toast.LENGTH_LONG).show()
sessionManager.clearError()
}
}
}
}

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

if (!isSessionChecked) {
LoadingScreen()
return
}

// Handle navigation based on auth state
LaunchedEffect(isAuthenticated) {
if (isAuthenticated) {
navController.navigate("home") {
Expand Down Expand Up @@ -145,4 +148,4 @@ class MainActivity : ComponentActivity() {
super.onDestroy()
sessionManager.cleanup()
}
}
}
Loading