Android - 解锁MVP新姿势

前言

本篇文章适合有Android开发基础,了解MVP开发模式的读者。

笔者觉得MVP开发模式在Android上的应用还不够完美,因为Presenter内部持有着View,而在Android中实现View接口的一般都是需要释放的资源,比如Activity,Fragment等,为了避免内存泄漏,一般是采用弱引用来保存View,或者在生命周期结束的时候把View置为空。

笔者一直在思考有没有办法让Presenter内部不持有这些需要释放的资源?最终java的动态代理给了笔者灵感,写了一个通信库:stream

下面将介绍如何利用这个库来解耦Presenter和View。以及库的实现原理。

新写法

这边通过一个简化的登陆例子来说明新写法。
为了提高可读性,省略了Model层,Presenter接口,简化了写法。

View
public interface ILoginView extends FStream {
    /**
     * 登录成功回调
     */
    void onLoginSuccess();
}
复制代码
Presenter
public class LoginPresenter {
    private final ILoginView mLoginView;

    public LoginPresenter(String tag) {
        mLoginView = new FStream.ProxyBuilder().setTag(tag).build(ILoginView.class);
    }

    /**
     * 登录方法
     */
    public void login() {
        // 模拟请求接口
        new Handler().postDelayed(new Runnable() {
            @Override
            public void run() {
                mLoginView.onLoginSuccess();
            }
        }, 2000);
    }
}
复制代码
Activity
public class MainActivity extends BaseActivity implements ILoginView {
    private final LoginPresenter mLoginPresenter = new LoginPresenter(toString());

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

        findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mLoginPresenter.login();
            }
        });
    }

    @Override
    public void onLoginSuccess() {
        Log.i(getClass().getSimpleName(), "onLoginSuccess");
    }
}
复制代码
public class BaseActivity extends AppCompatActivity implements FStream {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        FStreamManager.getInstance().register(this);
    }

    @Override
    public Object getTagForClass(Class<?> clazz) {
        return toString();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        FStreamManager.getInstance().unregister(this);
    }
}
复制代码

对比写法

对比发现,新写法和常规写法有几处不同:

  • ILoginView接口继承了FStream接口
  • LoginPresenter中默认创建了一个实现ILoginView接口的对象,不需要外部传入View对象了,而是传入一个字符串标识
  • BaseActivity中多了一些stream相关的代码

看到这里读者是否有疑问,既然LoginPresenter中没有持有实现ILoginView接口的MainActivity对象,那它是怎么通知到MainActivity对象这边呢?

stream原理分析

在开始之前先对关键名词做一下解释:

  • 流接口

    继承了FStream接口的接口,即上述例子中的ILoginView接口

  • 流对象

    实现流接口类的对象,即上述例子中的MainActivity对象

现在来分析一下库内部是如何通信的,首先我们得知道下面几个代码被触发后内部发生了什么:

1. 注册流对象后发生了什么?

FStreamManager.getInstance().register(this);
复制代码

上面代码执行之后,库内部做了以下事情:

搜索流对象实现的所有流接口,并把流接口和流对象做一个映射。

搜索流接口的规则是,从流对象类开始,不断往上搜索父类。(读者不用担心性能问题,内部有做处理)

就上述例子来说,传进去的thisMainActivity对象,由于MainActivity实现了ILoginView接口,并且ILoginView接口继承了FStream接口,即ILoginView接口是一个流接口,所以最终会找到ILoginView接口。

找到之后,库内部会用Map<Class, List<FStream>>来保存流接口和流对象的映射关系,如下:

keyvalue
ILoginView.class[MainActivity对象]

由于Map的Value是一个List,所以映射表中的“MainActivity对象”用中括号包裹,表示存到一个List中。

2. 创建代理对象后发生了什么?

mLoginView = new FStream.ProxyBuilder().setTag(tag).build(ILoginView.class);
复制代码

上面代码执行之后,库内部做了以下事情:

根据流接口class,创建一个流接口的代理对象,代理对象和传进来的tag关联,最后返回代理对象

就上述例子来说,实际上mLoginView所指向的是一个代理对象,库内部创建代理对象的代码简化如下:

ILoginView proxy = (ILoginView) Proxy.newProxyInstance(ILoginView.class.getClassLoader(), new Class<?>[]{ILoginView.class}, new InvocationHandler() {
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        return null;
    }
});
复制代码

熟悉java动态动态代理的读者对上面的代码应该不陌生,创建代理对象的时候会要求传入一个实现InvocationHandler接口的对象,InvocationHandler接口中只有一个invoke()方法,方法参数说明:

  • proxy,代理对象
  • method,代理对象的哪一个方法被调用了
  • args,该方法的参数

3. 代理对象的方法被调用的时候发生了什么?

mLoginView.onLoginSuccess();
复制代码

上面代码执行之后,发生了以下操作:

会触发InvocationHandler对象的invoke()方法,在invoke()方法内,从Map中取出与当前流接口映射的所有流对象,并调用流对象的目标方法。

内部通知流对象的代码简化如下:

// 从保存映射关系的Map中取出对应的流对象
final List<FStream> list = MAP_STREAM.get(ILoginView.class);
for (FStream item : list) {
    // 触发已注册的流对象的目标方法
    method.invoke(item, args);
}
复制代码

到此为止,整个通信流程已经理清了,具体的分发逻辑,有兴趣的读者可以看一下项目源码。

疑问

1. 如果流接口对应多个流对象,那么当代理对象方法被调用的时候,所有流对象都会被通知吗?

先看一下FStream内部的一个默认方法

default Object getTagForClass(Class clazz) {
    return null;
}
复制代码

实际上库内部在通知流对象之前,会先调用一下这个getTagForClass()方法返回一个tag,用返回的tag和代理对象的tag进行比较,只有tag相等,才会通知这个流对象。

如果代理对象未设置tag,则默认的tag为null
如果流对象未重写getTagForClass()方法,则默认返回的tag为null

所以默认情况下,流接口和它所映射的流对象的tag是相等的,都为null,表示代理对象的方法被调用的时候,所有流对象都会被通知。

库内部比较tag相等的规则如下:

private boolean checkTag(FStream stream) {
    // mTag为代理对象的tag
    final Object tag = stream.getTagForClass(mClass);
    if (mTag == tag)
        return true;
    
    return mTag != null && mTag.equals(tag);
}
复制代码

getTagForClass()方法的参数 “clazz”, 即创建代理对象时传入的流接口对应的class,在上述例子中,就是ILoginView.class,之所以需要这个参数,是因为流对象可能实现了多个流接口,多个流接口又对应多个不同类型的代理对象,各个代理对象的tag可能又不同。
这时候就可以通过class参数来判断是哪个类型的代理对象方法被调用,然后返回对应的tag。

举个例子?

就上述例子中,我们新增一个View接口,这个View的功能是显示隐藏进度框,改造如下:

View
public interface IProgressView extends FStream {
    void showProgress();

    void dismissProgress();
}
复制代码
Presenter
public class LoginPresenter implements ILoginPresenter {
    private final ILoginView mLoginView;
    // 改造:新增一个IProgressView
    private final IProgressView mProgressView;

    public LoginPresenter(String tag) {
        mLoginView = new FStream.ProxyBuilder().setTag(tag).build(ILoginView.class);

        // 改造:创建IProgressView接口对应的代理对象,注意这里传入的tag是hello字符串
        mProgressView = new FStream.ProxyBuilder().setTag("hello").build(IProgressView.class);
    }

    @Override
    public void login() {
        // 改造:显示进度框
        mProgressView.showProgress();
        
        // 延迟2秒后通知View,模拟请求接口
        new Handler().postDelayed(new Runnable() {
            @Override
            public void run() {
                // 改造:隐藏进度框
                mProgressView.dismissProgress();
                mLoginView.onLoginFinish();
            }
        }, 2000);
    }
}
复制代码
Activity
// 改造:实现IProgressView接口
public class BaseActivity extends AppCompatActivity implements FStream, IProgressView {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        FStreamManager.getInstance().register(this);
    }

    @Override
    public Object getTagForClass(Class<?> clazz) {
        // 改造:如果clazz == IProgressView.class,返回hello字符串作为tag,这样流对象和代理对象的tag才能匹配
        if (clazz == IProgressView.class)
            return "hello";

        return toString();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        FStreamManager.getInstance().unregister(this);
    }

    @Override
    public void showProgress() {
        Log.i(getClass().getSimpleName(), "showProgress");
    }

    @Override
    public void dismissProgress() {
        Log.i(getClass().getSimpleName(), "dismissProgress");
    }
}
复制代码

由于mProgressView是一个代理对象,tag被设置为“hello”字符串,所以在重写getTagForClass()方法内需要判断,clazz == IProgressView.class时返回“hello”字符串作为tag,这样流对象和代理对象的tag匹配之后,流对象才能被通知到。

当然实际开发中建议同一个Activity中的tag保持一致,除非是有特殊需求的可以根据上述例子所示,返回相应的tag。

2. 如果流接口对应多个流对象,不想通知所有的流对象,怎么处理?

可以在创建代理对象的时候传入一个实现DispatchCallback接口的对象,用来处理是否继续分发的逻辑。

    interface DispatchCallback {
        /**
         * 流对象的方法被通知之前触发
         *
         * @param stream       流对象
         * @param method       方法
         * @param methodParams 方法参数
         * @return true-停止分发,false-继续分发
         */
        boolean beforeDispatch(FStream stream, Method method, Object[] methodParams);

        /**
         * 流对象的方法被通知之后触发
         *
         * @param stream       流对象
         * @param method       方法
         * @param methodParams 方法参数
         * @param methodResult 流对象方法被调用后的返回值
         * @return true-停止分发,false-继续分发
         */
        boolean afterDispatch(FStream stream, Method method, Object[] methodParams, Object methodResult);
    }
复制代码
mLoginView = new FStream.ProxyBuilder()
        .setTag(tag)
        .setDispatchCallback(new FStream.DispatchCallback() {
            @Override
            public boolean beforeDispatch(FStream stream, Method method, Object[] methodParams) {
                // 处理是否继续分发的逻辑
                return false;
            }

            @Override
            public boolean afterDispatch(FStream stream, Method method, Object[] methodParams, Object methodResult) {
                // 处理是否继续分发的逻辑
                return false;
            }
        })
        .build(ILoginView.class);
复制代码

3. 如果被调用的代理对象方法有返回值,那么最终的返回值怎么确定?

这边简单改造一下上述例子:

View
public interface ILoginView extends FStream {
    void onLoginFinish();
    
    // 改造:返回需要登录的用户名
    String getUserName();
}
复制代码
Presenter
public class LoginPresenter implements ILoginPresenter {
    private final ILoginView mLoginView;

    public LoginPresenter(String tag) {
        mLoginView = new FStream.ProxyBuilder().setTag(tag).build(ILoginView.class);
    }

    @Override
    public void login() {
        // 改造:获得用户名来登录
        final String userName = mLoginView.getUserName();

        // 延迟2秒后通知View,模拟请求接口
        new Handler().postDelayed(new Runnable() {
            @Override
            public void run()
            {
                mLoginView.onLoginFinish();
            }
        }, 2000);
    }
}
复制代码

调用mLoginView.getUserName();的时候,有两种情况:

  1. 没有任何与之对应的流对象

    那么会根据返回值类型返回对应的值,比如数字类型返回0,布尔类型返回false,对象类型返回null

  2. 有一个或者多个与之对应的流对象

    那么默认会用最后一个注册的流对象的目标方法的返回值当做代理对象方法最终的返回值

第2种情况中如果代理对象需要筛选返回值,那如何处理?

可以在创建代理对象的时候传入一个实现ResultFilter接口的对象,用来筛选返回值

interface ResultFilter {
    /**
     * 过滤返回值
     *
     * @param method       方法
     * @param methodParams 方法参数
     * @param results      所有流对象的返回值
     * @return
     */
    Object filter(Method method, Object[] methodParams, List<Object> results);
}
复制代码
mLoginView = new FStream.ProxyBuilder()
        .setTag(tag)
        .setResultFilter(new FStream.ResultFilter() {
            @Override
            public Object filter(Method method, Object[] methodParams, List<Object> results) {
                // 这边筛选第一个流对象的返回值作为最终的返回值
                return results.get(0);
            }
        })
        .build(ILoginView.class);
复制代码

结束语

文章比较长,需要耐心的看,才能理解整个内部的原理,当然stream库还不止解耦Presenter和View这一个使用场景,后面有时间会写一下stream库的更多使用场景。

关于这个库,有疑问的,或者需要探讨的可以和笔者联系,大家一起学习。
邮箱:565061763@qq.com

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
使用android studio 运行,下面是一个简单的文档,这个代码是一个demo 一、Activity的使用 1、SNActivity 框架最基本的activity,可调用$(SNManager)进行操作activity,具体用法请参考文档或代码 2、SNNavigationSlidingActivity 包含SNActivity的功能,继承于com.jeremyfeinstein.slidingmenu.lib.app.SlidingActivity 支持导航条和左滑视图的Activity 加载导航条: loadNavBar(int height,int background_color_id) loadNavBarResId(int height_id,int background_id) 加载左侧视图: /** * load left view * @param left_id left layout id * @param offset_value offset value * @param shadow_width_value shadow width value * @param shadow_drawable_id shadow drawable style * @param fade fade value */ loadLeft(int left_id, int offset_value, int shadow_width_value, int shadow_drawable_id, float fade) /** * load left view * @param left_id left layout id * @param offset_id offset id * @param shadow_width_id shadow width id * @param shadow_drawable_id shadow drawable id * @param fade fade value */ loadLeftResId(int left_id, int offset_id, int shadow_width_id, int shadow_drawable_id, float fade) 二、SNElement的使用 View的伪装对象,支持所有View的功能,详细功能可参考文档或代码 手动伪装:$.create $.id $.findView 注入伪装:$.setContent(view class or layout id,inject class); 获取原型:elem.toView(); 三、注入 1、视图注入 A、创建注入类,属性名称必须和layout中的id对应,如果不对应请加入标签@SNInjectView class DemoInject{ @SNInjectView(id=R.id.tvTest) public SNElement test; } B、实例化注入对象 DemoInject di=new DemoInject(); C、调用$.inject或者$.setContent注入 $.inject(di); D、注入成功后即可调用对象 String text=di.test.text(); 2、依赖注入 A、需要绑定注入对象,建议写到Application中的onCreate SNBindInjectManager.instance().bind(ITest.class, Test.class); B、与视图注入不同的是属性必须添加标签@SNIOC,注入的对象(Test)必须包含只有一个SNManager参数的构造函数,且必须实现注入者 public class Test implements ITest{ SNManager $; public Test(SNManager _$){ this.$=_$; }; } class DemoInject{ @SNIOC public ITest test; } C、调用$.inject或者$.setContent注入 同视图注入 D、注入成功后即可调用对象 di.test.xxx(); 四、fragment的使用 1、SNFragment 2、SNLazyFragment 五、控件的使用 1、SNFragmentScrollable 2、SNPercentLinearLayout、SNPercentRelativeLayout 3、SNScrollable 4、SNSlipNavigation 5、XList 6、slidingtab

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值