Modern Android App for Sport Trading Progress Tracking
A comprehensive Kotlin app with MVVM, Jetpack Compose, and Clean Architecture
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 |
|
| domain | Business Logic |
|
| data | Data Management |
|
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
- Stock Trade Tracking - GitHub
- KABU Trading - GitHub
- Stock Market Tracker - GitHub
- Jetpack Compose - Android Developers
- Android Architecture - Android Developers
Recommended Further Queries
Last updated March 5, 2025