目的

架构设计的主要目的是为了解决软件系统复杂度带来的问题,通过设计使程序模块化,做到模块内部的高聚合和模块之间的低耦合。


常见的架构设计

常见的架构设计有MVC、MVP和MVVM,当前MVP和MVVM的使用相对比较广泛,下面将逐个介绍这几种架构设计。

MVC

MVC(Model View Controller):将控制器、模型和视图分离,降低耦合,但并未完全解耦

  • Model层:收到数据更新请求后,进行业务逻辑处理,并通知View层去更新界面显示
  • View层:一般采用xml文件进行界面的描述,当接收到用户的操作时,会将用户操作的事件传递给Controller
  • Controller层:收到请求后立即响应,进行业务逻辑处理,并通知Model层处理数据
    在这里插入图片描述
    上图为被动MVC流程图,和主动MVC不同的是View没有订阅Model数据变化的事件

MVP

MVP(Model View Presenter):从MVC框架演变过来的,将模型和视图完全分离,提高了扩展性

  • Model层:负责处理数据业务逻辑
  • View层:负责根据数据进行界面展示,并向Presenter层报告用户行为
  • Presenter层:负责从Model层拿数据,并交给View层,是Model和View之间的桥梁
    在这里插入图片描述

MVVM

MVVM(Model View ViewModel):跟MVP相似,将模型和视图完全分离,通过DataBinding将数据的变量值与视图绑定,当数据发生变化时,View会自动更新

  • Model层:负责处理数据模型
  • View层:对应Activity和xml,负责View的绘制以及用户交互
  • ViewModel层:负责完成View于Model间的交互
    在这里插入图片描述

DataBinding 是 Google 在 JetPack(Google推出的一些库的集合) 中推出的一款数据绑定的支持库,MVVM 是一种程序架构设计模式,主要目的是分离视图(View)和模型(Model),在MVVM模式中ViewModel和View是用绑定关系来实现的,View层不需要findViewById,也不需要拿到具体的View去设置数据,这些都可以通过DataBinding完成,它同时也增加了布局文件的复杂度。


MVC/MVP和MVVM演示

这里以“天气预报”的需求为案例:用户手动输入城市,点击刷新按钮,进行网络请求,返回数据之后再更新用户界面。同一个需求,同一套布局,同样的功能,不同的架构设计,通过MVC/MVP和MVVM模式去分别实现。
在这里插入图片描述

MVC代码示例

天气实体类:

public class Weather {
    
    private String city;
    private String temp;
    private String weather;
    private String wind;
    
    public String getCity() {
        return city;
    }
    public void setCity(String city) {
        this.city = city;
    }
    public String getTemp() {
        return temp;
    }
    public void setTemp(String temp) {
        this.temp = temp;
    }
    public String getWeather() {
        return weather;
    }
    public void setWeather(String weather) {
        this.weather = weather;
    }
    public String getWind() {
        return wind;
    }
    public void setWind(String wind) {
        this.wind = wind;
    }

}

Model层,收到数据更新请求后,进行业务逻辑处理,并通知View层去更新界面显示

public class WeatherModel {

    private WeatherView weatherView;
    private final static Handler handler = new Handler(Looper.getMainLooper());

    public WeatherModel(WeatherView weatherView) {
        this.weatherView = weatherView;
    }

    // 切换城市
    public void changeCity(final String city) {
        try {
            // 开启一个子线程进行网络请求获取天气预报信息(网络请求,通常会引用第三方开源库或自己封装成工具类)
            new Thread(new Runnable() {
                @Override
                public void run() {
                    StringBuilder builder = new StringBuilder();
                    try {
                        // http://wthrcdn.etouch.cn/weather_mini?city=
                        URL url = new URL("http://api.help.bj.cn/apis/weather2d/?id=" + city);
                        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
                        conn.setRequestMethod("GET");
                        conn.setConnectTimeout(2000);
                        conn.setReadTimeout(2000);
                        int code = conn.getResponseCode();
                        if (code == 200) {
                            InputStream inputStream = conn.getInputStream();
                            InputStreamReader inputStreamReader = new InputStreamReader(inputStream, "UTF-8");
                            BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
                            String line;
                            while ((line = bufferedReader.readLine()) != null) {
                                builder.append(line);
                            }
                        }
                    } catch (Exception e) {
                    }
                    String json = builder.toString();
                    Log.e("TAG", "json:" + json);
                    if (TextUtils.isEmpty(json)) {
                        // 如果网络请求不到数据,则模拟一份本地数据进行演示
                        json = "{\"city\":\"" + city + "\"," +
                                "\"temp\":\"20摄氏度\"," +
                                "\"wind\":\"西北风\"," +
                                "\"weather\":\"晴朗多云\"}";
                    }
                    try {
                        JSONObject jsonObject = new JSONObject(json);
                        String city = getJSONValue(jsonObject, "city");
                        String temp = getJSONValue(jsonObject, "temp");
                        String wind = getJSONValue(jsonObject, "wind");
                        String _weather = getJSONValue(jsonObject, "weather");
                        final Weather weather = new Weather();
                        weather.setCity(city);
                        weather.setTemp(temp);
                        weather.setWind(wind);
                        weather.setWeather(_weather);
                        Runnable callbackRunnable = new Runnable() {
                            @Override
                            public void run() {
                                if (null != weatherView) {
                                    weatherView.updateView(weather);
                                }
                            }
                        };
                        handler.post(callbackRunnable);
                    } catch (Exception ex) {
                    }
                }
            }).start();
        } catch (Exception ex) {
        }
    }

    // json解析,通常会引用第三方开源库或自己封装成工具类
    private String getJSONValue(JSONObject jsonObject, String key) {
        try {
            if (null != jsonObject && !TextUtils.isEmpty(key)) {
                return jsonObject.isNull(key) ? "" : jsonObject.getString(key);
            }
        } catch (Exception ex) {
        }
        return "";
    }

}

View层,一般采用xml文件进行界面的描述,当接收到用户的操作时,会将用户操作的事件传递给Controller

public class WeatherView {

    private TextView tvCity;
    private TextView tvTemp;
    private TextView tvWind;
    private TextView tvWeather;

    public WeatherView(WeatherActivity activity) {
        tvCity = activity.findViewById(R.id.tvCity);
        tvTemp = activity.findViewById(R.id.tvTemp);
        tvWeather = activity.findViewById(R.id.tvWeather);
        tvWind = activity.findViewById(R.id.tvWind);
        EditText etCity = activity.findViewById(R.id.etCity);
        // 用户输入框监听 (这里将事件传递给Controller层)
        etCity.addTextChangedListener(activity);
        Button btnRefresh = activity.findViewById(R.id.btnRefresh);
        // 用户按钮点击监听 (这里将事件传递给Controller层)
        btnRefresh.setOnClickListener(activity);
    }

    public void updateView(Weather weather) {
        // 更新视图 (Model层实体数据发生变化,通知View层更新视图)
        tvCity.setText(weather.getCity());
        tvTemp.setText(weather.getTemp());
        tvWind.setText(weather.getWind());
        tvWeather.setText(weather.getWeather());
    }

}

Controller层,收到请求后立即响应,进行业务逻辑处理,并通知Model层处理数据

public class WeatherActivity extends AppCompatActivity implements View.OnClickListener, TextWatcher {

    private String mCity;
    // 视图对象
    private WeatherView mView;
    // 模型对象
    private WeatherModel mModel;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_weather);
        mView = new WeatherView(this);
        mModel = new WeatherModel(mView);
    }

    // 用户点击按钮,互动操作(View层传递给事件给Controller层)
    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.btnRefresh:
                if (!TextUtils.isEmpty(mCity)) {
                    // 当切换城市时,调用该方法进行更新
                    mModel.changeCity(mCity);
                } else {
                    Toast.makeText(this, "城市不能为空,请先输入城市", Toast.LENGTH_SHORT).show();
                }
                break;
        }
    }

    // 用户输入框监听,互动操作(View层传递给事件给Controller层)
    @Override
    public void afterTextChanged(Editable s) {
        mCity = s.toString();
    }

    @Override
    public void beforeTextChanged(CharSequence s, int start, int count, int after) {
    }

    @Override
    public void onTextChanged(CharSequence s, int start, int before, int count) {
    }

}


MVP代码示例

Contract是一个契约,将Model、View、Presenter 进行约束管理,方便后期类的查找和维护

public interface WeatherContract {

    interface Model {
        void updateModel(String city, Presenter presenter);
    }

    interface View {
        void updateView(Weather weather);
    }

    interface Presenter {
        void updateWeather(Weather weather);
    }

}

天气实体类:

public class Weather {

    private String city;
    private String temp;
    private String weather;
    private String wind;

    public String getCity() {
        return city;
    }
    public void setCity(String city) {
        this.city = city;
    }
    public String getTemp() {
        return temp;
    }
    public void setTemp(String temp) {
        this.temp = temp;
    }
    public String getWeather() {
        return weather;
    }
    public void setWeather(String weather) {
        this.weather = weather;
    }
    public String getWind() {
        return wind;
    }
    public void setWind(String wind) {
        this.wind = wind;
    }

}

Model层,负责处理数据业务逻辑

public class WeatherModel implements WeatherContract.Model {

    @Override
    public void updateModel(final String city, final WeatherContract.Presenter presenter) {
        // 开启一个子线程进行网络请求获取天气预报信息(网络请求,通常会引用第三方开源库或自己封装成工具类)
        new Thread(new Runnable() {
            @Override
            public void run() {
                StringBuilder builder = new StringBuilder();
                try {
                    // http://wthrcdn.etouch.cn/weather_mini?city=
                    URL url = new URL("http://api.help.bj.cn/apis/weather2d/?id=" + city);
                    HttpURLConnection conn = (HttpURLConnection) url.openConnection();
                    conn.setRequestMethod("GET");
                    conn.setConnectTimeout(2000);
                    conn.setReadTimeout(2000);
                    int code = conn.getResponseCode();
                    if (code == 200) {
                        InputStream inputStream = conn.getInputStream();
                        InputStreamReader inputStreamReader = new InputStreamReader(inputStream, "UTF-8");
                        BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
                        String line;
                        while ((line = bufferedReader.readLine()) != null) {
                            builder.append(line);
                        }
                    }
                } catch (Exception e) {
                }
                String json = builder.toString();
                if (TextUtils.isEmpty(json)) {
                    // 如果网络请求不到数据,则模拟一份本地数据进行演示
                    json = "{\"city\":\"" + city + "\"," +
                            "\"temp\":\"20摄氏度\"," +
                            "\"wind\":\"西北风\"," +
                            "\"weather\":\"晴朗多云\"}";
                }
                try {
                    // 解析网络请求返回的天气预报数据
                    JSONObject jsonObject = new JSONObject(json);
                    String city = getJSONValue(jsonObject, "city");
                    String temp = getJSONValue(jsonObject, "temp");
                    String wind = getJSONValue(jsonObject, "wind");
                    String _weather = getJSONValue(jsonObject, "weather");
                    Weather weather = new Weather();
                    weather.setCity(city);
                    weather.setTemp(temp);
                    weather.setWind(wind);
                    weather.setWeather(_weather);
                    if (null != presenter) {
                        // model层完成业务逻辑后,presenter层更新实体对象
                        presenter.updateWeather(weather);
                    }
                } catch (Exception ex) {
                }
            }
        }).start();
    }

    // json解析,通常会引用第三方开源库或自己封装成工具类
    private String getJSONValue(JSONObject jsonObject, String key) {
        try {
            if (null != jsonObject && !TextUtils.isEmpty(key)) {
                return jsonObject.isNull(key) ? "" : jsonObject.getString(key);
            }
        } catch (Exception ex) {
        }
        return "";
    }

}

View层,负责根据数据进行界面展示,并向Presenter层报告用户行为

public class WeatherActivity extends AppCompatActivity implements View.OnClickListener, WeatherContract.View {

    // 各个控件
    private EditText etCity;
    private TextView tvCity;
    private TextView tvTemp;
    private TextView tvWind;
    private TextView tvWeather;

    // Presenter对象
    private WeatherPresenter mPresenter;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_weather);
        tvCity = findViewById(R.id.tvCity);
        tvTemp = findViewById(R.id.tvTemp);
        tvWeather = findViewById(R.id.tvWeather);
        tvWind = findViewById(R.id.tvWind);
        etCity = findViewById(R.id.etCity);
        Button btnRefresh = findViewById(R.id.btnRefresh);
        btnRefresh.setOnClickListener(this);

        // 初始化Presenter对象
        mPresenter = new WeatherPresenter(this);
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.btnRefresh:
                // 请求数据
                final String city = etCity.getText().toString();
                if (!TextUtils.isEmpty(city)) {
                    // 当切换城市时,调用该方法进行更新
                    mPresenter.changeCity(city);
                } else {
                    Toast.makeText(this, "城市不能为空,请先输入城市", Toast.LENGTH_SHORT).show();
                }
                break;
        }
    }

    @Override
    public void updateView(final Weather weather) {
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                // 当“天气实体对象”发生改变时,需要更新视图
                tvCity.setText(weather.getCity());
                tvTemp.setText(weather.getTemp());
                tvWind.setText(weather.getWind());
                tvWeather.setText(weather.getWeather());
            }
        });
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        // presenter会持有view对象,为防止异步请求耗时内存泄漏,需执行销毁方法
        mPresenter.onDestroy();
    }

}

Presenter层,负责从Model层拿数据,并交给View层,是Model和View之间的桥梁

public class WeatherPresenter implements WeatherContract.Presenter {

    // presenter层持有view对象
    private WeatherContract.View view;
    // presenter层持有model对象
    private WeatherContract.Model model;

    public WeatherPresenter(WeatherContract.View view) {
        this.view = view;
        this.model = new WeatherModel();
    }

    public void changeCity(String city) {
        // presenter层操纵model层,发起更新操作
        model.updateModel(city, this);
    }

    @Override
    public void updateWeather(Weather weather) {
        if (null != view) {
            // presenter层操纵view层,发起更新操作
            view.updateView(weather);
        }
    }

    public void onDestroy() {
        // 解绑view
        view = null;
    }

}

MVVM代码示例

Model更新和数据加载监听接口

public interface IWeatherMode {

    void updateModel(String city, OnDataLoadListener listener);

}

public interface OnDataLoadListener {

    void start();

    void onFinish(Weather weather);

}

天气实体类:

public class Weather {

    private String city;
    private String temp;
    private String weather;
    private String wind;

    public String getCity() {
        return city;
    }
    public void setCity(String city) {
        this.city = city;
    }
    public String getTemp() {
        return temp;
    }
    public void setTemp(String temp) {
        this.temp = temp;
    }
    public String getWeather() {
        return weather;
    }
    public void setWeather(String weather) {
        this.weather = weather;
    }
    public String getWind() {
        return wind;
    }
    public void setWind(String wind) {
        this.wind = wind;
    }

}

Model层,负责处理数据模型

public class WeatherModel implements IWeatherMode {

    private final static Handler handler = new Handler(Looper.getMainLooper());

    public WeatherModel() {
    }

    @Override
    public void updateModel(final String city, final OnDataLoadListener listener) {
        try {
            // 开启一个子线程进行网络请求获取天气预报信息(网络请求,通常会引用第三方开源库或自己封装成工具类)
            new Thread(new Runnable() {
                @Override
                public void run() {
                    if (null != listener) {
                        listener.start();
                    }
                    StringBuilder builder = new StringBuilder();
                    try {
                        // http://wthrcdn.etouch.cn/weather_mini?city=
                        URL url = new URL("http://api.help.bj.cn/apis/weather2d/?id=" + city);
                        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
                        conn.setRequestMethod("GET");
                        conn.setConnectTimeout(2000);
                        conn.setReadTimeout(2000);
                        int code = conn.getResponseCode();
                        if (code == 200) {
                            InputStream inputStream = conn.getInputStream();
                            InputStreamReader inputStreamReader = new InputStreamReader(inputStream, "UTF-8");
                            BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
                            String line;
                            while ((line = bufferedReader.readLine()) != null) {
                                builder.append(line);
                            }
                        }
                    } catch (Exception e) {
                    }
                    String json = builder.toString();
                    Log.e("TAG", "json:" + json);
                    if (TextUtils.isEmpty(json)) {
                        // 如果网络请求不到数据,则模拟一份本地数据进行演示
                        json = "{\"city\":\"" + city + "\"," +
                                "\"temp\":\"20摄氏度\"," +
                                "\"wind\":\"西北风\"," +
                                "\"weather\":\"晴朗多云\"}";
                    }
                    final String lastJson = json;
                    Runnable callbackRunnable = new Runnable() {
                        @Override
                        public void run() {
                            Weather weather = new Weather();
                            try {
                                JSONObject jsonObject = new JSONObject(lastJson);
                                String city = getJSONValue(jsonObject, "city");
                                String temp = getJSONValue(jsonObject, "temp");
                                String wind = getJSONValue(jsonObject, "wind");
                                String _weather = getJSONValue(jsonObject, "weather");
                                weather.setCity(city);
                                weather.setTemp(temp);
                                weather.setWind(wind);
                                weather.setWeather(_weather);
                            } catch (Exception ex) {
                            } finally {
                                if (null != listener) {
                                    listener.onFinish(weather);
                                }
                            }
                        }
                    };
                    handler.post(callbackRunnable);
                }
            }).start();
        } catch (Exception ex) {
        }
    }

    // json解析,通常会引用第三方开源库或自己封装成工具类
    private String getJSONValue(JSONObject jsonObject, String key) {
        try {
            if (null != jsonObject && !TextUtils.isEmpty(key)) {
                return jsonObject.isNull(key) ? "" : jsonObject.getString(key);
            }
        } catch (Exception ex) {
        }
        return "";
    }

}

View层,对应Activity和xml,负责View的绘制以及用户交互

public class WeatherActivity extends AppCompatActivity {

    // ViewModel对象
    private WeatherViewModel viewModel;
    // 这个是根据布局文件自动生成的类,布局文件名(首字母大写)+Binding
    private ActivityWeatherBinding dataBinding;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        dataBinding = DataBindingUtil.setContentView(this, R.layout.activity_weather);
        // 实例化ViewModel对象
        viewModel = new WeatherViewModel();
        // 绑定ViewModel对象
        dataBinding.setViewModel(viewModel);
        // 点击刷新按钮
        dataBinding.btnRefresh.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // 获取用户输入的“城市”名称
                String city = dataBinding.etCity.getText().toString();
                if (!TextUtils.isEmpty(city)) {
                    // 当切换城市时,调用该方法进行更新
                    viewModel.changeCity(city);
                } else {
                    Toast.makeText(getApplication(), "城市不能为空,请先输入城市", Toast.LENGTH_SHORT).show();
                }
            }
        });
    }

}

ViewModel层,负责完成View于Model间的交互

public class WeatherViewModel {

    private IWeatherMode model;

    public final ObservableField<String> cityField = new ObservableField<>();
    public final ObservableField<String> tempField = new ObservableField<>();
    public final ObservableField<String> weatherField = new ObservableField<>();
    public final ObservableField<String> windField = new ObservableField<>();

    public WeatherViewModel() {
        model = new WeatherModel();
    }

    public void changeCity(String city) {
        model.updateModel(city, new OnDataLoadListener() {
            @Override
            public void start() {
            }

            @Override
            public void onFinish(Weather weather) {
                cityField.set(weather.getCity());
                tempField.set(weather.getTemp());
                weatherField.set(weather.getWeather());
                windField.set(weather.getWind());
            }
        });
    }

}


布局文件

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

    <data>
        <!-- 引用 -->
        <import type="android.view.View" />
        <!-- 数据源 -->
        <variable
            name="ViewModel"
            type="com.wyq.design.mvvm.viewmodel.WeatherViewModel" />
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="#f5f5f5"
        android:orientation="vertical">

        <TextView
            android:layout_width="match_parent"
            android:layout_height="56dp"
            android:layout_gravity="center_vertical"
            android:background="@color/colorPrimary"
            android:gravity="center"
            android:text="天气预报"
            android:textColor="#ffffff"
            android:textSize="20sp" />

        <EditText
            android:id="@+id/etCity"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_margin="16dp"
            android:hint="请输入城市"
            android:singleLine="true"
            android:textColor="#000000"
            android:textSize="16sp" />

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginLeft="16dp"
            android:layout_marginTop="16dp"
            android:layout_marginRight="16dp"
            android:orientation="horizontal">

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="城市:"
                android:textColor="#4B0082"
                android:textSize="18sp" />

            <TextView
                android:id="@+id/tvCity"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginLeft="16dp"
                android:text="@{ViewModel.cityField}"
                android:textColor="#000000"
                android:textSize="18sp" />
        </LinearLayout>

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginLeft="16dp"
            android:layout_marginTop="16dp"
            android:layout_marginRight="16dp"
            android:orientation="horizontal">

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="温度:"
                android:textColor="#4B0082"
                android:textSize="18sp" />

            <TextView
                android:id="@+id/tvTemp"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginLeft="16dp"
                android:text="@{ViewModel.tempField}"
                android:textColor="#000000"
                android:textSize="18sp" />
        </LinearLayout>

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginLeft="16dp"
            android:layout_marginTop="16dp"
            android:layout_marginRight="16dp"
            android:orientation="horizontal">

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="气候:"
                android:textColor="#4B0082"
                android:textSize="18sp" />

            <TextView
                android:id="@+id/tvWeather"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginLeft="16dp"
                android:text="@{ViewModel.weatherField}"
                android:textColor="#000000"
                android:textSize="18sp" />
        </LinearLayout>

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginLeft="16dp"
            android:layout_marginTop="16dp"
            android:layout_marginRight="16dp"
            android:orientation="horizontal">

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="风向:"
                android:textColor="#4B0082"
                android:textSize="18sp" />

            <TextView
                android:id="@+id/tvWind"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginLeft="16dp"
                android:text="@{ViewModel.windField}"
                android:textColor="#000000"
                android:textSize="18sp" />
        </LinearLayout>

        <Button
            android:id="@+id/btnRefresh"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginLeft="16dp"
            android:layout_marginTop="48dp"
            android:layout_marginRight="16dp"
            android:text="刷新" />

    </LinearLayout>
</layout>

完整项目下载

通过java语言编写的一个Android应用程序,项目中围绕着MVC/MVP和MVVM架构设计,功能完整,注释齐全,同一个需求,同一套布局,同样的功能,不同的架构设计。下载地址:Android架构设计(MVC/MVP/MVVM)

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐