preface

This article is intended for those who have a background in Android development and are familiar with MVP development patterns.

In my opinion, THE APPLICATION of MVP development mode is not perfect on Android, because Presenter holds a View inside, and the implementation of View interface in Android is generally the need to release resources, such as activities, fragments, etc. In order to avoid memory leakage, A weak reference is used to hold the View, or the View is left empty at the end of its life cycle.

I have been wondering if there is a way for presenters not to hold these resources internally that need to be released. Finally, Java dynamic proxy inspired the author to write a communication library: Stream

Here’s how you can use this library to decouple Presenters and Views. And the realization principle of the library.

The new writing

Here is a simplified login example to illustrate the new writing method. In order to improve readability, the Model layer and Presenter interface are omitted to simplify writing.

View

public interface ILoginView extends FStream {
    /** * Successful login callback */
    void onLoginSuccess(a);
}
Copy the code

Presenter

public class LoginPresenter {
    private final ILoginView mLoginView;

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

    /** * Login method */
    public void login(a) {
        // Simulate the request interface
        new Handler().postDelayed(new Runnable() {
            @Override
            public void run(a) { mLoginView.onLoginSuccess(); }},2000); }}Copy the code

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(a) {
        Log.i(getClass().getSimpleName(), "onLoginSuccess"); }}Copy the code
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(a) {
        super.onDestroy();
        FStreamManager.getInstance().unregister(this); }}Copy the code

Compared to writing

There are several differences between the new and conventional writing:

  • ILoginViewInterface inheritsFStreaminterface
  • LoginPresenterAn implementation is created by default inILoginViewInterface object, you don’t need to pass in a View object, you pass in a string identifier
  • BaseActivityAdded some stream-related code to the stream

If LoginPresenter does not hold a MainActivity object that implements the ILoginView interface, how does it communicate to the MainActivity object?

Analysis of STREAM principle

Before we begin, a few explanations of key nouns:

  • Flow interface

    An interface that inherits the FStream interface, the ILoginView interface in the example above

  • Stream objects

    The object that implements the stream interface class, the MainActivity object in the example above

To analyze how the library communicates internally, first we need to know what happens when the following code is triggered:

1. What happens after the stream object is registered?

FStreamManager.getInstance().register(this);
Copy the code

After executing the above code, the library does the following internally:

Search all the stream interfaces implemented by the stream object and make a mapping between the stream interface and the stream object.

The rule for searching a stream interface is to start with the stream object class and work up to the parent class. (Readers do not have to worry about performance problems, internal processing)

Since MainActivity implements ILoginView, and ILoginView inherits FStream, that is, ILoginView is a stream interface, So you end up with the ILoginView interface.

List

> to store the mapping between the stream interface and the stream object, as follows:

key value
ILoginView.class [MainActivity object]

Since the Map Value is a List, the “MainActivity object” in the Map table is wrapped in brackets to indicate that it is stored in a List.

2. What happens after the proxy object is created?

mLoginView = new FStream.ProxyBuilder().setTag(tag).build(ILoginView.class);
Copy the code

After executing the above code, the library does the following internally:

Based on the stream interface class, create a proxy object for the stream interface, associate the proxy object with the tag passed in, and return the proxy object

In the above example, mLoginView actually points to a proxy object. The code inside the library to create a proxy object is simplified as follows:

ILoginView proxy = (ILoginView) Proxy.newProxyInstance(ILoginView.class.getClassLoader(), newClass<? >[]{ILoginView.class},new InvocationHandler() {
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        return null; }});Copy the code

Invoke () {invoke(); invoke(); invoke(); invoke(); invoke();

  • Proxy: indicates a proxy object
  • Method, which method of the proxy object is called
  • Args, the argument to the method

3. What happens when a proxy object’s method is called?

mLoginView.onLoginSuccess();
Copy the code

After the above code is executed, the following occurs:

The invoke() method of the InvocationHandler object is triggered. Within the invoke() method, all flow objects mapped to the current flow interface are fetched from the Map and the target method of the flow object is called.

The code for the internal notification flow object is simplified as follows:

// Fetch the corresponding stream object from the Map that holds the mapping
final List<FStream> list = MAP_STREAM.get(ILoginView.class);
for (FStream item : list) {
    // Fires the target method of the registered stream object
    method.invoke(item, args);
}
Copy the code

So far, the whole communication process has been clarified, specific distribution logic, interested readers can take a look at the project source code.

doubt

1. If the stream interface corresponds to multiple stream objects, will all stream objects be notified when the proxy object method is called?

Take a look at one of the default methods inside FStream

default Object getTagForClass(Class clazz) {
    return null;
}
Copy the code

In fact, the getTagForClass() method is called inside the library to return a tag before notifying the stream object. The tag returned is compared to the tag of the proxy object, and the stream object is notified only if the tag is equal.

If the proxy object has no tag set, the default tag is NULL. If the stream object does not override the getTagForClass() method, the default tag returned is null

So by default, the stream interface and the stream object it maps to have the same tag, both null, indicating that all stream objects are notified when the proxy object’s method is called.

The rules for comparing tag equality within libraries are as follows:

private boolean checkTag(FStream stream) {
    // mTag is the tag of the proxy object
    final Object tag = stream.getTagForClass(mClass);
    if (mTag == tag)
        return true;
    
    returnmTag ! =null && mTag.equals(tag);
}
Copy the code

The “clazz” argument to the getTagForClass() method, which is the class of the stream interface passed in when the proxy object is created, in this case iloginView.class, is needed because the stream object may implement multiple stream interfaces. Multiple stream interfaces correspond to multiple proxy objects of different types, which may have different tags. You can use the class parameter to determine which type of proxy object method was called and return the corresponding tag.

What’s an example?

In the above example, we added a new View interface, this View function is to show the hidden progress box, modified as follows:

View

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

    void dismissProgress(a);
}
Copy the code

Presenter

public class LoginPresenter implements ILoginPresenter {
    private final ILoginView mLoginView;
    // Add a new IProgressView
    private final IProgressView mProgressView;

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

        // Create a proxy object for the IProgressView interface. Note that the tag is a Hello string
        mProgressView = new FStream.ProxyBuilder().setTag("hello").build(IProgressView.class);
    }

    @Override
    public void login(a) {
        // Display the progress box
        mProgressView.showProgress();
        
        // Notify the View after a delay of 2 seconds, simulating the request interface
        new Handler().postDelayed(new Runnable() {
            @Override
            public void run(a) {
                // Modify: hide the progress boxmProgressView.dismissProgress(); mLoginView.onLoginFinish(); }},2000); }}Copy the code

Activity

// Implement IProgressView interface
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) {
        // Modify: If clazz == iProgressView.class, return the Hello string as a tag, so that the stream object and the proxy object's tags match
        if (clazz == IProgressView.class)
            return "hello";

        return toString();
    }

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

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

    @Override
    public void dismissProgress(a) {
        Log.i(getClass().getSimpleName(), "dismissProgress"); }}Copy the code

Since mProgressView is a proxy object, the tag is set to the “Hello” string, so in overriding getTagForClass() we need to check that clazz == iProgressView.class returns the “Hello” string as the tag. This allows the stream object to be notified only after its tag matches that of the proxy object.

Of course, in practice, it is recommended to keep the same tag in the same Activity, unless there is a special need to return the corresponding tag as shown in the above example.

2. If a stream interface corresponds to multiple stream objects, what can I do if I do not want to notify all stream objects?

An object implementing the DispatchCallback interface can be passed in when the proxy object is created to handle the logic of whether to continue distribution.

    interface DispatchCallback {
        /** * is emitted before the method of the stream object is notified@paramStream Stream object *@param* method method@paramMethodParams Method parameter *@returnTrue - Stop distribution, false- continue distribution */
        boolean beforeDispatch(FStream stream, Method method, Object[] methodParams);

        /** * emitted when the method of the stream object is notified@paramStream Stream object *@param* method method@paramMethodParams Method parameter *@paramMethodResult Returns the value * when the method is called@returnTrue - Stop distribution, false- continue distribution */
        boolean afterDispatch(FStream stream, Method method, Object[] methodParams, Object methodResult);
    }
Copy the code
mLoginView = new FStream.ProxyBuilder()
        .setTag(tag)
        .setDispatchCallback(new FStream.DispatchCallback() {
            @Override
            public boolean beforeDispatch(FStream stream, Method method, Object[] methodParams) {
                // Handle the logic of whether to continue distribution
                return false;
            }

            @Override
            public boolean afterDispatch(FStream stream, Method method, Object[] methodParams, Object methodResult) {
                // Handle the logic of whether to continue distribution
                return false;
            }
        })
        .build(ILoginView.class);
Copy the code

3. If the called proxy object method has a return value, how is the final return value determined?

Here’s a quick change to the above example:

View

public interface ILoginView extends FStream {
    void onLoginFinish(a);
    
    // Modify: returns the user name that needs to be logged in
    String getUserName(a);
}
Copy the code

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(a) {
        // Modify: get the username to log in
        final String userName = mLoginView.getUserName();

        // Notify the View after a delay of 2 seconds, simulating the request interface
        new Handler().postDelayed(new Runnable() {
            @Override
            public void run(a)
            { mLoginView.onLoginFinish(); }},2000); }}Copy the code

Call mLoginView. GetUserName (); There are two situations:

  1. There is no corresponding stream object

    The corresponding value will be returned based on the return value type, such as 0 for numeric type, false for Boolean type, and NULL for object type

  2. There are one or more corresponding stream objects

    By default, the return value of the target method of the last registered stream object is used as the final return value of the proxy object method

In case 2, what if the proxy object needs to filter the return value?

You can create a proxy object with an object that implements the ResultFilter interface to filter the returned values

interface ResultFilter {
    /** * Filter return value **@param* method method@paramMethodParams Method parameter *@paramResults Returns values * for all stream objects@return* /
    Object filter(Method method, Object[] methodParams, List<Object> results);
}
Copy the code
mLoginView = new FStream.ProxyBuilder()
        .setTag(tag)
        .setResultFilter(new FStream.ResultFilter() {
            @Override
            public Object filter(Method method, Object[] methodParams, List<Object> results) {
                // Filter the return value of the first stream object as the final return value
                return results.get(0);
            }
        })
        .build(ILoginView.class);
Copy the code

conclusion

The stream library is more than just a decoupled Presenter and View. I’ll write more about it later.

If you have any questions or need to discuss this library, please feel free to contact me. E-mail: 565061763 @qq.com