SolPan demonstrates a modern, clean Android architecture built on Jetpack Compose, combining single-activity pattern, reactive state management, and modular design. This document provides a comprehensive analysis of the architectural decisions and patterns used.
┌─────────────────────────────────────────────────────┐
│ SolPanActivity (Compose UI) │
│ • enableEdgeToEdge() for immersive UI │
│ • Theme application (Material 3 Expressive) │
└──────────────┬──────────────────────────────────────┘
│
┌──────▼──────────┐
│ SolPanApp() │
│ Navigation │
└──────┬──────────┘
│
┌─────────┴──────────┐
▼ ▼
NavigationSuiteScaffold NavDisplay
(Adaptive Layout) (Navigation3)
SolPan uses a multi-module structure following Android best practices:
:app (Application)
├── :feature:optimizer
│ └── :core:model
│ └── :core:data
│ └── :core:analytics
├── :core:designsystem
├── :core:solar
└── :build-logic (Convention plugins)
| Module | Purpose | Key Classes |
|---|---|---|
:app |
Main application, Activity, UI composition | SolPanActivity, SolPanApp |
:feature:optimizer |
Feature module with ViewModel and business logic | SolPanViewModel, SolPanUiState |
:core:model |
Pure data models (no dependencies) | TiltMode, LocationData, SolarPosition |
:core:data |
Repository implementations | DefaultLocationRepository, DataStoreUserPreferencesRepository |
:core:analytics |
Analytics abstraction | AnalyticsTracker, FirebaseAnalyticsTracker |
:core:designsystem |
Reusable UI components, theming | SolPanTheme, Material 3 components |
:core:solar |
Domain logic for solar calculations | SolarCalculator (wraps commons-suncalc) |
:build-logic |
Gradle convention plugins | AndroidApplicationConventionPlugin, AndroidLibraryConventionPlugin |
Each layer depends on abstractions, not concrete implementations:
SolPanViewModel
└── depends on
├── MagneticDeclinationProvider (interface)
├── LocationRepository (interface)
└── UserPreferencesRepository (interface)
Implementations are injected at composition time, enabling:
- Easy testing with fake implementations
- Swappable backends
- Clear separation of concerns
SolPanViewModel uses Kotlin Flow to combine multiple data streams:
// ViewModel combines multiple reactive sources
val optimalPanelParameters: StateFlow<OptimalPanelParameters?> = combine(
currentLocation, // Location from GPS
magneticDeclinationFlow, // Calculated or fetched
currentOrientation, // From device sensors
selectedTiltMode, // User selection
) { location, declination, orientation, mode ->
if (location != null) {
calculateOptimalPanel(location, declination, orientation, mode)
} else null
}.stateIn(viewModelScope, WhileSubscribed(5000), null)Why this pattern?
- Reactive: Updates propagate automatically
- Testable: Each stream can be mocked independently
- Efficient: Uses WhileSubscribed(5000) for resource management
- Type-safe: No casting needed
User Interaction
↓
ViewModel collects state
↓
StateFlow emits updates
↓
Composable recomposes
↓
Screen updates
class SolPanActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge() // Modern full-screen UI
setContent { SolPanTheme { SolPanApp(modifier = Modifier.fillMaxSize()) } }
}
}- ViewModel survives configuration changes (Activity recreation)
- ViewModelScope tied to ViewModel lifecycle
- Automatic cleanup via
WhileSubscribedscope - No memory leaks from Composables holding ViewModels
SolPan uses Navigation3 with Compose:
- RememberViewModelStoreNavEntryDecorator: Preserves ViewModel across navigation
- RememberSaveableStateHolderNavEntryDecorator: Preserves Compose state
SolPan uses Material Design 3's Expressive system:
SolPanTheme { // Provides colors, typography, shapes
NavigationSuiteScaffold { // Adaptive layout (phone/tablet)
SolPanScreen() // Main content
}
}NavigationSuiteScaffold automatically adapts:
- Phone: Bottom navigation bar
- Tablet: Lateral navigation rail
- Foldable: Responds to hinge position
NavigationSuiteScaffold(
navigationSuiteItems = {
TiltMode.entries.forEach { mode ->
item(
selected = currentScreen is SolPan && currentScreen.mode == mode,
onClick = { /* navigate */ },
icon = { Icon(mode.icon, ...) },
label = { Text(stringResource(mode.titleRes)) },
)
}
}
)Type-safe navigation using kotlinx.serialization:
@Serializable
data class SolPan(val mode: TiltMode) : NavKey
@Serializable
data object AboutLibraries : NavKeyBenefits:
- Compile-time type safety
- State preservation across app kills
- No string-based routes (less error-prone)
User grants location permission
↓
DeviceLocationManager.startLocationUpdates()
↓
Google Play Services Fused Location Provider
↓
LocationRepository.locationFlow emits LocationData
↓
SolPanViewModel receives update
↓
combine() recalculates OptimalPanelParameters
↓
optimalPanelParameters.collect() in UI
↓
Composable recomposes with new data
↓
Screen shows updated panel orientation
// Production
val viewModel = SolPanViewModel(
initialMode = TiltMode.REALTIME,
preferencesRepository = DataStoreUserPreferencesRepository(context),
locationRepository = DefaultLocationRepository(),
analytics = FirebaseAnalyticsTracker(),
)
// Testing
val viewModel = SolPanViewModel(
initialMode = TiltMode.REALTIME,
preferencesRepository = FakeUserPreferencesRepository(),
locationRepository = DefaultLocationRepository(),
analytics = FakeAnalyticsTracker(),
magneticDeclinationProvider = FakeMagneticDeclinationProvider(),
)GeomagneticField is a native Android API that fails in unit tests:
// Production: Uses real GeomagneticField
class AndroidMagneticDeclinationProvider : MagneticDeclinationProvider {
override fun getDeclination(...) = GeomagneticField(...).declination
}
// Testing: Returns fixed value
class FakeMagneticDeclinationProvider : MagneticDeclinationProvider {
override fun getDeclination(...) = 10f
}-
Cold start (TTID < 1.5s)
- enableEdgeToEdge() avoids window insets calculation
- Baseline profiles pre-compile critical paths
- ViewModel lazy initialization
-
Recomposition Efficiency
- Composables are pure functions
- Modifiers are immutable
- State changes only trigger affected branches
-
Memory Usage
- Location and sensor data not persisted
- DataStore for user preferences only
- Coroutines properly scoped
Located in :baselineprofile module:
- Pre-compiles startup path
- Reduces jank in critical user flows
- Generated via Chrome tracing
- Location: Only accessed with explicit user permission
- Sensors: Processed locally, never logged
- User Prefs: Encrypted in DataStore
- Analytics: Configured to exclude PII
permissions.forEach { permission ->
if (ActivityCompat.checkSelfPermission(context, permission) != PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(activity, arrayOf(permission), REQUEST_CODE)
}
}- Add variant to TiltMode enum
- Add calculation logic to SolarCalculator
- Add UI option to navigation
- ViewModel automatically adapts
- Implement Repository interface
- Inject into ViewModel
- Add to Compose remember { } block
- Fully testable
- Extend AnalyticsTracker interface
- Implement in FirebaseAnalyticsTracker
- Call from ViewModel or Composables
- Abstracting makes it swappable
- Single Responsibility: Each module has one reason to change
- Dependency Inversion: Depend on abstractions, not concretions
- Composition: Build UI from small, reusable composables
- Reactivity: Data flows drive UI updates
- Testability: Architecture enables easy unit testing
- Accessibility: Semantic descriptions throughout
| Pattern | SolPan | Use Case |
|---|---|---|
| MVI | ✓ | Uses ModelViewIntent-like patterns |
| MVVM | ✓ | ViewModel + StateFlow |
| Redux | ✗ | Too complex for single screen |
| MVP | ✗ | Compose replaces View layer |
| Clean Architecture | ✓ | Modular layers with clear contracts |
SolPan's architecture demonstrates best practices for modern Android development:
- Modular structure for scalability
- Reactive patterns for responsive UI
- Dependency injection for testability
- Clean separation of concerns
- Modern Jetpack libraries
- Accessibility-first design
The architecture scales from a single-screen app to a full-featured application while maintaining code quality and testability throughout.