Architecture: MVC / MVP / MVVM (Java)
Architecture is about organizing code so teams can add features safely, fix bugs quickly, and test logic easily. On Android, MVVM (Model–View–ViewModel) is a great default because it fits the platform’s lifecycles (Activity/Fragment), encourages a separation of concerns, and works seamlessly with ViewModel + LiveData.
At a Glance
MVC
Activity = Controller + View (often “Massive Activity”)
MVP
Presenter holds logic, View interface; lifecycle wiring needed
MVVM
ViewModel holds state & business logic; View observes it
Comparison Table
| Aspect | MVC | MVP | MVVM |
|---|---|---|---|
| UI State holder | Activity/Fragment | Presenter | ViewModel (LiveData) |
| Communication | Direct method calls | View ↔ Presenter (interfaces) | View observes ViewModel |
| Rotation handling | Manual save/restore | Presenter survives? usually no | ViewModel survives rotation |
| Boilerplate | Low but messy | Medium (interfaces) | Low–Medium (Jetpack helps) |
| Testability | Hard (UI mixed) | Good | Great (logic in VM/UseCases) |
| Recommended for Android | ❌ | ✅ (legacy) | ✅✅ (default) |
Simple MVVM Structure (Java)
Model & Repository
// model/User.java
public class User {
public final String id;
public final String name;
public User(String id, String name){ this.id=id; this.name=name; }
}
// data/UserRepository.java
public interface UserRepository {
Result<User> getUserById(String id); // sync example for brevity
}
// data/Result.java (generic success/error wrapper)
public abstract class Result<T> {
public static final class Success<T> extends Result<T> { public final T data; public Success(T d){data=d;} }
public static final class Error<T> extends Result<T> { public final Throwable error; public Error(Throwable e){error=e;} }
}
ViewModel (holds UI state)
// ui/viewmodel/ProfileViewModel.java
public class ProfileViewModel extends ViewModel {
private final UserRepository repo;
private final MutableLiveData<Boolean> loading = new MutableLiveData<>(false);
private final MutableLiveData<User> user = new MutableLiveData<>();
private final MutableLiveData<String> error = new MutableLiveData<>();
public ProfileViewModel(UserRepository r){ this.repo = r; }
public LiveData<Boolean> getLoading(){ return loading; }
public LiveData<User> getUser(){ return user; }
public LiveData<String> getError(){ return error; }
public void load(String id) {
loading.setValue(true);
// pretend background thread; in real apps use Executor/Retrofit callback
new Thread(() -> {
Result<User> res = repo.getUserById(id);
if (res instanceof Result.Success) {
User u = ((Result.Success<User>) res).data;
user.postValue(u);
} else {
error.postValue(((Result.Error<User>) res).error.getMessage());
}
loading.postValue(false);
}).start();
}
}
Activity observes state
// ui/ProfileActivity.java
public class ProfileActivity extends AppCompatActivity {
ProfileViewModel vm;
@Override protected void onCreate(Bundle s) {
super.onCreate(s);
setContentView(R.layout.activity_profile);
TextView tvName = findViewById(R.id.tvName);
ProgressBar pb = findViewById(R.id.progress);
TextView tvError = findViewById(R.id.tvError);
vm = new ViewModelProvider(this, new ViewModelProvider.Factory() {
@NonNull @Override
public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
return (T) new ProfileViewModel(new UserRepositoryImpl()); // basic DI
}
}).get(ProfileViewModel.class);
vm.getLoading().observe(this, show -> pb.setVisibility(show ? View.VISIBLE : View.GONE));
vm.getUser().observe(this, u -> tvName.setText(u.name));
vm.getError().observe(this, e -> tvError.setText(e));
vm.load("42");
}
}
Why MVVM Is a Great Default on Android
- Lifecycle-friendly: ViewModel survives configuration changes. LiveData stops/resumes automatically.
- Clear boundaries: UI code (View) stays simple; business rules live in ViewModel/UseCases.
- Scales well: Teams can work on UI and domain layers independently.
- Testable: ViewModel is plain Java; easy to unit test without Android framework.
- Less glue: Compared to MVP, fewer interfaces and no manual attach/detach of Presenters.
Clean Architecture Layers (Optional but Helpful)
| Layer | Responsibility | Contains | Depends On |
|---|---|---|---|
| UI | Render state, forward events | Activities/Fragments/Adapters | ViewModel |
| Presentation | State + logic | ViewModel, Mappers | Domain (use cases) or Repository |
| Domain | Business rules | UseCases, Entities | Pure Java (no Android) |
| Data | Access data sources | Repository, DAOs, API services | Networking/DB libs |
Error Handling & Result Types
Expose UI-friendly state: Loading, Success(Data), Error(message). Either use a sealed-style class or multiple LiveData.
// ui/common/ViewState.java
public abstract class ViewState<T> {
public static final class Loading<T> extends ViewState<T> {}
public static final class Success<T> extends ViewState<T> { public final T data; public Success(T d){data=d;} }
public static final class Error<T> extends ViewState<T> { public final String message; public Error(String m){message=m;} }
}
Threading Strategy
- Do network/DB work off the main thread (Executor, Retrofit callbacks).
- Push results to LiveData via postValue() from background threads.
- Transform data in ViewModel (formatting, mapping) before reaching the UI.
Dependency Injection (Dagger 2 – Java)
DI helps swap implementations (e.g., FakeRepository for tests). For Java projects, Dagger 2 is common.
// Example: provide UserRepository
@Module
class DataModule {
@Provides UserRepository provideRepo(){ return new UserRepositoryImpl(); }
}
Testing MVVM
| Layer | Test Type | What to Assert |
|---|---|---|
| ViewModel | Unit | LiveData emits Loading → Success/Error; mapping logic |
| UseCase (Domain) | Unit | Business rules, edge cases |
| Repository | Unit/Integration | Correct source chosen (cache vs network) |
| UI | Instrumentation | Views render given state; navigation works |
Common Pitfalls (and Fixes)
- Fat ViewModel: Move business rules to UseCases; keep ViewModel focused on UI state/orchestration.
- Leaking Activity: Don’t hold Activity/Context in long-lived objects; pass Application context if needed.
- Mutable state exposed: Expose LiveData (read-only) to the View, keep MutableLiveData private.
- UI logic in Repository: Keep repositories source-agnostic; format in ViewModel.
When MVVM Might Be Overkill
- Very small, one-screen demo apps.
- Throwaway prototypes. (But MVVM is still quick with templates.)
Hands-On Checklist
- Create a SearchViewModel with LiveData<ViewState<List<Item>>>.
- On search click: emit Loading → query repository → emit Success or Error.
- UI renders loading spinner, list, or error message based on state.
- Write a unit test stubbing repository to return fake data and verify state sequence.