Giter Club home page Giter Club logo

thirtyinch's Introduction

DEPRECATED - no longer actively maintained

Build Status License Gitter

ThirtyInch - a MVP library for Android

This library adds Presenters to Activities and Fragments. It favors the stateful Presenter pattern, where the Presenter survives configuration changes and dumb View pattern, where the View only sends user events and receives information from the Presenter but never actively asks for data. This makes testing very easy because no logic lives in the View (Activity, Fragment) except for fancy animations which anyways aren't testable.

The name

Keep Android At Arm’s Length

β€” Kevin Schultz, Droidcon NYC '14

The perfect distance to the Android Framework is approximately thirty inches, the average length of the human arm, shoulder to fingertips.

Story

Read the introduction article on Medium

See the slides of the latest talk on Speakerdeck

Get it

GitHub Packages

repositories {
    maven {
        url = uri("https://maven.pkg.github.com/GCX-HCI/ThirtyInch")
    }
}

dependencies {
    implementation "net.grandcentrix.thirtyinch:thirtyinch:$thirtyinchVersion"
    implementation "net.grandcentrix.thirtyinch:thirtyinch-rx2:$thirtyinchVersion"
    implementation "net.grandcentrix.thirtyinch:thirtyinch-logginginterceptor:$thirtyinchVersion"
    implementation "net.grandcentrix.thirtyinch:thirtyinch-kotlin:$thirtyinchVersion"
    implementation "net.grandcentrix.thirtyinch:thirtyinch-kotlin-coroutines:$thirtyinchVersion"
    
    // Legacy dependencies
    implementation "net.grandcentrix.thirtyinch:thirtyinch-rx:$thirtyinchVersion"
}

JCenter (deprecated)

repositories {
    jcenter()
}

dependencies {
    implementation "net.grandcentrix.thirtyinch:thirtyinch:$thirtyinchVersion"
    implementation "net.grandcentrix.thirtyinch:thirtyinch-rx2:$thirtyinchVersion"
    implementation "net.grandcentrix.thirtyinch:thirtyinch-logginginterceptor:$thirtyinchVersion"
    implementation "net.grandcentrix.thirtyinch:thirtyinch-kotlin:$thirtyinchVersion"
    implementation "net.grandcentrix.thirtyinch:thirtyinch-kotlin-coroutines:$thirtyinchVersion"
    
    // Lagacy dependencies
    implementation "net.grandcentrix.thirtyinch:thirtyinch-rx:$thirtyinchVersion"
}

Hello World MVP example with ThirtyInch

HelloWorldActivity.java

public class HelloWorldActivity 
        extends TiActivity<HelloWorldPresenter, HelloWorldView> 
        implements HelloWorldView {

    private TextView mOutput;

    @NonNull
    @Override
    public HelloWorldPresenter providePresenter() {
        return new HelloWorldPresenter();
    }

    @Override
    public void showText(final String text) {
        mOutput.setText(text);
    }

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

        mOutput = (TextView) findViewById(R.id.output);
    }
}

HelloWorldView.java

public interface HelloWorldView extends TiView {

    @CallOnMainThread
    void showText(final String text);
}

HelloWorldPresenter.java

public class HelloWorldPresenter extends TiPresenter<HelloWorldView> {

    @Override    
    protected void onAttachView(@NonNull final HelloWorldView view) {
        super.onAttachView(view);
        view.showText("Hello World!");
    }
}

ThirtyInch features

Presenter

  • survives configuration changes
  • survives when the Activity got killed in background
  • is not a singleton
  • dies when the Activity gets finished
Lifecycle

The TiPresenter lifecycle is very easy.

It can be CREATED and DESTROYED. The corresponding callbacks onCreate() and onDestroy() will be only called once!

The TiView can either be ATTACHED or DETACHED. The corresponding callbacks are onAttachView(TiView) and onDetachView() which maps to onStart() and onStop().

public class MyPresenter extends TiPresenter<MyView> {

    @Override
    protected void onCreate() {
        super.onCreate();
    }

    @Override
    protected void onAttachView(@NonNull final HelloWorldView view) {
        super.onAttachView(view);
    }

    @Override
    protected void onDetachView() {
        super.onDetachView();
    }

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

The lifecycle can be observed using TiLifecycleObserver

There is no callback for onResume() and onPause() in the TiPresenter. This is something the view layer should handle. Read more about this here Hannes Dorfmann - Presenters don't need lifecycle events

Configuration

The default behaviour might not fit your needs. You can disable unwanted features by providing a configuration in the TiPresenter constructor.

public class HelloWorldPresenter extends TiPresenter<HelloWorldView> {

    public static final TiConfiguration PRESENTER_CONFIG = 
            new TiConfiguration.Builder()
                .setRetainPresenterEnabled(true) 
                .setCallOnMainThreadInterceptorEnabled(true)
                .setDistinctUntilChangedInterceptorEnabled(true)
                .build();
            
    public HelloWorldPresenter() {
        super(PRESENTER_CONFIG);
    }
}

Or globally for all TiPresenters

public class MyApplication extends Application {

    @Override
    public void onCreate() {
        super.onCreate();
        TiPresenter.setDefaultConfig(MY_DEFAULT_CONFIG);
    }
}

TiView Annotations

Two awesome annotations for the TiView interface made it already into Ti saving you a lot of time.

public interface HelloWorldView extends TiView {

    @CallOnMainThread
    @DistinctUntilChanged
    void showText(final String text);
}
@CallOnMainThread

Whenever you call this method it will be called on the Android main thread. This allows to run code off the main thread but send events to the UI without dealing with Handlers and Loopers.

Requires to be a void method. Works only for TiView interfaces implemented by "Android Views" (TiActivity, TiFragment).

Enabled by default, can be disabled with the TiConfiguration

@DistinctUntilChanged

When calling this method the View receives no duplicated calls. The View swallows the second call when a method gets called with the same (hashcode) parameters twice.

Usecase: The Presenter binds a huge list to the View. The app loses focus (onDetachView()) and the exact same Activity instance gains focus again (onAttachView(view)). The Activity still shows the huge list. The Presenter binds the huge list again to the View. When the data has changed the list will be updated. When the data hasn't changed the call gets swallowed and prevents flickering.

Requires to be a void method and has at least one parameter.

Enabled by default, can be disabled with the TiConfiguration

View binding interceptors

View Annotations only work because ThirtyInch supports interceptors. Add interceptors (BindViewInterceptor) to TiActivity or TiFragment to intercept the binding process from TiView to TiPresenter. Interceptors are public API waiting for other great ideas.

public class HelloWorldActivity extends TiActivity<HelloWorldPresenter, HelloWorldView>
        implements HelloWorldView {

    public HelloWorldActivity() {
        addBindViewInterceptor(new LoggingInterceptor());
    }
}

LoggingInterceptor is available as module and logs all calls to the view.

Kotlin

Using Kotlin these days is a no-brainer. ThirtyInch provides some extension methods to improve itself even further!

SendToView

When using sendToView, repeating it.* inside the lambda is quite annoying. It's clear that the methods are called on the view. With the kotlin extension deliverToView the TiView will be give over to the lambda as this.

class HelloWorldPresenter : TiPresenter<HelloWorldView> {

  override fun onCreate() {
      // normal java API
      sendToView {
          it.showText("Hello World")
      }
      
      // kotlin extension
      deliverToView {
          showText("Hello World")
      }
  }
}

interface HelloWorldView : TiView {
    fun showText(text: String)
}

Back in the Java days we had to use it inside the sendToView-lambda.

Coroutines

If you're using Kotlin's Coroutines we offer a CoroutineScope that scopes to a presenter's lifecycle.

class HelloWorldPresenter : TiPresenter<HelloWorldView> {

  private val scope = TiCoroutineScope(this, Dispatchers.Default)

  override fun onCreate() {
      scope.launch { ... }
  }
}

The created Job will automatically be cancelled when the presenter is destroyed.

Alternatively, you can launch jobs that get cancelled when a TiView detaches:

class HelloWorldPresenter : TiPresenter<HelloWorldView> {

  private val scope = TiCoroutineScope(this, Dispatchers.Default)

  override fun onAttachView(view: HelloWorldView) {
      scope.launchUntilViewDetaches { ... }
  }
}

However, be careful that launchUntilViewDetaches can only be called when there is a view attached!

Using RxJava for networking is very often used. Observing a Model is another good usecase where Rx can be used inside of a TiPresenter. The Rx package provides helper classes to deal with Subscription or wait for an attached TiView.

public class HelloWorldPresenter extends TiPresenter<HelloWorldView> {

    // add the subscription helper to your presenter
    private RxTiPresenterSubscriptionHandler rxHelper = new RxTiPresenterSubscriptionHandler(this);

    @Override
    protected void onCreate() {
        super.onCreate();
        
        // automatically unsubscribe in onDestroy()
        rxHelper.manageSubscription(
                Observable.interval(0, 1, TimeUnit.SECONDS)
                    // cache the latest value when no view is attached
                    // emits when the view got attached
                    .compose(RxTiPresenterUtils.<Long>deliverLatestToView(this))
                    .subscribe(uptime -> getView().showPresenterUpTime(uptime))
        );
    }

    @Override
    protected void onAttachView(@NonNull final HelloWorldView view) {
        super.onAttachView(view);
        
        // automatically unsubscribe in onDetachView(view)
        rxHelper.manageViewSubscription(anotherObservable.subscribe());
    }
}

You can make Disposable handling even less intrusive in Kotlin. Just create the following interface and make your presenters implement it:

interface DisposableHandler {

    // Initialize with reference to your TiPresenter instance
    val disposableHandler: RxTiPresenterDisposableHandler

    // Dispose of Disposables dependent on the TiPresenter lifecycle
    fun Disposable.disposeWhenDestroyed(): Disposable = disposableHandler.manageDisposable(this)

    // Dispose of Disposables dependent on the TiView attached/detached state
    fun Disposable.disposeWhenViewDetached(): Disposable = disposableHandler.manageViewDisposable(this)
} 

Then just implement the interface in your presenter and you can use created extension functions to manage Disposables:

class MyPresenter : TiPresenter<MyView>(), DisposableHandler {

    override val disposableHandler = RxTiPresenterDisposableHandler(this)

    override fun onCreate() {
        super.onCreate()

        // Presenter lifecycle dependent Disposable
        myObservable
            .subscribe()
            .disposeWhenDestroyed()
    }

    override fun onAttachView(view: MyView) {
        super.onAttachView(view)

        // View attached/detached dependent Disposable
        myViewObservable
            .subscribe()
            .disposeWhenViewDetached()
    }
}

License

Copyright 2016 grandcentrix GmbH

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.

thirtyinch's People

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

thirtyinch's Issues

How to use sendToView ?

I'm trying to call a method in my View through sendToView. But it is not working ?
I have tried using getView. It returns NPE because i'm calling the getProductsByCat after the Presenter onAttach

public void getProductsByCat(String Category) {
//        final BrowseView view = getView();
        this.sendToView(BrowseView::showProgress);

        rxHelper.manageSubscription(
                RxFirebaseDatabase.observeValueEvent(mRef,
                        dataSnapshot -> {
                            List<Product> mProd = new ArrayList<>();
                            for (DataSnapshot postSnapshot : dataSnapshot.getChildren()) {
                                Timber.d("Count >>" + postSnapshot.getChildrenCount());
                                Product Prod = Product.create(postSnapshot);
                                mProd.add(Prod);
                            }
                            return mProd;
                        })
                        .subscribe(mProd -> {
                            Timber.d(mProd.get(0).productName());
                            sendToView(view -> {
                                view.hideProgress();
                                view.showText("Hello");
                                view.updateItems(mProd);
                            });
                        })
        );
    }

Presenter not destroyed when Fragment is popped off the backstack

When the back key is pressed to pop a TiFragment off the backstack, the Presenter does not get destroyed. Here is the log output from TiFragment:

11-02 09:17:04.985 V: onStop()
11-02 09:17:04.985 V: onDestroyView()
11-02 09:17:04.986 V: onDestroy() recreating=true
11-02 09:17:04.986 V: onDetach()

Because the activity is not being destroyed, the destroyPresenter flag in TiFragment#onDestroy() does not get set to true. This means that the Presenter is leaked.

rx.Single support for RxTiPresenterUtils

Most of my data is returned as Singles instead of Observables, so having Single support built-in for RxTiPresenterUtils would be great.

I can of course work around this limitation by simply doing this:

categoryService.getMainCategories()
    .toObservable()
    .compose(RxTiPresenterUtils.deliverLatestToView(this))
    .subscribe(categories -> ()));

IllegalStateException: The view is currently not attached. Caused by click listeners

We have in our crashlytics some NPE crashes caused by onClickListeners in ViewHolders and Fragments. They ain't many but still occurs. Do you have any idea why?

Fatal Exception: java.lang.IllegalStateException: The view is currently not attached. Use 'sendToView(ViewAction)' instead.
       at net.grandcentrix.thirtyinch.f.Q(TiPresenter.java:334)
       at classifieds.yalla.features.chats.chat.regular.i.p(ChatPresenter.java:207)
       at classifieds.yalla.features.chats.chat.regular.ChatFragment.b(ChatFragment.java:109)
       at classifieds.yalla.features.chats.chat.regular.a.onClick(Unknown Source)
       at android.view.View.performClick(View.java:4475)
       at android.view.View$PerformClick.run(View.java:18786)
       at android.os.Handler.handleCallback(Handler.java:730)
       at android.os.Handler.dispatchMessage(Handler.java:92)
       at android.os.Looper.loop(Looper.java:176)
       at android.app.ActivityThread.main(ActivityThread.java:5419)
       at java.lang.reflect.Method.invokeNative(Method.java)
       at java.lang.reflect.Method.invoke(Method.java:525)
       at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1046)
       at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:862)
       at dalvik.system.NativeStart.main(NativeStart.java)

Merge thirtyinch-test in thirtyinch

Currently we have a extra thirtyinch-test dependency which includes just a single file.
Because it is way more overhead to maintain these extra package we agreed that we want to merge it into the "main" thirtyinch module.

We want to have a similar idea like RxJava in tests.
Just calling:

val presenterInstructor = MyPresenterImpl().test()

Currently we are not really sure if we really want to return the PresenterInstructor or another (helper) object which hold the real presenter and the instructor...
Maybe later we want to add more functionality to it which can nicely delegate the "actions" (or maybe asserts πŸ€” ) to the correct object...

Get rid of instrumentation tests

The android instrumentation tests are slow, and starting the emulator takes ages. (reference #116)
Also the environment is flaky. Builds continue to fail because the emulator startup fails or the apk cannot be installed.

We only have a few instrumentation tests. Maybe we can convert them to unit tests (with some effort). Also we can get rid of the CompositeAndroid instrumentation tests once we get rid of this module #109.

[Question] Where is the best place to start loading data and persist it?

I am currently investigating different MVxx libraries and approaches on Android and stumbled upon this one. As a show case for each library I am creating simple list+detail (image gallery) app using REST services as data provider.

Lets assume we are using one Activity (MainActivity) and two fragments (GalleryFragment, DetailsFragment) in our app. Implementation of activity is quite straightforward as it requires no view or presenter - is simply starts and manages fragments.

Now, we need to implement GalleryFragment which needs to display a list of images.

So we need a view:

public interface GalleryView extends TiView {
    void startLoading(boolean pullToRefresh);
    void stopLoading();
    void showError(Throwable error);
    void showGallery(List<Image> images);
}

We need layout for fragment:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_marginLeft="16dp"
    android:layout_marginRight="16dp">

    <ProgressBar
        android:id="@+id/progress"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:indeterminate="true"/>

    <android.support.v4.widget.SwipeRefreshLayout
        android:id="@+id/contentView"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <android.support.v7.widget.RecyclerView
            android:id="@+id/recyclerView"
            android:name="pl.fzymek.tiimagegallery.gallery.GalleryFragment"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            tools:context="pl.fzymek.tiimagegallery.gallery.GalleryFragment"
            tools:listitem="@layout/fragment_gallery_item"/>


    </android.support.v4.widget.SwipeRefreshLayout>

    <TextView
        tools:text="Error happened! :("
        android:id="@+id/error"
        android:layout_gravity="center"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>

</FrameLayout>

And we need a Fragment:

public class GalleryFragment extends TiFragment<GalleryPresenter, GalleryView> implements GalleryView, SwipeRefreshLayout.OnRefreshListener {

    @BindView(R.id.recyclerView)
    RecyclerView recyclerView;
    @BindView(R.id.progress)
    ProgressBar progressBar;
    @BindView(R.id.error)
    TextView error;
    @BindView(R.id.contentView)
    SwipeRefreshLayout contentView;

    GalleryAdapter adapter;
    Unbinder unbinder;

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        return inflater.inflate(R.layout.fragment_gallery, container, false);
    }

    @Override
    public void onViewCreated(View view, Bundle savedInstanceState) {
        unbinder = ButterKnife.bind(this, view);
        contentView.setOnRefreshListener(this);

        Context context = view.getContext();
        if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT) {
            recyclerView.setLayoutManager(new LinearLayoutManager(context));
        } else {
            recyclerView.setLayoutManager(new GridLayoutManager(context, 3));
        }
        adapter = new GalleryAdapter();
        recyclerView.addItemDecoration(new SpaceDecoration());
        recyclerView.setAdapter(adapter);
    }

    @Override
    public void onDestroyView() {
        super.onDestroyView();
        unbinder.unbind();
    }

    @NonNull
    @Override
    public GalleryPresenter providePresenter() {
        Timber.d("providePresenter");
        return new GalleryPresenter();
    }

    @Override
    public void startLoading(boolean pullToRefresh) {
        progressBar.setVisibility(View.VISIBLE);
        contentView.setVisibility(View.GONE);
        error.setVisibility(View.GONE);
        contentView.setRefreshing(pullToRefresh);
    }

    @Override
    public void stopLoading() {
        contentView.setVisibility(View.VISIBLE);
        progressBar.setVisibility(View.GONE);
        error.setVisibility(View.GONE);
        contentView.setRefreshing(false);
    }

    @Override
    public void showError(Throwable err) {
        error.setVisibility(View.VISIBLE);
        contentView.setVisibility(View.GONE);
        progressBar.setVisibility(View.GONE);
        contentView.setRefreshing(false);
    }

    @Override
    public void showGallery(List<Image> images) {
        adapter.setData(images);
    }

    @Override
    public void onRefresh() {
        loadData(true);
    }

    private void loadData(boolean pullToRefresh) {
        getPresenter().loadData(pullToRefresh);
    }
}

And finally, we also have our presenter:

class GalleryPresenter extends TiPresenter<GalleryView> {

    private final static TiConfiguration PRESENTER_CONFIGURATION = new TiConfiguration.Builder()
            .setRetainPresenterEnabled(true)
            .build();

    private GettyImagesService service;

    GalleryPresenter() {
        super(PRESENTER_CONFIGURATION);
        Retrofit retrofit = new Retrofit.Builder()
                .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
                .addConverterFactory(GsonConverterFactory.create())
                .baseUrl(Config.GETTYIMAGES_API_URL)
                .build();


        service = retrofit.create(GettyImagesService.class);
    }

    @Override
    protected void onCreate() {
        super.onCreate();
        Timber.d("onCreate");
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        Timber.d("onDestroy");
    }

    @Override
    protected void onSleep() {
        super.onSleep();
        Timber.d("onSleep");
    }

    @Override
    protected void onWakeUp() {
        super.onWakeUp();
        Timber.d("onWakeUp");
    }

    public void loadData(String phrase, boolean pullToRefresh) {
        getView().startLoading(pullToRefresh);

        Timber.d("loadData %s", phrase);

        service.getImages(phrase)
                .subscribeOn(Schedulers.newThread())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(
                        result -> {
                            getView().showGallery(result.getImages());
                            getView().stopLoading();
                        },
                         error -> {
                             getView().showError(error);
                         }
                );
    }
}

Simple as that.

What I do not understand, is when should I call loadData() to trigger a network request, and how and when to persist retrieved data between screen orientation changes.

Usually we'd start network request somewhere in onViewCreated() or onCreateView method of fragment as in that moment we have our view ready. Unfortunately, when using ThirtyInch, this in not true, as view is binded to presenter in onStart() method and calling GalleryPresenter#loadData() throws NPE as getView() returns null.

Solution to this would be to defer it a little and load data in onStart() method of fragment or onWakeUp() method of presenter (as presented in sample, but when using onWakeUp() I cannot pass my constructed query to the presenter). This works like a charm, but...

Since I am retaining a presenter, I see a problem with this solution when rotating a screen while network request is pending. Flow is in this case:

  1. Fragment/View and Presenter gets created
  2. Fragment is started (onStart) and Presenter is woken up onWakeUp -> trigger request 1
  3. Screen gets rotated
  4. Fragment/View is recreated but Presenter is retained (ok)
  5. Fragment is started (onStart) and Presenter is woken up onWakeUp -> trigger request 2
  6. Request 1 finishes -> view shows data 1
  7. Rotate screen again
  8. We go back here to recreating view, retaining presenter and triggering another request(no 3)
  9. Now we have view displaying progress, although data was fetched (request 1) and displayed for a while and 2 pending requests which will trigger new view updates

What I don't understand here is, how to correctly request data and persist it using ThirtyInch - is the persistence handled by framework (setRetainPresenterEnabled(true) ?) or should I rely on androids onSaveInstanceState(). Not retaining a presenter would leave me nothing more that Mosby (ad described here). So what should I do in order to use all features of TI?

README improvements suggestion

Hi TI team, while browsing through your README after you got mentioned in Android Dev Weekly, I would like to suggest a few improvements :

  • composition should be favored over inheritance, that should be the primary use case.
  • we don't care much about dependencies, put this last.
  • compare to other similar frameworks.
  • link to a few MVP/MVVM resources.

Presenters are leaking if do FragmentManager.popBackStack()

First of all thank you for a library we are using it for all our presenters and fragments, around 50.

Recently we came across very strange behavior we have an activity with a chain of 10 fragments and when user reach the last one we are doing

getSupportFragmentManager().popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE)

then we create a different fragment. All these fragments are related to one scope which we keep inside activity and after the user leaves activity component is destroyed.

What we noticed that after one such scenario we are leaking 10 presenters. And if the user does it a couple of times we will have 20 leaked presenters.

Moreover, we investigated and found that we are using popBackStack very actively throughout our app, that mean we have many presenters which are leaking.

How can we fix it?

Initializing presenter state from intent

If I receive an intent in my activity that should initialize it's state, how do I pass this to my presenter? Shouldn't onCreate in the presenter receive this intent? Or maybe there's a better way?

By the way, thanks for the great library, I've found it the best one I've tried (out of a whole bunch..).

Use assertJ everywhere

Currently we have "hundreds" of different "test matchers".

  • The "build in" one assertEquals("ThirtyInch", "ThirtyInch")
  • Hamcrest assertThat("ThirtyInch", isEqualTo("ThirtyInch"))
  • assertJ assertThat("ThirtyInch").isEqualTo("ThirtyInch")

Each test look different. Which is bad.

We should use assertJ everywhere and get rid of the other matcher libs.
To prevent using Hamcrest in the future it would be great if we can exclude it from... Don't know which dependency has hamcrest as dependency. But we have to found out it and exclude it form them if possible.

Missing tests for TiTestPresenter

TiTestPresenter misses a few unit tests which verify that the lifecycle will be called correctly.

  • create only once,
  • automatic detach when attaching a new view
  • destory automatically detaches
  • ... other uncovered lines

Add clear to Rx*Handler

In the real world it could be happen that we want to clear all the subscriptions / disposables ,which are already added to the handler via manage*, earlier than the handler does .

So it would be great if you can provide a clear method to the Rx*Handler πŸ‘

@DistinctUntilChanged doesn't clear cache in TiFragment after orientation change

Reproduce:

  1. call showGallery(new ArrayList()) TiFragment implementing this view interface method
    @DistinctUntilChanged
    void showGallery(List<Image> images);
  1. make orientation change
  2. call view again with same argument (new ArrayList())

Expected behavior:
View method gets called

Actual behavior:
View method doesn't get called because it's the same data. This is wrong because onCreateView() was called and a new view implementation was created.

Workaround:

clear the cache manually in onCreateView()

final DistinctUntilChangedInterceptor distinctUntilChangedInterceptor = (DistinctUntilChangedInterceptor)
                getInterceptors(interceptor -> interceptor instanceof DistinctUntilChangedInterceptor).get(0);
        distinctUntilChangedInterceptor.clearCache(this);

Use DialogFragment?

How can I go about using this Library with a DialogFragment? Since we have to extend TiFragment I can't extend DialogFragment.

Possibly a dumb question.

Put codestyle into ThirtyInch

Since we open sourced our code style I think about to add it here.
We can add it together with a CONTRIBUTING.md ( πŸ€” ) to Ti. We won't have conflicts or "rerange the code everytime" if third parties contribute anymore...

What do you think @passsy @rberghegger ❓

Presenter is being killed most of the time

Hello Pascal, I have a problem and I need to understand what could cause it, so basically I started FastHub with version rc2 & then I noticed that you have updated the lib, since I like to be updated about everything I did actually update to rc3 however I noticed that whenever the activity goes in background for awhile not to long a minute or less the data is being saved in the presenter are lost which cause the app to somehow lose an important flag or an id. I couldn't really know what is going until i reverted back to rc2 and everything seems to work fine except now the data will live for a bit longer than the rc3 . I know it seem confusing but I couldn't really explained more than that.
Could you let me know what could cause this? is it a memory problem maybe with my phone or there is something I'm doing wrong.

fyi: I'm using the default configuration which mean I didn't touch the presenter nor the view configuration.

Thank you in advance & I hope to see the library continue it's awesome work.

LeakCanary pointing on net.grandcentrix.thirtyinch.internal.PresenterSavior.INSTANCE

We got several cases when LeakCanary pointing on net.grandcentrix.thirtyinch.internal.PresenterSavior.INSTANCE. Do you have any suggestions where we should look in?

* features.profile.my.sold.MySoldAdsFragment has leaked:
* GC ROOT static net.grandcentrix.thirtyinch.internal.PresenterSavior.INSTANCE
* references net.grandcentrix.thirtyinch.internal.PresenterSavior.mPresenters
* references java.util.HashMap.table
* references array java.util.HashMap$HashMapEntry[].[13]
* references java.util.HashMap$HashMapEntry.value
* references features.profile.my.favorites.MyFavoritesAdsPresenter.mView
* references features.profile.my.favorites.MyFavoritesAdsFragment.mFragmentManager
* references android.support.v4.app.FragmentManagerImpl.mActive
* references java.util.ArrayList.elementData
* references array java.lang.Object[].[2]
* leaks features.profile.my.sold.MySoldAdsFragment instance
* features.chats.list.ChatListFragment has leaked:
* GC ROOT static net.grandcentrix.thirtyinch.internal.PresenterSavior.INSTANCE
* references net.grandcentrix.thirtyinch.internal.PresenterSavior.mPresenters
* references java.util.HashMap.table
* references array java.util.HashMap$HashMapEntry[].[7]
* references java.util.HashMap$HashMapEntry.value
* references features.chats.list.ChatListPresenter.mView
* leaks features.chats.list.ChatListFragment instance

deliverLatestCacheToView delivers all previously emitted items

RxTiPresenterUtils.deliverLatestCacheToView caches and re-delivers all items emitted by the source.

Look at this log output. When the view is detached, several items are emitted by the source, but not delivered. This is correct so far.

After the view is reattached, all of the items are delivered again, including those which had already been delivered (c9915aa) and those which should have been dropped (6b60904).

11-02 12:45:45.392 D: Emitting c9915aa
11-02 12:45:45.421 I: Delivering c9915aa
11-02 12:46:02.199 D: Emitting 75d8118
11-02 12:46:02.210 I: Delivering 75d8118
11-02 12:46:02.211 D: Emitting a0e7956
11-02 12:46:02.255 I: Delivering a0e7956

VIEW DETACHED

11-02 12:46:44.932 D: Emitting 6b60904
11-02 12:46:45.052 D: Emitting 27a3122
11-02 12:46:45.668 D: Emitting 50cb170
11-02 12:46:45.682 D: Emitting 1ae3d6e
11-02 12:46:46.478 D: Emitting efbdc0f
11-02 12:46:46.493 D: Emitting b085ea5

VIEW REATTACHED

11-02 12:47:09.806 I: Delivering c9915aa
11-02 12:47:09.807 I: Delivering 75d8118
11-02 12:47:09.807 I: Delivering a0e7956
11-02 12:47:09.808 I: Delivering 6b60904
11-02 12:47:09.809 I: Delivering 27a3122
11-02 12:47:09.809 I: Delivering 50cb170
11-02 12:47:09.810 I: Delivering 1ae3d6e
11-02 12:47:09.811 I: Delivering efbdc0f
11-02 12:47:09.812 I: Delivering b085ea5
11-02 12:47:15.458 D: Emitting 4cdffd
11-02 12:47:15.459 I: Delivering 4cdffd

Here is the code:

val subscription =
        Observable.just(/* some object */)
                .doOnNext { c -> Timber.d("Emitting %x".format(c.hashCode())) }
                .delaySubscription(5, TimeUnit.SECONDS)
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .compose(RxTiPresenterUtils.deliverLatestCacheToView<Object>(this))
                .subscribe { c ->
                    Timber.i("Delivering %x".format(c.hashCode()))
                    view.showData(c)
                }
rxHelper.manageSubscription(subscription)

Is this the expected behaviour?

Listener keeps on called forever

I have an Listener on my presenter. But it is getting called forever.
If I use the same code in a Activity without Ti . Then it is working as expected.

How can i Solve this ?

public class MyPresenter extends TiPresenter<MyView> {

    public MyPresenter(String email, String from) {
        Email = email;
        From = from;
    }

    @Override
    protected void onAttachView(@NonNull MyView view) {
        super.onAttachView(view);
        listenChanges();
    }

//    @DistinctUntilChanged  Added this annotation to stop the continuus triggering . But no hope
    public void listenChanges() {
       DatabaseReference myRef = DB.child(ID);

        ChildEventListener myListener = cartRef.addChildEventListener(new ChildEventListener() {
            @Override
            public void onChildAdded(DataSnapshot dataSnapshot, String s) {
                Log.e("onChildAdded > ",  dataSnapshot.getKey());
            }

            @Override
            public void onChildChanged(DataSnapshot dataSnapshot, String s) {
                 Log.e("onChildChanged > ", dataSnapshot.getKey());
            }

            @Override
            public void onChildRemoved(DataSnapshot dataSnapshot) {
                 Log.e("onChildRemoved >", dataSnapshot.getKey());
            }

            @Override
            public void onChildMoved(DataSnapshot dataSnapshot, String s) {
            }

            @Override
            public void onCancelled(DatabaseError databaseError) {
            }
        });
        myRef .addChildEventListener(myListener);
        );
    }
}

Activity (which doesn't involve Ti. But works Perfectly)

public class TestActivity extends AppCompatActivity {

 @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
       DatabaseReference myRef = DB.child(ID);

        ChildEventListener myListener = cartRef.addChildEventListener(new ChildEventListener() {
            @Override
            public void onChildAdded(DataSnapshot dataSnapshot, String s) {
                Log.e("onChildAdded > ",  dataSnapshot.getKey());
            }

            @Override
            public void onChildChanged(DataSnapshot dataSnapshot, String s) {
                 Log.e("onChildChanged > ", dataSnapshot.getKey());
            }

            @Override
            public void onChildRemoved(DataSnapshot dataSnapshot) {
                 Log.e("onChildRemoved >", dataSnapshot.getKey());
            }

            @Override
            public void onChildMoved(DataSnapshot dataSnapshot, String s) {
            }

            @Override
            public void onCancelled(DatabaseError databaseError) {
            }
        });

        myRef.addChildEventListener(myListener);
    }

LOG CAT

D: onChildChanged > c02p01
D: onChildChanged > c02p01
D: onChildChanged > c02p01
D: onChildChanged > c02p01
D: onChildChanged > c02p01
D: onChildChanged > c02p01
D: onChildChanged > c02p01
D: onChildChanged > c02p01
D: onChildChanged > c02p01
D: onChildChanged > c02p01
D: onChildChanged > c02p01
D: onChildChanged > c02p01
D: onChildChanged > c02p01
D: onChildChanged > c02p01
D: onChildChanged > c02p01
D: onChildChanged > c02p01

Behaviours in RxUtils(Test)

While I'm working on #54 I've found some strange stuff.

  1. The test testManageViewSubscription_WithDetachSingleSub_ShouldUnsubscribe inside RxTiPresenterSubscriptionHandlerTest is equal to testManageViewSubscription_WithDetachView_ShouldUnsubscribe. So I guess we can remove it?!
    Probably I've copied the test code to the rx2 module which can be removed ❌ as well

  2. testManageViewDisposable_DetachBeforeAttach_ShouldThrowAssertError in RxiTiPresenterDisposableHandlerTest (and maybe in RxTiPresenterSubscriptionHandlerTest too) will - hopefully - never happen.
    Is it currently possible to detach a view without attaching? πŸ€”

  3. Should we move the TiMockPresenter inside the test module? πŸ€”

Presenters cannot restore instance state after process has died

It looks like there's no good way to restore the instance state of a Presenter in ThirtyInch when the process is killed and restarted.

The PresenterSavior handles the case when the process is NOT killed by essentially holding a static map of Presenters, but this map will get cleared when the process dies and the Activity gets restored.

This can be reproduced by running the sample code and taking the following steps:

  1. Go to Developer Options and turn on Don't Keep activities and set Background Process limit to "No background processes"
  2. Install the provided sample apk from https://github.com/grandcentrix/ThirtyInch/tree/v0.7.1/sample
  3. Open the app
  4. Press Home
  5. Navigate back to the app

After following the steps you can see in the logs

11-13 05:49:47.407 909 909 I ThirtyInch: HelloWorldActivity:TiActivity@9cbcc1d: could not recover the Presenter although it's not the first start of the Activity. This is normal when configured as .setRetainPresenterEnabled(false).

As far as I understand setRetainPresenterEnabled is true by default and no one sets it in the sample code.

Is this an oversight or is there a recommended way to take care of this case?

new Observer API

Our TiLifecycleObserver API is great an maybe the best feature of ThirtyInch. It allows everyone to add functionality to TiPresenters. We use it for our rx implementation and the proposed Renderer uses it, too.

Implementing a TiLifecycleObserver is hard because the callbacks has a state and a boolean indicating if the method was called before or after the state callback was called. Worst case scenario: You initialize things twice but only expected the initialization once.

Right now it is required to add a TiLifecycleObserver right after initializing the TiPresenter. If you add it later you won't receive previous emitted events. The TiLifecycleObserver also never receives the created event because there is no way to add the observer before getting it.

And without passing in the Presenter in the constructor it is impossible to know the current presenter state.

Also missing is a callback when the observer is added and removed from a presenter. Cleanup is currently hard/impossible.

TODOs

  • We should change the API in a way observers receive distinct events for pre- and post lifecycle callbacks.
  • When a observer is added it should receive all previous events to the current lifecycle state as if it was added in the beginning.
  • Add events when the observer was added to a presenter or got removed to cleanup resources
  • add getPresenter() to get the current presenter the observer is attached to and it's state
  • like now, call destructing events in reversed order to the constructing events

Use of compose(RxTiPresenterUtils.deliverLatestCacheToView(this)) creates too many instances of OperatorSemaphore

When using your lib, I noticed weird side-effect, when using

compose(RxTiPresenterUtils.deliverLatestCacheToView(this))

it creates too many instances of OperatorSemaphore. After config changes and moving between screens with "Don't Keep Activities" flag. I tried to trigger GC, but it didn't help.

screenshot from 2016-10-26 15-56-24

On the opposite side using

compose(RxTiPresenterUtils.deliverToView(this))

worked just fine. Firstly, it didn't create so many instances, secondly, after triggering GC, the existing instances of OperatorSemaphore was destroyed.

I did look into the internals of caching version it seemed to me that after presenter is destroyed lifecycle observers should be removed therefore triggering GC should clear OperatorSemaphore instances.

Can you better explain how it should work, and when GC should clear out those instances.

Couple of unrelated questions

  1. Should deliverLatestCacheToView propagate cached Error?
  2. Any actions with view in doOnSubscribe/doOnTerminate will crash the app when user will leave and go back to view during network request. I expect isViewReady(presenter) will emit items and deliver cache only when view fully attached.

Full code

  @Override protected void onWakeUp() {
    super.onWakeUp();

    loadChats();
  }

  public void loadChats() {
    rxSubscriptionHelper.manageSubscription(
        chatOperations.getChats(chatType)
            .observeOn(AndroidSchedulers.mainThread())
            .doOnSubscribe(() -> {
              if (getView() != null) {
                getView().showRefreshing();
              }
            })
            .doOnTerminate(() -> {
              if (getView() != null) {
                getView().stopRefresginAndHideMsgs();
              }
            })

            .compose(RxTiPresenterUtils.deliverLatestCacheToView(this))
            .subscribe(chats -> {
              getView().setChats(chats);
            }, e -> {
              if (e instanceof RxNetworkException) {
                getView().stopRefreshAndShowError(e.getMessage());
              } else {
                throw new RuntimeException(e);
              }
            })
    );
  }

Isn't this backwards?

I'm relatively new to MVP and the like--but with Mosby, it looks like I'd make a Presenter as an interface and fill it with methods I want to implement.

With Ti, I'm implementing those methods in the Activity.

Am I thinking about this wrong or am I doing something wrong?

User `addAll` instead of using an fori

Currently we are using an fori loop to add subscriptions to the CompositeSubscprion handler.
See here.

I found out that the CompositeSubscrption (and the RxJava2 part) have a addAll(...) method.
Think about to use these one instead of our own fori loop

How to pass SharedPreferences through CompositeActivity?

I need pass some data from SharedPrefrences to Presenter through addPlugin(). I have activity class like this, but i got npe error:

public class AddedBooksActivity extends  CompositeActivity implements AddedBooksView {
private final NavigationDrawerActivityPlugin mNavigationDrawerPlugin = new NavigationDrawerActivityPlugin();
private final TiActivityPlugin<AddedBooksPresenter, AddedBooksView> mPresenterPlugin =
             new TiActivityPlugin<>(new TiPresenterProvider<AddedBooksPresenter>() {
                  @NonNull
                  @Override
                  public AddedBooksPresenter providePresenter() {
                      String appKey = App.readFromPreferences(getApplicationContext(), 
                       "user_account_key", "");
                      Log.i("user_account_key", appKey);
                      addedBooksPresenter = new AddedBooksPresenter(appKey);
                      return addedBooksPresenter;
                 }
             });

      public AddedBooksActivity() {
          addPlugin(mPresenterPlugin);
          addPlugin(mNavigationDrawerPlugin);
    }
}

While I'm not using CompositeActivity and I pass SharedPreferences through providePresenter() everythings works fine.

    @NonNull
    @Override
    public AddedBooksPresenter providePresenter() {
        String appKey = App.readFromPreferences(this, "user_account_key", "");
        Log.i("app", appKey);
        addedBooksPresenter = new AddedBooksPresenter(appKey);
        return addedBooksPresenter;
    }

Can you help me with this problem?

How to free presenter manually / Nested fragments/presenter don't get destroyed

I have a situation when user switch fragments

  1. A -> B which contains nested fragment C
  2. user goes back from fragment B -> A, but C presenter retained in memory.

It happens many times and creates serious memory footprint. After 20 times or so the app may even freeze.

Basically when there is a check in onDestroy() isFragmentInBackstack() = false, but isFragmentRemoving() = false in nested fragment and nothing get cleared

How to free presenter manually in such case?

How to deal with Viewpager?

I use a viewpager
a) to show data in RecyclerViews
b) to create a new object
and I'm interested how to use it with your lib.

Let manageSubscription(Subscription) return Subscription

RxTiPresenterSubscriptionHandler.manageSubscription(Subscription...) returns void. It would be cool if there is a manageSubscription function with the following signature:

public Subscription manageViewSubscription(@NonNull final Subscription subscription)

Usage

// inside TiPresenter

val rxHelper: RxTiPresenterSubscriptionHandler(this)
val sub: Subscription? = null

/**
 * download some data
 */
fun download() {
    // subscription is managed, it will be cancelled when the Presenter gets destroyed
    sub = rxHelper.manageSubscription(myService.downloadSomething().subscribe{ data -> 
        sendToView { view -> view.showData(data) }
    })
}

/**
 * cancel download before it completes
 */
fun cancelDownload() {
    sub?.unsubscribe()
}

java.lang.NullPointerException: Attempt to invoke interface method

as per the title, calling getPresenter() on Activity onCreate causes this crash for example

@Override protected void onCreate(Bundle savedInstanceState) {
   super.onCreate(savedInstanceState);
   if (savedInstanceState == null) {
      getPresenter().onSubmit(getIntent());
   }
}
if (intent == null || intent.getExtras() == null) {
    getView().onShowError(R.string.general_error);
}

any idea why would this happen? why wouldn't the view being initialised onCreate?

How to handle onNewIntent()

I got a null view in onWakeUp(), if so, where can I yes be assured to have a view ready, isn't that the whole concept of having onWakeUp()?

ThirtyInch Lint Checks

ThirtyInch has quickly become my favourite MVP implementation. Still, and I guess it's mostly due to my forgetful mind, nearly every time I implement a new Ti-based Activity, connect everything & run, I encounter the dreaded error I've grown so accustomed to:
java.lang.IllegalArgumentException: This Activity doesn't implement a TiView interface. This is the default behaviour. Override provideView() to explicitly change this.

I'd like to propose some custom Lint checks that facilitate the daily workflow with ThirtyInch. I'm unsure how feasible this would be, especially since you allow this default behaviour to be changed on a per-Activity basis. The error would ideally be triggered on TiActivity & CompositeActivity sub-classes with the TiActivityPlugin applied, if no TiView interface is implemented.

Again, I'm probably the only one oblivious enough to always forget this step, and maybe the effort outweighs the benefit by a long shot.

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.