Case study · Agribusiness
From legacy MVP to MVVM and Clean Architecture
Migrating legacy MVP modules of agribusiness apps to MVVM and Clean Architecture, and automating delivery with CI/CD.
- Role
- Android Engineer I → Android Engineer II
- Company
- Terras App Solutions
- Period
- Apr 2021 – Sep 2023
Context
Terras App Solutions builds native mobile apps for the agribusiness sector. I joined in April 2021 as Android Engineer I, working on Kotlin and Java apps and integrating REST APIs and local Room databases for offline data persistence. In September 2021 I was promoted to Android Engineer II, and I stayed until September 2023.
Challenge
Part of the codebase still followed MVP. In that pattern, presenters hold direct references to their views and tend to mix business rules with UI logic, so changes ripple across layers and are harder to cover with unit tests. Deployments were also a manual process.
The challenge was to modernize that foundation incrementally, module by module, while the apps kept evolving.
Approach
Migrate module by module. Instead of a big-bang rewrite, I migrated legacy MVP modules to MVVM and Clean Architecture one at a time, so each step could be reviewed, tested and released on its own.
State instead of callbacks. Presenters that pushed data into views became ViewModels that expose state. The UI observes it and survives configuration changes without manual lifecycle handling.
// Before: the presenter drives the view directly
class FieldsPresenter(
private val view: FieldsView,
private val api: FieldsApi,
) {
fun load() {
view.showLoading()
api.getFields(
onSuccess = { fields -> view.showFields(fields) },
onError = { view.showError() },
)
}
}
// After: the ViewModel exposes state and the UI only renders it
@HiltViewModel
class FieldsViewModel @Inject constructor(
getFields: GetFieldsUseCase,
) : ViewModel() {
val uiState: StateFlow<FieldsUiState> = getFields()
.map<List<Field>, FieldsUiState> { FieldsUiState.Content(it) }
.catch { emit(FieldsUiState.Error) }
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = FieldsUiState.Loading,
)
}Clear layers. Use cases hold the business rules, and repositories became the single source of truth, combining REST APIs through Retrofit with Room for offline persistence. Dependency injection with Dagger and Hilt made every layer replaceable in tests.
Automated delivery. I set up CI/CD workflows with GitLab and GitHub Actions to build, test and deploy the apps automatically, replacing the manual release steps.
Quality as a habit. Code reviews and unit tests were part of the team’s routine, and the new structure made both easier.
Architecture
Results
- Legacy modules migrated to MVVM and Clean Architecture, with clear boundaries between UI, domain and data, and better long-term maintainability.
- Offline-ready data with Room behind repositories.
- Automated deployments with GitLab and GitHub Actions instead of manual release steps.
- A promotion from Android Engineer I to Android Engineer II.
Stack
The tools behind this work, all used in production:
- Java
- Kotlin
- MVVM
- Clean Architecture
- Room
- Dagger/Hilt
- GitLab CI
- GitHub Actions
Details are simplified and client names are omitted to respect confidentiality.
Next case study
Real-time delivery tracking for drivers
T10Want to build something like this?
I’m open to senior Android roles at international companies.