Skip to content

Latest commit

 

History

History
315 lines (287 loc) · 11.5 KB

GettingStarted.md

File metadata and controls

315 lines (287 loc) · 11.5 KB

Getting Started

MVX has 2 variants - plain vanilla and LCE. LCE is the Load-Content-Error pattern where you show the loading state till data is fetched, if fetch is successful you show the content otherwise the error. The 3 components for the two variants are as listed below.

Variant View interface abstract Presenter Model abstract Presenter
Vanilla XView XPresenterModel XPresenter
LCE XLceView XLcePresenterModel XLcePresenter

We'll first look at the plain vanilla implementation and checkout LCE at the end. We'll then discuss how to achieve some quick unit testing.

We'll look at the usage in the sample app where required, the guide is generic otherwise.

Step 1 - Adding MVX to your project

Add jitpack.io to your root build.gradle

allprojects {
    repositories {
        ...
        maven { url "https://jitpack.io" }
    }
}

Then add the dependency in your project build.gradle

dependencies {
    ...
    compile 'com.github.adroitandroid:MVX:v1.0'
    ...
}

You can find the latest version here.

Step 2 - Defining the view, presenter model and presenter

You'll define you view first, as the presenter model depends on the view and the presenter on both of them.

public interface MainView extends XView {

}

Next, your presenter model:

public class MainPresenterModel extends XPresenterModel<MainView> {
    public MainPresenterModel() {
        super("MainPresenterModel");
    }
}

It'll ask you to implement the getPresenter() method.

public class MainPresenterModel extends XPresenterModel<MainView> {
    ...
    public MainPresenterModel() {
        super("MainPresenterModel");
    }

    @Override
    public <xPresenterModel extends XPresenterModel<MainView>> XPresenter<MainView, xPresenterModel> getPresenter() {
        return new MainPresenter();
    }
    ...
}

where since you know the presenter model type, you can refactor it to

    @Override
    public XPresenter<MainView, MainPresenterModel> getPresenter() {
        return new MainPresenter();
    }

The IDE will ask you to make MainPresenter now, which for now you can leave as an empty class.

public class MainPresenter extends XPresenter<MainView, MainPresenterModel> {

}

Step 3 - Connecting the dots

Where your view implementation is initialised, we bind the presenter to it using the following call

XPresenter.bind(View view,
                Class<PresenterModel> presenterModelServiceClass,
                OnBindListener<Presenter> bindListener);

The three arguments here are tightly coupled, so incorrect types cannot be bound by mistake. Let's suppose we want to bind the MainView implementation above in the onCreate() of the MainActivity which holds the view. If the activity holds a single XView, it can implement it directly. Our bind() call then becomes

XPresenter.bind((MainView) this, MainPresenterModel.class,
        new XPresenter.OnBindListener<MainPresenter>() {
            @Override
            public void onBind(MainPresenter presenter) {
                mPresenter = presenter;
                ...
                // do initializations with this presenter
            }
});

For any business logic, you call the methods of mPresenter, the Presenter returned in onBind(). For any updates to the view, the MainPresenter instance can call the MainView's methods and for any updates in state variables, the MainPresenter will call MainPresenterModel's methods. The following XPresenter methods allow this

public abstract class XPresenter<vView extends XView, vPresenterModel extends XPresenterModel<vView>> {
    ...
    protected vView getView() {
        // returns the view the presenter is bound to
    }

    protected vPresenterModel getPresenterModel() {
        // returns the presenter model associated with this presenter
    }
    ...
}

Examples using this can be found here.

Finally, remember to unbind the bound Presenter.

    @Override
    protected void onDestroy() {
        super.onDestroy();
        mPresenter.unbind(this);
    }

Step 4 - Saving State

This might, as per your use case, be a requirement. Good part about MVX is that it allows to save the state without passing in bundles. That's because the PresenterModel we just saw, is actually an Android Service, and the Presenter an implementation of IBinder! We just need to put a few Presenter method calls at the right lifecycle methods of the Activity or Fragment to enable state saving, namely the following.

public class MainActivity extends AppCompatActivity implements MainView {
...
    @Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        mPresenter.saveState();
    }

    @Override
    protected void onRestoreInstanceState(Bundle savedInstanceState) {
        super.onRestoreInstanceState(savedInstanceState);
        if (mPresenter != null) {
            mPresenter.restoreState();
        }
    }

    @Override
    public void finish() {
        super.finish();
        mPresenter.disposeState();
    }
...
}

IMPORTANT: restoring of the view state from the Presenter should be done in onBind() seen in Step 3 as that is where the Presenter is sure to be present. It may be null by the time onRestoreInstanceState() is called.

Step 5 - Into the Manifest

As mentioned in Step 4, the PresenterModel is actually a Service implementation. So don't forget to add it to your AndroidManifest.xml.

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

    <application
        ...
        android:theme="@style/AppTheme">
        ...
        <service android:name=".model.MainPresenterModel" />
    </application>

</manifest>

LCE implementation

The LCE implementation is similar to the vanilla implementation in all steps except Step 2. The View now takes the type of the data that needs to be loaded in its definition as in the below example.

        mCurrentLocationView = new XLceView<IpLocationData>() {

            @Override
            public void onContentReady(IpLocationData content) {
                // load data
            }

            @Override
            public void onError(String error) {
                // show error state
            }

            @Override
            public void onFetchStart() {
                // show loading state
            }

            @Override
            public Context getContext() {
                return CurrentWeatherActivity.this;
            }
        };

The Presenter and the PresenterModel that makes the Presenter in the getPresenter() implementation should also be aware of the type of data to be loaded. Taking from the LCE example here:

public class CurrentLocationPresenterModel extends XLcePresenterModel<IpLocationData, XLceView<IpLocationData>> {
    public CurrentLocationPresenterModel() {
        super("CurrentLocationPresenterModel");
    }

    @Override
    public CurrentLocationPresenter getPresenter() {
        return new CurrentLocationPresenter();
    }
    ...
}
public class CurrentLocationPresenter
        extends XLcePresenter<IpLocationData, XLceView<IpLocationData>, CurrentLocationPresenterModel> {
    ...
    @Override
    protected void onFetchComplete(IpLocationData data) {
        // do anything you need to in the presenter or presenter model once the fetch is complete here
    }

    @Override
    protected void onFetchError(String error) {
        // do anything you need to in the presenter or presenter model when the fetch fails here
    }

    @Override
    protected void onStartFetch() {
        // your implementation of the fetch that returns the required datatype
    }
    ...
}

The data fetch is invoked when the following method of the Presenter is called.

presenter.startFetch();

To complete the fetch with data, you should call the Presenter method

this.complete(data);

or to set error in fetch, call

this.setError("This is a sample error message");

Unit Testing

As you can observe, the presenter is outside the bounds of Activities which hold the view or presenter models Services. The presenter is basically out of scope of Android lifecycles and so can be unit tested quickly [insert you favorite celebration meme here].

There's one caveat though. When testing an LCE presenter implementation, one needs to keep in mind that the presenter calls the view callbacks on the main thread. There's a quick way to mock that though.

Step 1

The library uses RxJava 2 internally for this, so the first step here is add to your app's build.gradle the following dependencies. Ignore the Rx test-dependencies if your app is already dependent on them.

dependencies {
    ...
    // Rx
    testCompile 'io.reactivex.rxjava2:rxjava:2.0.5'
    testCompile 'io.reactivex.rxjava2:rxandroid:2.0.1'
    testCompile 'junit:junit:4.12'
}

Step 2

Add the following to your test class.

public class SampleTest {

    @BeforeClass
    public static void setUpRxSchedulers() {

        RxJavaPlugins.setInitIoSchedulerHandler(new Function<Callable<Scheduler>, Scheduler>() {
            @Override
            public Scheduler apply(Callable<Scheduler> scheduler) throws Exception {
                return getNewSchedulerForTest();
            }
        });
        RxJavaPlugins.setInitComputationSchedulerHandler(new Function<Callable<Scheduler>, Scheduler>() {
            @Override
            public Scheduler apply(Callable<Scheduler> scheduler) throws Exception {
                return getNewSchedulerForTest();
            }
        });
        RxJavaPlugins.setInitNewThreadSchedulerHandler(new Function<Callable<Scheduler>, Scheduler>() {
            @Override
            public Scheduler apply(Callable<Scheduler> scheduler) throws Exception {
                return getNewSchedulerForTest();
            }
        });
        RxJavaPlugins.setInitSingleSchedulerHandler(new Function<Callable<Scheduler>, Scheduler>() {
            @Override
            public Scheduler apply(Callable<Scheduler> scheduler) throws Exception {
                return getNewSchedulerForTest();
            }
        });
        RxAndroidPlugins.setInitMainThreadSchedulerHandler(new Function<Callable<Scheduler>, Scheduler>() {
            @Override
            public Scheduler apply(Callable<Scheduler> scheduler) throws Exception {
                return getNewSchedulerForTest();
            }
        });
    }

    @NonNull
    private static Scheduler getNewSchedulerForTest() {
        return new Scheduler() {
            @Override
            public Worker createWorker() {
                return new ExecutorScheduler.ExecutorWorker(new ScheduledThreadPoolExecutor(1) {
                    @Override
                    public void execute(@NonNull Runnable runnable) {
                        runnable.run();
                    }
                });
            }
        };
    }
    ...
}

And you should be good to go. Checkout how tests are implemented in the sample app here.

Since the view and presenter model are passive components, you're business logic should be well tested by now!