Git Product home page Git Product logo

easymvp's Introduction

EasyMVP

Build Status Download Android Arsenal

A powerful, and very simple MVP library with annotation processing and bytecode weaving.

EasyMVP eliminates the boilerplate code for dealing with MVP implementation.

📖 Chinese Readme 中文文档

Features

  • Easy integration
  • Less boilerplate
  • Composition over inheritance
  • Implement MVP with just few annotations
  • Use Loaders to preserve presenters across configurations changes
  • Support Clean Architecture approach.

Installation

Configure your project-level build.gradle to include the 'easymvp' plugin:

buildscript {
  repositories {
    ...
    maven { url  "http://dl.bintray.com/6thsolution/easymvp" }
   }
  dependencies {
    classpath 'com.sixthsolution.easymvp:easymvp-plugin:1.2.0-beta10'
  }
}
allprojects {
  repositories {
      ...
      maven { url  "http://dl.bintray.com/6thsolution/easymvp" }
  }
}

Then, apply the 'easymvp' plugin in your module-level build.gradle:

apply plugin: 'easymvp'

android {
  ...
}

There is no need for android-apt plugin for android gradle plugin version 2.2.0-alpha1 or higher. But if your are using it, please apply easymvp plugin after android-apt plugin.

apply plugin: 'com.neenbedankt.android-apt'
apply plugin: 'easymvp'

For reactive API, simply apply the 'easymvp-rx' plugin in your module-level build.gradle and then add the RxJava dependency:

apply plugin: 'easymvp-rx'

dependencies {
  compile 'io.reactivex:rxjava:x.y.z'
}

Also EasyMVP supports RxJava2:

apply plugin: 'easymvp-rx2'

dependencies {
  compile 'io.reactivex.rxjava2:rxjava:x.y.z'
}

Note: All snapshot versions are available on jfrog

Usage

First thing you will need to do is to create your view interface.

public interface MyView {
    void showResult(String resultText);

    void showError(String errorText);
}

Then you should implement MyView in your Activity, Fragment or CustomView. But why?

  • Improve unit testability. You can test your presenter without any android SDK dependencies.
  • Decouple the code from the implementation view.
  • Easy stubbing. For example, you can replace your Activity with a Fragment without any changes in your presenter.
  • High level details (such as the presenter), can't depend on low level concrete details like the implementation view.

Presenter

Presenter acts as the middle man. It retrieves data from the data-layer and shows it in the View.

You can create a presenter class by extending of the AbstractPresenter or RxPresenter (available in reactive API).

public class MyPresenter extends AbstractPresenter<MyView> {

}

To understand when the lifecycle methods of the presenter are called take a look at the following table:

Presenter Activity Fragment View
onViewAttached onStart onResume onAttachedToWindow
onViewDetached onStop onPause onDetachedFromWindow
onDestroyed onDestroy onDestroy onDetachedFromWindow

View Annotations

Well, here is the magic part. There is no need for any extra inheritance in your Activity, Fragment or View classes to bind the presenter lifecycle.

Presenter's creation, lifecycle-binding, caching and destruction gets handled automatically by these annotations.

For injecting presenter into your activity/fragment/view, you can use @Presenter annotation. Also during configuration changes, previous instance of the presenter will be injected.

EasyMVP uses Loaders to preserve presenters across configurations changes.

Presenter instance will be set to null, after onDestroyed method injection.

@ActivityView example:

@ActivityView(layout = R.layout.my_activity, presenter = MyPresenter.class)
public class MyActivity extends AppCompatActivity implements MyView {

    @Presenter
    MyPresenter presenter;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    }

    @Override
    protected void onStart() {
        super.onStart();
        // Now presenter is injected.
    }
    
    @Override
    public void showResult(String resultText) {
        //do stuff
    }
    
    @Override
    public void showError(String errorText) {
        //do stuff
    }
}
  • You can specify the layout in @ActivityView#layout and EasyMVP will automatically inflate it for you.

@FragmentView example:

@FragmentView(presenter = MyPresenter.class)
public class MyFragment extends Fragment implements MyView {

    @Presenter
    MyPresenter presenter;
    
    @Override
    public void onResume() {
        super.onResume();
        // Now presenter is injected.
    }
    
    @Override
    public void showResult(String resultText) {
        //do stuff
    }
    
    @Override
    public void showError(String errorText) {
        //do stuff
    }
}

@CustomView example:

@CustomView(presenter = MyPresenter.class)
public class MyCustomView extends View implements MyView {

    @Presenter
    MyPresenter presenter;
    
    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        // Now presenter is injected.
    }
    
    @Override
    public void showResult(String resultText) {
        //do stuff
    }
    
    @Override
    public void showError(String errorText) {
        //do stuff
    }
}

Injecting with Dagger

@Presenter annotation will instantiate your presenter class by calling its default constructor, So you can't pass any objects to the constructor.

But if you are using Dagger, you can use its constructor injection feature to inject your presenter.

So what you need is make your presenter injectable and add @Inject annotation before @Presenter. Here is an example:

public class MyPresenter extends AbstractPresenter<MyView> {

    @Inject
    public MyPresenter(UseCase1 useCase1, UseCase2 useCase2){
    
    }
}

@ActivityView(layout = R.layout.my_activity, presenter = MyPresenter.class)
public class MyActivity extends AppCompatActivity implements MyView {

    @Inject
    @Presenter
    MyPresenter presenter;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        SomeDaggerComponent.injectTo(this);
        super.onCreate(savedInstanceState);
     }
        
    //...
}

Don't inject dependencies after super.onCreate(savedInstanceState); in activities, super.onActivityCreated(bundle); in fragments and super.onAttachedToWindow(); in custom views.

Clean Architecture Usage

You can follow the principles of Clean Architecture by applying 'easymvp-rx' plugin. Previous part was all about the presentation-layer, Now lets talk about the domain-layer.

Domain Layer holds all your business logic, it encapsulates and implements all of the use cases of the system. This layer is a pure java module without any android SDK dependencies.

UseCase

UseCases are the entry points to the domain layer. These use cases represent all the possible actions a developer can perform from the presentation layer.

Each use case should run off the main thread(UI thread), to avoid reinventing the wheel, EasyMVP uses RxJava to achieve this.

You can create a use case class by extending of the following classes:

public class SuggestPlaces extends ObservableUseCase<List<Place>, String> {

    private final SearchRepository searchRepository;

    public SuggestPlaces(SearchRepository searchRepository, 
                         UseCaseExecutor useCaseExecutor,
                         PostExecutionThread postExecutionThread) {
        super(useCaseExecutor, postExecutionThread);
        this.searchRepository = searchRepository;
    }

    @Override
    protected Observable<List<Place>> interact(@NonNull String query) {
        return searchRepository.suggestPlacesByName(query);
    }
}
public class InstallTheme extends CompletableUseCase<File> {

    private final ThemeManager themeManager;
    private final FileManager fileManager;
    
    public InstallTheme(ThemeManager themeManager,
                           FileManager fileManager,
                           UseCaseExecutor useCaseExecutor,
                           PostExecutionThread postExecutionThread) {
        super(useCaseExecutor, postExecutionThread);
        this.themeManager = themeManager;
        this.fileManager = fileManager;
    }

    @Override
    protected Completable interact(@NonNull File themePath) {
        return themeManager.install(themePath)
                .andThen(fileManager.remove(themePath))
                .toCompletable();
    }

}

And the implementations of UseCaseExecutor and PostExecutionThread are:

public class UIThread implements PostExecutionThread {
    
    @Override
    public Scheduler getScheduler() {
        return AndroidSchedulers.mainThread();
    }
}

public class BackgroundThread implements UseCaseExecutor {

    @Override
    public Scheduler getScheduler() {
        return Schedulers.io();
    }
}

DataMapper

Each DataMapper transforms entities from the format most convenient for the use cases, to the format most convenient for the presentation layer.

But, why is it useful?

Let's see SuggestPlaces use case again. Assume that you passed the Mon query to this use case and it emitted:

  • Montreal
  • Monterrey
  • Montpellier

But you want to bold the Mon part of each suggestion like:

  • Montreal
  • Monterrey
  • Montpellier

So, you can use a data mapper to transform the Place object to the format most convenient for your presentation layer.

public class PlaceSuggestionMapper extends DataMapper<List<SuggestedPlace>, List<Place>> {
    
    @Override
    public List<SuggestedPlace> call(List<Place> places) {
        //TODO for each Place object, use SpannableStringBuilder to make a partial bold effect
    }
}

Note that Place entity lives in the domain layer but SuggestedPlace entity lives in the presentation layer.

So, How to bind DataMapper to ObservableUseCase?

public class MyPresenter extends RxPresenter<MyView> {

    private SuggestPlace suggestPlace;
    private SuggestPlaceMapper suggestPlaceMapper;
    
    @Inject
    public MyPresenter(SuggestPlace suggestPlace, SuggestPlaceMapper suggestPlaceMapper){
        this.suggestPlace = suggestPlace;
        this.suggestPlaceMapper = suggestPlaceMapper;
    }
    
    void suggestPlaces(String query){
        addSubscription(
                       suggestPlace.execute(query)
                                     .map(suggetsPlaceMapper)
                                     .subscribe(suggestedPlaces->{
                                           //do-stuff
                                      })
                        );
    }
}

FAQ

How does EasyMVP work under the hood?

  • For each annotated class with @ActivityView, @FragmentView or @CustomView, EasyMVP generates *_ViewDelegate class in the same package. These classes are responsible for binding presenter's lifecycle.
  • EasyMVP uses bytecode weaving to call delegate classes inside your view implementation classes. You can find these manipulated classes in build/weaver folder.

Is there any restrictions on using EasyMVP?

Does it support kotlin?

  • Yes, See this issue for details.

Documentations

EasyMVP API: Javadocs for the current API release

EasyMVP RX-API: Javadocs for the current RX-API (Clean Architecture API) release

EasyMVP RX2-API: Javadocs for the current RX2-API (Clean Architecture API) release

Demo

CleanTvMaze Shows how to use EasyMVP with Kotlin

TVProgram_Android Shows how to use EasyMVP with Java

Author

Saeid Masoumihajiagha

License

Copyright 2016-2017 6thSolution Technologies Inc.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

   http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

easymvp's People

Contributors

joblack33 avatar lujiajing1126 avatar zi-yang-zhang avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

easymvp's Issues

[Discussion] Improving @PresenterId annotation

From the last comment on #28:

@RiccardoM Thanks, EasyMVP calls getLoaderManager().initLoader(ID, ... , ... ); method, by default the ID is a simple counter object so when you create multiple instances of a class it will assign same id to them. There will be better solutions maybe, I think it can be simplified with putting ids in Bundle, but i encountered some bugs in custom Views.

What about using UUIDs as the presenteres' id?

Instead of passing the presenter id to its constructor and then associating it to the @PresenterId annotated object, a solution I came up with would be to have the same annotation (i.e. @Presenter) with a new boolean parameter (let's say keepInstance) that will act as follows:

  • if keepInstance is set to true, no action will be taken, and everything will be the same;
  • if keepInstance is set to false, the presenter id will be generated each time that a new instance of the view is created, using UUID.randomUUID().toString().replace("-", "");

I think that the resulting code inside the generator class would be something along the lines of this:

if (keepInstance) {
     presenterId = LOADER_ID.incrementAndGet() + "";
} else {
    presenterId = "view." + UUID.randomUUID().toString().replace("-", "");
}

This method shouldn't have problem with custom views or stuff like that, as the UUID is indipendent from the object that creates it.

What are your thoughts on this solution @SaeedMasoumi?

Updating com.android.support:design to 27.1.1 causes null presenter

Hi, I implemented easy MVP in my application. Everything used to work fine.

But I have to update com.android.support:design to 27.1.1.

When I do this my presenter is not created in my activity :

@ActivityView(layout = R.layout.activity_login, presenter = LoginPresenter.class)
public class LoginActivity extends AppCompatActivity implements LoginActivityContract.View {
 @Override
    protected void onStart() {
        super.onStart();
        // Now presenter is injected.

        SharedPreferences pref = PreferenceManager
                .getDefaultSharedPreferences(this.getBaseContext());
        this.presenter.init(pref, this.queue); ---> here presenter is null
    }

What am I doing wrong ?

NullPointerException on Presenter#onViewAttached while using android.app.Activity

Bug
Today I found out that the following code produces a NullPointerException:

public class WebViewActivity extends android.app.Activity implements WebViewActivityView {
    @Presenter WebViewActivityPresenter presenter;
}

How to replicate
The exception can be replacated as follows:

  1. Start the activity.
  2. Rotate the screen.

At this point, the following exception is thrown:
ComponentInfo: java.lang.NullPointerException: Attempt to invoke virtual method 'void WebViewActivityPresenter.onViewAttached(WebViewActivityView)' on a null object reference

How to avoid
I've found out that if android.support.v7.app.AppCompatActivity is used instead of android.app.Activity, then the exception won't be thrown, and everything executes smoothly.

I think that this is a pretty big bug, which may cause a lot of problems, and should be solved as soon as possibile. @SaeedMasoumi

Activity view's presenter is not attached on first creation

Hi,

I ran into this weird problem where the Activity view's presenter is created but not attached the first time the activity is created. After that, if for example I press the home button and return to the activity everything works fine and the view is attached to the presenter with onStart().

Here is my code:

@ActivityView(layout = R.layout.activity_wallet_new, presenter = TestPresenter::class)
class TestActivity : AppCompatActivity(), TestView {

    @Presenter
    @JvmField
    var presenter: TestPresenter? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        Horizon.zero(this).statusBarColor(R.color.statusBarColor).dawn()
    }

    override fun onStart() {
        super.onStart()
        Timber.d("onStart: presenter=%s", presenter)
    }
}

presenter:

class TestPresenter: AbstractPresenter<TestView>() {

    init {
        Timber.d("init: test presenter created")
    }

    override fun onViewAttached(view: TestView?) {
        super.onViewAttached(view)
        Timber.d("onViewAttached: called")
    }

    override fun onViewDetached() {
        super.onViewDetached()
        Timber.d("onViewDetached: called.")
    }
}

view interface:

interface TestView {
}

build.gradle:

android {
    compileSdkVersion 27
    defaultConfig {
        applicationId "com.exmample.foo"
        minSdkVersion 19
        targetSdkVersion 27
        versionCode 6
        versionName "1.0.6"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
        vectorDrawables.useSupportLibrary = true
    }
    buildTypes {
        release {
            minifyEnabled true
            shrinkResources true
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
    buildToolsVersion '28.0.1'
}

easymvp version:

        classpath 'com.sixthsolution.easymvp:easymvp-plugin:1.2.0-beta10'

Note that the presenter constructor is called even on the first creation, it is just that it isn't attached (i.e. onViewAttached is not called and presenter is null in onStart() of the activity)

I tried rewriting everything in Java just to make sure it's not some freaky kapt problem. I also tried using an injected presenter and everything was the same.

Any ideas?

UPDATE:

here's the log output. I just realized when it's on Java, neither attach or detach is called the first time. On Kotlin, it even calls onViewDetached the first time but onViewAttached is only called from the second time on, here's the log output for Kotlin:

D/TestActivity: onStart: presenter=null
D/TestPresenter: init: test presenter created

[HIT HOME BUTTON AND RETURN TO ACTIVITY]

D/TestPresenter: onViewDetached: called.
D/TestPresenter: onViewAttached: called
D/TestActivity: onStart: presenter=com.example.foo.domain.TestPresenter@9c8b130

Now I'm really confused.

Presenter null

Hi, im trying implement EasyMvp to my project here are my Fragment

@FragmentView(presenter = LoginPresenter.class) public class LoginFragment extends Fragment
    implements LoginView {

  @Presenter LoginPresenter presenter;

  public LoginFragment() {
    // Required empty public constructor
  }

  public static LoginFragment newInstance() {
    Bundle args = new Bundle();
    LoginFragment fragment = new LoginFragment();
    fragment.setArguments(args);
    return fragment;
  }

  @Override public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
  }

  @Override public View onCreateView(LayoutInflater inflater, ViewGroup container,
      Bundle savedInstanceState) {
    // Inflate the layout for this fragment
    return inflater.inflate(R.layout.fragment_login, container, false);
  }

  @Override public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
    super.onViewCreated(view, savedInstanceState);
    ButterKnife.bind(this, view);
  }

  @Override public void onStart() {
    super.onStart();
  }

  @Override public void onStop() {
    super.onStop();
  }

  @OnClick(R.id.btnLogin) void onLoginClick() {
    presenter.onLoginClick();
  }

  @OnClick(R.id.btnForgotPassword) void onForgotPassword() {
    EventBus.getDefault().post(new ClickEvent(ClickEvent.Event.FORGOT_PASSWORD));
  }

  @OnClick(R.id.btnSignUp) void onSignUpClick() {
    EventBus.getDefault().post(new ClickEvent(ClickEvent.Event.REGISTER));
  }

  @Override public String getUsername() {
    return null;
  }

  @Override public void showEmptyUsername() {

  }

  @Override public String getPassword() {
    return null;
  }

  @Override public void showEmptyPassword() {

  }

  @Override public void loginSuccesfull() {

  }

  @Override public void loginFailed() {

  }

  @Override public void showMessage(String message) {
    Toast.makeText(getActivity(), message, Toast.LENGTH_SHORT).show();
  }
}

and for my Presenter

public class LoginPresenter extends RxPresenter<LoginView> {
  private LoginRepository repository;

  public LoginPresenter() {
  }

  @Override public void onViewAttached(LoginView view) {
    super.onViewAttached(view);
  }

  @Override public void onViewDetached() {
    super.onViewDetached();
  }

  public void onLoginClick() {
    final String username = getView().getUsername();
    final String password = getView().getPassword();
    if (username.isEmpty()) {
      getView().showEmptyUsername();
    } else if (password.isEmpty()) {
      getView().showEmptyPassword();
    } else {
      getView().loginSuccesfull();
    }
  }
}

but when im trying access my presenter its still null, i have been add

apply plugin: 'easymvp'
apply plugin: 'easymvp-rx'

to my build.gradle

Injection is broken with the latest Dagger 2 release

EasyMVP works fine with Dagger 2.11. It is broken when migrating to Dagger 2.12. You can easily replicate this by compiling the sample tvProgram_android project against this Dagger version. When the view tries to inject the presenter you will get an error like:

java.lang.NullPointerException: Attempt to invoke interface method 'java.lang.Object javax.inject.Provider.get()' on a null object reference at easymvp.loader.SupportPresenterLoader.onForceLoad(SupportPresenterLoader.java:32) at android.support.v4.content.Loader.forceLoad(Loader.java:329) at easymvp.loader.SupportPresenterLoader.onStartLoading(SupportPresenterLoader.java:26) at android.support.v4.content.Loader.startLoading(Loader.java:272) at android.support.v4.app.LoaderManagerImpl$LoaderInfo.start(LoaderManager.java:270) at android.support.v4.app.LoaderManagerImpl.doStart(LoaderManager.java:770) at android.support.v4.app.FragmentHostCallback.doLoaderStart(FragmentHostCallback.java:243) at android.support.v4.app.FragmentController.doLoaderStart(FragmentController.java:386) at android.support.v4.app.FragmentActivity.onStart(FragmentActivity.java:566) at android.support.v7.app.AppCompatActivity.onStart(AppCompatActivity.java:177) at com.leonard.www.tvprog.feature.channelList.view.ChannelListActivity.onStart(ChannelListActivity.java:0)

This is because the providerFactory passed to the view delegate class is null. I suspect this may be caused by a change to the way Dagger is generating its internal classes. Therefore the Dagger2Extension.apply() method is failing to correctly detect the Dagger classes.

The simply fix would be to update this class to support the new class format, however a better solution would be to rewrite the class such that its detection is based on the public annotations, etc rather than inferring information from the internal Dagger implementation.

Disable / remove getSupportLoaderManager() dependency ?

Hi, your library looks amazing !

Is it possible to get rid of getSupportLoaderManager() dependency ? I need to extend an android.appActivity (third party library) and EasyMVP generates code that refer to the getSupportLoaderManager() method which cannot be resolved.

private LoaderManager getLoaderManager(GVRMainActivity view) {
   return view.getSupportLoaderManager(); // Symbol not found
}

A question about ButterKnife

I added compile 'com.jakewharton:butterknife:8.5.0'
annotationProcessor 'com.jakewharton:butterknife-compiler:8.5.1' to my application module, and called method ButterKnife.bind(this); in method onCreate,but widgets is still null, how could solution it?

Forgetting @FragmentView makes Gradle crash without any error

Bug
If you forget the @FragmentView on a Fragment class, but you annotate with @Presenter a field, Gradle will crash while compiling without giving any error.

How to replicate
1. Create the following class

public class MyFragment extent Fragment extends MyView {
    @Presenter MyPresenter presenter;

}

where MyPresenter and MyView are the presenter and view of this fragment.

2. Try to build the project.

Verified behaviour
The build craches telling Cannot write ( null )

Expected behaviour
Throw some kind of exception

Integrating with Architecture Components

This is not a bug, maybe a suggestion.
Is there any plan on integrating Architecture Components with this library?
I have some suggestions like:

  • Having some modules to integrate with LiveData in presentation layer or data layer
  • Using Lifecycle owner and Lifecycle observer in presenters and views

Thanks.

Don't keep activities

Saving presenters doesn't work with enabled don't keep activities flag in developer options.

Support dagger 2 for injecting presenter

User can inject presenter like this:


public class MyPresenter extends AbstractPresenter<SomeView>{
@Inject
public MyPresenter(SomeRepository repository){}
}
@ActivityView(presenter = MyPresenter.class)
public class MyActivity extends AppCompatActivity{
    @Inject  @Presenter 
    MyPresenter presenter;
}

Solution
During byte code manipulation, we should do these things

  1. Add a field in the view for getting provided presenter( $$presenterProvider)
  2. Manipulate MyActivity_MembersInjector, so in injectMembers method replace instance.presenter = (MyPresenter )this.presenter.get() with instance.$$presenterProvider = this.presenter
  3. Pass $$presenterProvider to view delegate implementation of this view.

EasyMVP and Robolectric annotations problem

I have problem while trying to use Robolectric to test activity which is using EasyMvp. All of the classes are written in Kotlin.

Part of Activity:

@ActivityView(layout = R.layout.activity_access, presenter = AccessPresenterImpl::class)
class AccessActivity : BaseActivity(), AccessView {

@Presenter
lateinit var presenter: AccessPresenter

override fun providePresenter(): BasePresenter? {
    return presenter
}

I was trying to introduce Robolectric tests in my app.

var activity: AccessActivity? = null
var loginEditText: EditText? = null
var passwordEditText: EditText? = null

@Before
fun initData() {
    activity = Robolectric.setupActivity(AccessActivity::class.java)
    loginEditText = activity?.findViewById(R.id.loginEditText)
    passwordEditText = activity?.findViewById(R.id.passwordEditText)
}

But while Running the test, I am always getting error:

kotlin.UninitializedPropertyAccessException: lateinit property presenter has not been initialized
I tried numerous of ideas like changing presenter to nullable object, but EasyMVP do not compile with it.

Any solution for that?

Same instance of Presenter injected in all instances of a class

Problem
I've encountered a pretty big bug (or maybe not-thought feature?).
Let's say we have the following presenter class:

public class Presenter extends AbstractPresenter<View> {
    private static int counter = 0;
    private int count = counter++;

    public void setupView(){
        getView().setLabelText(String.valueOf(count));
    }

}

Which is used by the following view class:

@CustomView(presenter = Presenter.class)
public class ViewImpl extends android.View implements View {
    @Presenter Presenter presenter;
    @BindView(R2.id.icon_label_textview) TextView labelTextView;

    public ViewImpl(Context context){
        View view = LayoutInflater.from(context).inflate(R.layout.view_layout, this);
        ButterKnife.bind(this, view);
    }

    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        presenter.setupView();
    }

    @Override
    public void setLabelText(String text) {
        labelTextView.setText(text);
    }

That implements the followin interface:

public interface IconView {
    void setLabelText(String text);

Now, inside another class (let's say a fragment), we exceute the following piece of code:

for (int i = 0 ; i < 10; i ++){
   View view = new View(getActivity());  // Create a new View instance
   addView(view);  // Adds the view to the layout
}

What will happen is that the first View that gets created triggers the Presenter creation, and thus will correctly show a label displaying the number 0 (initial value of counter). But then, starting from the second icon onwards, the Presenter instances associated with the new View instances will all refer the first Presenter instance, thus showing other 9 labels all with the value 0 instead of the supposed increasing 1, 2, 3, ..., 9.

Solution
I think that this issue might be solved by adding a field to the @Presenter annotation, let's call it refresh which, if set to true performs the injection each time that a new instance of the class referring that field is created, otherwise it will remain all the same. Let's have a look at how it could be used in both cases:

@CustomView(presenter = Presenter.class)
public class ViewImpl implements View{
    @Presenter(refresh = true) // This instance will be re-injected each time that a ViewImpl istance gets created
    Presenter presenter;
    
    ...
}
@CustomView(presenter = Presenter.class)
public class ViewImpl implements View{
    // @Presenter(refresh = false) if the same of writing @Presenter, false is the default value
    @Presenter(refresh = false) // This instance will not be re-injected each time that a ViewImpl istance gets created
    Presenter presenter;
    
    ...
}

NullPointerException in ViewDelegates

In a legacy project using EasyMVP have a @CustomView extending LinearLayout and for some reason it throws NPE in attachView (everything extends AppCompatActivity, so it's not related to #34) and in PresenterLoaderCallbacks.onLoaderReset

Is this project still alive? Could You provide me some help with that?
It's using version 1.0.5 of easyMVP

The app returns to screen containing this custom view after opening another activity for result (it might be some hint)

Custom view presenter:

It has actually one method doin thing specific for our model, so i'll hide that one for readability, but the body of presenter:

public class AddressPresenter extends BasePresenter<AddressView> {

    public AddressPresenter() {
    }

    public void geocode(Context context, double lat, double lng) {
        // RxJava stuff
    }

}

Custom view:

As above, just the body, to keep it clean:

@CustomView(presenter = AddressPresenter.class)
public class AddressForm extends LinearLayout implements AddressView {

}

Attach view NPE

Line of code causing crash:

    presenter.onViewAttached((AddressView)view);

Sony Xperia Z5 Compact (E5823), 2048MB RAM, Android 7.1

java.lang.RuntimeException:

  at android.app.ActivityThread.performLaunchActivity (ActivityThread.java:2720)
  at android.app.ActivityThread.handleLaunchActivity (ActivityThread.java:2781)
  at android.app.ActivityThread.handleRelaunchActivity (ActivityThread.java:4615)
  at android.app.ActivityThread.-wrap19 (ActivityThread.java)
  at android.app.ActivityThread$H.handleMessage (ActivityThread.java:1514)
  at android.os.Handler.dispatchMessage (Handler.java:102)
  at android.os.Looper.loop (Looper.java:241)
  at android.app.ActivityThread.main (ActivityThread.java:6274)
  at java.lang.reflect.Method.invoke (Native Method)
  at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run (ZygoteInit.java:886)
  at com.android.internal.os.ZygoteInit.main (ZygoteInit.java:776)

Caused by: java.lang.NullPointerException:

  at xx.xx.mobile.ui.widget.address.AddressForm_ViewDelegate.attachView (AddressForm_ViewDelegate.java:33)
  at xx.xx.mobile.ui.widget.address.AddressForm.onAttachedToWindow (AddressForm.java)
  at android.view.View.dispatchAttachedToWindow (View.java:15561)
  at android.view.ViewGroup.dispatchAttachedToWindow (ViewGroup.java:2916)
  at android.view.ViewGroup.dispatchAttachedToWindow (ViewGroup.java:2923)
  at android.view.ViewGroup.dispatchAttachedToWindow (ViewGroup.java:2923)
  at android.view.ViewGroup.dispatchAttachedToWindow (ViewGroup.java:2923)
  at android.view.ViewGroup.dispatchAttachedToWindow (ViewGroup.java:2923)
  at android.view.ViewGroup.addViewInner (ViewGroup.java:4456)
  at android.view.ViewGroup.addView (ViewGroup.java:4258)
  at android.view.ViewGroup.addView (ViewGroup.java:4198)
  at android.view.ViewGroup.addView (ViewGroup.java:4171)
  at android.support.v4.app.FragmentManagerImpl.moveToState (FragmentManager.java:1309)
  at android.support.v4.app.FragmentManagerImpl.moveFragmentToExpectedState (FragmentManager.java:1528)
  at android.support.v4.app.FragmentManagerImpl.moveToState (FragmentManager.java:1595)
  at android.support.v4.app.FragmentManagerImpl.dispatchActivityCreated (FragmentManager.java:2900)
  at android.support.v4.app.FragmentController.dispatchActivityCreated (FragmentController.java:201)
  at android.support.v4.app.FragmentActivity.onStart (FragmentActivity.java:603)
  at android.support.v7.app.AppCompatActivity.onStart (AppCompatActivity.java:178)
  at android.app.Instrumentation.callActivityOnStart (Instrumentation.java:1249)
  at android.app.Activity.performStart (Activity.java:6737)
  at android.app.ActivityThread.performLaunchActivity (ActivityThread.java:2683)

onLoaderReset NPE

Line of code causing crash:

      delegate.get().detachView();

Samsung Galaxy S7 Edge (hero2lte), 4096MB RAM, Android 8.0

java.lang.RuntimeException:

  at android.app.ActivityThread.performDestroyActivity (ActivityThread.java:4605)
  at android.app.ActivityThread.handleDestroyActivity (ActivityThread.java:4623)
  at android.app.ActivityThread.-wrap5 (Unknown Source)
  at android.app.ActivityThread$H.handleMessage (ActivityThread.java:1757)
  at android.os.Handler.dispatchMessage (Handler.java:105)
  at android.os.Looper.loop (Looper.java:164)
  at android.app.ActivityThread.main (ActivityThread.java:6944)
  at java.lang.reflect.Method.invoke (Native Method)
  at com.android.internal.os.Zygote$MethodAndArgsCaller.run (Zygote.java:327)
  at com.android.internal.os.ZygoteInit.main (ZygoteInit.java:1374)

Caused by: java.lang.NullPointerException:

  at xx.xx.mobile.ui.widget.address.AddressForm_ViewDelegate$PresenterLoaderCallbacks.onLoaderReset (AddressForm_ViewDelegate.java:96)
  at android.support.v4.app.LoaderManagerImpl$LoaderInfo.destroy (LoaderManager.java:357)
  at android.support.v4.app.LoaderManagerImpl.doDestroy (LoaderManager.java:832)
  at android.support.v4.app.FragmentHostCallback.doLoaderDestroy (FragmentHostCallback.java:285)
  at android.support.v4.app.FragmentController.doLoaderDestroy (FragmentController.java:420)
  at android.support.v4.app.FragmentActivity.onDestroy (FragmentActivity.java:391)
  at android.support.v7.app.AppCompatActivity.onDestroy (AppCompatActivity.java:209)
  at android.app.Activity.performDestroy (Activity.java:7470)
  at android.app.Instrumentation.callActivityOnDestroy (Instrumentation.java:1255)
  at android.app.ActivityThread.performDestroyActivity (ActivityThread.java:4592)

Presenter in injected too late

Nowadays the Presenter class is injected only inside the Fragment#onResume() method, which is really late inside the Fragments' life cycle.
As the presenter should be used to tell what to show inside the view, it should be injected before Fragment#onCreateView() so that we can use it inside that method to define the view we want to display.

Support composition

Describe the following scenario :

  • We have an annotation called @Composite that auto wiring the activity/fragment/view lifecycle.
  • Each annotated field should extends of the ActivityComposite, FragmentComposite or ViewComposite classes.
  • Weaver part automatically dispatch all the lifecycle methods for composite objects.

Support presenters for RecyclerView.Adapter

Right now there is now way to use the @Presenter annotation inside a class which extends from RecyclerView.Adapter, preventing the use of the library while working with this kind of objects.
I think that this should be supported as adapters are becoming nowadays a major part of Android application, and having the possibility to divide the view and the model part inside them could lead to a great improvement of code quality.

Possible solution

Fragment class.

@FragmentView(
   presenter = FragmentWithAdapterPresenter.class,
   adapter = FragmentAdapter.class  // Tells the adapter class
)
public class FragmentWithAdapter extends Fragment implements FragmentView {
  @Presenter
  FragmentWithAdapterPresenter presenter;

  // Bind the adapter
  @Adapter
  FragmentAdapter adapter;

}

Adapter class

@Adapter(presenter = FragmentAdapterPresenter.class)
public class FragmentAdapter extends AbstractMVPAdapter<FragmentView> implements AdapterView {
  @Presenter // Binds the presenter to the adapter
  FragmentAdapterPresenter presenter;

  private FragmentView view;

  // Called after Fragment#onCreateView(), Activity#onCreate() or View#onDraw() has finished
  @Override
  protected void onViewAttached(View view){
    this.view = view;
    super.onViewAttached(view);
  }
}

AbstractMVPAdapter class

public abstract class AbstractMVPAdapter<V>{
  protected void onViewAttached(V view);  // Called when the Activity, Fragment or View gets created
  protected void onViewDetached();  // Called when the Activity, Fragment or View gets destroyed

  protected V getView();  // Returns the bound view
}

FragmentAdapterPresenter class

public class FragmentAdapterPresenter extends BaseAdapterPresenter<AdapterView>{
  // Methods of a common presenter
}

BaseAdapterPresenter class

public abstract class BaseAdapterPresenter<V>{
  Same methods as the actual AbstractPresenter
}

How it works

Note. The following working mechanism will use Activityas a reference, but it may be also Fragment or even View.

Creation lifecycle.

  1. When an Activity gets created, after the onCreate() method, it calls the AbstractMVPAdapter#onViewAttached(V1 view), which stores a WeakReference to the interface that the activity implements (V1).

  2. AbstractMVPAdapter#onViewAttached(V1 view) calls BaseAdapterPresenter#onViewAttached(V2 view), which stores inside the adapter's presenter, a WeakReference to the interface that the adapter implements (V2).

Destruction lifecycle

  1. When an Activity gets destroyed, after the onDestroy() method, it calls the AbstractMVPAdapter#onViewDetached(V1 view), which deletes the WeakReference to the interface implemented by the activity (V1).
  2. Before destroying the WeakReference to V1, AbstractMVPAdapter#onViewDetached(V1 view) calls BaseAdapterPresenter#onViewAttached(V2 view) which tells the adapter's presenter to delete the WeakReference to V2, which is the view interface implemented by the adapter.

Conclusions

Following this path should garantee that each component (activity, adapter and the adapter's presenter) is separated and does not depend on its dependencies. Also, when the Activity gets destroyed, it triggers the deletion of all its dependencies and the dependencies' dependencies too, avoiding memory leaks.

Cannot access ParametersAreNullableByDefault

I've tried to create the following presenter:

public class MainPresenter extends RxPresenter<MainView> {

    private MainView view;

    private GetRandomContactUseCase getRandomContact;

    @Inject
    public MainPresenter(GetRandomContactUseCase getRandomContact){
        this.getRandomContact = getRandomContact;
    }

    ...

}

With java GetRandomContactUseCase being:

public class GetRandomContactUseCase extends ObservableUseCase<String, Void> {

    ContactsDataSource source;

    @Inject
    public GetRandomContactUseCase(ContactsDataSource source,
                                   UseCaseExecutor executor,
                                   PostExecutionThread executionThread){
        super(executor, executionThread);
        this.source = source;
    }

    @Override
    protected Observable<String> interact(Void param) {
        return Observable.just(source.getRandomContact());
    }
}

When I try to compile this, i get the following error:

Error:cannot access ParametersAreNullableByDefault  
Error:Execution failed for task ':module:compileReleaseJavaWithJavac'.  
> Compilation failed; see the compiler error output for details.

Using the following UseCase, which does not extend ObservableUseCase, however, I don't get any error:

public class GetRandomContactUseCase {

    ContactsDataSource source;

    @Inject
    public GetRandomContactUseCase(ContactsDataSource source,
                                   UseCaseExecutor executor,
                                   PostExecutionThread executionThread){
        this.source = source;
    }

I think that this is due to ObservableUseCase having no default constructor, or no constructor marked as @Inject.
Is there a way you can fix this issue, maybe creating a plugin which has all classes with a constructor marked with @Inject if that is the error, or having an empty constructor?

Support for different types of UseCases

As of RxJava 2.0 new types have been added, such as Maybe, and also many more specific ones are already implemented in RxJava 1.x, such as Single and Completable.
I think that adding few abstract UseCase classes that let introduce this kinds of return types would be great, letting us creating more specific use cases (e.g. If I need just a string there's no need to return an Observable object). Some examples could be MaybeUseCase or SingleUseCase, and so on.

How to work with ButterKnife

mudule-level:

build.gradle

apply plugin: 'com.android.application'
apply plugin: 'easymvp'
apply plugin: 'android-apt'

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])

    compile 'com.jakewharton:butterknife:8.4.0'
    apt 'com.jakewharton:butterknife-compiler:8.4.0'

    compile 'com.android.support:appcompat-v7:24.2.1'
}

the activity:

MainActivity.java

@ActivityView(layout = R.layout.activity_main, presenter = MainPresenter.class)
public class MainActivity extends AppCompatActivity implements MainView{

    @Presenter
    MainPresenter mMainPresenter;

    @BindView(R.id.pb_loading)
    ProgressBar mPbLoading;

    @BindView(R.id.tv_content)
    TextView mTvContent;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        ButterKnife.bind(this);
    }
}

it doesn't work, and i can't find the _ViewDelegate class in build/generated/source/apt/.

Mocking views in presenter testing

When writing unit tests I can't change the view object in the presenter without calling onViewAttached
I think it would be nice if there was a method (with @VisibleForTesting annotation maybe) to change the view object of the presenter without calling any extra method.
This way we can easily mock views in presenter objects and write unit tests for any presenter and test them separately like MVP pattern suggests.
P.s: RxJava 2 API hasn't been uploaded to jcenter yet: http://jcenter.bintray.com/com/sixthsolution/easymvp/

Support for RxJava2

Please add support for RxJava2 too :)
Based on the existing RxJava module in EasyMVP, adding a new module for RxJava2 would be straightforward.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.