日期: 2021 年 7 月 21 日

关于 MVP in Android

 

一、概述

对于MVP(Model View Presenter),大多数人都能说出一二:“MVC的演化版本”,“让Model和View完全解耦”等等。本篇博文仅是为了做下记录,提出一些自己的看法,和帮助大家如何针对一个Activity页面去编写针对MVP风格的代码。

对于MVP,我的内心有一个问题:

为何这个模式出来后,就能被广大的Android的程序员接受呢?

问了些程序员,他们对于MVP的普遍的认识是:“代码很清晰,不过增加了很多类”。我在*次看到MVP的时候,看了一个demo,看完以后觉得非常nice(但是回过头来,自己想个例子写,就头疼写不出来,当然这在后文会说)。nice的原因还是因为,这个模式的确让代码的清晰度有了很大的提升。

那么,提升一般都是对比出来的,回顾下,没有应用MVP的代码结构。很多人说明显是MVC么:

  • View:对应于布局文件
  • Model:业务逻辑和实体模型
  • Controllor:对应于Activity

看起来的确像那么回事,但是细细的想想这个View对应于布局文件,其实能做的事情特别少,实际上关于该布局文件中的数据绑定的操作,事件处理的代码都在Activity中,造成了Activity既像View又像Controller(当然了Data-Binder的出现,可能会让View更像View吧)。这可能也就是为何,在该文中有一句这样的话:

Most of the modern Android applications just use View-Model architecture,everything is connected with Activity.

而当将架构改为MVP以后,Presenter的出现,将Actvity视为View层,Presenter负责完成View层与Model层的交互。现在是这样的:

  • View 对应于Activity,负责View的绘制以及与用户交互
  • Model 依然是业务逻辑和实体模型
  • Presenter 负责完成View于Model间的交互

ok,先简单了解下,文中会有例子到时候可以直观的感受下。

小总结下,也就是说,之所以让人觉得耳目一新,是因为这次的跳跃是从并不标准的MVCMVP的一个转变,减少了Activity的职责,简化了Activity中的代码,将复杂的逻辑代码提取到了Presenter中进行处理。与之对应的好处就是,耦合度更低,更方便的进行测试。借用两张图(出自:该文),代表上述的转变:

%title插图%num

转变为:

%title插图%num

二、MVP 与 MVC 区别

ok,上面说了一堆理论,下面我们还是需要看一看MVC与MVP的一个区别,请看下图(来自:本文):

%title插图%num

其实*明显的区别就是,MVC中是允许Model和View进行交互的,而MVP中很明显,Model与View之间的交互由Presenter完成。还有一点就是Presenter与View之间的交互是通过接口的(代码中会体现)。

还有一堆概念性的东西,以及优点就略了,有兴趣自行百度。下面还是通过一些简单的需求来展示如何编写MVP的demo。

三、Simple Login Demo

效果图:

%title插图%num

看到这样的效果,先看下完工后的项目结构:

%title插图%num

ok,接下来开始一步一步的编写思路。

(一)Model

首先实体类User不用考虑这个肯定有,其次从效果图可以看到至少有一个业务方法login(),这两点没什么难度,我们首先完成:

package com.zhy.blogcodes.mvp.bean;

/**
 * Created by zhy on 15/6/18.
 */
public class User
{
    private String username ;
    private String password ;

    public String getUsername()
    {
        return username;
    }

    public void setUsername(String username)
    {
        this.username = username;
    }

    public String getPassword()
    {
        return password;
    }

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

 


package com.zhy.blogcodes.mvp.biz;

/**
 * Created by zhy on 15/6/19.
 */
public interface IUserBiz
{
    public void login(String username, String password, OnLoginListener loginListener);
}

 


package com.zhy.blogcodes.mvp.biz;

import com.zhy.blogcodes.mvp.bean.User;

/**
 * Created by zhy on 15/6/19.
 */
public class UserBiz implements IUserBiz
{

    @Override
    public void login(final String username, final String password, final OnLoginListener loginListener)
    {
        //模拟子线程耗时操作
        new Thread()
        {
            @Override
            public void run()
            {
                try
                {
                    Thread.sleep(2000);
                } catch (InterruptedException e)
                {
                    e.printStackTrace();
                }
                //模拟登录成功
                if ("zhy".equals(username) && "123".equals(password))
                {
                    User user = new User();
                    user.setUsername(username);
                    user.setPassword(password);
                    loginListener.loginSuccess(user);
                } else
                {
                    loginListener.loginFailed();
                }
            }
        }.start();
    }
}

 


package com.zhy.blogcodes.mvp.biz;

import com.zhy.blogcodes.mvp.bean.User;

/**
 * Created by zhy on 15/6/19.
 */
public interface OnLoginListener
{
    void loginSuccess(User user);

    void loginFailed();
}

 

实体类不用说,至于业务类,我们抽取了一个接口,一个实现类这也很常见~~login方法,一般肯定是连接服务器的,是个耗时操作,所以我们开辟了子线程,Thread.sleep(2000)模拟了耗时,由于是耗时操作,所以我们通过一个回调接口来通知登录的状态。

其实这里还是比较好写的,因为和传统写法没区别。

(二) View

上面我们说过,Presenter与View交互是通过接口。所以我们这里需要定义一个ILoginView,难点就在于应该有哪些方法,我们看一眼效果图:

可以看到我们有两个按钮,一个是login,一个是clear;

login说明了要有用户名、密码,那么对应两个方法:


    String getUserName();

    String getPassword();

 

再者login是个耗时操作,我们需要给用户一个友好的提示,一般就是操作ProgressBar,所以再两个:

    void showLoading();

    void hideLoading();

 

login当然存在登录成功与失败的处理,我们主要看成功我们是跳转Activity,而失败可能是去给个提醒:

    void toMainActivity(User user);

    void showFailedError();

 

ok,login这个方法我们分析完了~~还剩个clear那就简单了:

    void clearUserName();

    void clearPassword();

 

综上,接口完整为:

package com.zhy.blogcodes.mvp.view;

import com.zhy.blogcodes.mvp.bean.User;

/**
 * Created by zhy on 15/6/19.
 */
public interface IUserLoginView
{
    String getUserName();

    String getPassword();

    void clearUserName();

    void clearPassword();

    void showLoading();

    void hideLoading();

    void toMainActivity(User user);

    void showFailedError();

}

 

有了接口,实现就太好写了~~~

总结下,对于View的接口,去观察功能上的操作,然后考虑:

  • 该操作需要什么?(getUserName, getPassword)
  • 该操作的结果,对应的反馈?(toMainActivity, showFailedError)
  • 该操作过程中对应的友好的交互?(showLoading, hideLoading)

下面贴一下我们的View的实现类,哈,其实就是Activity,文章开始就说过,MVP中的View其实就是Activity。

package com.zhy.blogcodes.mvp;

import android.os.Bundle;
import android.support.v7.app.ActionBarActivity;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ProgressBar;
import android.widget.Toast;

import com.zhy.blogcodes.R;
import com.zhy.blogcodes.mvp.bean.User;
import com.zhy.blogcodes.mvp.presenter.UserLoginPresenter;
import com.zhy.blogcodes.mvp.view.IUserLoginView;

public class UserLoginActivity extends ActionBarActivity implements IUserLoginView
{


    private EditText mEtUsername, mEtPassword;
    private Button mBtnLogin, mBtnClear;
    private ProgressBar mPbLoading;

    private UserLoginPresenter mUserLoginPresenter = new UserLoginPresenter(this);

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

        initViews();
    }

    private void initViews()
    {
        mEtUsername = (EditText) findViewById(R.id.id_et_username);
        mEtPassword = (EditText) findViewById(R.id.id_et_password);

        mBtnClear = (Button) findViewById(R.id.id_btn_clear);
        mBtnLogin = (Button) findViewById(R.id.id_btn_login);

        mPbLoading = (ProgressBar) findViewById(R.id.id_pb_loading);

        mBtnLogin.setOnClickListener(new View.OnClickListener()
        {
            @Override
            public void onClick(View v)
            {
                mUserLoginPresenter.login();
            }
        });

        mBtnClear.setOnClickListener(new View.OnClickListener()
        {
            @Override
            public void onClick(View v)
            {
                mUserLoginPresenter.clear();
            }
        });
    }


    @Override
    public String getUserName()
    {
        return mEtUsername.getText().toString();
    }

    @Override
    public String getPassword()
    {
        return mEtPassword.getText().toString();
    }

    @Override
    public void clearUserName()
    {
        mEtUsername.setText("");
    }

    @Override
    public void clearPassword()
    {
        mEtPassword.setText("");
    }

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

    @Override
    public void hideLoading()
    {
        mPbLoading.setVisibility(View.GONE);
    }

    @Override
    public void toMainActivity(User user)
    {
        Toast.makeText(this, user.getUsername() +
                " login success , to MainActivity", Toast.LENGTH_SHORT).show();
    }

    @Override
    public void showFailedError()
    {
        Toast.makeText(this,
                "login failed", Toast.LENGTH_SHORT).show();
    }
}

 

对于在Activity中实现我们上述定义的接口,是一件很容易的事,毕竟接口引导我们去完成。

*后看我们的Presenter。

(三)Presenter

Presenter是用作Model和View之间交互的桥梁,那么应该有什么方法呢?

其实也是主要看该功能有什么操作,比如本例,两个操作:login和clear。

package com.zhy.blogcodes.mvp.presenter;

import android.os.Handler;

import com.zhy.blogcodes.mvp.bean.User;
import com.zhy.blogcodes.mvp.biz.IUserBiz;
import com.zhy.blogcodes.mvp.biz.OnLoginListener;
import com.zhy.blogcodes.mvp.biz.UserBiz;
import com.zhy.blogcodes.mvp.view.IUserLoginView;


/**
 * Created by zhy on 15/6/19.
 */
public class UserLoginPresenter
{
    private IUserBiz userBiz;
    private IUserLoginView userLoginView;
    private Handler mHandler = new Handler();

    public UserLoginPresenter(IUserLoginView userLoginView)
    {
        this.userLoginView = userLoginView;
        this.userBiz = new UserBiz();
    }

    public void login()
    {
        userLoginView.showLoading();
        userBiz.login(userLoginView.getUserName(), userLoginView.getPassword(), new OnLoginListener()
        {
            @Override
            public void loginSuccess(final User user)
            {
                //需要在UI线程执行
                mHandler.post(new Runnable()
                {
                    @Override
                    public void run()
                    {
                        userLoginView.toMainActivity(user);
                        userLoginView.hideLoading();
                    }
                });

            }

            @Override
            public void loginFailed()
            {
                //需要在UI线程执行
                mHandler.post(new Runnable()
                {
                    @Override
                    public void run()
                    {
                        userLoginView.showFailedError();
                        userLoginView.hideLoading();
                    }
                });

            }
        });
    }

    public void clear()
    {
        userLoginView.clearUserName();
        userLoginView.clearPassword();
    }



}

 

注意上述代码,我们的presenter完成二者的交互,那么肯定需要二者的实现类。大致就是从View中获取需要的参数,交给Model去执行业务方法,执行的过程中需要的反馈,以及结果,再让View进行做对应的显示。

在谈MVP之前,你真的懂MVC吗?

*近看到很多文章在谈论MVP或者MVVM模式的,但其实无论MVP还是MVVM都只是MVC模式的一种变种。而如果你对MVC的设计理念都还没有理解透彻,那么即使换成MVP亦或MVVM也不可能让你杂乱不堪的代码突然变得清晰明了起来,模式*不是救命的稻草,它只是一种表现形式,真正要学的其蕴含的思维方式。

什么才是MVC?

这是一个非新手都就会嗤之以鼻的问题,试问哪个程序猿不知道什么是MVC,但在此我希望大家先忘记之前对MVC的所有知识,很多时候学习的*步就是承认自己的无知,这是一个多么重要的步骤,又是一个多么容易遗忘的步骤啊。包括我自己也是如此,经常因为固有思维而变得傲慢而不自知,今天我们就一起重头来学习一下MVC的历史。

对于MVC的概念我想没人不知,但是大部分人其实并不知道MVC的概念其实不止一个,从纵向来看,它经过了历史上很多的演进和变种;从横向来看,它也有许许多多不同的细微差异。即使包括后来的MVP和MVVM也都只能算它的一个变种而已。

经典MVC模式

大家坐稳了,老司机要带大家穿越时空回到1978年,来听听MVC模式的创始人挪威教授Trygve Reenskaug是怎么定义MVC模式的。

Models:

A Model is an active representation of an abstraction in the form of data in a computing system.

Views:

To any given Model there is attached one or more Views, each View being capable of showing one or more pictorial representations of the Model on the screen and on hardcopy. A View is also able to perform such operations upon the Model that is reasonabely associated with that View.

Controller:

A controller is the link between a user and the system. It provides the user with input by arranging for relevant views to present themselves in appropriate places on the screen. It provides means for user output by presenting the user with menus or other means of giving commands and data. The controller receives such user output, translates it into the appropriate messages and pass these messages on .to one or more of the views.

A controller should never supplement the views, it should for example never connect the views of nodes by drawing arrows between them.

Conversely, a view should never know about user input, such as mouse operations and keystrokes. It should always be possible to write a method in a controller that sends messages to views which exactly reproduce any sequence of user commands.

有兴趣的同学可以去阅读完整的论文 The original MVC reports。

这是*早期的MVC模式,其中三者的定义可以简单的理解为:

  • Model,负责的是数据,这里的“数据”不仅限于数据本身,还包括处理数据的逻辑。
  • View,负责数据的表现形式,将数据及数据的变化呈现给用户。
  • Controller,负责用户的输入,将用户的命令转化成消息传递给model或view,是一个翻译者。

注意先不要喷,在早期的MVC模式中,Controller的设计目的其实并不是为了隔离Model和View的,而后来这点才发现了变化。

Model & View

Model和View的关系,也可以分为两种形式:

  1. push model:View在model上将自己注册为数据的监听者,model的数据发生变化时会发送通知,view接到通知后用新数据更新自己。
  2. pull model:View负责在它需要的时候调用model来获取当前*新的数据。

但不管是那种模式,model对于view都是没有感知的,push model是在数据变化的时候简单的发送广播,告之所有对该数据变化感兴趣的监听者,而pull model是view对model进行调用。因此在任何一种模式下,model都是不能直接操作view

这种model-view模式也就是俗称的观察者模式,model是被观察者,view是观察者,当被观察者发送变化的时候,通知注册在它上面的所有观察者。这样设计带来的另一个好处就在于多个view可以监听同一个model的变化。

Controller & View

关于Controller和View的关系,Controller是绑定在View上面的,意思是用户任何在View上的操作(例如点击按钮等)都会调用Controller上的一个回调方法。其实也意味着View是持有Controller的引用的,当用户做相应操作时,是由View来调用合适的Controller方法的,而Controller对View的操作在早期概念中则不太明确。

Controller & Model

Controller是可以向Model直接通信的。例如用户点击了删除按钮,那么Controller将用户的这个操作翻译成“用户需要删除这条数据”的消息传递给Model,Model负责删除数据,然后通知View来更新页面以告知用户数据的变化。

现代MVC模式

与经典MVC模式不同,很多现代系统设计中,如Apple Cocoa框架,*大的改变在于将Controller的位置放在了Model和View之间。

主要区别就在于Controller的位置变了:Controller将消息传递给Model,M处理完数据后是先将数据的变化通知给C,再由C来通知View来变化视图的。也就是说Controller变成了在Model和View之间双向传递数据的中间协调者,关系变成了: View <-> Controller <-> Model 。

Model & View

Model和View之间没有了任何关系,所有通信都是通过Controller传递。

Model & Controller & View

View通过Controller向model传递用户操作的消息,而model在处理完数据后通过Controller来向View来传递结果。Controller从经典MVC模式中的单向翻译官变成了双向的中间人。

为什么要这样变?

其实大家都看出来了,这样变的主要目的就是为了让Model和View之间不再直接联系。从而使得三者的关系理得更清楚了。

这货不就是MVP吗?

了解MVP概念的同学可能读到这里可以会产生巨大的问号:这货不就是MVP吗?MVP里面的Presenter不就是充当Controller和View之间的中间人吗?

可以说Apple Cocoa这类框架使用的这种进化版的MVC确实离MVP相差很小了,可以说只差一步而已,MVP只是把三者之间的关系解藕得更厉害而已。

*后说两句

不管是传统的MVC,还是进化的MVC,亦或者MVP和MVVM模式,你会发现其实它们的设计理念都是一致的,逐步进化也只是为了更进一步的达到这个理念的目标,那就是:

上帝的归上帝,凯撒的归凯撒!

如果你没有理解这一点,那么用什么模式也是混乱的;而如果你理解了这一点,什么模式不用也是清晰的。

MVP模式是你的救命稻草吗

为什么要学习架构?

不管是MVC还是MVP,亦或则其他架构,它们的设计目的都是为了达到编码的*高境界,那就是:低藕合,高复用,易测试,好维护。

而要达到这个终*目标,首先要理解的是每个部分各自负责些什么,以及如何组合在一起。因此我个人认为,学习架构关键在两步:

  1. 如何把缠在一起的代码拆分
  2. 如何把拆开的代码再组合

很多新手在刚做项目时,都会把所有的代码,如数据的访问和处理,数据的展示,用户的输入都写在一起,代码和思维都呈现出一种线性的形式,一切都是直来直往的。这样代码量确实少,写的时候也觉得方便,但是带来了几个致命的问题:

  1. 当业务越来越复杂时,修改代码成了噩梦。同样的逻辑放在不同的地方,本来只用改一处的变成了需要改几百处。而又因为所有的逻辑都互相牵扯住,导致本来只想改一处的,结果却影响了几百处。
  2. 当时间越来越遥远时,理解代码成了噩梦。本来只需要阅读几行的时候,却因为所有的代码都杂在一起变成了要阅读几千行。时间一长,重新阅读时,别说别人,就是自己也很难一下就能掌握关键点。
  3. 当需要做一个功能时,却发现代码无法复用,明明是一样的逻辑也只能靠Ctrl+CCtrl+V。这又为今后修改代码时增加了工作量。
  4. 当需要测试时确发现很难进行测试,每次修改一处代码后都必须进行重复的人工测试,无法进行自动化测试,模块和模块也无法拆开来进行独立的单元测试,每次都要整体的测一遍才能确保一切完好如初。

要换的不是架构,而是思维方式

其实目前市面上的架构模式已经有很多种,各有不同,但模式终究只是一种设计理念的表现形式,学习再多的架构,你也只是多会用了几种工具而已,学习一种模式其实是在学一种思维方式:

如何在解决问题的时候把问题合理的拆分,又如何将拆分的零件合理的组装成解决问题的工具。

将这种思维方式深入到你的大脑里,血液里,直至骨髓,让它成为你思考问题的一种习惯和定式,逐渐成为你的一部分,那么这时你已达到无招胜有招的境界了。

闲话先不扯了,回正题。

实现架构没有唯一标准

这里首先需要说明的是无论MVP或MVC还是MVVM等任何一种架构和模式其实都没有谁优谁劣之分,而且就算是同一种架构,也可以根据不同的使用场景来做不同的实现方式,这里并没有宇宙*对的对错标准和概念定义。这和张三丰在教无忌太*拳以后让其先忘掉招式是一样的道理,在应用型领域,定式和概念只应是在学习的过程中才存在的,而真正学会以后应该马上忘掉定式,忘掉概念,将其用熟用活才是关键。

所以说我在此描述的概念也不是唯一的标准,而只是个人对其的理解。

什么是MVC

因为MVP是MVC的一个变种,因此我们先来看在MVC里代码是如何拆分和组合的,这里简要的描述常见的定义:

  1. 拆分:Model负责数据,View负责显示,Controller负责用户输入。
  2. 组合:View将用户操作和事件传递给Controller,Controller将用户命令翻译成消息来传递给Model,Model在处理完数据后,通知View来显示给用户,而View又从Model取出数据。

我认为在学习MVP之前,必须深刻理解什么叫做MVC,因为两者的区别其实没有你想象中的那么大,与其神话并盲目追崇新的架构,期望其能解脱你于苦海,还不如深刻的理解MVC的设计理念,这就和学习一门新语言一样,只有你真正深刻的理解了其思维方式,那么学习新的一门语言其实都是易如反掌的。因为它们其实都是在做一件事,只是做的过程上有些许差异而已。

更多MVC的理解,可以参考我之前写的一篇文章:在谈MVP之前,你真的懂MVC吗?

什么是MVP

MVP与MVC*大的区别就在与将Model和View通过Presenter隔开了,不再允许其互相直接通信,而所有的消息都是通过Presenter这个中间人来传递。

而这样做的目的主要是为了将数据和展示划出更明确的界限。

首先我们来看MVP到底是在解决什么问题的:

  • 数据管理
    1. 什么是数据?(Model)
    2. 如何筛选数据?(Selections)
    3. 如何改变数据?(Commands)
  • 用户界面
    1. 如何展示数据?(View)
    2. 如何通过事件来改变数据?(Interactor)
    3. 如何将这些放在一起?(Presenter)

可以看出其实一个GUI程序的核心,还是在于用户和数据之间的关系。而架构的引入也是为了解决这几个核心的问题。

那么MVP又是如何解决这几个问题的,Model,View,Presenter三者又各自负责什么呢?谁来负责请求网络数据,访问和存储本地数据,谁来负责处理数据,谁来负责显示数据,谁又来负责和用户交互呢?

具体表现在代码上,也可以说:网络请求应该放在哪,数据库访问应该放在哪,业务逻辑应该放在哪里,处理用户输入应该放在哪,谁又来控制显示或隐藏View的等具体的细节问题。

带着这些具体问题,我们一起来学习。

如何拆分

首先来看MVP各自负责什么:

  1. Model,负责定义数据(解决什么是数据)
  2. Presenter, 负责在Model和View之间,从model里取出数据,格式化后在View上展示(解决如何把数据和用户界面放在一起)。
  3. View,负责担任一个被动界面,用于展示数据。(解决如何展示数据)

和MVC比较而已,这里出现一个*大的疑问就是:那么谁来负责和用户的操作交互呢?答案是,用户在View上的所有操作(事件)都由View路由给Presenter,由Presenter来与其交互。

而和MVC*大的区别在于Model和View完全被Presenter隔开了,Presenter作为它们之间的中间人去传递所有数据。

如何组合

三者又是如何组合起来的呢?

很显然Presenter作为中间者,它是同时拥有View和Model的引用的,为了在它们之间起到桥梁作用,即Presenter会主动和View和Model进行通信

Model和View必须是完全隔离的,不允许两者之间互相通信,保持对彼此的不感知,这样的好处是你彻底将数据和展示分离来开,并且可以独立的为Model去做测试。

Model在三者中是独立性*高的,Model不应该拥有对View的引用,而且Model也不需要保存对Presenter的引用,对于Presenter而已,Model只需要提供接口,等着Presenter来调用时返回相应数据即可,这和经典MVC模式中是非常不同的,在MVC中Model在数据发送变化后,是需要发送广播来告之View去更新用户界面的,而在MVP中,Model是不应该去通知View,而是通知Presenter来间接的更新View的。

Presenter和Model的关系也应该是基于接口来通信,这样才能把Model和Presenter的耦合度也降到*低,那么在需要改变Model内部实现,甚至彻底替换Model的时候,Presenter则是无需随之改变的。这样做带来的另一个好处就是你可以通过Mock一个Model来对Presenter以及View做模拟测试了,从而提高了可测试性。

那么View和Presenter的关系呢?View是需要拥有对Presenter的引用,但仅仅是为了将用户的操作和事件立即传递给Presenter,为了让View和Presenter耦合较低,View也只应该通过接口与Presenter通信,从而保证View是完全被动的,一方面它由用户的操作触发来和Presenter通信,另一方面它完全受Presenter控制,唯一需要做的事情就是如何展示数据。

简要总结三者之间的关系是:View和Model之间没有联系,View通过接口向Presenter来传递用户操作,Model不主动和Presenter联系,被动的等着Presenter来调用其接口,Presenter通过接口和View/Model来联系。

View <- 接口 <- Presenter ->接口 -> Model

View -> 接口 -> Presenter <- 接口 <- Model

Talk is cheap, show me the code

为了便于理解,这里提供一些伪代码加注释演示一下如何实现MVP模式:

View

interface IUserView {

  void setPresenter(presenter);
  void showUsers(users);
  void showDeleteUserComplete();
  void showDeleteUserError();

}

class UserView implements IUserView {

  UserPresenter presenter;

  // 保持对Presenter的引用,用于路由用户操作
  void setPresenter(presenter) {
      this.presenter = presenter;
  }

  // 将Presenter传递来的数据展示出来
  void showUsers(users) {
      draw(users);
  }

  // Model操作数据成功后,通过Presenter来告之View需要更新用户界面
  void showDeleteUserComplete() {
      alert("Delete User Complete");
  }

  // Model操作数据失败后,也是通过Presenter来告之View需要更新用户界面
  void showDeleteUserError() {
      alert("Delete User Fail");
  }

  // 当用户点击某个按钮时,将用户操作路由给presenter,由presenter去处理
  void onDeleteButtonClicked(event) {
      presenter.deleteUser(event);
  }

}

Model

interface IUserModel {

  List<User> getUsers();
  boolean deleteUserById();

}

class UserModel implements IUserModel {

  // 在数据库里查找数据,并将数据返回给presenter
  List<User> getUsers() {
       return getUsersInDatabase(id);
  }

  // 在数据库里删除数据,并将结果返回给presenter
  User deleteUserById(id) {
      return deleteUserByIdInDatabase(id);
  }

}

Presenter

interface IUserUserPresenter {

  void deleteUser(event);

}

class UserUserPresenter implements IUserPresenter {

  // 保持对View的引用
  IUserView view;
  // 保持对Model的引用
  IUserModel model;

  UserUserPresenter(IUserView view, IUserModel model) {
    this.view = view;    
    this.model = model;

    this.view.setPresenter(this);   
  }

  void start() {
    // 从Model中取出数据
    List<User> users = model.getUsers();
    // 将数据发送给View,让其展示到用户界面
    view.showUsers(users);
  }

  void deleteUser(event) {
    // View将用户操作路由过来,由Presenter来处理
    long uid = whichUserNeedToDeleteBy(event);
    // 将用户操作翻译成命令或消息传递给model,以改变数据
    boolean success = model.deleteUserById(uid);
    // 将Model操作数据后的结果通知View来改变用户界面
    if (success) {
          view.onDeleteUserSuccess();
    } else {
        view.onDeleteUserFail();  
    }
  }
}

OK,到此一个*简单的MVP模式就实现了,可以看到整个架构的轮廓已经很清晰了,而且面向接口编程也带来了很多的好处,让代码之间藕和较少,对测试支持也更为友好,理解和维护起来也更加方便了。

以后有时间再为大家描述如何在android里面实现MVP模式,以便更具体的理解。有疑问欢迎参与讨论,大家一起学习进步。

使用MVP模式重构代码

之前写了两篇关于MVP模式的文章,主要讲得都是一些概念,这里谈谈自己在Android项目中使用MVP模式的真实感受,并以实例的形式一起尝试来使用MVP模式去重构我们现有的代码。

有兴趣的童鞋可以先去阅读之前的文章,因为这里将不再重复概念的部分了,本文会假设你对MVP有一点了解了:

1. 在谈MVP之前,你真的懂MVC吗?

2. MVP模式是你的救命稻草吗?

臃肿的Activity

大部分谈Android架构的时候,都基本会提到Activity越来越臃肿的问题,这几乎是一个普遍现象,而包括我本人在内的,都会首先将这个罪责推到MVC架构上,但如果你真的花时间去重构activity的时候,你会发现问题其实往往出在自己身上。

一般的MVC里的 Controller 需要做的事情:

  1. 负责获取和处理用户的输入。
  2. 负责将输入传给负责业务逻辑层去做数据上的操作(如增删改查)。
  3. 负责将业务逻辑层对于数据操作的结果,传给View层去做展示。

因此如果完全按照这种定义的话,你应该很难看到一个非常臃肿的Controller,因为Controller在MVC模式中,本来就应该是很轻的,而不是很重的部分,重的应该是M层,甚至在前端交互复杂的时候,V层都应该比C层要重。

我认为对于Controller的理解,就是一个站在M和V两者之间的一个翻译家,M来自地球,V来自火星。而如果站在中间的这个翻译者,话比他两的话还多,老是抢话,自言自语,这样显然是不合适的。

那么我们再来看典型的Activity的代码,处理的业务是常见的登录页面:

public class UserActivity extends Activity {

  private RequestQueue mQueue = Volley.newRequestQueue(this);

  private TextView mUsernameTextView;
  private TextView mPasswordTextView;
  private Button mLoginBtn;

  @Override
  public void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
    setContentView(R.layout.login);

    mUsernameTextView = (TextView) findViewById(R.id.username);
    mPasswordTextView = (TextView) findViewById(R.id.password);

    mLoginBtn = (Button) findViewById(R.id.login_btn);
    mLoginBtn.setOnClickListener(new View.OnClickListener() {
          public void onClick(View v)  {
              String username = mUsernameTextView.getText().toString();
               String password = mPasswordTextView.getText().toString();

               String loginUrl = "http://somesite.com/login.php";

               JSONObjectRequest request = new JSONObjectRequest(loginUrl, Method.POST,
                 new Response.Listener<JSONObject>() {
                      public void onResponse(JSONObject json) {
                          if (json != null && json.get("isOk") == true) {
                              Toast.makeToast(getApplicationContext(), "Login Success", Toast.LENGTH_SHORT).show();
                                startActivity(new Intent(LoginActivity.this, MainActivity.class));
                        } else {
                              Toast.makeToast(getApplicationContext(), "Login Fail", Toast.LENGTH_SHORT).show();
                        }
                    }
               },
                new Response.ErrorListener(){
                      public void onError(VolleyError error) {
                          Toast.makeToast(getApplicationContext(), "Login Fail", Toast.LENGTH_SHORT).show();
                    }
               });
              mQueue.add(request);
        } 
    });
  }

  @Override
  protected void onStop() {
      super.onStop();
    if (mQueue != null) {
          mQueue.cancelAll();
    }
  }

}

其实这已经是*度简化过的代码了,真正的一个LoginActivity很容易就会超过几千行,不信你去看自己项目里面的代码就明白我在说什么了。

而一对比概念,我相信大部分人一下子就会发现问题,我们还是来看Activity作为一个 Controller 到底都负责干什么了:

  1. 首先activity必须去操作View的控件,设置它们的回调函数,有时也需要用代码去控制它们如何展示的属性。
  2. 然后activity一定需要去处理用户的输入,例如输入的值,以及点击事件等用户行为。
  3. 而几乎大部分异步网络请求都从activity发起,以及服务器返回数据的处理。
  4. activity一般还需要根据数据的操作结果,负责在页面上将结果告之用户,例如Toast或者其他View的操作。
  5. 除此之外,activity还需要管理其生命周期相关的所有事务,例如在页面退出的时候处理一下View控件和其他与生命期相关的逻辑。

你会发现Activity天生的责任太重,其中确实覆盖了 Controller 的原本的责任,例如处理用户输入,将用户操作转换成传递给业务逻辑层的命令等职责。

但如果你仔细的分析,你会发现activity不仅仅需要承担Controller的责任,还需要处理大量View的逻辑,例如控件的监听的属性,如何展示数据的职责也往往落到了它的肩上。更何况你很容易在activity写操作数据和网络请求的代码,也就是让它又承担了Model的责任,那么请问这样的Activity能不臃肿吗?

当然这是一个坏的例子,其实很多代码是可以封装到独立的层去的,例如网络请求,数据解析等。但就算你怎么封装和重构,你*多能做的事情也就是把本来就不应该放在Controller里的Model层分离出去,这是你原本就应该做的事情。但你很难在activity将controller和view分离开来,怎么写activity作为Controller,都和View的关系太紧密,必须多多少少去控制如何展示数据这个View的责任。

Presenter是来给activity减负的吗?

很多人会认为MVP中引入Presenter的概念,是为了给日益臃肿的activity来减负的,而我不这样认为,我认为Presenter和Controller的责任是差不多的,它们后期承担的目的都其实很简单,就是用来隔离Model和View的,也就是常说的展示层和业务层的解藕。

那么该如何解决activity的问题呢?目前常见的MVP在Android里的实践有两种解决方案:

  1. 直接将Activity看作View,让它只承担View的责任。
  2. 将Activity看作一个MVP三者以外的一个Controller,只控制生命周期。

在Google推出的官方MVP实例里,使用的就是第2种思路,它让每一个Activity都拥有一个Fragment来作为View,然后每个Activity也对应一个Presenter,在Activity里只处理与生命周期有关的内容,并跳出MVP之外,负责实例化Model,View,Presenter,并负责将三者合理的建立联系,承担的就是一个上帝视角。

在实践中,也有很多观点会简化掉Fragment,直接将Activity视为View,这个也是我比较赞同的,更简便一些,而且这样观念上也容易理解一些,你就把activity看作View的一部分,永远只让它处理展示的逻辑,不允许它去处理数据,和拥有业务逻辑。但是这样也有一个缺点,就是V和P的依赖关系不太规范了,理论上你是不应该在View里面去实例化Presenter和Model的,这其实是不合理的,正确的依赖关系,确实是应该在一个独立的更上层去实例化Model,View,Presenter的,这样依赖才是较为合理的关系,这点来看Google的架构模式确实更合理,但实操上也会麻烦一点,必须让每个activity拥有一个独立的fragment,这个我是觉得可以自由取舍,你是要概念上的合理,还是现实中的方便,其实都可以。

因为重点还是在于如何分离展示层和业务层,activity具体承担什么责任都可以,但只能承担一个责任。

例如之前的代码可以被重构成如下结构:

/**
 * View负责展示数据
 */
public interface UserView {

  void showLoginSuccessMsg(User loginedUser);
  void showLoginFailMsg(String errorMsg); 

}
/**
 * Presenter负责做View和Model的中间人
 */
public interface UserPresenter {

  void login(String username, String password);

}
/**
 * Model负责数据的处理和业务逻辑
 */
public interface UserModel {

  void login(String username, String password, Callback callback);

}

这里将Activity被视为View, 仅负责数据的展示,并且将用户的操作事件路由给P去做处理。

public class UserActivity extends Activity implements UserView {

  private UserContract.Presenter mPresenter;

  private TextView mUsernameTextView;
  private TextView mPasswordTextView;
  private Button mLoginBtn;

  @Override
  public void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
    setContentView(R.layout.login);

    mPresenter = new AddressListPresenter(this, new UserModelImpl());

    mUsernameTextView = (TextView) findViewById(R.id.username);
    mPasswordTextView = (TextView) findViewById(R.id.password);

    mLoginBtn = (Button) findViewById(R.id.login_btn);
    mLoginBtn.setOnClickListener(new View.OnClickListener() {
          public void onClick(View v)  {
              String username = mUsernameTextView.getText().toString();
              String password = mPasswordTextView.getText().toString();
            // View将用户的点击事件直接路由给Presenter区处理
            mPresenter.login(username, password); 
        } 
    });
  }

  @Override
  public void showLoginSuccessMsg(User loginedUser) {
    // Presenter在处理完毕后, 会通知View更新UI来通知用户数据操作的结果
    Toast.makeToast(getApplicationContext(), "Login Success", Toast.LENGTH_SHORT).show();
  }

  @Override
  public void showLoginFailMsg(String errorMsg) {
    // Presenter在处理完毕后, 会通知View更新UI来通知用户数据操作的结果
    Toast.makeToast(getApplicationContext(), "Login Fail", Toast.LENGTH_SHORT).show();
  }

  @Override
  protected void onResume() {
      super.onResume();
      mPresenter.subscribe();
  }

  @Override
  protected void onPause() {
      super.onPause();
      mPresenter.unSubscribe();
  }

}

Presenter层则负责将在View和Model做中间人:

/**
 * Model负责数据的处理和业务逻辑
 */
public class UserPresenterImpl implements UserPresenter {

  private UserView mUserView;
  private UserModel mUserModel;

  public UserPresenterImpl(UserView view, UserModel model) {
    mUserView = view;
    mUserModel = model;
  }

  public void login(String username, String password) {
    // Presenter处理View路由过来的用户操作,
    // 将其转换成相对的命令,传递给Model来做数据操作
    mUserModel.login(username, password, new Callback(){
      public void onSuccess(User user) {
        // Model层对数据操作后,将结果返回给Presenter,
        // 再由Presenter来通知View去更新UI来通知用户数据操作的结果
        mView.showLoginSuccessMsg(user);
      }
      public void onFail(String errorMsg) {
        mView.showLoginFailMsg(user);
      }
    });
  }

}

而Model层大家则已经可以脑补出来了,只负责对于数据的操作而已了。例如请求服务器获取数据,获取查询本地数据库都可以。

从1个类变为3个类

在MVP的实践中,很明显的结构变化就是很多页面从1个类变成了3个甚至更多的类。

例如,原来只有一个 LoginActivity ,而现在会变成至少3个类:

  1. LoginActivity(View)
  2. LoginPresenterImpl (Presenter)
  3. LoginModelImpl (Model)

而你以为这些就够了,就太天真的,在MVP里,为了解藕三者之间的关系,还需要通过接口来通信,P层是通过接口来和M层通信的,P层和V层之间也是通过接口来互相通信的(但V层对P层的通信被视为被动通信,而非主动通信)

接口列表:

  1. LoginView (interface for View)
  2. LoginPresenter (interface for Presenter)
  3. LoginMode (interface for Model)

这里插一句题外话,在Google官方的MVP实例里的,有一个契约类的概念,这个契约类的概念引入我觉得真的很赞,其实它只是将View和Presenter的接口写到了一个类里面,但这样写则会使得读代码的人一目了然就可以了解这个页面需要展示些什么,有什么操作。

如果你以为MVP各一个接口这样就应该够了,我只能说你还是太年轻太天真。要知道很多消息在MVP三者之间传递,不仅仅是同步消息,还有很多异步消息,例如用户点击了一个按钮,View将该事件传递给Presenter,Presenter异步的向Model请求数据,Model异步的返回数据给Presenter,Presenter再将Model处理的结果异步的传递给View,让其向用户作出回应。

可想而之,这样异步操作,自然少不了一些Callback的接口类,虽然可以用内部类来解决,但如果不用范型的话,这些Callback的接口类数目还是很多的。

这里也插一句提外话,我个人推荐使用rxJava来解决回调恶魔的问题,不过这仅仅是个人偏好而已。

从直来直去变成跳来跳去

上文说了,从1个类的代码,分离到了N个文件,三个层面以后,原本直来直去的代码结构,就会变成跳来跳去,例如之前1000行代码是写在一起的,现在把其中View的部分代码独立到了View的文件里,把其中Model的部分独立到Model的文件中,然后用Presenter放在它们中间,做一个中间人。

而且再加上很多消息的传递是异步的,因为在看代码的时候,或者在调试的时候,你必须从过去线性的思维变成跳跃式的,很多代码过去你开一个文件,顺着看下来就明白的,DEBUG模式下,一顺运行下来的,现在变成了你需要开N个文件,DEBUG模式下就看着从View的一个方法,跳到Presenter的一个方法,然后再跳到Model的一个方法,然后再原路跳回来,友情提示,刚开始用MVP的时候,很容易代码很清晰,但大脑却很混乱,甚至有晕车的感觉。

并且我认为这样的代码结构,甚至加大了调试的困难,过去直来直去,你很容易判断出数据是断在哪里,而现在你很难判断出数据断在哪一个层面,例如用户点击了刷新,需要从服务器拉回数据刷新到列表。但当页面没有正常展示数据的时候,你必须知道在哪个环节出错了,而我告诉你,因为分成了三个层,并且消息和数据在三个层之间传递,那么出错的可能性也变多了:

  1. 可能是View层没有把用户的事件传递给Presenter层。
  2. Presenter层可能接受到View层事件,但没有将操作传递给Model层。
  3. Model层可能接受到Presenter的请求,但没有将数据传回给Presenter层。
  4. Presenter层可能接受到Model的返回值了,但没有正确的将数据传回给View层。
  5. View层可能接受到了Presenter返回值,只是没有正确的将数据显示到页面而已。

在调试的时候,你会发现,跟踪一个问题变复杂了,消息在MVP之间传来传去的,你很难一下定位到问题出在哪个层面。

那么为什么要用MVP?

说了这么MVP带来的麻烦,例如多写了很多类,思维跳来跳去,消息传来传去,层层回调把人转晕,那回头去思考:我们为什么要用MVP,为什么要这样拆分代码,不是说这样代码更清晰,更容易理解了吗,为什么我看不懂我的代码了,为什么调试起来如何麻烦?

其实,这样我要反复说的,如果你只是学会怎么使用MVP,那么你只是换了一个架构而已,这就和你换了一个IDE写代码,却期望换了IDE就可以让代码突然变的更好一样。而你真正需要做的,依然是我之前说过的:

你需要换的是脑子,而不是架构。

如果你还在每次修改一行代码,就整体去测试你的系统,那么你把代码写在一个文件里,还是拆分到几个文件里,其实是没有区别的。你只是把代码拆开在放,而这样的拆注定只是形式的,*终我相信写着写着,你会在View里面写Model的逻辑,在Model里面写View的逻辑,并且和过去一样,Presenter越来越臃肿。

为什么要把架构里的各个层次分得清清楚楚,每个层面负责什么,不应该负责负责,如何组合起来都需要严格的定义起来,你要知道,每一种架构都不是编码规范,也不是组织代码的规范,它们都是一种思维方式。

之前说过,良好的架构都是在解决几个问题:低藕合,高复用,易测试,好维护。

如果你还在你的类和类之间new来new去,你引用我,我引用你,互相依赖,层层依赖,那么你把它们写在一个文件里,和把它们几个文件里有区别吗?

如果你的一个类还承担多个职责,明明这是个叫 Car 的类,却又在承担轮子,又在承担引擎的责任,那么你抽象和不抽像,封装不封装真的有区别吗?

如果你的一个方法还在做两件甚至三件事情,甚至把一整套事情都做完了,动辄超过几屏的函数,那么你真的觉得用不用架构真的有区别吗?

单元测试&MVP

为什么要把代码拆分成不同的文件,为什么要把架构拆分成不同的层面,其实思想都是在将一个复杂的整体拆分成一个个独立的模块,然后再用合理的接口将这些模块组装到一起,成为一个完整而稳定的系统。

很多文章都会提到“易测试”的概念,在编码里面,易测试*对不是易于测试人员去测试的意思,而只有一个意思,那就是易于单元测试,易于将整体拆分成独立的单元进行测试。

但是很多时候,我们都会认为写单元测试是一种浪费时间的事情,但其实这是非常错误的一种观点,单元测试反倒是在节省时间。

就像上文提到的,如果你还是修改了一处代码,然后就跑一遍系统,整体的测试一遍,那么不使用MVP反而比使用MVP调试要轻松。但你反过来想,你如果还是每次都是整体的测试,那么你把代码分开的意义又何在呢?将代码拆分成独立的层次,独立的模块,一来是为了更好的复用,二来就是为了能够独立的测试。

可以说使用MVP,如果只是按照Google的实例去拆分代码,这只做到了*步,而第二步就是去看Google实例中是如何写单元测试的,如何独立的对Model层去做测试,对View层去做测试,以及Presenter层如何测试。你就会发现之所以拆分,带来的*大好处就是测试友好了。你可以独立的去做测试,因为拆分了,所以互相藕合低了,互相藕合低了,所以各自更独立了,各个更独立了就使得单元测试成为了可能性,你可以独立的对MVP里的每一个层面,每一个模块,每一个公开函数进行独立的测试,当你确保了每一个独立的函数,每一个类,每一个包都能都独立的完成自己的逻辑,那么通过接口把它们组合在一起后,整体测试反而变成依然轻松了,你不需要关心代码跳来跳去,消息传来传去,只要每个模块,每个层次的逻辑是正确的,是经过单元测试的,那么整体系统就不会出现太大的问题。

所以说,*终我认为MVP的关键还是在于 单元测试 ,不管你是用MVC,还是MVP,如果你的代码是能够进行良好的单元测试,那么说明你的架构就不可能有太大问题,而使用什么架构只是表象,真正起区别代码高低境界的还是思考问题的方式。

iOS学习笔记之二-触发事件改变label的值 如何退出键盘

用属性描述label

(拖拽到.m文件中并关联取名)

需要监听uilabel,所以要用

@property(nonatomic,weak)IBOutlet UILabel *differenceLbel

UILabel中返回值有一个*text 可以通过它来设定label中显示的内容,他是NSString 类型的

//用label显示一个数字(数是整形,所以需要类型转换)

self.differenceLabel.text = [NSString stringWithFormat: @“%d”,difference];

//用label显示一个字符串

self.differenceLabel.text = @“hello”

退出键盘

//释放*响应者

(每次叫出键盘就是设定键盘为*响应者)

方法一:代码重复性高,不提倡

[self.textField resignFirstResponder];

其中textField是叫出键盘的控件名称,哪个控件唤醒键盘,就用哪个的名称

方法二:结束编辑状态,结束父类编辑状态

[self.view endEditing:YES];

ios开发笔记之一 ios9弹窗、 接收textfield信息、button监听

主题

1.获取storyboard中文本框中的内容

2.类型之间的强制转换

3.弹出框属性

IBAction类型的返回值用于监听方法(按钮等)

IBOutlet类型返回值用于坚挺属性(文本框)

int类型在oc中 全局变量默认值是0,局部变量默认值是随机数。

类型强制转换

NSString text;

[text intValue]强制把text变量转换成int类型。

如何关联文本框和代码

在viewcontroller.h中在@interface下一行(自己写好的语句,当然也可以先拖拽在取名)

弹出框:

//创建弹窗 ios7

UIAlertView *allert = [[UIAlertView alloc] initWithTitle: @“提示标题” message:@“提示内容” delegate:nil(委托) cancelButtonTitle:@“取消按钮的文字” otherButtonTitles:nil,nil];

//显示

[alert show];

包括取消按钮

//创建弹窗 ios8

UIAlertController *alertVc = [UIAlertController alertControllerWithTitle:@”提示标题” message:@”提示内容” preferredStyle:<#(UIAlertControllerStyle)#>弹窗的模式,传什么返回值就以什么形式弹出];

//有取消按钮

[alertVc addAction:[UIAlertAction actionWithTitle:@”取消” style:/*枚举类型,点进去看属性*/UIAlertActionStyleCancel handler:/*点击后做什么*/nil]];;

//显示弹窗

self presentViewController:alertVc animated:NO/*是否有动画*/ completion:^(void)completion //弹窗完成后要做什么事情,没有要做的就设定为nil];

如果没有取消按钮步骤,此时弹出框中没有取消按钮!!

ios开发笔记之十二 –viewDidload的方法的基本介绍用代码设置按钮的属性

主题

拖控件有局限性

无法添加动态控件,比如弹窗

viewDidload

加载完视图之后自动调用,系统运行自动调用

在viewcontroller.m中

– (void)viewDidLoad {
[super viewDidLoad];

// Do any additional setup after loading the view, typically from a nib.

NSLog(@”a good weather”);

//创建一个按钮

UIButton *head = [[UIButton alloc] init];

//设定位置

head.frame = CGRectMake(100, 100, 120, 120);

//添加按钮

[self.view addSubview:head];

//设定背景图片

//按command点击UIBotton看参数和函数

//设置普通状态按钮背景

UIImage *nonalImage = [UIImage imageNamed:@”登录”];

[head setBackgroundImage:nonalImage forState:UIControlStateNormal];

//设置高亮状态

UIImage *highImage = [UIImage imageNamed:@”login_h”];

//直接照着选择函数自动出现的函数输入,然后互提示能选择的属性

//此时显示点击的效果图片都是正常的,不会出现那种变灰的效果,因为现在那个system属性直接就是costume

[head setBackgroundImage:highImage forState:UIControlStateHighlighted];

//设定按钮文字 costume下,文字默认白色

[head setTitle:@”login” forState:UIControlStateNormal];

[head setTitle:@”click” forState:UIControlStateHighlighted];

//设定文字字体颜色

//普通状态文字颜色

[head setTitleColor:[UIColor redColor] forState:UIControlStateNormal];

//高亮状态文字颜色

[head setTitleColor:[UIColor blueColor] forState:UIControlStateHighlighted];

}

ios开发笔记之十三 –用代码来监听按钮的点击

button里面的type有很多不同表现形式

在viewcontroller.m中续上一篇

//监听按钮点击方法

//addTarget 标识监听器,控制器监听,所以addTarget得值应该是self

//action 表示监听到按钮点击后要做的事情

//btn方法得自己实现,必须用selector包装不能直接写

//此时将-(void)btn变成-(void)btn:(UIButton *)btn执行会报错,

//解决方法:

/*需要把

[head addTarget:self action:@selector(btn) forControlEvents:UIControlEventTouchUpInside];

换成

[head addTarget:self action:@selector(btn:) forControlEvents:UIControlEventTouchUpInside];

下同

*/

[head addTarget:self action:@selector(btn:) forControlEvents:UIControlEventTouchUpInside];

//添加一个加号,用add接收,默认位置在原点

UIButton *addBtn = [UIButton buttonWithType:UIButtonTypeContactAdd];

[self.view addSubview:addBtn];

//监听多个按钮点击

[addBtn addTarget:self action:@selector(btn:) forControlEvents:UIControlEventTouchUpInside];

}

//隐藏状态栏,由控制器掌管,写一个控制器的方法就可以隐藏

-(BOOL)prefersStatusBarHidden{
return YES;

}

//测试是否监听到button的动作

-(void)btn:(UIButton *)btn{

NSLog(@”%@”,btn);

}

此时并不需要区分函数到底监听了哪个按钮,因为点击哪个按钮系统会自动判断

监听测试结果如下

iOS 代码触发button点击事件 objective-c怎么代码触发按钮点击事件

OS 代码触发button点击事件 objective-c怎么代码触发按钮点击事件

在项目中,为了方便首次进入之后,通过button的点击事件进行切换。这边代码实现点击

[self.button_push sendActionsForControlEvents:UIControlEventTouchUpInside];//代码点击

前端经典面试题解密-add(1)(2)(3)(4) == 10到底是个啥?

前端经典面试题解密-add(1)(2)(3)(4) == 10到底是个啥?
写在前面:本期教程算是个经常会遇到的面试题,更多的前端视频教程也会继续给大家出,文末有给大家总结有相关的视频学习教程,大家按需自行学习哈!不全的地方,大家可以留言或私我,我再发。
前端的小伙伴在面试的时候,几乎都会遇到一道这样的面试题: add(1)(2)(3)(4)输出结果为10。在*次看到这道面试题的时候,很多小伙伴感到了迷茫!借用王宝强在《人在囧途》中的表演:啥啥啥,这写的都是啥?下面为各位小伙伴带来这道题的揭秘。
一、核心点-基础函数的变种-函数柯里化
我们从0开始,一点点儿的观察。add(1)(2)(3)(4)输出的值怎么成为10,很简单,大家都明白是1+2+3+4的累加。那使用基础函数是怎么实现的呢?
function add (a, b, c, d) {
return a + b + c + d
}
add(1, 2, 3, 4) // 10
那如何add(1)(2)(3)(4)如何也输出10呢?小伙伴接下来可能会想到这样:
function add (a) {
return function (b) {
return function (c) {
return function (d) {
return a + b + c + d
}
}
}
}
是不是很完美!
但是如果你这么回答面试官,面试官肯定会立刻怼死你,累加到100怎么办?(PS:没有说10000已经很客气了)
你老师经典语录:下面的是重点,圈起来,一定要考!!
函数柯里化概念: 柯里化(Currying)是把接受多个参数的函数转变为接受一个单一参数的函数,并且返回接受余下的参数且返回结果的新函数的技术。
二、函数柯里化解决方案
函数柯里化有两种不同的场景,一种为函数参数个数定长的函数,另外一种为函数参数个数不定长的函数。
1.函数参数个数定长的柯里化解决方案
// 定长参数
function add (a, b, c, d) {
return [
  …arguments
].reduce((a, b) => a + b)
}
function currying (fn) {
let len = fn.length
let args = []
return function _c (…newArgs) {
// 合并参数
args = [
…args,
…newArgs
]
// 判断当前参数集合args的长度是否 < 目标函数fn的需求参数长度
if (args.length < len) {
// 继续返回函数
return _c
} else {
// 返回执行结果
return fn.apply(this, args.slice(0, len))
}
}
}
let addCurry = currying(add)
let total = addCurry(1)(2)(3)(4) // 同时支持addCurry(1)(2, 3)(4)该方式调用
console.log(total) // 10
2.函数参数个数不定长的柯里化解决方案
问题升级:那这个问题再升级一下,函数的参数个数不确定时,如何实现呢?
function add (…args) {
return args.reduce((a, b) => a + b)
}
function currying (fn) {
let args = []
return function _c (…newArgs) {
if (newArgs.length) {
args = [
…args,
…newArgs
]
return _c
} else {
return fn.apply(this, args)
}
}
}
let addCurry = currying(add)
// 注意调用方式的变化
console.log(addCurry(1)(2)(3)(4, 5)())
*后:有不清楚的地方,伙伴们可以留言,今天的教程先到这里,另外给伙伴们些干货,可以私下给自己镀镀金!
Web前端全套视频教程https://pan.baidu.com/s/1a2xUpK5CwoV3qB7GB04RfQ 提取码:uuai
友情链接: SITEMAP | 旋风加速器官网 | 旋风软件中心 | textarea | 黑洞加速器 | jiaohess | 老王加速器 | 烧饼哥加速器 | 小蓝鸟 | tiktok加速器 | 旋风加速度器 | 旋风加速 | quickq加速器 | 飞驰加速器 | 飞鸟加速器 | 狗急加速器 | hammer加速器 | trafficace | 原子加速器 | 葫芦加速器 | 麦旋风 | 油管加速器 | anycastly | INS加速器 | INS加速器免费版 | 免费vqn加速外网 | 旋风加速器 | 快橙加速器 | 啊哈加速器 | 迷雾通 | 优途加速器 | 海外播 | 坚果加速器 | 海外vqn加速 | 蘑菇加速器 | 毛豆加速器 | 接码平台 | 接码S | 西柚加速器 | 快柠檬加速器 | 黑洞加速 | falemon | 快橙加速器 | anycast加速器 | ibaidu | moneytreeblog | 坚果加速器 | 派币加速器 | 飞鸟加速器 | 毛豆APP | PIKPAK | 安卓vqn免费 | 一元机场加速器 | 一元机场 | 老王加速器 | 黑洞加速器 | 白石山 | 小牛加速器 | 黑洞加速 | 迷雾通官网 | 迷雾通 | 迷雾通加速器 | 十大免费加速神器 | 猎豹加速器 | 蚂蚁加速器 | 坚果加速器 | 黑洞加速 | 银河加速器 | 猎豹加速器 | 海鸥加速器 | 芒果加速器 | 小牛加速器 | 极光加速器 | 黑洞加速 | movabletype中文网 | 猎豹加速器官网 | 烧饼哥加速器官网 | 旋风加速器度器 | 哔咔漫画 | PicACG | 雷霆加速