Skip to Content
FeaturesDashboardMobile integration

Mobile Dashboard Integration

Guide to the Android dashboard implementation using Clean Architecture and MVI — unified data source, widget aggregation, and card state management.

Overview

Successfully migrated the mobile app (Android) to use a unified dashboard data source, following Clean Architecture principles and MVI (Model-View-Intent) pattern, consistent with mobile best practices.

Architecture

Clean Architecture layers for the Android dashboard — ViewModel injects DashboardUseCases, never the repository or service directly.

✅ What Was Created

0. Clean Architecture Layers

Following the established pattern in the mobile app (similar to tasks module), the home feature is structured in three layers:

Data Layer (home_data)

Location: /home/home_data/src/main/java/.../

  • DashboardApi.kt: Retrofit interface for API endpoints
  • DashboardDataSource.kt: Interface defining data operations
  • DashboardRemoteDataSource.kt: Implementation fetching from server API
  • DashboardRepositoryImpl.kt: Repository implementation (implements domain interface)
  • HomeDataModule.kt: Dagger module providing data layer dependencies
@Module @InstallIn(SingletonComponent::class) object HomeDataModule { @Provides fun provideDashboardApi(client: OkHttpClient): DashboardApi @Provides fun provideDashboardRepository( @Remote remoteDataSource: DashboardDataSource, networkHelper: NetworkHelper ): DashboardRepository }

Domain Layer (home_domain)

Location: /home/home_domain/src/main/java/.../

  • DashboardRepository.kt: Repository interface (no implementation details)
  • GetDashboard.kt: Use case for fetching dashboard data
  • RefreshDashboard.kt: Use case for refreshing dashboard
  • DashboardUseCases.kt: Wrapper bundling all use cases
  • HomeDomainModule.kt: Dagger module providing use cases
@Module @InstallIn(ViewModelComponent::class) object HomeDomainModule { @Provides fun provideDashboardUseCases( repository: DashboardRepository ): DashboardUseCases }

Presentation Layer (home_presentation)

Location: /home/home_presentation/src/main/java/.../

  • DashboardIntent.kt: User actions (MVI Intent)
  • DashboardState.kt: UI state (MVI State)
  • DashboardEffect.kt: Side effects like navigation (MVI Effect)
  • DashboardViewModel.kt: ViewModel using use cases (NOT repository or service)

Key Principle: ViewModel injects DashboardUseCases, NOT DashboardService or DashboardRepository

1. MVI Layer (Intent, State, Effect)

Location: /home/home_presentation/src/main/java/.../intent/

DashboardIntent.kt

sealed class DashboardIntent : UserIntent { data object FetchDashboard data object RefreshDashboard data class MarkTaskDone(val task: Task) data class MarkHabitDone(val habit: Habit) data object NavigateToTasks data object NavigateToHabits data object NavigateToWorkout // ... and more }

DashboardState.kt

data class DashboardState( val isLoading: Boolean, val nextTask: Task?, val taskStats: TaskStats?, val overdueTasks: List<Task>, val nextHabit: Habit?, val habitStats: HabitStats?, val overdueHabits: List<Habit>, val todayCalories: Int, val todayWorkout: WorkoutDTO?, // ... 40+ dashboard fields ) : ViewState

DashboardEffect.kt

sealed class DashboardEffect : Effect { data object NavigateToTasks data object NavigateToHabits data class ShowSnackBar(message: String, type: SnackbarType) // ... navigation effects }

2. ViewModel with MVI Pattern

Location: /home/home_presentation/src/main/java/.../viewmodel/DashboardViewModel.kt

@HiltViewModel class DashboardViewModel @Inject constructor( private val dashboardUseCases: DashboardUseCases, private val tasksUseCases: TasksUseCases, private val habitUseCases: HabitUseCases ) : BaseViewModel<DashboardIntent, DashboardState, DashboardEffect>()

Key Features:

  • Follows existing MVI pattern used in mobile app
  • Extends BaseViewModel<Intent, State, Effect>
  • Injects DashboardUseCases (Clean Architecture - domain layer)
  • Uses emitState {} for state updates
  • Uses sendEffect {} for navigation
  • Automatic dashboard fetch on initialization
  • Pull-to-refresh support
  • Mark tasks/habits as done with optimistic UI updates

Example: Fetching Dashboard Data

private fun fetchDashboard() { viewModelScope.launch { emitState { copy(isLoading = true, error = null) } val result = dashboardUseCases.getDashboard() result.onSuccess { response -> emitState { DashboardState.fromDashboardResponse(response) } } result.onFailure { e -> emitState { copy(isLoading = false, error = e.message) } sendErrorEffect(UiText.DynamicString(e.message ?: "Failed to load")) } } }

Notice: Use cases return Result<T>, enabling clean error handling with onSuccess/onFailure.

3. Dependency Injection (Clean Architecture)

The dependency injection follows Clean Architecture principles with clear separation:

HomeDataModule (Data → Domain)

Location: /home/home_data/src/main/java/.../di/HomeDataModule.kt

Provides data layer dependencies and binds repository implementation to domain interface:

@Module @InstallIn(SingletonComponent::class) object HomeDataModule { @Provides @Singleton fun provideDashboardApi(client: OkHttpClient): DashboardApi @Provides @Singleton fun provideDashboardRepository( @Remote remoteDataSource: DashboardDataSource, networkHelper: NetworkHelper ): DashboardRepository // Returns domain interface }

HomeDomainModule (Domain → Presentation)

Location: /home/home_domain/src/main/java/.../di/HomeDomainModule.kt

Provides use cases to ViewModels:

@Module @InstallIn(ViewModelComponent::class) object HomeDomainModule { @ViewModelScoped @Provides fun provideDashboardUseCases( repository: DashboardRepository ): DashboardUseCases // Injected into ViewModels }

Important: Presentation layer has NO DI module. ViewModels directly inject use cases from domain layer.

📋 How to Integrate into HomeScreen

Step 1: Add Dashboard ViewModel to HomeScreen

@OptIn(ExperimentalMaterialApi::class) @Composable fun HomeScreen( habitViewModel: HabitViewModel = hiltViewModel(), taskViewModel: TaskViewModel = hiltViewModel(), workoutViewModel: WorkoutDetailViewModel = hiltViewModel(), dashboardViewModel: DashboardViewModel = hiltViewModel(), // ← ADD THIS homeViewModel: HomeViewModel = hiltViewModel(), onGoToTasks: () -> Unit, onGoToWorkout: () -> Unit, onCurrentHabitClick: (Habit?) -> Unit, onLogout: () -> Unit = {} ) { // Collect dashboard state val dashboardState by dashboardViewModel.viewState.collectAsState() // Collect dashboard effects for navigation LaunchedEffect(Unit) { dashboardViewModel.effect.collect { effect -> when (effect) { is DashboardEffect.NavigateToTasks -> onGoToTasks() is DashboardEffect.NavigateToHabits -> { /* navigate */ } is DashboardEffect.NavigateToWorkout -> onGoToWorkout() // Handle other effects else -> {} } } } // ... rest of HomeScreen }

Step 2: Update Pull-to-Refresh

Replace the manual refresh with dashboard refresh:

val pullRefreshState = rememberPullRefreshState( refreshing = dashboardState.isRefreshing, onRefresh = { dashboardViewModel.performAction(DashboardIntent.RefreshDashboard) } )

Step 3: Use Dashboard Data Instead of Separate ViewModels

Before (separate calls):

val habitState = habitViewModel.viewState.collectAsState() val taskState = taskViewModel.viewState.collectAsState() val workoutState = workoutViewModel.viewState.collectAsState()

After (unified dashboard):

val dashboardState by dashboardViewModel.viewState.collectAsState() // Access data from dashboard state val nextTask = dashboardState.nextTask val overdueTasks = dashboardState.overdueTasks val nextHabit = dashboardState.nextHabit val overdueHabits = dashboardState.overdueHabits val todayWorkout = dashboardState.todayWorkout

Step 4: Update Mark As Done Actions

// For tasks onClick = { dashboardViewModel.performAction( DashboardIntent.MarkTaskDone(task) ) } // For habits onClick = { dashboardViewModel.performAction( DashboardIntent.MarkHabitDone(habit) ) }

Step 5: Update Initial Data Fetch

Remove the separate fetch calls:

// DELETE THESE: LaunchedEffect(Unit) { habitViewModel.performAction(HabitIntent.FetchHabits()) taskViewModel.performAction(TaskIntent.FetchTasks()) workoutViewModel.performAction(WorkoutIntent.FetchWorkoutByDay(...)) }

Dashboard ViewModel fetches everything automatically on initialization!

📊 Available Dashboard Data

The DashboardState provides all dashboard data from the unified API:

Tasks

  • nextTask: Task? - Next upcoming task
  • taskStats: TaskStats? - Total, completed, high priority counts
  • overdueTasks: List<Task> - All overdue tasks
  • weeklyTaskCompletion: Float - Completion rate (0.0-1.0)
  • tasksCompletedPerDay: List<Int> - Last 7 days

Habits

  • nextHabit: Habit? - Next scheduled habit
  • habitStats: HabitStats? - Total, completed, current streak
  • overdueHabits: List<Habit> - All overdue habits
  • weeklyHabitCompletion: Float - Completion rate
  • habitsCompletedPerDay: List<Int> - Last 7 days

Workout

  • todayWorkout: WorkoutDTO? - Today’s workout
  • caloriesBurned: Int - Total calories burned
  • workoutStreak: Int - Current streak
  • weeklyWorkoutCompletion: Float - Completion rate
  • workoutsCompletedPerDay: List<Int> - Last 7 days

Nutrition

  • todayCalories: Int - Total calories today
  • mealsLogged: Int - Number of meals logged
  • nextMeal: MealDTO? - Next scheduled meal
  • plannedMeals: Int - Planned meals today
  • unexpectedMeals: Int - Unplanned meals today
  • weeklyMealLogging: Float - Logging rate
  • mealsLoggedPerDay: List<Int> - Last 7 days

Finance

  • recentTransactions: List<TransactionDTO> - Latest transactions
  • accountBalance: Double - Current balance

Journal

  • journalCompleted: Boolean - Today’s journal status
  • journalStreak: Int - Current streak
  • recentJournalEntries: List<JournalEntryDTO> - Recent entries

🎨 UI Components

You can continue using existing mobile components:

  • SharedSectionCard - For dashboard sections
  • SharedTaskCard - For individual tasks
  • SharedWelcomeCard - For welcome section
  • OterDesignSystem - For consistent styling

Example: Display Overdue Tasks

SharedSectionCard( title = "Overdue Tasks", subtitle = "${dashboardState.overdueTasks.size} tasks need attention", iconColor = OterDesignSystem.colors.Error, onHeaderClick = { dashboardViewModel.performAction(DashboardIntent.NavigateToTasks) }, iconContent = { Icon( Icons.Default.Error, contentDescription = null, tint = Color.White ) } ) { dashboardState.overdueTasks.take(3).forEach { task -> SharedTaskCard( task = task, onTaskClick = { /* handle click */ }, onMarkDone = { dashboardViewModel.performAction( DashboardIntent.MarkTaskDone(task) ) } ) } }

🔄 Migration Benefits

  1. Single API Call: One /api/v1/dashboard call instead of multiple
  2. Consistent Data: Server calculates everything (next task, overdue, stats)
  3. Clean Architecture: Clear separation of concerns (Data → Domain → Presentation)
  4. MVI Compliance: Follows established mobile architecture pattern
  5. Type Safety: Full Kotlin type safety with data classes
  6. Automatic Refresh: Dashboard auto-fetches on init
  7. Optimistic Updates: Instant UI feedback for mark done actions
  8. Testability: Each layer can be tested independently
  9. Maintainability: Domain layer is platform-agnostic and reusable

🧪 Testing

Each layer can be tested independently following Clean Architecture principles:

Testing Use Cases (Domain Layer)

@Test fun `GetDashboard returns success when repository succeeds`() = runTest { // Given val mockRepository = mock<DashboardRepository>() val expectedResponse = DashboardResponse(...) whenever(mockRepository.getDashboardData()).thenReturn(Result.success(expectedResponse)) val useCase = GetDashboard(mockRepository) // When val result = useCase() // Then assertTrue(result.isSuccess) assertEquals(expectedResponse, result.getOrNull()) }

Testing ViewModel (Presentation Layer)

@Test fun `fetch dashboard updates state correctly`() = runTest { // Given val mockUseCases = mock<DashboardUseCases>() val mockGetDashboard = mock<GetDashboard>() whenever(mockUseCases.getDashboard).thenReturn(mockGetDashboard) whenever(mockGetDashboard()).thenReturn(Result.success(DashboardResponse(...))) val viewModel = DashboardViewModel(mockUseCases, ...) // When viewModel.performAction(DashboardIntent.FetchDashboard) // Then verify(mockGetDashboard).invoke() assertEquals(false, viewModel.currentState.isLoading) }

Testing Repository (Data Layer)

@Test fun `DashboardRepository returns data from remote source`() = runTest { // Given val mockDataSource = mock<DashboardDataSource>() val mockNetworkHelper = mock<NetworkHelper>() whenever(mockNetworkHelper.isNetworkAvailable()).thenReturn(true) val repository = DashboardRepositoryImpl(mockDataSource, mockNetworkHelper) // When val result = repository.getDashboardData() // Then verify(mockDataSource).getDashboardData() assertTrue(result.isSuccess) }

📝 Next Steps

  1. Update HomeScreen.kt to use DashboardViewModel
  2. Remove old ViewModels (HabitViewModel, TaskViewModel, WorkoutViewModel from HomeScreen)
  3. Test pull-to-refresh functionality
  4. Test mark as done actions
  5. Verify navigation effects work correctly
  6. Add error handling UI for failed dashboard loads

🚀 Performance Notes

  • Dashboard data is cached by the server
  • Mobile app makes 1 API call vs 3+ previously
  • Reduced network usage and battery consumption
  • Faster initial load time

Mobile Implementation (Clean Architecture)

  • Domain Layer: /home/home_domain/src/main/java/.../
    • DashboardRepository.kt (interface)
    • GetDashboard.kt, RefreshDashboard.kt (use cases)
    • HomeDomainModule.kt (DI)
  • Data Layer: /home/home_data/src/main/java/.../
    • DashboardApi.kt (Retrofit)
    • DashboardRepositoryImpl.kt (implementation)
    • HomeDataModule.kt (DI)
  • Presentation Layer: /home/home_presentation/src/main/java/.../
    • DashboardIntent.kt, DashboardState.kt, DashboardEffect.kt (MVI)
    • DashboardViewModel.kt

Shared/Server

  • Shared Models: /shared/src/commonMain/.../models/dashboard/DashboardModels.kt
  • Server Endpoint: /server/.../routing/DashboardRouting.kt
  • Server Service: /server/.../service/DashboardService.kt

Other Platforms

  • Desktop Implementation: /composeApp/src/desktopMain/.../DashboardScreen.kt

Documentation

  • API Documentation: /docs/agents/dashboard-api-reference.md
  • This Guide: docs/features/dashboard/mobile-integration.md

Status: ✅ Ready for integration Architecture: Clean Architecture + MVI (Model-View-Intent) Platform: Android Pattern: Data → Domain → Presentation Testing: Unit tests can be added for each layer