Evolution of Android Architecture Patterns: MVC, MVP, MVVM, MVI and Compose Integration
The article traces Android’s architectural evolution from MVC through MVP and MVVM to MVI, explains each pattern with code examples, shows how MVI’s unidirectional flow combined with Jetpack Compose achieves full UI‑logic‑data decoupling for serial workflows like login‑verify‑thumb‑up, and advises developers to adopt MVI + Compose for new projects.
Android architecture patterns have evolved rapidly, ranging from MVC, MVP, MVVM to the newer MVI and Jetpack Compose approaches. This article systematically explains each pattern, compares their pros and cons, and demonstrates how to achieve complete decoupling by combining MVI with Compose.
Business scenario
The example scenario involves three sequential steps: login with a phone number, verify whether the account is a specific user (Account A), and if it is, perform a like (thumb‑up) operation. Each step requires a server request and the result determines the next step.
Why MVI + Compose?
Existing patterns (MVC, MVP, MVVM) cannot fully decouple the UI, business logic, and data layers, especially for serial workflows like login → verification → thumb‑up. MVI introduces a unidirectional data flow (Intent → Model → State → View) that isolates each concern and simplifies unit testing.
MVC
Typical MVC components:
Model – network, database, I/O
View – XML layout or dynamic view code
Controller – Activity handling UI logic
(1) MVC controller example
public class MvcLoginActivity extends AppCompatActivity {
private EditText userNameEt;
private EditText passwordEt;
private User user;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_mvc_login);
user = new User();
userNameEt = findViewById(R.id.user_name_et);
passwordEt = findViewById(R.id.password_et);
Button loginBtn = findViewById(R.id.login_btn);
loginBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
LoginUtil.getInstance().doLogin(userNameEt.getText().toString(), passwordEt.getText().toString(), new LoginCallBack() {
@Override
public void loginResult(@NonNull com.example.mvcmvpmvvm.mvc.Model.User success) {
if (null != user) {
Toast.makeText(MvcLoginActivity.this, " Login Successful", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(MvcLoginActivity.this, "Login Failed", Toast.LENGTH_SHORT).show();
}
}
});
}
});
}
}(2) MVC model example
public class LoginService {
public static LoginUtil getInstance() {
return new LoginUtil();
}
public void doLogin(String userName, String password, LoginCallBack loginCallBack) {
User user = new User();
if (userName.equals("123456") && password.equals("123456")) {
user.setUserName(userName);
user.setPassword(password);
loginCallBack.loginResult(user);
} else {
loginCallBack.loginResult(null);
}
}
}MVP
MVP moves the complex logic from Activity to a Presenter, making the View (Activity) thin.
(1) MVP presenter example
public class LoginPresenter {
private UserBiz userBiz;
private IMvpLoginView iMvpLoginView;
public LoginPresenter(IMvpLoginView iMvpLoginView) {
this.iMvpLoginView = iMvpLoginView;
this.userBiz = new UserBiz();
}
public void login() {
String userName = iMvpLoginView.getUserName();
String password = iMvpLoginView.getPassword();
boolean isLoginSuccessful = userBiz.login(userName, password);
iMvpLoginView.onLoginResult(isLoginSuccessful);
}
}(2) MVP view (Activity) example
public class MvpLoginActivity extends AppCompatActivity implements IMvpLoginView {
private EditText userNameEt;
private EditText passwordEt;
private LoginPresenter loginPresenter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_mvp_login);
userNameEt = findViewById(R.id.user_name_et);
passwordEt = findViewById(R.id.password_et);
Button loginBtn = findViewById(R.id.login_btn);
loginPresenter = new LoginPresenter(this);
loginBtn.setOnClickListener(v -> loginPresenter.login());
}
@Override
public String getUserName() { return userNameEt.getText().toString(); }
@Override
public String getPassword() { return passwordEt.getText().toString(); }
@Override
public void onLoginResult(Boolean isLoginSuccess) {
if (isLoginSuccess) {
Toast.makeText(this, "Login Successful", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(this, "Login Failed", Toast.LENGTH_SHORT).show();
}
}
}MVVM
MVVM replaces Presenter with ViewModel and uses LiveData/MutableLiveData for data binding. The View observes the ViewModel state.
MVVM ViewModel example
public class LoginViewModel extends ViewModel {
private User user;
private MutableLiveData
isLoginSuccessfulLD = new MutableLiveData<>();
public LoginViewModel() {
user = new User();
}
public MutableLiveData
getIsLoginSuccessfulLD() { return isLoginSuccessfulLD; }
public void login(String userName, String password) {
if (userName.equals("123456") && password.equals("123456")) {
user.setUserName(userName);
user.setPassword(password);
isLoginSuccessfulLD.postValue(true);
} else {
isLoginSuccessfulLD.postValue(false);
}
}
}MVVM Activity example
public class MvvmLoginActivity extends AppCompatActivity {
private LoginViewModel loginVM;
private EditText userNameEt;
private EditText passwordEt;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_mvvm_login);
userNameEt = findViewById(R.id.user_name_et);
passwordEt = findViewById(R.id.password_et);
Button loginBtn = findViewById(R.id.login_btn);
loginVM = new ViewModelProvider(this).get(LoginViewModel.class);
loginBtn.setOnClickListener(v -> loginVM.login(userNameEt.getText().toString(), passwordEt.getText().toString()));
loginVM.getIsLoginSuccessfulLD().observe(this, isSuccess -> {
if (isSuccess) {
Toast.makeText(this, "登录成功", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(this, "登录失败", Toast.LENGTH_SHORT).show();
}
});
}
}MVI
MVI introduces three core concepts: Model (State), View, and Intent (user actions). The data flow is unidirectional: Intent → Model processes → new State → View renders.
MVI ViewModel (Kotlin) example
class LoginViewModel : ViewModel() {
private val _repository = LoginRepository()
val loginActionIntent = Channel
(Channel.UNLIMITED)
private val _loginActionState = MutableSharedFlow
()
val state: SharedFlow
get() = _loginActionState
init { initActionIntent() }
private fun initActionIntent() {
viewModelScope.launch {
loginActionIntent.consumeAsFlow().collect {
when (it) {
is LoginActionIntent.DoLogin -> doLogin(it.username, it.password)
else -> {}
}
}
}
}
private fun doLogin(username: String, password: String) {
viewModelScope.launch {
if (username.isEmpty() || password.isEmpty()) return@launch
_loginActionState.emit(LoginActionState.LoginLoading(username, password))
val loginResult = _repository.requestLoginData(username, password)
if (!loginResult) {
_loginActionState.emit(LoginActionState.LoginFailed(username, password))
return@launch
}
_loginActionState.emit(LoginActionState.LoginSuccessful(username, password))
// further steps: account verification, thumb‑up, etc.
}
}
}MVI Compose integration
By using Jetpack Compose, the UI is declared in Kotlin code and directly bound to the ViewModel state, preventing accidental modifications from other layers.
Compose Activity example
class MviComposeLoginActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launch {
setContent {
BoxWithConstraints(
modifier = Modifier
.background(colorResource(id = R.color.white))
.fillMaxSize()
) {
loginConstraintToDo()
}
}
}
}
@Composable
fun EditorTextField(textFieldState: TextFieldState, label: String, modifier: Modifier = Modifier) {
TextField(
value = textFieldState.text,
onValueChange = { textFieldState.text = it },
modifier = modifier,
label = { Text(text = label) },
placeholder = { Text(text = "123456") }
)
}
@SuppressLint("CoroutineCreationDuringComposition")
@Composable
internal fun loginConstraintToDo(model: ComposeLoginViewModel = viewModel()) {
val state by model.uiState.collectAsState()
val context = LocalContext.current
loginConstraintLayout(
onLoginBtnClick = { text1, text2 ->
lifecycleScope.launch { model.sendEvent(TodoEvent.DoLogin(text1, text2)) }
},
thumbUpSuccessful = state.isThumbUpSuccessful
)
when {
state.isLoginSuccessful -> {
Toast.makeText(context, "登录成功,开始校验账号", Toast.LENGTH_SHORT).show()
model.sendEvent(TodoEvent.VerifyAccount("123456", "123456"))
}
state.isAccountSuccessful -> {
Toast.makeText(context, "账号校验成功,开始点赞", Toast.LENGTH_SHORT).show()
model.sendEvent(TodoEvent.ThumbUp("123456", "123456"))
}
state.isThumbUpSuccessful -> {
Toast.makeText(context, "点赞成功", Toast.LENGTH_SHORT).show()
}
}
}
@Composable
fun loginConstraintLayout(onLoginBtnClick: (String, String) -> Unit, thumbUpSuccessful: Boolean) {
ConstraintLayout {
val (firstText, secondText, button, text) = createRefs()
val firstEditor = remember { TextFieldState() }
val secondEditor = remember { TextFieldState() }
EditorTextField(firstEditor, "123456", Modifier.constrainAs(firstText) {
top.linkTo(parent.top, margin = 16.dp)
start.linkTo(parent.start)
centerHorizontallyTo(parent)
})
EditorTextField(secondEditor, "123456", Modifier.constrainAs(secondText) {
top.linkTo(firstText.bottom, margin = 16.dp)
start.linkTo(firstText.start)
centerHorizontallyTo(parent)
})
Button(
onClick = { onLoginBtnClick("123456", "123456") },
modifier = Modifier.constrainAs(button) {
top.linkTo(secondText.bottom, margin = 20.dp)
start.linkTo(secondText.start, margin = 10.dp)
}
) { Text("Login") }
Text(
if (thumbUpSuccessful) "点赞成功" else "点赞失败",
Modifier.constrainAs(text) {
top.linkTo(button.bottom, margin = 36.dp)
start.linkTo(button.start)
centerHorizontallyTo(parent)
}
)
}
}
}Choosing the right architecture
If the page is simple (single network request), MVC is sufficient.
If multiple pages share similar logic (loading, error handling), consider MVP, MVVM, or MVI.
If you need to observe data changes from many places, use LiveData/Flow – MVVM or MVI.
For serial workflows (login → verification → thumb‑up), MVI provides a clear, testable flow; MVI + Compose further guarantees that the UI cannot be altered outside the defined state.
Overall, the article encourages developers to understand the trade‑offs of each pattern and adopt the most suitable one, with a recommendation to start with the latest MVI + Compose approach for new projects.
vivo Internet Technology
Sharing practical vivo Internet technology insights and salon events, plus the latest industry news and hot conferences.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.