Android 项目架构设计实战:从 MVC、MVP 到 MVVM,梳理职责拆分、数据回传与 DataBinding 落地


前言

这篇文章围绕一个完整的 Android 示例工程展开,按工程从 MVC 演进到 MVP、再到 MVVM 的实际推进顺序,逐步梳理三种架构模式在页面职责划分、网络请求回传、界面更新方式上的差异。文章不会把过程重写成一套脱离实现现场的标准答案,而是尽量保留每一步为什么这样改、代码改在什么位置、数据如何在各层之间流转,以及最终界面是如何被更新起来的。

在这一套示例里,GET 请求用于按用户 id 查询昵称,POST 请求用于提交账号密码并返回登录结果。MVC 侧重理解 Controller 如何在 View 与 Model 之间充当桥梁,MVP 重点看 Presenter 如何接管交互流程,MVVM 则进一步把界面状态交给 LiveData 与 DataBinding 驱动,让 View 不再手动维护大部分 UI 更新逻辑。

在这里插入图片描述

目录

1. 为什么项目需要架构模式

在 Android 项目里,如果页面、数据请求、结果处理和 UI 更新全部堆进一个 Activity,代码往往会越来越难维护。页面一多、交互一复杂,就很容易出现职责混乱、改一处牵一片、定位问题困难的情况。架构模式的价值,本质上就是先把职责切开,再让各层按照清晰的边界协作。

把这个目标拆开来看,架构模式至少解决了几件事:

  1. 它能把 UI、业务逻辑和数据访问拆分开,降低直接耦合。
  2. 它能让逻辑代码更容易单独测试,而不是每次都依赖页面环境。
  3. 它能让数据流向更清晰,新成员接手时更容易看懂页面是怎么跑起来的。
  4. 它能给后续扩展留空间,例如增加状态字段、增加新接口、替换页面实现时,不必整页重写。

围绕 Android 开发,最常见的三种模式就是 MVC、MVP 和 MVVM。它们并不是互相割裂的三套知识点,而是一条很清晰的演进链路:先解决“页面和数据分层”的问题,再解决“控制逻辑过重”的问题,最后解决“页面更新依赖手动调用”的问题。后面三章就沿着这条链路,逐步把同一组页面请求示例改造成三种不同架构。

2. MVC 架构模式

2.1 MVC 的职责划分

MVC 是最经典的分层方式之一,它把应用拆成 Model、View 和 Controller 三部分。

  1. Model 负责处理数据与业务逻辑,例如网络请求、参数校验、结果回传。
  2. View 负责展示界面和承接用户输入。
  3. Controller 负责从 View 收集输入,再调用 Model,最后把结果回传给 View。

放到 Android 里,Activity 往往天然同时承担 View 和 Controller 的一部分职责,这也是 MVC 在简单页面里比较容易落地的原因。不过这种便利也是它的限制来源,因为一旦交互变多,Activity 很容易变得臃肿。

image-20260316220102413

上面这张图对应的就是 MVC 的基本关系:View 不直接处理数据,Model 不直接操作界面,中间依赖 Controller 进行协调。

2.2 前置代码:先把网络请求和页面骨架搭起来

在进入职责拆分之前,先把这个示例要用到的基础能力准备好。因为页面里要发起 GET 和 POST 请求,所以首先要在清单文件中声明网络权限,否则请求根本没有出网能力。

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">
    <uses-permission android:name="android.permission.INTERNET"/>

FrameworkModeByJavaProject/app/src/main/AndroidManifest.xml

这里的权限声明很简单,但它决定了后面所有 Retrofit 请求能否正常执行。页面层、数据层怎么拆都建立在这个基础之上。

接着把依赖补齐。示例里需要 Gson 做 JSON 解析,需要 Retrofit 组织接口请求,也需要 LiveData 和 ViewModel 为后面的 MVVM 章节做准备,所以依赖会统一放在 app 模块里。

plugins {
    alias(libs.plugins.androidApplication)
}

android {
    namespace 'com.ls.frameworkmodebyjavaproject'
    compileSdk 34
    dataBinding {
        enabled = true
    }
    defaultConfig {
        applicationId "com.ls.frameworkmodebyjavaproject"
        minSdk 26
        targetSdk 34
        versionCode 1
        versionName "1.0"
    }

    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
}

dependencies {

    implementation libs.appcompat
    implementation libs.material
    implementation libs.activity
    implementation libs.constraintlayout

    implementation 'com.google.code.gson:gson:2.11.0'
    implementation 'com.squareup.okhttp3:okhttp:4.12.0'
    implementation 'com.squareup.retrofit2:retrofit:2.11.0'
    implementation 'com.squareup.retrofit2:converter-gson:2.11.0'

    implementation "androidx.lifecycle:lifecycle-livedata:2.6.1"
    implementation "androidx.lifecycle:lifecycle-viewmodel:2.6.1"
}

FrameworkModeByJavaProject/app/build.gradle

这一步把后面三种架构都会用到的基础能力先统一准备好。尤其是 Retrofit 与 converter-gson,它们会让 Model 层直接拿到解析后的实体对象,而不必自己手动处理原始字符串。

页面布局先保持简单,核心是两个输入场景:上半部分按用户 id 发起 GET 查询,下半部分输入账号密码发起 POST 登录请求。

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="16dp"
    tools:context=".mvc.MvcActivity">
    <TextView
        android:id="@+id/tv_get_label"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="GET请求:"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <EditText
        android:id="@+id/et_user_id"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="请输入要查询的用户id"
        android:inputType="number"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/tv_get_label" />

    <Button
        android:id="@+id/btn_get"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="发起GET请求"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/et_user_id" />

    <TextView
        android:id="@+id/tv_post_label"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="40dp"
        android:text="GET请求:"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/btn_get" />

    <EditText
        android:id="@+id/et_user_name"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="请输入用户名"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/tv_post_label" />

    <EditText
        android:id="@+id/et_password"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="请输入密码"
        android:inputType="textPassword"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/et_user_name" />

    <Button
        android:id="@+id/btn_post"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="发起POST请求"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/et_password" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Retrofit"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

FrameworkModeByJavaProject/app/src/main/res/layout/activity_mvc.xml

这个布局本身并不复杂,但它已经明确了 View 层需要承担的责任:负责展示输入框、按钮和结果区域,采集用户输入,等待上层控制逻辑决定什么时候发请求、什么时候展示结果。

image-20260316220925892

有了页面骨架后,再定义网络接口。这里把所有请求声明集中到 ApiService,后面的 Model 只需要面向接口发请求,不需要关心底层 OkHttp 细节。

public interface ApiService {

    @GET("addons/cms/api.user/userInfo")
    Call<UserInfoQuery> getUserInfo(
            @Query("user_id") String userId,
            @Query("type") String type);

    @POST("addons/cms/api.login/login")
    Call<ResLogin> login(@Body ReqLogin login);
}

FrameworkModeByJavaProject/app/src/main/java/com/ls/frameworkmodebyjavaproject/reterofit/ApiService.java

这里的两个方法分别对应页面里的两种操作。getUserInfo 通过 @Query 传入查询参数,login 通过 @Body 提交请求体。接口先定义清楚,后续无论是 MVC、MVP 还是 MVVM,数据层都可以直接复用。

为了让 Retrofit 自动把 JSON 映射成 Java 对象,还需要先定义返回实体。用户信息查询接口的数据层次比较深,所以实体类里既要保留根节点字段,也要保留内部的 user、list 等嵌套结构。

public class UserInfoQuery {
   private int code;
   private String msg;
   private String time;
   private DataBean data;

   public static class DataBean {

      private UserBean user;
      private ListBean list;
      private int archives;
      private int comments;

      public static class UserBean {
         private int id;
         private String nickname;
         private String bio;
         private String avatar;
         private String status;
         private String url;
      }

      public static class ListBean {
         private int total;
         private int per_page;
         private int current_page;
         private int last_page;
      }
   }

   // 省略 getter、setter 方法
}

FrameworkModeByJavaProject/app/src/main/java/com/ls/frameworkmodebyjavaproject/bean/UserInfoQuery.java

这个实体类的作用不是单纯“接数据”,而是把返回结构拆成了可以逐级访问的对象层次。后面无论是在 Activity、Presenter 还是 ViewModel 中取昵称,最终都会沿着 getData().getUser().getNickname() 这条链路读取数据。

请求发起前,页面端先把基础交互跑通。在最初的 MVC 写法里,MvcActivity 直接初始化控件、监听按钮点击,再在内部完成网络请求和结果更新。

public class MvcActivity extends AppCompatActivity implements View.OnClickListener {

    private EditText etUserId;
    private EditText etUserName;
    private EditText etPassword;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_mvc);

        findViewById(R.id.btn_get).setOnClickListener(this);
        findViewById(R.id.btn_post).setOnClickListener(this);

        etUserId = findViewById(R.id.et_user_id);
        etUserName = findViewById(R.id.et_user_name);
        etPassword = findViewById(R.id.et_password);
    }

    @Override
    public void onClick(View v) {
        if (v.getId() == R.id.btn_get) {
            String userId = etUserId.getText().toString().trim();
            sendGetRequest(userId);

        } else if (v.getId() == R.id.btn_post) {
            String account = etUserName.getText().toString().trim();
            String password = etPassword.getText().toString().trim();
            sendPostRequest(account, password);
        }
    }
}

FrameworkModeByJavaProject/app/src/main/java/com/ls/frameworkmodebyjavaproject/mvc/MvcActivity.java

这种写法足够直接,优点是上手快,页面、点击事件、请求调用都能在一个类里顺着看下来。但它也埋下了后面要拆分的问题,因为一旦 sendGetRequest 和 sendPostRequest 里塞进完整请求逻辑,Activity 很快就会同时承担 View 和 Controller 的双重职责。

2.3 MVC 中 View 获取 Model 处理结果

既然 Activity 里既有页面代码,又有网络请求代码,那么第一步自然就是把数据处理逻辑抽到 Model。这里新建 MvcUserModel,专门接管登录请求和查询请求。

public class MvcUserModel {

    private static final String TAG = "MvcUserModel";

    public MvcUserModel() {
    }

    protected void sendPostRequest(String account, String password) {

        if (account == null || account.isEmpty()) {
            Log.i(TAG, "sendPostRequest: 用户名不能为空!");
            return;
        }

        if (password == null || password.isEmpty()) {
            Log.i(TAG, "sendPostRequest: 密码不能为空!");
            return;
        }

        Retrofit retrofit = new Retrofit.Builder()
                .baseUrl("https://titok.fzqq.fun/")
                .addConverterFactory(GsonConverterFactory.create())
                .build();

        ApiService apiService = retrofit.create(ApiService.class);
        Call<ResLogin> loginCall = apiService.login(new ReqLogin(account, password));

        loginCall.enqueue(new Callback<ResLogin>() {
            @Override
            public void onResponse(Call<ResLogin> call, Response<ResLogin> response) {
                ResLogin resLogin = response.body();

                if (!response.isSuccessful()) {
                    Log.i(TAG, "onResponse: 请求失败,message = " + response.message());
                    return;
                }

                if (resLogin == null) {
                    Log.i(TAG, "onResponse: 返回数据为空");
                    return;
                }

                if (resLogin.getCode() == 1) {
                    Log.i(TAG, "onResponse: 登录成功,resLogin = " + resLogin);
                } else {
                    Log.i(TAG, "onResponse: 登录失败,msg = " + resLogin.getMsg());
                }
            }

            @Override
            public void onFailure(Call<ResLogin> call, Throwable throwable) {
                Log.i(TAG, "onFailure: 网络请求失败");
            }
        });
    }

    protected void sendGetRequest(String userId) {

        if (userId == null || userId.isEmpty()) {
            Log.i(TAG, "sendGetRequest: 查询id不能为空");
            return;
        }

        Retrofit retrofit = new Retrofit.Builder()
                .baseUrl("https://titok.fzqq.fun/")
                .addConverterFactory(GsonConverterFactory.create())
                .build();

        ApiService apiService = retrofit.create(ApiService.class);
        Call<UserInfoQuery> queryCall = apiService.getUserInfo(userId, "archives");

        queryCall.enqueue(new Callback<UserInfoQuery>() {
            @Override
            public void onResponse(Call<UserInfoQuery> call, Response<UserInfoQuery> response) {
                if (!response.isSuccessful()) {
                    Log.i(TAG, "onResponse: 请求失败,message = " + response.message());
                    return;
                }

                UserInfoQuery body = response.body();
                if (body == null || body.getData() == null || body.getData().getUser() == null) {
                    Log.i(TAG, "onResponse: 返回数据为空");
                    return;
                }

                String nickname = body.getData().getUser().getNickname();
                Log.i(TAG, "onResponse: body = " + nickname);
            }

            @Override
            public void onFailure(Call<UserInfoQuery> call, Throwable throwable) {
                Log.i(TAG, "onFailure: 网络请求失败!");
            }
        });
    }
}

FrameworkModeByJavaProject/app/src/main/java/com/ls/frameworkmodebyjavaproject/mvc/MvcUserModel.java

这一改动的意义在于,Model 终于开始真正承担“处理数据”的责任了。参数校验、Retrofit 初始化、接口调用、响应判空、日志输出,这些都从页面层移走,页面不再需要自己直接拼请求。

接着在 Controller 里实例化 Model,并把用户输入作为参数传进去。这样 Activity 不再直接处理网络请求,而是专注于从 View 拿输入,再把输入交给 Model。

private MvcUserModel mMvcUsmerModel;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_mvc);

    mMvcUsmerModel = new MvcUserModel(this);
}

@Override
public void onClick(View v) {
    if (v.getId() == R.id.btn_get) {

        String userId = etUserId.getText().toString();
        mMvcUsmerModel.sendGetRequest(userId);

    } else if (v.getId() == R.id.btn_post) {

        String account = etUserName.getText().toString();
        String password = etPassword.getText().toString();
        mMvcUsmerModel.sendPostRequest(account, password);
    }
}

FrameworkModeByJavaProject/app/src/main/java/com/ls/frameworkmodebyjavaproject/mvc/MvcActivity.java

这里最关键的一点,不是简单地“把方法挪走”,而是明确了层之间的调用方向:View 上的输入先进入 Controller,再由 Controller 决定调用哪个 Model 方法。View 和 Model 本身并不直接通信。

2.4 MVC 中 Model 获取 View 中的数据

当 Model 只负责请求、Controller 只负责发起调用后,新的问题马上出现了:请求结果如何再回到页面?Model 不能直接拿到控件实例更新界面,所以需要通过回调把结果交回 Controller。

先在 MvcUserModel 中定义回调接口和回调对象,让 Model 知道结果该回传给谁。

public class MvcUserModel {

    UserModelCallback mCallback;

    public MvcUserModel(UserModelCallback callback) {
        this.mCallback = callback;
    }

    protected interface UserModelCallback {

        void onUserInfoQuerySuccess(UserInfoQuery userInfo);

        void onUserInfoQueryFail(String errorMessage);

        void onLoginSuccess(ResLogin login);

        void onLoginFail(String errorMessage);
    }
}

FrameworkModeByJavaProject/app/src/main/java/com/ls/frameworkmodebyjavaproject/mvc/MvcUserModel.java

有了回调定义之后,Model 就能在请求成功或失败时,把结果通过统一接口抛回去。这一步相当于给 Model -> Controller 这条返回链路补齐出口。

下面把回调真正接入到数据处理逻辑里。登录请求和查询请求在拿到结果后,不再只写日志,而是直接调用回调方法把结果交给上层。

protected void sendPostRequest(String account, String password) {

    if (account == null || account.isEmpty()) {
        mCallback.onLoginFail(" 用户名不能为空!");
        return;
    }

    if (password == null || password.isEmpty()) {
        mCallback.onLoginFail(" 密码不能为空!");
        return;
    }

    Retrofit retrofit = new Retrofit.Builder()
            .baseUrl("https://titok.fzqq.fun/")
            .addConverterFactory(GsonConverterFactory.create())
            .build();

    ApiService apiService = retrofit.create(ApiService.class);
    Call<ResLogin> loginCall = apiService.login(new ReqLogin(account, password));

    loginCall.enqueue(new Callback<ResLogin>() {
        @Override
        public void onResponse(Call<ResLogin> call, Response<ResLogin> response) {
            ResLogin resLogin = response.body();
            if (response.isSuccessful() && resLogin.getCode() == 1) {
                mCallback.onLoginSuccess(resLogin);
            } else {
                mCallback.onLoginFail(resLogin.getMsg());
            }
        }

        @Override
        public void onFailure(Call<ResLogin> call, Throwable throwable) {
            mCallback.onLoginFail("网络请求失败");
        }
    });
}

protected void sendGetRequest(String userId) {

    if (userId == null || userId.isEmpty()) {
        Log.i(TAG, "sendGetRequest: 查询id不能为空");
        return;
    }

    Retrofit retrofit = new Retrofit.Builder()
            .baseUrl("https://titok.fzqq.fun/")
            .addConverterFactory(GsonConverterFactory.create())
            .build();

    ApiService apiService = retrofit.create(ApiService.class);
    Call<UserInfoQuery> queryCall = apiService.getUserInfo(userId, "archives");

    queryCall.enqueue(new Callback<UserInfoQuery>() {
        @Override
        public void onResponse(Call<UserInfoQuery> call, Response<UserInfoQuery> response) {
            UserInfoQuery body = response.body();
            mCallback.onUserInfoQuerySuccess(body);
        }

        @Override
        public void onFailure(Call<UserInfoQuery> call, Throwable throwable) {
            mCallback.onUserInfoQueryFail("网络请求失败!");
        }
    });
}

FrameworkModeByJavaProject/app/src/main/java/com/ls/frameworkmodebyjavaproject/mvc/MvcUserModel.java

到这里为止,Model 已经只负责两件事:接收输入、处理结果。至于结果拿来怎么展示,已经不归它管了。

最后让 MvcActivity 实现 MvcUserModel.UserModelCallback,把页面展示逻辑写在回调实现里。这样当 Model 把结果回传回来时,Controller 就可以顺手把它交给 View。

public class MvcActivity extends AppCompatActivity
        implements View.OnClickListener, MvcUserModel.UserModelCallback {

    private EditText etUserId;
    private EditText etUserName;
    private EditText etPassword;
    private MvcUserModel mMvcUsmerModel;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_mvc);

        mMvcUsmerModel = new MvcUserModel(this);

        findViewById(R.id.btn_get).setOnClickListener(this);
        findViewById(R.id.btn_post).setOnClickListener(this);
        etUserId = findViewById(R.id.et_user_id);
        etUserName = findViewById(R.id.et_user_name);
        etPassword = findViewById(R.id.et_password);
    }

    @Override
    public void onClick(View v) {
        if (v.getId() == R.id.btn_get) {
            String userId = etUserId.getText().toString();
            mMvcUsmerModel.sendGetRequest(userId);
        } else if (v.getId() == R.id.btn_post) {
            String account = etUserName.getText().toString();
            String password = etPassword.getText().toString();
            mMvcUsmerModel.sendPostRequest(account, password);
        }
    }

    @Override
    public void onUserInfoQuerySuccess(UserInfoQuery userInfo) {
        String nickname = userInfo.getData().getUser().getNickname();
        Toast.makeText(MvcActivity.this, "查询到:" + nickname, Toast.LENGTH_SHORT).show();
    }

    @Override
    public void onUserInfoQueryFail(String errorMessage) {
        Toast.makeText(MvcActivity.this, errorMessage, Toast.LENGTH_SHORT).show();
    }

    @Override
    public void onLoginSuccess(ResLogin login) {
        Toast.makeText(MvcActivity.this, "欢迎你," +
                login.getData().getUser_id(), Toast.LENGTH_SHORT).show();
    }

    @Override
    public void onLoginFail(String errorMessage) {
        Toast.makeText(MvcActivity.this, errorMessage, Toast.LENGTH_SHORT).show();
    }
}

FrameworkModeByJavaProject/app/src/main/java/com/ls/frameworkmodebyjavaproject/mvc/MvcActivity.java

这一版 MVC 已经形成了完整闭环:用户输入从页面进入 Controller,Controller 调用 Model 发起请求,Model 再通过回调把结果还给 Controller,最后由 Controller 更新界面。

2.5 MVC 的工作流程与优缺点

围绕刚才这套代码,MVC 的工作流可以按下面的顺序理解:

  1. 用户在 View 中输入用户 id、账号或密码,并点击按钮。
  2. Controller 也就是 MvcActivity 读取输入参数,决定调用 sendGetRequest 还是 sendPostRequest。
  3. MvcUserModel 完成参数校验、请求发起和结果解析。
  4. MvcUserModel 通过 UserModelCallback 把结果交回 MvcActivity。
  5. MvcActivity 根据成功或失败结果,通过 Toast 更新界面反馈。

先把 Model 抽出来,页面内部原本直接写请求逻辑的部分就被独立成了一个数据处理类:

image-20260316225102690

接着 Activity 只负责把输入参数传给 Model,不再自己手动拼接完整请求流程:

image-20260316225248696

为了让 Model 能把结果回交给页面,还要先声明回调接口和回调对象来源:

image-20260316225627337

在 Controller 实例化 Model 时,把 this 作为回调实现对象传进去,返回链路才算真正打通:

image-20260316230027326

之后 Model 在请求成功或失败的分支里,就可以直接通过回调方法把结果回抛给 Controller:

image-20260316225728170

而 Controller 只需要实现这些回调,把结果转成具体的 UI 展示行为即可:

image-20260316225832152

整个过程中,View 与 Model 并不直接互通,所有数据流转都要经过 Controller:

image-20260316230721261

从优点上看,MVC 至少完成了最基础的职责拆分:页面不再直接承载所有数据逻辑,Model 可以被独立复用,结构比把所有代码堆在一个 Activity 中清晰很多。

但它的局限也非常明显:

  1. Activity 仍然很重,因为它同时承担 View 展示和 Controller 协调。
  2. 页面一复杂,回调、输入获取、结果展示仍会继续堆到同一个类里。
  3. View 与 Controller 的边界并不彻底清晰,扩展性有限。

也正因为这个问题,下一步才需要把“流程编排”再进一步从 Activity 中抽出去,这就是 MVP 要解决的核心。

3. MVP 架构模式

3.1 为什么要从 MVC 继续演进到 MVP

学完 MVC 之后,最直接的感受就是:虽然 Model 已经抽出来了,但 Activity 依旧同时承担着页面输入采集、结果接收和流程控制三类职责。换句话说,View 与 Controller 并没有真正分开。

MVP 的思路,就是把原本堆在 Activity 里的“流程协调能力”继续抽出去,单独交给 Presenter。这样:

  1. View 只负责展示和暴露 UI 操作接口。
  2. Model 只负责处理数据逻辑。
  3. Presenter 负责组织请求、接收结果、驱动界面更新。

这也是为什么 MVP 常常比 MVC 更适合交互更复杂的 Android 页面。

image-20260316231420926

从这张关系图就能看出,View 不再直接持有数据处理过程,而是通过 Presenter 与 Model 协作。Activity 还在,但它更多扮演的是 View 的实现类。

3.2 前置代码:保留页面输入,先抽出 Presenter 入口

MVP 的页面骨架和 MVC 基本一致,区别在于页面中新增了一个 ProgressBar,用于后面演示“请求开始时显示加载、请求结束时隐藏加载”的 UI 控制。

public class MvpActivity extends AppCompatActivity implements View.OnClickListener {
    private EditText etUserId;
    private EditText etUserName;
    private EditText etPassword;
    private MvpUserPresenter mPresenter;
    private ProgressBar loading;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_mvp);

        findViewById(R.id.btn_get).setOnClickListener(this);
        findViewById(R.id.btn_post).setOnClickListener(this);
        etUserId = findViewById(R.id.et_user_id);
        etUserName = findViewById(R.id.et_user_name);
        etPassword = findViewById(R.id.et_password);

        loading = findViewById(R.id.loading);
    }

    @Override
    public void onClick(View v) {
        if (v.getId() == R.id.btn_get) {

        } else if (v.getId() == R.id.btn_post) {

        }
    }
}

FrameworkModeByJavaProject/app/src/main/java/com/ls/frameworkmodebyjavaproject/mvp/MvpActivity.java

这份代码先保留页面输入控件和点击入口,但暂时不直接落请求逻辑,目的是给 Presenter 留出接管空间。

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="16dp"
    tools:context=".mvp.MvpActivity">

    <TextView
        android:id="@+id/tv_get_label"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="GET请求:"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <EditText
        android:id="@+id/et_user_id"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="请输入要查询的用户id"
        android:inputType="number"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/tv_get_label" />

    <Button
        android:id="@+id/btn_get"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="发起GET请求"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/et_user_id" />

    <TextView
        android:id="@+id/tv_post_label"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="40dp"
        android:text="Post请求:"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/btn_get" />

    <EditText
        android:id="@+id/et_user_name"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="请输入用户名"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/tv_post_label" />

    <EditText
        android:id="@+id/et_password"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="请输入密码"
        android:inputType="textPassword"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/et_user_name" />

    <Button
        android:id="@+id/btn_post"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="发起POST请求"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/et_password" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Retrofit"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent" />

    <ProgressBar
        android:id="@+id/loading"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:visibility="invisible"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

FrameworkModeByJavaProject/app/src/main/res/layout/activity_mvp.xml

这份布局和 MVC 的主要差异在于 loading 控件。它会成为观察职责拆分是否到位的一个很好的切口,因为“什么时候显示、什么时候隐藏”正是流程编排逻辑,而不应该散落在多个请求回调里。

image-20260316231941117

Model 在 MVP 中依旧负责数据处理,不过返回结果的方式从“成员回调对象”改成了“把回调作为方法参数传入”,这样 Presenter 可以按不同场景组织不同回调。

public class MvpUserModel {

    protected void sendPostRequest(String account, String password, UserLoginCallback callback) {

        if (account == null || account.isEmpty()) {
            callback.onLoginFail(" 用户名不能为空!");
            return;
        }

        if (password == null || password.isEmpty()) {
            callback.onLoginFail(" 密码不能为空!");
            return;
        }

        Retrofit retrofit = new Retrofit.Builder()
                .baseUrl("https://titok.fzqq.fun/")
                .addConverterFactory(GsonConverterFactory.create())
                .build();

        ApiService apiService = retrofit.create(ApiService.class);
        Call<ResLogin> loginCall = apiService.login(new ReqLogin(account, password));

        loginCall.enqueue(new Callback<ResLogin>() {
            @Override
            public void onResponse(Call<ResLogin> call, Response<ResLogin> response) {
                ResLogin resLogin = response.body();
                if (response.isSuccessful() && resLogin.getCode() == 1) {
                    callback.onLoginSuccess(resLogin);
                } else {
                    callback.onLoginFail(resLogin.getMsg());
                }
            }

            @Override
            public void onFailure(Call<ResLogin> call, Throwable throwable) {
                callback.onLoginFail("网络请求失败");
            }
        });
    }

    protected void sendGetRequest(String userId, UserQueryCallback callback) {

        if (userId == null || userId.isEmpty()) {
            Log.i("MvcUserModel", "sendGetRequest: 查询id不能为空");
            return;
        }

        Retrofit retrofit = new Retrofit.Builder()
                .baseUrl("https://titok.fzqq.fun/")
                .addConverterFactory(GsonConverterFactory.create())
                .build();

        ApiService apiService = retrofit.create(ApiService.class);
        Call<UserInfoQuery> queryCall = apiService.getUserInfo(userId, "archives");

        queryCall.enqueue(new Callback<UserInfoQuery>() {
            @Override
            public void onResponse(Call<UserInfoQuery> call, Response<UserInfoQuery> response) {
                UserInfoQuery body = response.body();
                callback.onUserInfoQuerySuccess(body);
            }

            @Override
            public void onFailure(Call<UserInfoQuery> call, Throwable throwable) {
                callback.onUserInfoQueryFail("网络请求失败!");
            }
        });
    }
}

FrameworkModeByJavaProject/app/src/main/java/com/ls/frameworkmodebyjavaproject/mvp/MvpUserModel.java

Model 仍然保持“只关心数据”的定位,但它不再关心结果到底回给哪个具体类,而是由调用方在发起请求时显式传入回调实现。

3.3 Presenter:接管 View 与 Model 之间的流程编排

Presenter 是 MVP 的核心。它既不直接持有控件,也不直接发起网络实现细节,但它决定了请求什么时候开始、结果回来后应该调用哪些界面方法。

一开始先定义出 Presenter 壳子,并在构造方法里持有 Model。

public class MvpUserPresenter {

    private MvpUserModel mModel;

    public MvpUserPresenter(MvpUserView view) {
        mModel = new MvpUserModel();
    }
}

FrameworkModeByJavaProject/app/src/main/java/com/ls/frameworkmodebyjavaproject/mvp/MvpUserPresenter.java

这个初始版本还没有真正驱动界面,但已经把 Model 的入口固定在 Presenter 中了。接下来页面不再直接调用 Model,而是先调用 Presenter 暴露出来的方法。

在页面层里,先声明 Presenter,再把点击事件转交给它:

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_mvp);

    mPresenter = new MvpUserPresenter(this);
}

@Override
public void onClick(View v) {
    if (v.getId() == R.id.btn_get) {
        String userId = etUserId.getText().toString().trim();
        mPresenter.getUserInfo(userId);
    } else if (v.getId() == R.id.btn_post) {
        String account = etUserName.getText().toString().trim();
        String password = etPassword.getText().toString().trim();
        mPresenter.login(account, password);
    }
}

FrameworkModeByJavaProject/app/src/main/java/com/ls/frameworkmodebyjavaproject/mvp/MvpActivity.java

也就是说,View 的职责开始被压缩成“采集输入 + 分发事件”。真正决定下一步走向的,已经不是 Activity,而是 Presenter。

随后在 Presenter 中补齐请求入口方法,让它真正能调起 Model 的两个数据接口。

public void getUserInfo(String userId) {
    mView.showLoading();
    mModel.sendGetRequest(userId, new MvpUserModel.UserQueryCallback() {
        @Override
        public void onUserInfoQuerySuccess(UserInfoQuery userInfo) {

        }

        @Override
        public void onUserInfoQueryFail(String errorMessage) {

        }
    });
}

public void login(String account, String password) {
    mView.showLoading();
    mModel.sendPostRequest(account, password, new MvpUserModel.UserLoginCallback() {
        @Override
        public void onLoginSuccess(ResLogin login) {

        }

        @Override
        public void onLoginFail(String errorMessage) {

        }
    });
}

FrameworkModeByJavaProject/app/src/main/java/com/ls/frameworkmodebyjavaproject/mvp/MvpUserPresenter.java

这里先把方法骨架搭出来,能清楚看到 Presenter 的位置:它先通知界面显示加载,再调用 Model,然后等待回调结果,最后根据成功或失败决定更新哪些界面。

为了配合这种写法,MvpUserModel 里把查询和登录拆成了两种不同回调接口。这样 Presenter 在处理查询昵称和处理登录结果时,不必被迫实现一大组混合接口。

protected interface UserQueryCallback {

    void onUserInfoQuerySuccess(UserInfoQuery userInfo);

    void onUserInfoQueryFail(String errorMessage);
}

protected interface UserLoginCallback {

    void onLoginSuccess(ResLogin login);

    void onLoginFail(String errorMessage);
}

FrameworkModeByJavaProject/app/src/main/java/com/ls/frameworkmodebyjavaproject/mvp/MvpUserModel.java

这一步其实是在继续做职责细分:不仅层级在拆,连“返回结果的协议”也在按业务场景拆。查询和登录各走各的回调,Presenter 处理起来会更清晰。

Model 的数据处理方法本身也随之修改为“回调入参模式”。这样发起请求时,谁来处理结果完全由调用方决定。

protected void sendPostRequest(String account, String password, UserLoginCallback callback) {
    if (account == null || account.isEmpty()) {
        callback.onLoginFail(" 用户名不能为空!");
        return;
    }

    if (password == null || password.isEmpty()) {
        callback.onLoginFail(" 密码不能为空!");
        return;
    }

    Retrofit retrofit = new Retrofit.Builder()
            .baseUrl("https://titok.fzqq.fun/")
            .addConverterFactory(GsonConverterFactory.create())
            .build();

    ApiService apiService = retrofit.create(ApiService.class);
    Call<ResLogin> loginCall = apiService.login(new ReqLogin(account, password));

    loginCall.enqueue(new Callback<ResLogin>() {
        @Override
        public void onResponse(Call<ResLogin> call, Response<ResLogin> response) {
            ResLogin resLogin = response.body();
            if (response.isSuccessful() && resLogin.getCode() == 1) {
                callback.onLoginSuccess(resLogin);
            } else {
                callback.onLoginFail(resLogin.getMsg());
            }
        }

        @Override
        public void onFailure(Call<ResLogin> call, Throwable throwable) {
            callback.onLoginFail("网络请求失败");
        }
    });
}

protected void sendGetRequest(String userId, UserQueryCallback callback) {
    if (userId == null || userId.isEmpty()) {
        Log.i("MvcUserModel", "sendGetRequest: 查询id不能为空");
        return;
    }

    Retrofit retrofit = new Retrofit.Builder()
            .baseUrl("https://titok.fzqq.fun/")
            .addConverterFactory(GsonConverterFactory.create())
            .build();

    ApiService apiService = retrofit.create(ApiService.class);
    Call<UserInfoQuery> queryCall = apiService.getUserInfo(userId, "archives");

    queryCall.enqueue(new Callback<UserInfoQuery>() {
        @Override
        public void onResponse(Call<UserInfoQuery> call, Response<UserInfoQuery> response) {
            UserInfoQuery body = response.body();
            callback.onUserInfoQuerySuccess(body);
        }

        @Override
        public void onFailure(Call<UserInfoQuery> call, Throwable throwable) {
            callback.onUserInfoQueryFail("网络请求失败!");
        }
    });
}

FrameworkModeByJavaProject/app/src/main/java/com/ls/frameworkmodebyjavaproject/mvp/MvpUserModel.java

到这里,Presenter 已经不需要像 MVC 那样去实现 Model 的统一接口,它只要在调用时内联提供当前场景需要的回调逻辑即可。

3.4 MVP 处理 View 的交互

前面把 Model -> Presenter 这条数据回传链路打通了,现在还差 Presenter -> View。为此需要先定义一组界面更新能力,让 View 通过接口把自己能做的事情暴露出来。

public interface MvpUserView {

    void showUserInfo(UserInfoQuery info);

    void onLoginSuccess(ResLogin login);

    void showError(String error);

    void showLoading();

    void hideLoading();
}

FrameworkModeByJavaProject/app/src/main/java/com/ls/frameworkmodebyjavaproject/mvp/MvpUserView.java

这个接口不是在替 Activity 写代码,而是在声明“页面能做什么”。比如它能展示用户信息,能展示错误,能控制加载动画,但它不再关心这些动作应该在什么时候发生。

对应地,Presenter 中要显式持有这个 View 接口对象:

public class MvpUserPresenter {

    private final MvpUserView mView;
    private MvpUserModel mModel;
}

FrameworkModeByJavaProject/app/src/main/java/com/ls/frameworkmodebyjavaproject/mvp/MvpUserPresenter.java

有了这层持有关系,Presenter 才能在请求开始时调用 showLoading,在请求结束时调用 hideLoading,在成功或失败时调用不同的界面反馈方法。

接着让 MvpActivity 实现 MvpUserView,这样它虽然还是页面类,但已经不再承担流程控制职责,而是作为一个“界面能力提供者”存在。

public class MvpActivity extends AppCompatActivity
        implements View.OnClickListener, MvpUserView {

    private EditText etUserId;
    private EditText etUserName;
    private EditText etPassword;
    private MvpUserPresenter mPresenter;
    private ProgressBar loading;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_mvp);

        mPresenter = new MvpUserPresenter(this);

        findViewById(R.id.btn_get).setOnClickListener(this);
        findViewById(R.id.btn_post).setOnClickListener(this);
        etUserId = findViewById(R.id.et_user_id);
        etUserName = findViewById(R.id.et_user_name);
        etPassword = findViewById(R.id.et_password);

        loading = findViewById(R.id.loading);
    }

    @Override
    public void showUserInfo(UserInfoQuery info) {
        String nickname = info.getData().getUser().getNickname();
        Toast.makeText(this, "查询到:" + nickname, Toast.LENGTH_SHORT).show();
    }

    @Override
    public void onLoginSuccess(ResLogin login) {
        Toast.makeText(this, "欢迎你," +
                login.getData().getUser_id(), Toast.LENGTH_SHORT).show();
    }

    @Override
    public void showError(String error) {
        Toast.makeText(this, error, Toast.LENGTH_SHORT).show();
    }

    @Override
    public void showLoading() {
        loading.setVisibility(View.VISIBLE);
    }

    @Override
    public void hideLoading() {
        loading.setVisibility(View.INVISIBLE);
    }
}

FrameworkModeByJavaProject/app/src/main/java/com/ls/frameworkmodebyjavaproject/mvp/MvpActivity.java

可以看到,页面层现在只保留了非常明确的 UI 行为:展示查询结果、展示登录成功提示、展示错误、控制加载条显隐。这些方法都和“如何发请求、何时调用”无关。

最后把 Presenter 补成完整版本,让它统一协调 View 与 Model:

public class MvpUserPresenter {

    private final MvpUserView mView;
    private MvpUserModel mModel;

    public MvpUserPresenter(MvpUserView view) {
        mModel = new MvpUserModel();
        mView = view;
    }

    public void getUserInfo(String userId) {
        mView.showLoading();
        mModel.sendGetRequest(userId, new MvpUserModel.UserQueryCallback() {
            @Override
            public void onUserInfoQuerySuccess(UserInfoQuery userInfo) {
                mView.showUserInfo(userInfo);
                mView.hideLoading();
            }

            @Override
            public void onUserInfoQueryFail(String errorMessage) {
                mView.showError(errorMessage);
                mView.hideLoading();
            }
        });
    }

    public void login(String account, String password) {
        mView.showLoading();
        mModel.sendPostRequest(account, password, new MvpUserModel.UserLoginCallback() {
            @Override
            public void onLoginSuccess(ResLogin login) {
                mView.onLoginSuccess(login);
                mView.hideLoading();
            }

            @Override
            public void onLoginFail(String errorMessage) {
                mView.showError(errorMessage);
                mView.hideLoading();
            }
        });
    }
}

FrameworkModeByJavaProject/app/src/main/java/com/ls/frameworkmodebyjavaproject/mvp/MvpUserPresenter.java

这一版的职责分工已经非常清楚:

  1. View 负责把“能做哪些 UI 更新”暴露出来。
  2. Model 负责发请求并回传结果。
  3. Presenter 负责定义整条交互流程,决定先显示加载、后发请求,再根据结果更新页面。

这就是 MVP 相对 MVC 的核心进步,它让页面不再直接承担流程编排。

3.5 MVP 的工作流程与优缺点

结合这一套实现,MVP 的流程可以概括成下面五步:

  1. 用户在 View 中点击按钮。
  2. View 把事件交给 Presenter。
  3. Presenter 调用 Model 执行数据逻辑。
  4. Model 通过回调把结果返回给 Presenter。
  5. Presenter 再调用 View 接口更新页面。

这张流程图对应的就是这种交互关系:

image-20260316231420926

从这张图也能看出,View 与 Model 的联系已经被 Presenter 完整接管。页面负责展示,数据层负责处理,中间的协调关系不再堆在 Activity 里。

MVP 的优点很明显:

  1. 界面更新和业务处理解耦更彻底,页面更容易维护。
  2. Presenter 可以独立承接流程逻辑,职责边界更清楚。
  3. 页面层只暴露接口,更适合做单元测试和替换实现。

但它也有新的成本:

  1. View 接口、Presenter 类、回调接口都会增加代码量。
  2. 页面越多,接口与 Presenter 的数量会明显上升。
  3. Presenter 仍然依赖 View 接口,本质上还没有完全摆脱对界面的感知。

因此,如果还想继续降低手动控制 UI 的负担,就需要再往前走一步,让页面更多地“观察状态”而不是“等待方法调用”,这就是 MVVM 的目标。

4. MVVM 架构模式

4.1 为什么要从 MVP 继续演进到 MVVM

MVP 已经把流程协调从 Activity 中抽到了 Presenter,但它仍有一个明显特点:Presenter 需要主动调用 showLoading、hideLoading、showUserInfo 这一类界面方法。也就是说,界面更新虽然被统一编排了,但本质上仍是“命令式调用”。

MVVM 要解决的,就是把这种命令式调用进一步改造成状态驱动。具体做法是:

  1. Model 继续专注数据处理。
  2. ViewModel 接收 Model 结果,并把结果转换成可观察的 UI 状态。
  3. View 只观察这些状态,一旦状态变化,就自动刷新界面。

当 LiveData 和 DataBinding 接进来之后,页面层需要手动写的更新代码会明显减少。

image-20260317002459991

这张关系图体现的重点,不再是“谁调用谁的方法”,而是 ViewModel 如何作为中间状态层,把 Model 的结果转成 View 能直接消费的界面数据。

4.2 Model:继续专注数据处理

到了 MVVM,Model 本身的职责其实没有变化,依然负责参数校验、网络请求和结果回调。也正因为职责稳定,前两种架构里的数据处理代码可以非常自然地沿用到这里。

public class MvvmUserModel {

    protected void sendPostRequest(String account, String password, UserLoginCallback callback) {

        if (account == null || account.isEmpty()) {
            callback.onLoginFail(" 用户名不能为空!");
            return;
        }

        if (password == null || password.isEmpty()) {
            callback.onLoginFail(" 密码不能为空!");
            return;
        }

        Retrofit retrofit = new Retrofit.Builder()
                .baseUrl("https://titok.fzqq.fun/")
                .addConverterFactory(GsonConverterFactory.create())
                .build();

        ApiService apiService = retrofit.create(ApiService.class);
        Call<ResLogin> loginCall = apiService.login(new ReqLogin(account, password));

        loginCall.enqueue(new Callback<ResLogin>() {
            @Override
            public void onResponse(Call<ResLogin> call, Response<ResLogin> response) {
                ResLogin resLogin = response.body();
                if (response.isSuccessful() && resLogin.getCode() == 1) {
                    callback.onLoginSuccess(resLogin);
                } else {
                    callback.onLoginFail(resLogin.getMsg());
                }
            }

            @Override
            public void onFailure(Call<ResLogin> call, Throwable throwable) {
                callback.onLoginFail("网络请求失败");
            }
        });
    }

    protected void sendGetRequest(String userId, UserQueryCallback callback) {

        if (userId == null || userId.isEmpty()) {
            Log.i("MvvmUserModel", "sendGetRequest: 查询id不能为空");
            return;
        }

        Retrofit retrofit = new Retrofit.Builder()
                .baseUrl("https://titok.fzqq.fun/")
                .addConverterFactory(GsonConverterFactory.create())
                .build();

        ApiService apiService = retrofit.create(ApiService.class);
        Call<UserInfoQuery> queryCall = apiService.getUserInfo(userId, "archives");

        queryCall.enqueue(new Callback<UserInfoQuery>() {
            @Override
            public void onResponse(Call<UserInfoQuery> call, Response<UserInfoQuery> response) {
                UserInfoQuery body = response.body();
                callback.onUserInfoQuerySuccess(body);
            }

            @Override
            public void onFailure(Call<UserInfoQuery> call, Throwable throwable) {
                callback.onUserInfoQueryFail("网络请求失败!");
            }
        });
    }
}

FrameworkModeByJavaProject/app/src/main/java/com/ls/frameworkmodebyjavaproject/mvvm/MvvmUserModel.java

这正是 MVVM 值得注意的一点:它不是让所有层都推倒重来,而是在保留数据层定位不变的前提下,把“界面如何更新”这件事从方法调用变成状态观察。

4.3 View:先完成页面初始化,再把事件交给 ViewModel

先看页面层的初始写法。MvvmActivity 仍然负责初始化控件、绑定点击事件和获取用户输入,但接下来这些输入不会再直接送给 Model,而是交给 ViewModel。

public class MvvmActivity extends AppCompatActivity implements View.OnClickListener {

    private EditText etUserId;
    private EditText etUserName;
    private EditText etPassword;
    private ProgressBar loading;
    private MvvmViewModel mViewModel;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_mvvm);

        binding.setLifecycleOwner(this);
        binding.setViewModel(mViewModel);

        findViewById(R.id.btn_get).setOnClickListener(this);
        findViewById(R.id.btn_post).setOnClickListener(this);
        etUserId = findViewById(R.id.et_user_id);
        etUserName = findViewById(R.id.et_user_name);
        etPassword = findViewById(R.id.et_password);

        loading = findViewById(R.id.loading);
    }

    @Override
    public void onClick(View v) {
        if (v.getId() == R.id.btn_get) {
            String userId = etUserId.getText().toString().trim();

        } else if (v.getId() == R.id.btn_post) {
            String account = etUserName.getText().toString().trim();
            String password = etPassword.getText().toString().trim();

        }
    }
}

FrameworkModeByJavaProject/app/src/main/java/com/ls/frameworkmodebyjavaproject/mvvm/MvvmActivity.java

这里最需要关注的是 binding.setLifecycleOwner(this) 和 binding.setViewModel(mViewModel) 这两行,因为它们为后面的数据绑定做了准备。页面不再只是拿控件引用,它还要把自身生命周期和 ViewModel 绑定到布局上。

要让 ViewModel 真正成为中间层,还需要把生命周期依赖补齐:

implementation "androidx.lifecycle:lifecycle-livedata:2.6.1"
implementation "androidx.lifecycle:lifecycle-viewmodel:2.6.1"

FrameworkModeByJavaProject/app/build.gradle

这两个依赖一个负责可观察数据容器,一个负责 ViewModel 生命周期管理。没有它们,后面的状态驱动更新就无从谈起。

4.4 ViewModel:把流程控制改成状态驱动

先创建 MvvmViewModel,让它像 Presenter 一样持有 Model,并暴露查询与登录两个入口方法。这样页面层的点击事件就有了新的接收方。

public class MvvmViewModel extends ViewModel {

    private final MvvmUserModel mModel;

    public MvvmViewModel() {
        mModel = new MvvmUserModel();
    }

    public void getUserInfo(String userId) {

        mModel.sendGetRequest(userId, new MvvmUserModel.UserQueryCallback() {
            @Override
            public void onUserInfoQuerySuccess(UserInfoQuery userInfo) {

            }

            @Override
            public void onUserInfoQueryFail(String errorMessage) {

            }
        });
    }

    public void login(String account, String password) {

        mModel.sendPostRequest(account, password, new MvvmUserModel.UserLoginCallback() {
            @Override
            public void onLoginSuccess(ResLogin login) {

            }

            @Override
            public void onLoginFail(String errorMessage) {

            }
        });
    }
}

FrameworkModeByJavaProject/app/src/main/java/com/ls/frameworkmodebyjavaproject/mvvm/MvvmViewModel.java

这个阶段的 ViewModel 看起来很像 Presenter,但真正的差别马上就会出现:Presenter 直接调用界面方法,ViewModel 则会把界面变化先转换成状态。

第一个要改成状态的,就是加载中的显示逻辑。把 loading 的显示状态封装成 MutableLiveData<Boolean>,然后在请求开始与结束时修改这个值,而不是直接去操作控件。

public class MvvmViewModel extends ViewModel {

    private final MvvmUserModel mModel;

    private final MutableLiveData<Boolean> mShowLoading = new MutableLiveData<>();

    public MvvmViewModel() {
        mModel = new MvvmUserModel();
    }

    public MutableLiveData<Boolean> getShowLoading() {
        return mShowLoading;
    }

    public void getUserInfo(String userId) {
        mShowLoading.setValue(true);
        mModel.sendGetRequest(userId, new MvvmUserModel.UserQueryCallback() {
            @Override
            public void onUserInfoQuerySuccess(UserInfoQuery userInfo) {
                mShowLoading.setValue(false);
            }

            @Override
            public void onUserInfoQueryFail(String errorMessage) {
                mShowLoading.setValue(false);
            }
        });
    }

    public void login(String account, String password) {
        mShowLoading.setValue(true);
        mModel.sendPostRequest(account, password, new MvvmUserModel.UserLoginCallback() {
            @Override
            public void onLoginSuccess(ResLogin login) {
                mShowLoading.setValue(false);
            }

            @Override
            public void onLoginFail(String errorMessage) {
                mShowLoading.setValue(false);
            }
        });
    }
}

FrameworkModeByJavaProject/app/src/main/java/com/ls/frameworkmodebyjavaproject/mvvm/MvvmViewModel.java

这一步是 MVVM 的关键转折点。界面不再等待“有人来调用 showLoading”,而是改成“观察 mShowLoading 是否变化”。只要这个布尔值变了,界面就知道自己该如何刷新。

对应地,页面层开始观察 ViewModel 状态,而不是在请求回调里手动操作控件:

public class MvvmActivity extends AppCompatActivity implements View.OnClickListener {

    private ProgressBar loading;
    private MvvmViewModel mViewModel;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_mvvm);

        mViewModel = new ViewModelProvider(this).get(MvvmViewModel.class);

        loading = findViewById(R.id.loading);

        mViewModel.getShowLoading().observe(this, new Observer<Boolean>() {
            @Override
            public void onChanged(Boolean show) {
                loading.setVisibility(show ? View.VISIBLE : View.INVISIBLE);
            }
        });
    }
}

FrameworkModeByJavaProject/app/src/main/java/com/ls/frameworkmodebyjavaproject/mvvm/MvvmActivity.java

这里已经能看出 MVVM 的味道了:页面层不再关心请求是成功还是失败,它只关心“当前状态值是什么”。当 mShowLoading 变成 true 时显示进度条,变成 false 时隐藏进度条。

最后把页面点击事件接到 ViewModel 上,让数据流正式跑起来:

@Override
public void onClick(View v) {
    if (v.getId() == R.id.btn_get) {
        String userId = etUserId.getText().toString().trim();
        mViewModel.getUserInfo(userId);
    } else if (v.getId() == R.id.btn_post) {
        String account = etUserName.getText().toString().trim();
        String password = etPassword.getText().toString().trim();
        mViewModel.login(account, password);
    }
}

FrameworkModeByJavaProject/app/src/main/java/com/ls/frameworkmodebyjavaproject/mvvm/MvvmActivity.java

这样一来,View 负责发事件,ViewModel 负责改状态,Model 负责处理数据,三层之间的职责关系就开始稳定下来。

4.5 MVVM 中的被观察对象

仅仅观察一个加载状态,还不足以体现 MVVM 的优势。真正关键的是,页面更新时并不需要一整个返回对象,它往往只需要几个能直接驱动 UI 的字段。

因此,MvvmViewModel 会进一步把查询昵称、登录用户 id、错误信息这些界面状态分别拆成独立的可观察对象:

public class MvvmViewModel extends ViewModel {

    private final MvvmUserModel mModel;

    private final MutableLiveData<Boolean> mShowLoading = new MutableLiveData<>();
    private final MutableLiveData<String> mNickName = new MutableLiveData<>();
    private final MutableLiveData<Integer> mUserId = new MutableLiveData<>();
    private final LiveData<String> mStrUserId = Transformations.map(mUserId, new Function1<Integer, String>() {
        @Override
        public String invoke(Integer integer) {
            String content = "我查询到了用户id:" + integer;
            return content;
        }
    });
    private final MutableLiveData<String> mErrorMsg = new MutableLiveData<>();

    public LiveData<String> getStrUserId() {
        return mStrUserId;
    }

    public MutableLiveData<String> getErrorMsg() {
        return mErrorMsg;
    }

    public MutableLiveData<Boolean> getShowLoading() {
        return mShowLoading;
    }

    public MutableLiveData<String> getNickName() {
        return mNickName;
    }

    public MutableLiveData<Integer> getUserId() {
        return mUserId;
    }

    public void getUserInfo(String userId) {
        mShowLoading.setValue(true);
        mModel.sendGetRequest(userId, new MvvmUserModel.UserQueryCallback() {
            @Override
            public void onUserInfoQuerySuccess(UserInfoQuery userInfo) {
                String nickname = userInfo.getData().getUser().getNickname();
                mNickName.setValue(nickname);
                mShowLoading.setValue(false);
            }

            @Override
            public void onUserInfoQueryFail(String errorMessage) {
                mNickName.setValue(null);
                mShowLoading.setValue(false);
                mErrorMsg.setValue(errorMessage);
            }
        });
    }

    public void login(String account, String password) {
        mShowLoading.setValue(true);
        mModel.sendPostRequest(account, password, new MvvmUserModel.UserLoginCallback() {
            @Override
            public void onLoginSuccess(ResLogin login) {
                int userId = login.getData().getUser_id();
                mUserId.setValue(userId);
                mShowLoading.setValue(false);
            }

            @Override
            public void onLoginFail(String errorMessage) {
                mUserId.setValue(0);
                mShowLoading.setValue(false);
                mErrorMsg.setValue(errorMessage);
            }
        });
    }
}

FrameworkModeByJavaProject/app/src/main/java/com/ls/frameworkmodebyjavaproject/mvvm/MvvmViewModel.java

这一版 ViewModel 已经完全体现出 MVVM 的状态驱动思想了:

  1. 查询成功时,页面只需要昵称,所以写入 mNickName。
  2. 登录成功时,页面只需要用户 id,所以写入 mUserId。
  3. 错误发生时,页面只需要错误文案,所以写入 mErrorMsg。

页面层则分别观察这些字段,不必再拿整个响应对象自己拆解:

mViewModel.getShowLoading().observe(this, new Observer<Boolean>() {
    @Override
    public void onChanged(Boolean show) {
        loading.setVisibility(show ? View.VISIBLE : View.INVISIBLE);
    }
});

mViewModel.getUserId().observe(this, new Observer<Integer>() {
    @Override
    public void onChanged(Integer userId) {
        int i = userId.intValue();

        if (i > 0) {
            Toast.makeText(MvvmActivity.this,
                    "欢迎你," + i, Toast.LENGTH_SHORT).show();
        }
    }
});

mViewModel.getNickName().observe(this, new Observer<String>() {
    @Override
    public void onChanged(String s) {
        Toast.makeText(MvvmActivity.this, "查询到:" + s, Toast.LENGTH_SHORT).show();
    }
});

mViewModel.getErrorMsg().observe(this, new Observer<String>() {
    @Override
    public void onChanged(String error) {
        Toast.makeText(MvvmActivity.this, error, Toast.LENGTH_SHORT).show();
    }
});

FrameworkModeByJavaProject/app/src/main/java/com/ls/frameworkmodebyjavaproject/mvvm/MvvmActivity.java

这正是 MVVM 相比 MVP 的一个重要优势:页面不需要拿完整结果再自己决定如何处理,而是直接观察那些已经被整理成“可用于更新 UI 的最小状态”的字段。

4.6 在 MVVM 中添加 DataBinding

前面只是把页面更新改成了 LiveData.observe。如果想继续减少样板代码,还可以把布局文件直接接到 ViewModel 上,让部分属性通过数据绑定自动更新。

首先在 android 配置中启用 DataBinding:

android {
    namespace 'com.ls.frameworkmodebyjavaproject'
    compileSdk 34
    dataBinding {
        enabled = true
    }
}

FrameworkModeByJavaProject/app/build.gradle

启用后,布局文件就可以从普通的根布局切换成 layout 包裹结构,并在 data 节点中声明要绑定的 ViewModel 类型。

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">

    <data>

        <variable
            name="viewModel"
            type="com.ls.frameworkmodebyjavaproject.mvvm.MvvmViewModel" />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:id="@+id/main"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:padding="16dp"
        tools:context=".mvvm.MvvmActivity">

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

FrameworkModeByJavaProject/app/src/main/res/layout/activity_mvvm.xml

这一步的意义在于,布局文件不再只是被动展示控件,它开始显式声明“我要绑定哪个 ViewModel”。后面控件属性就可以直接读取 viewModel 暴露出来的状态。

接着在控件上使用 @{} 或 @={} 表达式,把布局属性和 ViewModel 字段直接绑定起来:

<ProgressBar
    android:id="@+id/loading"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:visibility="@{viewModel.showLoading}"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent" />

<TextView
    android:id="@+id/tv_nick_name"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@={viewModel.nickName}"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    tools:text="nickName" />

<TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginLeft="16dp"
    android:text="@{viewModel.userId}"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintStart_toEndOf="@id/tv_nick_name"
    tools:text="userId" />

FrameworkModeByJavaProject/app/src/main/res/layout/activity_mvvm.xml

这里可以看到三个典型绑定场景:

  1. loading 直接绑定 showLoading。
  2. 昵称文本绑定 nickName。
  3. 用户 id 文本绑定 userId。

如果控件需要的类型和 LiveData 当前提供的类型不一致,可以在 ViewModel 内部先做一次类型转换。例如 mUserId 是 Integer,而文本展示通常更适合 String,这时就可以用 Transformations.map 包一层新的可观察字段。

private final MutableLiveData<Integer> mUserId = new MutableLiveData<>();

private final LiveData<String> mStrUserId = Transformations.map(mUserId, new Function1<Integer, String>() {
    @Override
    public String invoke(Integer integer) {
        String content = "我查询到了用户id:" + integer;
        return content;
    }
});

FrameworkModeByJavaProject/app/src/main/java/com/ls/frameworkmodebyjavaproject/mvvm/MvvmViewModel.java

这样处理后,布局就不需要自己拼字符串,而是直接绑定转换后的字段:

<TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginLeft="16dp"
    android:text="@{viewModel.strUserId}"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintStart_toEndOf="@id/tv_nick_name"
    tools:text="userId" />

FrameworkModeByJavaProject/app/src/main/res/layout/activity_mvvm.xml

这种写法更符合 MVVM 的核心思路:页面只消费适合展示的数据,不在 View 层再做一次二次加工。

如果继续使用 DataBinding,页面初始化方式也要切换成 DataBindingUtil.setContentView,这样才能拿到自动生成的 ActivityMvvmBinding 对象:

ActivityMvvmBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_mvvm);

mViewModel = new ViewModelProvider(this).get(MvvmViewModel.class);

binding.setLifecycleOwner(this);
binding.setViewModel(mViewModel);

FrameworkModeByJavaProject/app/src/main/java/com/ls/frameworkmodebyjavaproject/mvvm/MvvmActivity.java

这几行代码把布局、生命周期和 ViewModel 真正绑定到了一起。之后只要 LiveData 变化,布局表达式就能自动拿到新值。

4.7 使用 BindingAdapter 处理类型兼容

除了在 ViewModel 中做类型转换,DataBinding 还提供了另一种方式来解决控件属性类型不匹配的问题,也就是 BindingAdapter。

image-20260317012620445

它的思路是:当某个控件属性接收到的数据类型和属性原生要求不一致时,可以由自定义适配方法接管这次赋值过程。

例如下面这个文本控件,直接把 viewModel.userId 绑定到 android:text:

<TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginLeft="16dp"
    android:text="@{viewModel.userId}"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintStart_toEndOf="@id/tv_nick_name"
    tools:text="userId" />

FrameworkModeByJavaProject/app/src/main/res/layout/activity_mvvm.xml

这个时候,android:text 原本期望的是文本类型,而 viewModel.userId 暴露出来的是 Integer。这时就可以定义一个 BindingAdapter,专门负责把 int 转成控件需要的文本。

public class BindingAdapters {

    @BindingAdapter("android:text")
    public static void mySetText(TextView textView, int value) {
        if (value == 0) {
            return;
        }
        textView.setText("我接收到了一个userid = " + value);
    }
}

FrameworkModeByJavaProject/app/src/main/java/com/ls/frameworkmodebyjavaproject/mvvm/BindingAdapters.java

一旦这个适配器存在,DataBinding 在给 TextView 的 android:text 赋值时,就会先看有没有匹配的适配方法可用。如果类型不兼容但能找到这个方法,就会把值交给 mySetText 处理。

同理,ProgressBar 的 android:visibility 也可以通过 BindingAdapter 来接收布尔值,再由适配器内部把它转换成 View.VISIBLE 或 View.INVISIBLE:

@BindingAdapter("android:visibility")
public static void setVisibility(ProgressBar progressBar, boolean show) {
    progressBar.setVisibility(show ? View.VISIBLE : View.INVISIBLE);
}

FrameworkModeByJavaProject/app/src/main/java/com/ls/frameworkmodebyjavaproject/mvvm/BindingAdapters.java

对应的布局绑定仍然保持简单:

<ProgressBar
    android:id="@+id/loading"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:visibility="@{viewModel.showLoading}"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent" />

FrameworkModeByJavaProject/app/src/main/res/layout/activity_mvvm.xml

这样一来,布局依然只负责表达“我要绑定哪个状态”,至于具体怎么把状态转成控件可接受的值,则交给 BindingAdapter 统一处理。

4.8 MVVM 的工作流程与优缺点

到这里,MVVM 的完整工作流已经比较清晰了:

  1. 用户在 View 中操作输入框或按钮。
  2. View 把事件交给 ViewModel。
  3. ViewModel 调用 Model 执行数据处理。
  4. Model 把结果回传给 ViewModel。
  5. ViewModel 把结果整理成 LiveData 状态。
  6. View 通过观察或数据绑定自动响应这些状态变化。

相比 MVP,它最大的变化不是“多了一个类”,而是页面更新模式从“主动调用界面方法”变成了“被动观察状态变化”。这会带来几个很明显的优点:

  1. View 与 Model 的耦合进一步降低,页面只关心状态,不关心请求细节。
  2. 数据驱动 UI,减少了大量手动 setText、setVisibility 一类重复更新代码。
  3. ViewModel 独立于 Activity 的具体实现,更利于测试与复用。
  4. 当页面状态越来越多时,拆成多个 LiveData 字段也更利于维护。

当然,MVVM 也有自己的成本:

  1. LiveData、DataBinding、BindingAdapter 都需要额外学习和调试成本。
  2. 布局一复杂,数据绑定表达式和适配器会让问题定位变得更隐蔽。
  3. 如果状态建模不合理,ViewModel 同样可能变成新的复杂中心。

但从 Android 官方推荐方向和复杂页面可维护性来看,MVVM 确实更适合作为现代 Android 项目的主流架构方案。

5. 相关代码附录

5.1 app 入口与公共请求定义

整个示例工程先通过入口页把三种架构模式串起来,方便在同一个应用里分别验证 MVC、MVP 和 MVVM 的页面行为。下面这份入口页代码只负责跳转,不承担业务逻辑。

package com.ls.frameworkmodebyjavaproject;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        findViewById(R.id.btn_mvc).setOnClickListener(view ->{
            startActivity(new Intent(this, MvcActivity.class));
        });
        findViewById(R.id.btn_mvp).setOnClickListener(view ->{
            startActivity(new Intent(this, MvpActivity.class));
        });
        findViewById(R.id.btn_mvvm).setOnClickListener(view ->{
            startActivity(new Intent(this, MvvmActivity.class));
        });
    }
}

FrameworkModeByJavaProject/app/src/main/java/com/ls/frameworkmodebyjavaproject/MainActivity.java

入口布局同样保持极简,只提供三个按钮,分别进入三种架构示例页。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/main"
    android:layout_width="match_parent"
    android:padding="16dp"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <Button
        android:id="@+id/btn_mvc"
        android:text="MVC架构模式"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>
    <Button
        android:id="@+id/btn_mvp"
        android:text="MVP架构模式"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>
    <Button
        android:id="@+id/btn_mvvm"
        android:text="MVVM架构模式"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>
</LinearLayout>

FrameworkModeByJavaProject/app/src/main/res/layout/activity_main.xml

清单文件中注册了入口页和三种架构页面,同时声明了网络权限,保证所有示例请求都能正常执行。

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">
    <uses-permission android:name="android.permission.INTERNET"/>
    <application
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.FrameworkModeByJavaProject"
        tools:targetApi="31">
        <activity
            android:name=".mvvm.MvvmActivity"
            android:exported="false" />
        <activity
            android:name=".mvp.MvpActivity"
            android:exported="false" />
        <activity
            android:name=".mvc.MvcActivity"
            android:exported="false" />
        <activity
            android:name=".MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
</manifest>

FrameworkModeByJavaProject/app/src/main/AndroidManifest.xml

app 模块的构建文件则统一放置网络、Lifecycle 与 DataBinding 依赖,保证三种模式都运行在同一套基础设施上。

plugins {
    alias(libs.plugins.androidApplication)
}

android {
    namespace 'com.ls.frameworkmodebyjavaproject'
    compileSdk 34
    dataBinding {
        enabled = true
    }
    defaultConfig {
        applicationId "com.ls.frameworkmodebyjavaproject"
        minSdk 26
        targetSdk 34
        versionCode 1
        versionName "1.0"
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }

    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
}

dependencies {

    implementation libs.appcompat
    implementation libs.material
    implementation libs.activity
    implementation libs.constraintlayout
    testImplementation libs.junit
    androidTestImplementation libs.ext.junit
    androidTestImplementation libs.espresso.core

    implementation 'com.google.code.gson:gson:2.11.0'
    implementation 'com.squareup.okhttp3:okhttp:4.12.0'
    implementation 'com.squareup.retrofit2:retrofit:2.11.0'
    implementation 'com.squareup.retrofit2:converter-gson:2.11.0'

    implementation "androidx.lifecycle:lifecycle-livedata:2.6.1"
    implementation "androidx.lifecycle:lifecycle-viewmodel:2.6.1"
}

FrameworkModeByJavaProject/app/build.gradle

请求接口和实体类是三种模式共享的数据基础。ApiService 定义了查询用户与登录两个接口,是 Model 层与远端通信的统一入口。

package com.ls.frameworkmodebyjavaproject.reterofit;

public interface ApiService {

    @GET("addons/cms/api.user/userInfo")
    Call<UserInfoQuery> getUserInfo(
            @Query("user_id") String userId,
            @Query("type") String type);

    @POST("addons/cms/api.login/login")
    Call<ResLogin> login(@Body ReqLogin login);
}

FrameworkModeByJavaProject/app/src/main/java/com/ls/frameworkmodebyjavaproject/reterofit/ApiService.java

登录请求体只承载账号和密码,是 POST 请求提交参数的简单封装。

package com.ls.frameworkmodebyjavaproject.bean;

public class ReqLogin {
    private String account;
    private String password;

    public ReqLogin(String account, String password) {
        this.account = account;
        this.password = password;
    }

    public String getAccount() {
        return account;
    }

    public void setAccount(String account) {
        this.account = account;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }
}

FrameworkModeByJavaProject/app/src/main/java/com/ls/frameworkmodebyjavaproject/bean/ReqLogin.java

登录返回体负责承接 token 与 user_id,是登录成功提示与后续用户状态更新的来源。

package com.ls.frameworkmodebyjavaproject.bean;

public class ResLogin {

    private int code;
    private String msg;
    private String time;
    private DataBean data;

    public static class DataBean {
        private String token;
        private int user_id;

        // 省略 getter、setter 方法
    }

    // 省略 getter、setter 方法
}

FrameworkModeByJavaProject/app/src/main/java/com/ls/frameworkmodebyjavaproject/bean/ResLogin.java

用户查询返回体负责承接昵称等嵌套结构,是后面三个页面展示查询结果时共同依赖的数据模型。

package com.ls.frameworkmodebyjavaproject.bean;

public class UserInfoQuery {
   private int code;
   private String msg;
   private String time;
   private DataBean data;

   public static class DataBean {

      private UserBean user;
      private ListBean list;
      private int archives;
      private int comments;

      public static class UserBean {
         private int id;
         private String nickname;
         private String bio;
         private String avatar;
         private String status;
         private String url;
      }

      public static class ListBean {
         private int total;
         private int per_page;
         private int current_page;
         private int last_page;
      }
   }

   // 省略 getter、setter 方法
}

FrameworkModeByJavaProject/app/src/main/java/com/ls/frameworkmodebyjavaproject/bean/UserInfoQuery.java

5.2 MVC 相关代码

MVC 页面布局负责收集查询与登录输入,是 View 层最直接的承载体。

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="16dp"
    tools:context=".mvc.MvcActivity">
    <TextView
        android:id="@+id/tv_get_label"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="GET请求:"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <EditText
        android:id="@+id/et_user_id"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="请输入要查询的用户id"
        android:inputType="number"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/tv_get_label" />

    <Button
        android:id="@+id/btn_get"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="发起GET请求"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/et_user_id" />

    <TextView
        android:id="@+id/tv_post_label"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="40dp"
        android:text="GET请求:"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/btn_get" />

    <EditText
        android:id="@+id/et_user_name"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="请输入用户名"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/tv_post_label" />

    <EditText
        android:id="@+id/et_password"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="请输入密码"
        android:inputType="textPassword"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/et_user_name" />

    <Button
        android:id="@+id/btn_post"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="发起POST请求"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/et_password" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Retrofit"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

FrameworkModeByJavaProject/app/src/main/res/layout/activity_mvc.xml

MvcUserModel 是 MVC 中的数据处理中心,负责请求发起与回调结果转发,它对应正文里“把请求逻辑从 Activity 中抽离到 Model”的关键步骤。

package com.ls.frameworkmodebyjavaproject.mvc;

public class MvcUserModel {

    private static final String TAG = "MvcUserModel";

    UserModelCallback mCallback;

    public MvcUserModel(UserModelCallback callback) {
        this.mCallback = callback;
    }

    protected void sendPostRequest(String account, String password) {

        if (account == null || account.isEmpty()) {
            Log.i(TAG, "sendPostRequest: 用户名不能为空!");
            mCallback.onLoginFail(" 用户名不能为空!");
            return;
        }

        if (password == null || password.isEmpty()) {
            Log.i(TAG, "sendPostRequest: 密码不能为空!");
            mCallback.onLoginFail(" 密码不能为空!");
            return;
        }

        Retrofit retrofit = new Retrofit.Builder()
                .baseUrl("https://titok.fzqq.fun/")
                .addConverterFactory(GsonConverterFactory.create())
                .build();

        ApiService apiService = retrofit.create(ApiService.class);
        Call<ResLogin> loginCall = apiService.login(new ReqLogin(account, password));

        loginCall.enqueue(new Callback<ResLogin>() {
            @Override
            public void onResponse(Call<ResLogin> call, Response<ResLogin> response) {
                ResLogin resLogin = response.body();
                if (response.isSuccessful() && resLogin.getCode() == 1) {
                    mCallback.onLoginSuccess(resLogin);
                } else {
                    mCallback.onLoginFail(resLogin.getMsg());
                }
            }

            @Override
            public void onFailure(Call<ResLogin> call, Throwable throwable) {
                mCallback.onLoginFail("网络请求失败");
            }
        });
    }

    protected void sendGetRequest(String userId) {

        if (userId == null || userId.isEmpty()) {
            Log.i(TAG, "sendGetRequest: 查询id不能为空");
            return;
        }

        Retrofit retrofit = new Retrofit.Builder()
                .baseUrl("https://titok.fzqq.fun/")
                .addConverterFactory(GsonConverterFactory.create())
                .build();

        ApiService apiService = retrofit.create(ApiService.class);
        Call<UserInfoQuery> queryCall = apiService.getUserInfo(userId, "archives");

        queryCall.enqueue(new Callback<UserInfoQuery>() {
            @Override
            public void onResponse(Call<UserInfoQuery> call, Response<UserInfoQuery> response) {
                UserInfoQuery body = response.body();
                String nickname = body.getData().getUser().getNickname();

                mCallback.onUserInfoQuerySuccess(body);
                Log.i(TAG, "onResponse: body " + nickname);
            }

            @Override
            public void onFailure(Call<UserInfoQuery> call, Throwable throwable) {
                mCallback.onUserInfoQueryFail("网络请求失败!");
            }
        });
    }

    protected interface UserModelCallback {

        void onUserInfoQuerySuccess(UserInfoQuery userInfo);

        void onUserInfoQueryFail(String errorMessage);

        void onLoginSuccess(ResLogin login);

        void onLoginFail(String errorMessage);
    }
}

FrameworkModeByJavaProject/app/src/main/java/com/ls/frameworkmodebyjavaproject/mvc/MvcUserModel.java

MvcActivity 同时承担 View 与 Controller 的职责,这正是 MVC 结构特点最集中的体现,也是后面演进到 MVP 的起点。

package com.ls.frameworkmodebyjavaproject.mvc;

public class MvcActivity extends AppCompatActivity
        implements View.OnClickListener, MvcUserModel.UserModelCallback {

    private static final String TAG = "MvcActivity";
    private EditText etUserId;
    private EditText etUserName;
    private EditText etPassword;
    private MvcUserModel mMvcUsmerModel;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_mvc);

        mMvcUsmerModel = new MvcUserModel(this);

        findViewById(R.id.btn_get).setOnClickListener(this);
        findViewById(R.id.btn_post).setOnClickListener(this);
        etUserId = findViewById(R.id.et_user_id);
        etUserName = findViewById(R.id.et_user_name);
        etPassword = findViewById(R.id.et_password);
    }

    @Override
    public void onClick(View v) {
        if (v.getId() == R.id.btn_get) {
            String userId = etUserId.getText().toString();
            mMvcUsmerModel.sendGetRequest(userId);
        } else if (v.getId() == R.id.btn_post) {
            String account = etUserName.getText().toString();
            String password = etPassword.getText().toString();
            mMvcUsmerModel.sendPostRequest(account, password);
        }
    }

    @Override
    public void onUserInfoQuerySuccess(UserInfoQuery userInfo) {
        String nickname = userInfo.getData().getUser().getNickname();
        Toast.makeText(MvcActivity.this, "查询到:" + nickname, Toast.LENGTH_SHORT).show();
    }

    @Override
    public void onUserInfoQueryFail(String errorMessage) {
        Toast.makeText(MvcActivity.this, errorMessage, Toast.LENGTH_SHORT).show();
    }

    @Override
    public void onLoginSuccess(ResLogin login) {
        Toast.makeText(MvcActivity.this, "欢迎你," +
                login.getData().getUser_id(), Toast.LENGTH_SHORT).show();
    }

    @Override
    public void onLoginFail(String errorMessage) {
        Toast.makeText(MvcActivity.this, errorMessage, Toast.LENGTH_SHORT).show();
    }
}

FrameworkModeByJavaProject/app/src/main/java/com/ls/frameworkmodebyjavaproject/mvc/MvcActivity.java

5.3 MVP 相关代码

MVP 页面布局在 MVC 基础上增加了 loading 控件,它对应正文里“由 Presenter 统一控制页面状态”的演示场景。

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="16dp"
    tools:context=".mvp.MvpActivity">

    <TextView
        android:id="@+id/tv_get_label"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="GET请求:"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <EditText
        android:id="@+id/et_user_id"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="请输入要查询的用户id"
        android:inputType="number"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/tv_get_label" />

    <Button
        android:id="@+id/btn_get"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="发起GET请求"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/et_user_id" />

    <TextView
        android:id="@+id/tv_post_label"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="40dp"
        android:text="Post请求:"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/btn_get" />

    <EditText
        android:id="@+id/et_user_name"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="请输入用户名"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/tv_post_label" />

    <EditText
        android:id="@+id/et_password"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="请输入密码"
        android:inputType="textPassword"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/et_user_name" />

    <Button
        android:id="@+id/btn_post"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="发起POST请求"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/et_password" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Retrofit"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent" />

    <ProgressBar
        android:id="@+id/loading"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:visibility="invisible"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

FrameworkModeByJavaProject/app/src/main/res/layout/activity_mvp.xml

MvpUserView 是页面层暴露给 Presenter 的能力清单,它对应正文里“先定义界面能做什么,再由 Presenter 决定何时调用”的那一步。

package com.ls.frameworkmodebyjavaproject.mvp;

public interface MvpUserView {

    void showUserInfo(UserInfoQuery info);

    void onLoginSuccess(ResLogin login);

    void showError(String error);

    void showLoading();

    void hideLoading();
}

FrameworkModeByJavaProject/app/src/main/java/com/ls/frameworkmodebyjavaproject/mvp/MvpUserView.java

MvpUserModel 延续数据处理职责,但改成“回调作为方法参数”的方式,为 Presenter 提供更灵活的流程组织能力。

package com.ls.frameworkmodebyjavaproject.mvp;

public class MvpUserModel {

    private static final String TAG = "MvcUserModel";

    public MvpUserModel() {
    }

    protected void sendPostRequest(String account, String password, UserLoginCallback callback) {

        if (account == null || account.isEmpty()) {
            Log.i(TAG, "sendPostRequest: 用户名不能为空!");
            callback.onLoginFail(" 用户名不能为空!");
            return;
        }

        if (password == null || password.isEmpty()) {
            Log.i(TAG, "sendPostRequest: 密码不能为空!");
            callback.onLoginFail(" 密码不能为空!");
            return;
        }

        Retrofit retrofit = new Retrofit.Builder()
                .baseUrl("https://titok.fzqq.fun/")
                .addConverterFactory(GsonConverterFactory.create())
                .build();

        ApiService apiService = retrofit.create(ApiService.class);
        Call<ResLogin> loginCall = apiService.login(new ReqLogin(account, password));

        loginCall.enqueue(new Callback<ResLogin>() {
            @Override
            public void onResponse(Call<ResLogin> call, Response<ResLogin> response) {
                ResLogin resLogin = response.body();
                if (response.isSuccessful() && resLogin.getCode() == 1) {
                    callback.onLoginSuccess(resLogin);
                } else {
                    callback.onLoginFail(resLogin.getMsg());
                }
            }

            @Override
            public void onFailure(Call<ResLogin> call, Throwable throwable) {
                callback.onLoginFail("网络请求失败");
            }
        });
    }

    protected void sendGetRequest(String userId, UserQueryCallback callback) {

        if (userId == null || userId.isEmpty()) {
            Log.i(TAG, "sendGetRequest: 查询id不能为空");
            return;
        }

        Retrofit retrofit = new Retrofit.Builder()
                .baseUrl("https://titok.fzqq.fun/")
                .addConverterFactory(GsonConverterFactory.create())
                .build();

        ApiService apiService = retrofit.create(ApiService.class);
        Call<UserInfoQuery> queryCall = apiService.getUserInfo(userId, "archives");

        queryCall.enqueue(new Callback<UserInfoQuery>() {
            @Override
            public void onResponse(Call<UserInfoQuery> call, Response<UserInfoQuery> response) {
                UserInfoQuery body = response.body();
                String nickname = body.getData().getUser().getNickname();

                callback.onUserInfoQuerySuccess(body);
                Log.i(TAG, "onResponse: body " + nickname);
            }

            @Override
            public void onFailure(Call<UserInfoQuery> call, Throwable throwable) {
                callback.onUserInfoQueryFail("网络请求失败!");
            }
        });
    }

    protected interface UserQueryCallback {
        void onUserInfoQuerySuccess(UserInfoQuery userInfo);
        void onUserInfoQueryFail(String errorMessage);
    }

    protected interface UserLoginCallback {
        void onLoginSuccess(ResLogin login);
        void onLoginFail(String errorMessage);
    }
}

FrameworkModeByJavaProject/app/src/main/java/com/ls/frameworkmodebyjavaproject/mvp/MvpUserModel.java

MvpUserPresenter 是 MVP 的流程中枢,它对应正文里“显示加载、发请求、处理回调、更新界面”的整套编排逻辑。

package com.ls.frameworkmodebyjavaproject.mvp;

public class MvpUserPresenter {

    private final MvpUserView mView;
    private MvpUserModel mModel;

    public MvpUserPresenter(MvpUserView view) {
        mModel = new MvpUserModel();
        mView = view;
    }

    public void getUserInfo(String userId) {
        mView.showLoading();
        mModel.sendGetRequest(userId, new MvpUserModel.UserQueryCallback() {
            @Override
            public void onUserInfoQuerySuccess(UserInfoQuery userInfo) {
                mView.showUserInfo(userInfo);
                mView.hideLoading();
            }

            @Override
            public void onUserInfoQueryFail(String errorMessage) {
                mView.showError(errorMessage);
                mView.hideLoading();
            }
        });
    }

    public void login(String account, String password) {
        mView.showLoading();
        mModel.sendPostRequest(account, password, new MvpUserModel.UserLoginCallback() {
            @Override
            public void onLoginSuccess(ResLogin login) {
                mView.onLoginSuccess(login);
                mView.hideLoading();
            }

            @Override
            public void onLoginFail(String errorMessage) {
                mView.showError(errorMessage);
                mView.hideLoading();
            }
        });
    }
}

FrameworkModeByJavaProject/app/src/main/java/com/ls/frameworkmodebyjavaproject/mvp/MvpUserPresenter.java

MvpActivity 在 MVP 中只作为 View 实现类存在,它负责提供 UI 操作方法,并把点击事件交给 Presenter。

package com.ls.frameworkmodebyjavaproject.mvp;

public class MvpActivity extends AppCompatActivity
        implements View.OnClickListener, MvpUserView {
    private EditText etUserId;
    private EditText etUserName;
    private EditText etPassword;
    private MvpUserPresenter mPresenter;
    private ProgressBar loading;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_mvp);

        mPresenter = new MvpUserPresenter(this);

        findViewById(R.id.btn_get).setOnClickListener(this);
        findViewById(R.id.btn_post).setOnClickListener(this);
        etUserId = findViewById(R.id.et_user_id);
        etUserName = findViewById(R.id.et_user_name);
        etPassword = findViewById(R.id.et_password);

        loading = findViewById(R.id.loading);
    }

    @Override
    public void onClick(View v) {
        if (v.getId() == R.id.btn_get) {
            String userId = etUserId.getText().toString().trim();
            mPresenter.getUserInfo(userId);
        } else if (v.getId() == R.id.btn_post) {
            String account = etUserName.getText().toString().trim();
            String password = etPassword.getText().toString().trim();
            mPresenter.login(account,password);
        }
    }

    @Override
    public void showUserInfo(UserInfoQuery info) {
        String nickname = info.getData().getUser().getNickname();
        Toast.makeText(this, "查询到:" + nickname, Toast.LENGTH_SHORT).show();
    }

    @Override
    public void onLoginSuccess(ResLogin login) {
        Toast.makeText(this, "欢迎你," +
                login.getData().getUser_id(), Toast.LENGTH_SHORT).show();
    }

    @Override
    public void showError(String error) {
        Toast.makeText(this, error, Toast.LENGTH_SHORT).show();
    }

    @Override
    public void showLoading() {
        loading.setVisibility(View.VISIBLE);
    }

    @Override
    public void hideLoading() {
        loading.setVisibility(View.INVISIBLE);
    }
}

FrameworkModeByJavaProject/app/src/main/java/com/ls/frameworkmodebyjavaproject/mvp/MvpActivity.java

5.4 MVVM 相关代码

MVVM 布局是数据绑定的直接落点,它对应正文里“把 ViewModel 暴露给布局,再让控件直接读取状态”的那部分内容。

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">

    <data>

        <variable
            name="viewModel"
            type="com.ls.frameworkmodebyjavaproject.mvvm.MvvmViewModel" />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:id="@+id/main"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:padding="16dp"
        tools:context=".mvvm.MvvmActivity">

        <TextView
            android:id="@+id/tv_get_label"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="GET请求:"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <EditText
            android:id="@+id/et_user_id"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:hint="请输入要查询的用户id"
            android:inputType="number"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/tv_get_label" />

        <Button
            android:id="@+id/btn_get"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="发起GET请求"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/et_user_id" />

        <TextView
            android:id="@+id/tv_post_label"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="40dp"
            android:text="GET请求:"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/btn_get" />

        <EditText
            android:id="@+id/et_user_name"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:hint="请输入用户名"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/tv_post_label" />

        <EditText
            android:id="@+id/et_password"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:hint="请输入密码"
            android:inputType="textPassword"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/et_user_name" />

        <Button
            android:id="@+id/btn_post"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="发起POST请求"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/et_password" />

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Retrofit"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent" />

        <ProgressBar
            android:id="@+id/loading"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:visibility="@{viewModel.showLoading}"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <TextView
            android:id="@+id/tv_nick_name"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@={viewModel.nickName}"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            tools:text="nickName" />

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginLeft="16dp"
            android:text="@{viewModel.userId}"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintStart_toEndOf="@id/tv_nick_name"
            tools:text="userId" />
    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

FrameworkModeByJavaProject/app/src/main/res/layout/activity_mvvm.xml

MvvmUserModel 继续负责数据请求,它和前两种模式的 Model 共享同样的职责边界,对应正文里“数据层职责稳定不变”的部分。

package com.ls.frameworkmodebyjavaproject.mvvm;

public class MvvmUserModel {

    private static final String TAG = "MvvmUserModel";

    public MvvmUserModel() {
    }

    protected void sendPostRequest(String account, String password, UserLoginCallback callback) {

        if (account == null || account.isEmpty()) {
            Log.i(TAG, "sendPostRequest: 用户名不能为空!");
            callback.onLoginFail(" 用户名不能为空!");
            return;
        }

        if (password == null || password.isEmpty()) {
            Log.i(TAG, "sendPostRequest: 密码不能为空!");
            callback.onLoginFail(" 密码不能为空!");
            return;
        }

        Retrofit retrofit = new Retrofit.Builder()
                .baseUrl("https://titok.fzqq.fun/")
                .addConverterFactory(GsonConverterFactory.create())
                .build();

        ApiService apiService = retrofit.create(ApiService.class);
        Call<ResLogin> loginCall = apiService.login(new ReqLogin(account, password));

        loginCall.enqueue(new Callback<ResLogin>() {
            @Override
            public void onResponse(Call<ResLogin> call, Response<ResLogin> response) {
                ResLogin resLogin = response.body();
                if (response.isSuccessful() && resLogin.getCode() == 1) {
                    callback.onLoginSuccess(resLogin);
                } else {
                    callback.onLoginFail(resLogin.getMsg());
                }
            }

            @Override
            public void onFailure(Call<ResLogin> call, Throwable throwable) {
                callback.onLoginFail("网络请求失败");
            }
        });
    }

    protected void sendGetRequest(String userId, UserQueryCallback callback) {

        if (userId == null || userId.isEmpty()) {
            Log.i(TAG, "sendGetRequest: 查询id不能为空");
            return;
        }

        Retrofit retrofit = new Retrofit.Builder()
                .baseUrl("https://titok.fzqq.fun/")
                .addConverterFactory(GsonConverterFactory.create())
                .build();

        ApiService apiService = retrofit.create(ApiService.class);
        Call<UserInfoQuery> queryCall = apiService.getUserInfo(userId, "archives");

        queryCall.enqueue(new Callback<UserInfoQuery>() {
            @Override
            public void onResponse(Call<UserInfoQuery> call, Response<UserInfoQuery> response) {
                UserInfoQuery body = response.body();
                String nickname = body.getData().getUser().getNickname();

                callback.onUserInfoQuerySuccess(body);
                Log.i(TAG, "onResponse: body " + nickname);
            }

            @Override
            public void onFailure(Call<UserInfoQuery> call, Throwable throwable) {
                callback.onUserInfoQueryFail("网络请求失败!");
            }
        });
    }

    protected interface UserQueryCallback {
        void onUserInfoQuerySuccess(UserInfoQuery userInfo);
        void onUserInfoQueryFail(String errorMessage);
    }

    protected interface UserLoginCallback {
        void onLoginSuccess(ResLogin login);
        void onLoginFail(String errorMessage);
    }
}

FrameworkModeByJavaProject/app/src/main/java/com/ls/frameworkmodebyjavaproject/mvvm/MvvmUserModel.java

MvvmViewModel 是 MVVM 的核心,它把网络结果拆成可观察状态,并负责类型转换、错误透出与加载状态控制,对应正文里“从流程控制转向状态驱动”的整章内容。

package com.ls.frameworkmodebyjavaproject.mvvm;

public class MvvmViewModel extends ViewModel {

    private final MvvmUserModel mModel;

    private final MutableLiveData<Boolean> mShowLoading = new MutableLiveData<>();
    private final MutableLiveData<String> mNickName = new MutableLiveData<>();
    private final MutableLiveData<Integer> mUserId = new MutableLiveData<>();
    private final LiveData<String> mStrUserId = Transformations.map(mUserId, new Function1<Integer, String>() {
        @Override
        public String invoke(Integer integer) {
            String content = "我查询到了用户id:" + integer;
            return content;
        }
    });
    private final MutableLiveData<String> mErrorMsg = new MutableLiveData<>();

    public MvvmViewModel() {
        mModel = new MvvmUserModel();
    }

    public LiveData<String> getStrUserId() {
        return mStrUserId;
    }

    public MutableLiveData<String> getErrorMsg() {
        return mErrorMsg;
    }

    public MutableLiveData<Boolean> getShowLoading() {
        return mShowLoading;
    }

    public MutableLiveData<String> getNickName() {
        return mNickName;
    }

    public MutableLiveData<Integer> getUserId() {
        return mUserId;
    }

    public void getUserInfo(String userId) {
        mShowLoading.setValue(true);
        mModel.sendGetRequest(userId, new MvvmUserModel.UserQueryCallback() {
            @Override
            public void onUserInfoQuerySuccess(UserInfoQuery userInfo) {
                String nickname = userInfo.getData().getUser().getNickname();
                mNickName.setValue(nickname);
                mShowLoading.setValue(false);
            }

            @Override
            public void onUserInfoQueryFail(String errorMessage) {
                mNickName.setValue(null);
                mShowLoading.setValue(false);
                mErrorMsg.setValue(errorMessage);
            }
        });
    }

    public void login(String account, String password) {
        mShowLoading.setValue(true);
        mModel.sendPostRequest(account, password, new MvvmUserModel.UserLoginCallback() {
            @Override
            public void onLoginSuccess(ResLogin login) {
                int userId = login.getData().getUser_id();
                mUserId.setValue(userId);
                mShowLoading.setValue(false);
            }

            @Override
            public void onLoginFail(String errorMessage) {
                mUserId.setValue(0);
                mShowLoading.setValue(false);
                mErrorMsg.setValue(errorMessage);
            }
        });
    }
}

FrameworkModeByJavaProject/app/src/main/java/com/ls/frameworkmodebyjavaproject/mvvm/MvvmViewModel.java

BindingAdapters 负责处理布局属性与状态类型不一致的场景,它对应正文里“用 BindingAdapter 兜住文本和显隐属性转换”的那一步。

package com.ls.frameworkmodebyjavaproject.mvvm;

public class BindingAdapters {

    @BindingAdapter("android:text")
    public static void mySetText(TextView textView, int value) {
        if (value == 0) {
            return;
        }
        textView.setText("我接收到了一个userid = " + value);
    }

    @BindingAdapter("android:visibility")
    public static void setVisibility(ProgressBar progressBar, boolean show) {
        progressBar.setVisibility(show ? View.VISIBLE : View.INVISIBLE);
    }
}

FrameworkModeByJavaProject/app/src/main/java/com/ls/frameworkmodebyjavaproject/mvvm/BindingAdapters.java

MvvmActivity 最终只负责初始化绑定、把用户操作交给 ViewModel,以及在仍保留观察者写法的地方响应状态变化,它对应正文里“页面成为状态消费者”的最终落点。

package com.ls.frameworkmodebyjavaproject.mvvm;

public class MvvmActivity extends AppCompatActivity implements View.OnClickListener {

    private EditText etUserId;
    private EditText etUserName;
    private EditText etPassword;
    private ProgressBar loading;
    private MvvmViewModel mViewModel;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        ActivityMvvmBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_mvvm);

        mViewModel = new ViewModelProvider(this).get(MvvmViewModel.class);

        binding.setLifecycleOwner(this);
        binding.setViewModel(mViewModel);

        findViewById(R.id.btn_get).setOnClickListener(this);
        findViewById(R.id.btn_post).setOnClickListener(this);
        etUserId = findViewById(R.id.et_user_id);
        etUserName = findViewById(R.id.et_user_name);
        etPassword = findViewById(R.id.et_password);

        loading = findViewById(R.id.loading);

        mViewModel.getShowLoading().observe(this, new Observer<Boolean>() {
            @Override
            public void onChanged(Boolean show) {
                loading.setVisibility(show ? View.VISIBLE : View.INVISIBLE);
            }
        });

        mViewModel.getUserId().observe(this, new Observer<Integer>() {
            @Override
            public void onChanged(Integer userId) {
                int i = userId.intValue();

                if (i > 0) {
                    Toast.makeText(MvvmActivity.this,
                            "欢迎你," + i, Toast.LENGTH_SHORT).show();
                }
            }
        });

        mViewModel.getNickName().observe(this, new Observer<String>() {
            @Override
            public void onChanged(String s) {
                Toast.makeText(MvvmActivity.this, "查询到:" + s, Toast.LENGTH_SHORT).show();
            }
        });

        mViewModel.getErrorMsg().observe(this, new Observer<String>() {
            @Override
            public void onChanged(String error) {
                Toast.makeText(MvvmActivity.this, error, Toast.LENGTH_SHORT).show();
            }
        });
    }

    @Override
    public void onClick(View v) {
        if (v.getId() == R.id.btn_get) {
            String userId = etUserId.getText().toString().trim();
            mViewModel.getUserInfo(userId);
        } else if (v.getId() == R.id.btn_post) {
            String account = etUserName.getText().toString().trim();
            String password = etPassword.getText().toString().trim();
            mViewModel.login(account, password);
        }
    }
}

FrameworkModeByJavaProject/app/src/main/java/com/ls/frameworkmodebyjavaproject/mvvm/MvvmActivity.java

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐