Ask me anything
Ithy Logo
Sign in to save

Modern Android App for Sport Trading Progress Tracking

A comprehensive Kotlin app with MVVM, Jetpack Compose, and Clean Architecture

outdoor sport training equipment

Key Highlights

  • Modular Architecture: Splits the project into app, domain, and data modules for clean separation of concerns.
  • Modern Tools: Uses Kotlin's latest features, Jetpack Compose, Coroutines, Flow, and Hilt for dependency injection.
  • Clean Code Practices: Implements MVVM and Clean Architecture patterns to ensure maintainability and scalability.

Project Overview

This Android application serves as a tool to track the trading progress of a person learning a new sport. It is built using Kotlin and modern Android development practices. The app leverages Jetpack Compose for building a dynamic UI, and implements the MVVM pattern alongside Clean Architecture to keep code modular, testable, and maintainable.

Architecture and Project Structure

The project is divided into three main modules:

  • App Module: Contains all presentation-layer code including UI components built with Jetpack Compose and ViewModels.
  • Domain Module: Contains core business logic, such as use cases and entities that represent progress data.
  • Data Module: Manages data handling including local data (using Room) and remote data sources along with repository implementations.

Module Breakdown

Module Description Key Components
app UI and Presentation
  • MainActivity.kt
  • SportProgressScreen.kt (Compose UI)
  • SportProgressViewModel.kt
  • Navigation using Compose Navigation
domain Business Logic
  • SportProgressEntity.kt
  • GetSportProgressUseCase.kt
  • UpdateSportProgressUseCase.kt
data Data Management
  • SportProgressRepository.kt
  • LocalDataSource (Room implementation)
  • RemoteDataSource (API calls via Retrofit)

Detailed Implementation

App Module

MainActivity.kt

The main entry point to the application handles the initialization of the UI using Jetpack Compose. It sets up the navigation host for screen transitions.


// MainActivity.kt
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import com.example.sporttradetracker.ui.theme.SportTrackerTheme

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            SportTrackerTheme {
                Surface(color = MaterialTheme.colors.background) {
                    NavigationHost()
                }
            }
        }
    }
}
  

SportProgressScreen.kt

A Jetpack Compose screen that displays the trading progress. It observes the ViewModel state and updates the UI accordingly.


// SportProgressScreen.kt
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel

@Composable
fun SportProgressScreen(viewModel: SportProgressViewModel = hiltViewModel()) {
    val progressState = viewModel.progress.collectAsState()
    Column(modifier = Modifier.padding(16.dp)) {
        Text(text = "Trading Progress: ${progressState.value.percentage}%")
        Button(onClick = { viewModel.updateProgress(TrainingProgress(id = 1, percentage = progressState.value.percentage + 5)) }) {
            Text(text = "Update Progress")
        }
    }
}
  

SportProgressViewModel.kt

The ViewModel handles UI-related logic and state management. It uses Kotlin Coroutines for asynchronous operations and integrates business logic via use cases.


// SportProgressViewModel.kt
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import javax.inject.Inject

data class TrainingProgress(val id: Int, val percentage: Int)

class SportProgressViewModel @Inject constructor(
    private val getSportProgressUseCase: GetSportProgressUseCase,
    private val updateSportProgressUseCase: UpdateSportProgressUseCase
) : ViewModel() {
    private val _progress = MutableStateFlow(TrainingProgress(id = 1, percentage = 0))
    val progress: StateFlow<TrainingProgress> = _progress

    init {
        viewModelScope.launch {
            val progressData = getSportProgressUseCase.invoke()
            _progress.value = progressData
        }
    }

    fun updateProgress(newProgress: TrainingProgress) {
        viewModelScope.launch {
            updateSportProgressUseCase.invoke(newProgress)
            _progress.value = newProgress
        }
    }
}
  

Domain Module

SportProgressEntity.kt

Defines the data model for tracking sport progress. This entity is used throughout the business logic and data layers.


// SportProgressEntity.kt
package com.example.sporttradetracker.domain

data class SportProgressEntity(val id: Int, val percentage: Int)
  

Use Cases

The use cases encapsulate business logic for retrieving and updating progress.


// GetSportProgressUseCase.kt
package com.example.sporttradetracker.domain

interface GetSportProgressUseCase {
    suspend fun invoke(): TrainingProgress
}

class GetSportProgressUseCaseImpl(private val repository: SportProgressRepository) : GetSportProgressUseCase {
    override suspend fun invoke(): TrainingProgress {
        val entity = repository.getSportProgress()
        return TrainingProgress(entity.id, entity.percentage)
    }
}
  

// UpdateSportProgressUseCase.kt
package com.example.sporttradetracker.domain

interface UpdateSportProgressUseCase {
    suspend fun invoke(newProgress: TrainingProgress)
}

class UpdateSportProgressUseCaseImpl(private val repository: SportProgressRepository) : UpdateSportProgressUseCase {
    override suspend fun invoke(newProgress: TrainingProgress) {
        repository.updateSportProgress(SportProgressEntity(newProgress.id, newProgress.percentage))
    }
}
  

Data Module

SportProgressRepository.kt

The repository acts as the single source of truth, handling data flows from local and remote sources. Room is used for local persistence, while Retrofit can be integrated to manage remote data.


// SportProgressRepository.kt
package com.example.sporttradetracker.data

import com.example.sporttradetracker.domain.SportProgressEntity

interface SportProgressRepository {
    suspend fun getSportProgress(): SportProgressEntity
    suspend fun updateSportProgress(progress: SportProgressEntity)
}

class SportProgressRepositoryImpl(
    private val localDataSource: LocalSportProgressDataSource,
    private val remoteDataSource: RemoteSportProgressDataSource
) : SportProgressRepository {
    override suspend fun getSportProgress(): SportProgressEntity {
        // Try local data and fallback to remote if necessary
        return localDataSource.getSportProgress() ?: remoteDataSource.getSportProgress()
    }
    
    override suspend fun updateSportProgress(progress: SportProgressEntity) {
        localDataSource.updateSportProgress(progress)
        // Optionally update remote source as well
        remoteDataSource.updateSportProgress(progress)
    }
}
  

Local and Remote Data Sources

The local data source uses Room for data persistence. In contrast, the remote data source might leverage Retrofit for API interactions.


// LocalSportProgressDataSource.kt
package com.example.sporttradetracker.data

import com.example.sporttradetracker.domain.SportProgressEntity

interface LocalSportProgressDataSource {
    suspend fun getSportProgress(): SportProgressEntity?
    suspend fun updateSportProgress(progress: SportProgressEntity)
}

class LocalSportProgressDataSourceImpl(private val sportProgressDao: SportProgressDao) : LocalSportProgressDataSource {
    override suspend fun getSportProgress(): SportProgressEntity? {
        return sportProgressDao.getSportProgress()
    }
    
    override suspend fun updateSportProgress(progress: SportProgressEntity) {
        sportProgressDao.update(progress)
    }
}
  

// RemoteSportProgressDataSource.kt
package com.example.sporttradetracker.data

import com.example.sporttradetracker.domain.SportProgressEntity

interface RemoteSportProgressDataSource {
    suspend fun getSportProgress(): SportProgressEntity
    suspend fun updateSportProgress(progress: SportProgressEntity)
}

class RemoteSportProgressDataSourceImpl(private val apiService: SportProgressApiService) : RemoteSportProgressDataSource {
    override suspend fun getSportProgress(): SportProgressEntity {
        return apiService.fetchSportProgress()
    }
    
    override suspend fun updateSportProgress(progress: SportProgressEntity) {
        apiService.sendSportProgress(progress)
    }
}
  

Dependency Injection

To orchestrate dependencies across modules, a dependency injection framework such as Hilt or Dagger can be used. Integration with Hilt allows ViewModel and repository dependencies to be easily injected into the appropriate components.


// Sample Hilt module providing dependencies
package com.example.sporttradetracker.di

import com.example.sporttradetracker.data.LocalSportProgressDataSource
import com.example.sporttradetracker.data.LocalSportProgressDataSourceImpl
import com.example.sporttradetracker.data.RemoteSportProgressDataSource
import com.example.sporttradetracker.data.RemoteSportProgressDataSourceImpl
import com.example.sporttradetracker.data.SportProgressRepository
import com.example.sporttradetracker.data.SportProgressRepositoryImpl
import com.example.sporttradetracker.domain.GetSportProgressUseCase
import com.example.sporttradetracker.domain.GetSportProgressUseCaseImpl
import com.example.sporttradetracker.domain.UpdateSportProgressUseCase
import com.example.sporttradetracker.domain.UpdateSportProgressUseCaseImpl
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton

@Module
@InstallIn(SingletonComponent::class)
object AppModule {
    @Singleton @Provides
    fun provideLocalDataSource(dao: SportProgressDao): LocalSportProgressDataSource = LocalSportProgressDataSourceImpl(dao)

    @Singleton @Provides
    fun provideRemoteDataSource(apiService: SportProgressApiService): RemoteSportProgressDataSource = RemoteSportProgressDataSourceImpl(apiService)

    @Singleton @Provides
    fun provideRepository(
        localDataSource: LocalSportProgressDataSource,
        remoteDataSource: RemoteSportProgressDataSource
    ): SportProgressRepository = SportProgressRepositoryImpl(localDataSource, remoteDataSource)

    @Singleton @Provides
    fun provideGetProgressUseCase(repository: SportProgressRepository): GetSportProgressUseCase = GetSportProgressUseCaseImpl(repository)

    @Singleton @Provides
    fun provideUpdateProgressUseCase(repository: SportProgressRepository): UpdateSportProgressUseCase = UpdateSportProgressUseCaseImpl(repository)
}
  

Additional Considerations and Enhancements

Reactive Data Handling

Leveraging Kotlin Coroutines with StateFlow or LiveData allows for seamless UI updates when data changes. The unidirectional data flow ensures that the UI remains in sync with the application state, while errors and exceptions from asynchronous operations are handled gracefully.

User Experience Improvements

Future enhancements may include:

  • Improved UI responsiveness using animations in Jetpack Compose.
  • Detailed analytics with charts displaying progress over time.
  • Integration with cloud storage for synchronizing user data across devices.

Testing and Maintenance

By segregating components into clear modules, testing is significantly simplified. Unit tests can target the business logic in the domain layer while integration tests can verify the proper communication between the UI and data sources.


References

Recommended Further Queries

tradingtracker.app
Trading Tracker

Last updated March 5, 2025

AI-assisted research. Sources, methodology and corrections · Source-checked guides and tools · Explore published answers

Ask Ithy AI