Mobile Development 29 min read

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.

vivo Internet Technology
vivo Internet Technology
vivo Internet Technology
Evolution of Android Architecture Patterns: MVC, MVP, MVVM, MVI and Compose Integration

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.

design patternsmobile developmentarchitectureAndroidKotlinComposeMVI
vivo Internet Technology
Written by

vivo Internet Technology

Sharing practical vivo Internet technology insights and salon events, plus the latest industry news and hot conferences.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.