Need help with ThirtyInch?
Click the “chat” button below for chat support from the developer who created it, or find similar developers for support.

About the developer

GCX-HCI
1.1K Stars 103 Forks Apache License 2.0 702 Commits 34 Opened issues

Description

a MVP library for Android favoring a stateful Presenter

Services available

!
?

Need anything else?

Contributors list

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
```java public class HelloWorldActivity extends TiActivity 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
```java public interface HelloWorldView extends TiView {
@CallOnMainThread
void showText(final String text);

}

`HelloWorldPresenter.java`
```java
public class HelloWorldPresenter extends TiPresenter {

@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 {

@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 {

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
```java 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
        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
. ```kotlin class HelloWorldPresenter : TiPresenter {

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 {

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: ```kotlin class HelloWorldPresenter : TiPresenter {

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!

RxJava

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 {

// 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 -&gt; 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

Disposable
s:
class MyPresenter : TiPresenter(), 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.

We use cookies. If you continue to browse the site, you agree to the use of cookies. For more information on our use of cookies please see our Privacy Policy.