专栏首页Android开发者家园通过例子介绍架构

通过例子介绍架构

关于架构

关于架构的概念很宽泛,不是一句 MVP、MVC、MVVM 就能说清楚的。

一般开发软件的时候,我们是如何进行架构设计的呢?

首先一个 APP 软件是一个大的系统,我们通常可以把这个大的系统划分为许多个小的模块,比如:登录注册功能,首页展示功能、个人信息功能等等某个具体的模块的功能。然后我们就可把这几个相对独立的模块分别划分给不同的人员进行开发。

当然在进行模块划分前,我们需要先把整个软件的架子搭建起来,比如:请求网络需要用什么架构、加载图片需要使用什么架构、传递数据需要什么架构等等,这里的架构用作第三方库替代一下可能更加贴切一下。但是这的确是我们整个 App 架构的一部分,可以说是最底层的一部分,我把这部分称为底层技术架构。

然后在这一层的基础上,衍生出相对公共的业务层。比如登录、注册、一些通用的业务,这些模块相对来说比较独立,变化性不大,在这一层上面就是核心的业务层了,变动比较大。

注意这些层次并不是物理上的层次。

至于 MVC、MVP、MVVM 也常被称为软件架构,维基百科的定义就是:是软件工程中的一种软件架构模式。把软件系统分为了不同的部分,比如 MVC 把软件系统分为了三个基本部分:模型(Model)、视图(View)和控制器(Controller)。

其实提到 MVC、MVP、MVVM 我个人更倾向于,这是针对软件中的某个功能或者业务使用这种书写方式,每个模块都是用了这种模式,那么整体的软件说起来就可以说是这个 APP 是用了 MVC 模式。其实说白了 MVC、MVP、MVVM 等等都是一种写代码的设计规范(可能这个词不太准确)没有具体的概念,为了以后更加方便的迭代代码,使代码看起来更加的清晰。更偏向于一种书写代码的模型,一种代码理念,而不是新的技术。这种理念就是为了让代码更清晰,耦合性降低,还要根据具体的业务来使用,不能照搬硬套。

下面再来具体说一下 MVC、MVP、MVVM 只是介绍一下用这三种模式来写代码,代码的结构应该是什么样的,只是通用,并不是说只能这样,具体根据业务来 下面所说的软件架构模式都是指的某种编写代码的理念,比如:MVP、MVC 甚至是你自己的理念。

为什么会出现架构模式

架构模式的出现是为了让代码更加的清晰,相互之间耦合性低,非常庞大的项目,便于以后的迭代升级,让程序划分更加清晰(视图显示、业务逻辑/数据处理都独立开)这就是进行架构模式的意义,你想如果你的程序非常庞大结果你就全部都写在了一个java 类中,一个 java 类中几万行代码,是不是想想都恐怖,当然任何普通人都不会这么写吧,都会有自己的架构模式,比如有的人把这个 java 类拆分了 4 个类,有的拆了 10 个类。等等这其中就有一种是比较适合的,随着不断的发展,就有人提出了 MVC 这种架构模式,使用这种架构模式,可以让 java 类中的不同内容分离,比其他人的方式更加合理,于是就有了 MVC 架构模式

上面的三种架构模式,都有自己的使用模型,使用不同的模式,代码层次划分的也不一样。

架构模式的目的

  • 模块化功能 不同的功能使用不同的模块,不要全部大杂烩。合理的架构模式会让代码各部分相互独立,耦合性降低。
  • 提高开发效率 开发人员只需要专于某一块内容(视图显示、业务逻辑)
  • 便于日后维护 便于扩展维护

不能为了设计而设计,要根据项目实际情况来,如果项目很小,弄上复杂的架构模式,效果反而不好。

对于量级比较小的项目,只需要划分一下层次,规范一下,提炼一下方法就可以了。

对于量级较大的项目,才需要引入较为复杂的框架。

下面分别来讲 MVC MVP MVVM 在 Android 开发中的运用,只是针对 Android 项目开发。

通过项目分析

这里有个登录功能,功能很简单。如图:

就是一个模拟登录的功能,输入用户名,密码后点击登录进行页面登录,登录结果会给页面一个反馈。

没有架构

我们先来看看没有架构的时候,是怎么进行开发的

为了便于说明我在代码中有标识数字,便有后面解释。

页面内容很简单,这里就不再粘贴 xml 页面部分了,只粘贴 java 代码部分。

public class LoginActivity extends BaseActivity {

    @BindView(R.id.tv_username)
    TextView tvUsername;
    @BindView(R.id.et_username)
    EditText etUsername;
    @BindView(R.id.et_password)
    EditText etPassword;
    @BindView(R.id.bt_login)
    Button btLogin;
    @BindView(R.id.bt_clear)
    Button btClear;
    @BindView(R.id.pb)
    ProgressBar pb;
    @BindView(R.id.tv_status)
    TextView tvStatus;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_mvp_login);
        ButterKnife.bind(this);
    }

    @Override
    public void initView() {
        super.initView();
        btClear.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // ① 
                etPassword.setText("");
                etUsername.setText("");

            }
        });
        // 这里接受用户点击事件传递,再进行登录前需要进行页面逻辑判断
        btLogin.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (etUsername.getText() == null || etUsername.getText().toString().trim().equals("")) {
                    Toast.makeText(LoginActivity.this, "用户名不能为 null", Toast.LENGTH_SHORT).show();
                    return;
                }
                if (etPassword.getText() == null || etPassword.getText().toString().trim().equals("")) {
                    Toast.makeText(LoginActivity.this, "密码不能为 null", Toast.LENGTH_SHORT).show();
                    return;
                }
                login(etUsername.getText().toString(), etPassword.getText().toString());

            }
        });
    }

    /**
     * 登录业务,牵扯到数据
     * 真实情况下,有可能是 网络请求、本地数据库操作、大量数据逻辑操作
     * @param name
     * @param password
     */
    public void login(String name, String password) {
        FormBody formBody = new FormBody.Builder()
                .add("username", name)
                .add("password", password)
                .build();
        Request request = new Request.Builder()
                .post(formBody)
                .url("http://192.168.1.120/login")
                .build();
        OkHttpUtil.enqueue(request, new Callback() {
            @Override
            public void onFailure(@NotNull Call call, @NotNull IOException e) {
                // 处理一些业务逻辑
                tvStatus.setText("登录失败");
            }

            @Override
            public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException {
                // 来更新页面内容
                tvStatus.setText("登录成功");
            }
        });
    }
}

这就是没有使用架构进行开发的代码,很明显的不足就是所有的代码都放在了 Activity 中,不管是对 view 的操作还是对数据处理还是一些页面的逻辑判断,这样的代码写的多了,可读性和可扩展性都会变的非常差。

不得不说,我上面写的代码这样看上去还是不够乱,其实一方面是我们的功能不够复杂,功能很简单,你才能够一眼就看清楚,试想里面如果有几十个几百个逻辑呢?(你可能会说了,我可以把一些内容抽离出去,其实这就是一种简单的架构思想,只不过可能不太符合 mvc 架构的核心思想,但是只要是适合你的项目就是最好的架构),另一方面是,其实是 Android 开发本身是遵循一定的 MVC 思想的。View 放在 xml 中与 Java 代码解耦,然后在 Activity 中充当 Controller 处理逻辑控制,但是这样有一个问题就是没有对 Model 进行划分,而且 xml 功能太简单只能作为一个静态的页面,难免会在 Activity 中操作页面。这样也就导致了 Activity 里面的功能过多,既要充当 View 的功能又要充当 controller 的功能。

MVC

一般MVC

首先一般在 Android 中提到 MVC 的时候,都是这样来分类的

  • View:对应布局文件
  • Model:牵扯到数据的业务逻辑和实体模型
  • Controller:对应 Activity,进行操控 Model 和 页面,还有部分页面逻辑判断

但是细看你就会发现,View 对应布局文件,布局文件就是一个 xml 可以做的事情实在是太少了,一般就是写完页面就完事了,真正动态修改页面的时候还是需要通过在 Activity 中获取对应的控件来进行修改。很显然这样的结果就是 Activity 既要做 View 的部分工作又要做 Controller 的部分工作,其实这种模式就是 MV 没有了 C,是一种不标准的 MVC 模式。

按照这个思路写代码

// 定义了一个 View 的接口,把 View 要做的功能列出来
// 因为 Model 需要通知 View 刷新页面,通过接口的方式可以减少 Model 对某个页面的依赖
public interface IView {
     void setLoginStatus(String loginStatus);

     void loginFail(String fail);
}

// 这就是我们的 View 和 Controller 的集合体
public class LoginMVCActivity extends BaseActivity implements IView {
    @BindView(R.id.tv_username)
    TextView tvUsername;
    @BindView(R.id.et_username)
    EditText etUsername;
    @BindView(R.id.et_password)
    EditText etPassword;
    @BindView(R.id.bt_login)
    Button btLogin;
    @BindView(R.id.bt_clear)
    Button btClear;
    @BindView(R.id.pb)
    ProgressBar pb;
    @BindView(R.id.tv_status)
    TextView tvStatus;
    LoginModel loginModel;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_mvp_login);
        ButterKnife.bind(this);
    }

    @Override
    public void initView() {
        super.initView();
        loginModel = new LoginModel(this);
        btClear.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                etPassword.setText("");
                etUsername.setText("");

            }
        });
        btLogin.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // 这样的判断逻辑是属于 Controller 的职责的
                if (etUsername.getText() == null || etUsername.getText().toString().trim().equals("")) {
                    Toast.makeText(LoginMVCActivity.this, "用户名不能为 null", Toast.LENGTH_SHORT).show();
                    return;
                }
                // 这样的判断逻辑是属于 Controller 的职责的
                if (etPassword.getText() == null || etPassword.getText().toString().trim().equals("")) {
                    Toast.makeText(LoginMVCActivity.this, "密码不能为 null", Toast.LENGTH_SHORT).show();
                    return;
                }
                // 调用 Model 中的登录业务逻辑
                loginModel.login(etUsername.getText().toString(), etPassword.getText().toString());

            }
        });
    }


    @Override
    public void setLoginStatus(String loginStatus) {
        tvUsername.setText(loginStatus);
    }

    @Override
    public void loginFail(String fail) {
        tvUsername.setText(fail);
    }

}


// 有了 IView ,这样 LoginModel 就不会依赖某个具体的 activity 了,想要复用也可以了,
// 不然 LoginModel 就是专门为某个 Activity 而准备的
public class LoginModel {
    private IView iView;
    public LoginModel(IView iView){
        this.iView = iView;
    }


    public void login(String name,String password){
        FormBody formBody = new FormBody.Builder()
                .add("username",name)
                .add("password",password)
                .build();
        Request request = new  Request.Builder()
                .post(formBody)
                .url("http://192.168.1.120/login")
                .build();
        OkHttpUtil.enqueue(request, new Callback() {
            @Override
            public void onFailure(@NotNull Call call, @NotNull IOException e) {
                // 登录失败了,真实情况下可能会经过一些逻辑判断,然后通知页面发生更新
                //..... 等等一些逻辑处理
                iView.loginFail("");
            }

            @Override
            public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException {
                //..... 等等一些逻辑处理
                // 来更新页面内容
                iView.setLoginStatus("登录成功");
            }
        });
    }

}

这种写法是把 Controller 和 View 混在一起了,让 M 分离了,实际这就模式是 MV,Activity 责任不明确、十分臃肿,除了担任了 View 层的部分职责(加载应用布局、接受用户操作、更新布局内容)还有担任 Controller 层的职责(部分关于页面的逻辑),这样随着界面内容增多,逻辑变得根据复杂的话,Activity 中的代码会不断增加。下面来进一步升级一下这个不太标准的 MVC。

升级版MVC

// 首先把 Activity 中应该 Controller 来做的内容抽离出来
// 使用接口的好处就是依赖没有那么强,可以有不同的实现,相互不影响,再一个就是便于阅读,有的时候我们不需要关心具体是怎么实现的,只要知道有哪些方法,这些方法可以做什么就可以了,这样在接口里面一眼就可以看到了,如果没有接口的话就要看方法,方法就意味这有方法体,如果实现过程很复杂,一个类中有好几十个这种方法,那么看起来很费劲。
public interface ILoginController {

    void login(String name,String password);
}


public class LoginController implements ILoginController{
    private IView iView;
    private ILoginModel iLoginModel;

    public LoginController(IView iView,ILoginModel iLoginModel){
        this.iView = iView;
        this.iLoginModel = iLoginModel;
    }

    @Override
    public void login(String name, String password) {
        if (name== null || name.trim().equals("")) {
            iView.loginFail("用户名不能为 null");
            return;
        }
        if (password == null || password.trim().equals("")) {
            iView.loginFail("密码不能为 null");
            return;
        }
        iLoginModel.login(name, password);
    }
}

public interface IView {
     void setLoginStatus(String loginStatus);

     void loginFail(String fail);

     void error(String error);
}

public class LoginMVCActivity extends BaseActivity implements IView {
    @BindView(R.id.tv_username)
    TextView tvUsername;
    @BindView(R.id.et_username)
    EditText etUsername;
    @BindView(R.id.et_password)
    EditText etPassword;
    @BindView(R.id.bt_login)
    Button btLogin;
    @BindView(R.id.bt_clear)
    Button btClear;
    @BindView(R.id.pb)
    ProgressBar pb;
    @BindView(R.id.tv_status)
    TextView tvStatus;
    LoginModel loginModel;
    ILoginController loginController;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_mvp_login);
        ButterKnife.bind(this);
    }

    @Override
    public void initView() {
        super.initView();
        loginModel = new LoginModel(this);
        loginController = new LoginController(this, loginModel);
        btClear.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                etPassword.setText("");
                etUsername.setText("");

            }
        });
        btLogin.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                loginController.login(etUsername.getText().toString(),
                        etPassword.getText().toString());

            }
        });
    }

    @Override
    public void setLoginStatus(String loginStatus) {
        tvUsername.setText(loginStatus);
    }

    @Override
    public void loginFail(String fail) {
        tvUsername.setText(fail);
    }

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

}


public interface ILoginModel {
    void login(String name,String password);
}

public class LoginModel implements ILoginModel{
    // 通常情况下 Model不会直接持有 View,而是通过接口回调(具体的接口实现在 Controller 中实现)来更新View
    private IView iView;
    public LoginModel(IView iView){
        this.iView = iView;
    }


    public void login(String name,String password){
        FormBody formBody = new FormBody.Builder()
                .add("username",name)
                .add("password",password)
                .build();
        Request request = new  Request.Builder()
                .post(formBody)
                .url("http://192.168.1.120/login")
                .build();
        OkHttpUtil.enqueue(request, new Callback() {
            @Override
            public void onFailure(@NotNull Call call, @NotNull IOException e) {
                // 登录失败了,真实情况下可能会经过一些逻辑判断,然后通知页面发生更新
                //..... 等等一些逻辑处理
                iView.loginFail("");
            }

            @Override
            public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException {
                //..... 等等一些逻辑处理
                // 来更新页面内容
                iView.setLoginStatus("登录成功");
            }
        });
    }

}

好了,到此这个升级版本的 MVC 就完成了,这样结构看上去就很清晰了,各层次之间互相分离。

现在的 MVC,M就是一个单独的类,V 就是 Activity 和 xml,C 也用了一个单独类

虽然我们已经做了升级比之前的 MV 版本好了很多,但是你会发现 MVC 之间相互依赖的关系太多,V 可以和 C 直接进行通信,而 M 和 V 之间也可以直接进行通信,这就导致了 M 需要依赖 V,彼此之间依赖关系太多。于是就出现了 MVP,MVP的出现就是为了彻底断绝 M 和 V 之间的联系。

总结:MVC

M:包括用于操作与数据有关的复杂逻辑和实体模型

V:专门用于页面的展示

C:一些简单的逻辑(不牵扯到数据),串联 M 和 V

MVC最致命的问题就是 M 可以和 V进行通信

来自网络

通过这张图就可以看出,View Controller Model 之间相互依赖关系过多。

MVP

为了解决升级版 MVC存在的一些问题,出现了 MVP

  • M Model 数据层,用于操作与数据有关的复杂的业务逻辑和定义实体模型
  • V 视图层,View 的绘制刷新,用户交互 对应 Android 中的 Activity 和 xml
  • P Presenter 用于连接 V 层和 M层,处理一些简单的业务逻辑

在 MVP 中 M和V之间是不能通信的,必须要借助 P

代码实现:

// M 部分
public interface ILoginModel {
    void login(String name,String password);
}

public class LoginModel implements ILoginModel {
    // 这里也可以不持有 LoginPresenter 通过持有一个回调来完成
    ILoginPresenter iLoginPresenter;
    public LoginModel(ILoginPresenter iLoginPresenter){
        this.iLoginPresenter = iLoginPresenter;
    }
    public void login(String name,String password){
        FormBody formBody = new FormBody.Builder()
                .add("username",name)
                .add("password",password)
                .build();
        Request request = new  Request.Builder()
                .post(formBody)
                .url("http://192.168.1.120/login")
                .build();
        OkHttpUtil.enqueue(request, new Callback() {
            @Override
            public void onFailure(@NotNull Call call, @NotNull IOException e) {
                // 登录失败了,真实情况下可能会经过一些逻辑判断,然后通知 Presenter
                //..... 等等一些逻辑处理
                iLoginPresenter.result("");
            }

            @Override
            public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException {
                //..... 等等一些逻辑处理
                // 通知 Presenter
                iLoginPresenter.result("");
            }
        });
    }
}

// V 部分

public interface IView {
     void setLoginStatus(String loginStatus);

     void loginFail(String fail);

     void error(String error);
}

public class LoginMVPActivity extends BaseActivity implements IView {


    @BindView(R.id.tv_username)
    TextView tvUsername;
    @BindView(R.id.et_username)
    EditText etUsername;
    @BindView(R.id.et_password)
    EditText etPassword;
    @BindView(R.id.bt_login)
    Button btLogin;
    @BindView(R.id.bt_clear)
    Button btClear;
    @BindView(R.id.pb)
    ProgressBar pb;
    @BindView(R.id.tv_status)
    TextView tvStatus;
    // Presenter
    ILoginPresenter loginPresenter;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_mvp_login);
        ButterKnife.bind(this);
    }

    @Override
    public void initView() {
        super.initView();
        loginPresenter = new LoginPresenter(this);
        btClear.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                etPassword.setText("");
                etUsername.setText("");

            }
        });
        btLogin.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                loginPresenter.login(etUsername.getText().toString(),
                        etPassword.getText().toString());

            }
        });
    }

    @Override
    public void setLoginStatus(String loginStatus) {
        tvUsername.setText(loginStatus);
    }

    @Override
    public void loginFail(String fail) {
        tvUsername.setText(fail);
    }

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

// Presenter

public interface ILoginPresenter {
    void login(String name,String password);
    void result(String name);
}


public class LoginPresenter implements ILoginPresenter {

    IView iView;
    ILoginModel iLoginModel;

    public LoginPresenter(IView iView){
        this.iView = iView;
        // 不想让 Model持有 Presenter 的时候,同样这里可以传入一个 CallBack的实现
        // 当 Model 完成后回调一下就可以了
        iLoginModel = new LoginModel(this);
    }

    @Override
    public void login(String name, String password) {
        if (name== null || name.trim().equals("")) {
            iView.loginFail("用户名不能为 null");
            return;
        }
        if (password == null || password.trim().equals("")) {
            iView.loginFail("密码不能为 null");
            return;
        }
        iLoginModel.login(name, password);
    }

    @Override
    public void result(String name) {
        // 这里就是可以进行一些逻辑判断,然后根据情况通知页面刷新
        if(name.equals("==")){
            iView.loginFail(name);
        }else {
            iView.setLoginStatus(name);
        }
    }
}

特点总结

这样一个最简单的 MVP 模式的代码就完成了,对比升级版 MVC最大的特点就是阻断了 M 和 V 之间的关系。P 持有 Model 和 View,Model 和View 的一切通信都是通过 P 来完成。

这样就演变成只有 P 依赖于 Model 和 View 了(都通过接口实现,不依赖具体的类),做到了彼此的分离。

来自网络

M:Model 与数据有关的逻辑业务

View:View 的绘制,视图展示,与用户交互 Activity

P:Presenter 串联 M 和 V,完成 M 和 V 层的交互,部分业务逻辑处理,持有 M 和 V 的对象。

MVP 的封装

MVP 写法是有一定规律的,并且层次分的很明显,这里我们封装一下 MVP 中共用的一些操作,作为三个对应的底层接口

interface BaseModel{

}

interface BaseView{
    void showError(String msg);
}

public abstract class BasePresenter<V extends BaseView,M extends BaseModel>{
    protected V view;
    protected M model;

    public BasePresenter(){
        model = createModel();
    }

    void attachView(V view){
        this.view = view;
    }
    // 为了防止内存泄漏 Activity 销毁的时候将 View 置空
    void detachView(){
        this.view = null;
    }

    abstract M createModel();
}

// 这里这三个就是各个层的基类,可以根据需求在里面写一些通用的方法
// BasePresenter 利用了泛型,Presenter 必须同时持有 View 和 Model 的引用,但是在底层接口中无法确定他们的类型,
// 只能确定他们是 BaseView 和 BaseModel 的子类,所以采用泛型的方式来引用,就解决这个问题了,在 BasePresenter 的子类中只要定义好 View 和 Model 的类型,就会自动应用他们的对象了。

然后为了方便查看和修改把同一个页面的内容全部放到一个 Contract 契约接口中

// 这样查看起来就非常的清晰了
interface TestContract{
    interface Model extends BaseModel{
        void method1(Callback1 callback1);
        void method2(Callback2 callback2);
        .....根据项目所需的业务添加方法声明;
    }
    interface View extends BaseView{
        void updateUI1();
        void updateUI2();
        ....根据实际其他添加方法声明;
    }
    abstract class Presenter extends BasePresenter<View,Model>{
        abstract void request1();
        abstract void request2();
           .....根据项目添加适当的方法声明;
    }

    interface Callback1{
        void handlerResult();
        ......根据实际情况添加;
    }

}

然后在 Activity 或者 Fragment 中去实现 TestContract.View 的接口,再分别创建两个类用来实现 Test.Contract.Presenter 和 Test.Contract.Model 方法就可以了。

总结

所有的架构目的都是为了让代码扩展性更强,彼此依赖性更低,各个层功能更独立更专一。这样就导致不断的分层,理论上说分的层次越多,各个层次的功能就越专一,付出 的代价就是代码结构会变得复杂。所以这个分几层要根据项目来决定!

M 是负责业务逻辑和数据模型的组合,其实要说的话,M 下面应该还有层次(获取数据)(进行具体的网络查询业务等等不过一般情况就这就把这些内容写入到 M 里面了)

V 就是视图层

X 应该是用来进行表现层业务逻辑的,表现层说简单一点其实就是操控页面应该如何显示,与页面有直接关系的一些逻辑。这些逻辑不叫做业务逻辑而叫做表现层逻辑。

参考:https://www.jianshu.com/p/f17f5d981de7

https://mp.weixin.qq.com/s?__biz=MzIwMTAzMTMxMg==&mid=2649492837&idx=1&sn=631988689fe5bd6f9aadb6bf123051ed&chksm=8eec869ab99b0f8c55be7f28f92f51e22f38b9d7bd15969ff1bbee2bc2a434082c7938135d2c&scene=38#wechat_redirect

https://blog.csdn.net/lmj623565791/article/details/46596109

https://juejin.im/post/5d762cd9e51d4561e0516b7d

https://mp.weixin.qq.com/s/_6p6vfce7m5E8AwGDg2cZg

https://mp.weixin.qq.com/s/OEzcsPZHCVkjeUCh6hMuWg

https://www.jianshu.com/p/f17f5d981de7

本文分享自微信公众号 - Android开发者家园(Android_De_Home),作者:sydMobile

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-12-04

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • ViewStub的使用

    ViewStub 是一个不可见的,没有尺寸,不会绘制任何东西的布局。主要用于实现 View 的延迟加载,可以避免浪费资源,减少布局的绘画,只有需要的时候才会加载...

    开发者
  • 从0系统学Android--3.6 RecyclerView

    首先说明一点昨天发了一篇关于 ListView 的使用入门文章,得到了大家的一致调侃。我的想法是这样的,虽然现在 ListView 已经被 RecyclerVi...

    开发者
  • 从0系统学Android--4.1探究碎片

    平板电脑和手机最大的区别就在于屏幕的大小,一般手机的屏幕大小会在 3 英寸到 6 英寸之间,而一般平板电脑屏幕大小会在 7 英寸到 10 英寸之间。屏幕大小差距...

    开发者
  • 一个基础的Android项目

    AndroidBasicProject是一个简易的Android基础项目,方便您快速进行开发。 包含以下内容:

    续写经典
  • 优雅地烘焙 DBService

    记得大二那年第一次接触 GreenDao 这个神奇的数据库,惊叹道,哇,原来代码还能这么写啊,不用自己手撸 SQLiteDatabase,不用写那些麻烦的 SQ...

    HelloVass
  • 菜鸡的MVP架构漫谈

    相信大家在网上看过关于MVP架构的博客数不胜数,至于MVP到底是什么,也不需要我再从百度百科复制一遍了,通俗的说MVP就是解决Model和View的耦合,没有使...

    萬物並作吾以觀復
  • Android使用Service播放音乐

    像音乐这种耗时的和适合后台操作的应当放在Service中进行操作,而不是放在Activity,下面就介绍使用Service的音乐播放器

    夜雨飘零
  • Java描述设计模式(06):建造者模式

    知了一笑
  • RxBinding详解: 规范而强大的安卓UI响应式编程

    原文:Reactive Android UI Programming with RxBinding 作者:Donn Felker 翻译:DreamWinte...

    冰之角
  • Android-实用的MVP

    code_horse

扫码关注云+社区

领取腾讯云代金券