{"id":15132021,"url":"https://github.com/gcx-hci/thirtyinch","last_synced_at":"2025-09-28T23:32:29.648Z","repository":{"id":90540410,"uuid":"66281816","full_name":"GCX-HCI/ThirtyInch","owner":"GCX-HCI","description":"a MVP library for Android favoring a stateful Presenter","archived":true,"fork":false,"pushed_at":"2021-03-16T13:38:42.000Z","size":1478,"stargazers_count":1032,"open_issues_count":34,"forks_count":101,"subscribers_count":47,"default_branch":"master","last_synced_at":"2024-09-27T04:01:59.663Z","etag":null,"topics":["activity","android","architecture","fragment","library","mvp","mvvm","rxjava","rxjava2"],"latest_commit_sha":null,"homepage":"","language":"Java","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/GCX-HCI.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGES.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2016-08-22T14:55:09.000Z","updated_at":"2024-09-14T00:01:09.000Z","dependencies_parsed_at":null,"dependency_job_id":"27611bd0-a3e2-49eb-93bf-6a1699230554","html_url":"https://github.com/GCX-HCI/ThirtyInch","commit_stats":null,"previous_names":["grandcentrix/thirtyinch"],"tags_count":18,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/GCX-HCI%2FThirtyInch","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/GCX-HCI%2FThirtyInch/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/GCX-HCI%2FThirtyInch/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/GCX-HCI%2FThirtyInch/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/GCX-HCI","download_url":"https://codeload.github.com/GCX-HCI/ThirtyInch/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":234575214,"owners_count":18854924,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["activity","android","architecture","fragment","library","mvp","mvvm","rxjava","rxjava2"],"created_at":"2024-09-26T04:01:57.027Z","updated_at":"2025-09-28T23:32:28.924Z","avatar_url":"https://github.com/GCX-HCI.png","language":"Java","funding_links":[],"categories":[],"sub_categories":[],"readme":"# DEPRECATED - no longer actively maintained\n\n[![Build Status](https://travis-ci.org/grandcentrix/ThirtyInch.svg?branch=master)](https://travis-ci.org/grandcentrix/ThirtyInch)\n![License](https://img.shields.io/badge/license-Apache%202-green.svg?style=flat) \n[![Gitter](https://badges.gitter.im/ThirtyInch/gitter.svg)](https://gitter.im/ThirtyInch/Lobby)\n# ThirtyInch - a MVP library for Android\n\n\nThis 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.\n\n#### The name\n\n\u003e Keep Android At Arm’s Length\n\u003e\n\u0026mdash; Kevin Schultz, Droidcon NYC '14\n\nThe perfect distance to the Android Framework is approximately **thirty inches**, the average length of the human arm, shoulder to fingertips.\n\n## Story\n\nRead the introduction article on [Medium](https://medium.com/@passsy/thirtyinch-a-new-mvp-library-for-android-bd1a27262fd6#.xihev9jxq)\n\nSee the slides of the latest talk on [Speakerdeck](https://speakerdeck.com/passsy/thirtyinch-living-next-to-the-activity)\n\n\n## Get it\n\n### GitHub Packages\n\n```gradle\nrepositories {\n    maven {\n        url = uri(\"https://maven.pkg.github.com/GCX-HCI/ThirtyInch\")\n    }\n}\n\ndependencies {\n    implementation \"net.grandcentrix.thirtyinch:thirtyinch:$thirtyinchVersion\"\n    implementation \"net.grandcentrix.thirtyinch:thirtyinch-rx2:$thirtyinchVersion\"\n    implementation \"net.grandcentrix.thirtyinch:thirtyinch-logginginterceptor:$thirtyinchVersion\"\n    implementation \"net.grandcentrix.thirtyinch:thirtyinch-kotlin:$thirtyinchVersion\"\n    implementation \"net.grandcentrix.thirtyinch:thirtyinch-kotlin-coroutines:$thirtyinchVersion\"\n    \n    // Legacy dependencies\n    implementation \"net.grandcentrix.thirtyinch:thirtyinch-rx:$thirtyinchVersion\"\n}\n```\n\n### JCenter (deprecated)\n\n```gradle\nrepositories {\n    jcenter()\n}\n\ndependencies {\n    implementation \"net.grandcentrix.thirtyinch:thirtyinch:$thirtyinchVersion\"\n    implementation \"net.grandcentrix.thirtyinch:thirtyinch-rx2:$thirtyinchVersion\"\n    implementation \"net.grandcentrix.thirtyinch:thirtyinch-logginginterceptor:$thirtyinchVersion\"\n    implementation \"net.grandcentrix.thirtyinch:thirtyinch-kotlin:$thirtyinchVersion\"\n    implementation \"net.grandcentrix.thirtyinch:thirtyinch-kotlin-coroutines:$thirtyinchVersion\"\n    \n    // Lagacy dependencies\n    implementation \"net.grandcentrix.thirtyinch:thirtyinch-rx:$thirtyinchVersion\"\n}\n```\n\n## Hello World MVP example with ThirtyInch\n\n`HelloWorldActivity.java`\n```java\npublic class HelloWorldActivity \n        extends TiActivity\u003cHelloWorldPresenter, HelloWorldView\u003e \n        implements HelloWorldView {\n\n    private TextView mOutput;\n\n    @NonNull\n    @Override\n    public HelloWorldPresenter providePresenter() {\n        return new HelloWorldPresenter();\n    }\n\n    @Override\n    public void showText(final String text) {\n        mOutput.setText(text);\n    }\n\n    @Override\n    protected void onCreate(final Bundle savedInstanceState) {\n        super.onCreate(savedInstanceState);\n        setContentView(R.layout.activity_hello_world);\n\n        mOutput = (TextView) findViewById(R.id.output);\n    }\n}\n```\n\n`HelloWorldView.java`\n```java\npublic interface HelloWorldView extends TiView {\n\n    @CallOnMainThread\n    void showText(final String text);\n}\n\n```\n\n`HelloWorldPresenter.java`\n```java\npublic class HelloWorldPresenter extends TiPresenter\u003cHelloWorldView\u003e {\n\n    @Override    \n    protected void onAttachView(@NonNull final HelloWorldView view) {\n        super.onAttachView(view);\n        view.showText(\"Hello World!\");\n    }\n}\n\n```\n## ThirtyInch features\n\n### Presenter\n\n- survives configuration changes\n- survives when the `Activity` got killed in background\n- is not a singleton\n- dies when the `Activity` gets finished\n\n##### Lifecycle\n\nThe `TiPresenter` lifecycle is very easy.\n\nIt can be `CREATED` and `DESTROYED`. \nThe corresponding callbacks `onCreate()` and `onDestroy()` will be only called once!\n\nThe `TiView` can either be `ATTACHED` or `DETACHED`.\nThe corresponding callbacks are `onAttachView(TiView)` and `onDetachView()` which maps to `onStart()` and `onStop()`.\n\n\n```java\npublic class MyPresenter extends TiPresenter\u003cMyView\u003e {\n\n    @Override\n    protected void onCreate() {\n        super.onCreate();\n    }\n\n    @Override\n    protected void onAttachView(@NonNull final HelloWorldView view) {\n        super.onAttachView(view);\n    }\n\n    @Override\n    protected void onDetachView() {\n        super.onDetachView();\n    }\n\n    @Override\n    protected void onDestroy() {\n        super.onDestroy();\n    }\n}\n\n```\n\nThe lifecycle can be observed using `TiLifecycleObserver`\n\nThere is no callback for `onResume()` and `onPause()` in the `TiPresenter`. \nThis is something the view layer should handle.\nRead more about this here [Hannes Dorfmann - Presenters don't need lifecycle events](http://hannesdorfmann.com/android/presenters-dont-need-lifecycle)\n\n##### Configuration\n\nThe default behaviour might not fit your needs. \nYou can disable unwanted features by providing a configuration in the `TiPresenter` constructor.\n\n```java\npublic class HelloWorldPresenter extends TiPresenter\u003cHelloWorldView\u003e {\n\n    public static final TiConfiguration PRESENTER_CONFIG = \n            new TiConfiguration.Builder()\n                .setRetainPresenterEnabled(true) \n                .setCallOnMainThreadInterceptorEnabled(true)\n                .setDistinctUntilChangedInterceptorEnabled(true)\n                .build();\n            \n    public HelloWorldPresenter() {\n        super(PRESENTER_CONFIG);\n    }\n}\n```\n\nOr globally for all `TiPresenters`\n```java\npublic class MyApplication extends Application {\n\n    @Override\n    public void onCreate() {\n        super.onCreate();\n        TiPresenter.setDefaultConfig(MY_DEFAULT_CONFIG);\n    }\n}\n```\n\n### TiView Annotations\n\nTwo awesome annotations for the `TiView` interface made it already into `Ti` saving you a lot of time.\n\n```java\npublic interface HelloWorldView extends TiView {\n\n    @CallOnMainThread\n    @DistinctUntilChanged\n    void showText(final String text);\n}\n```\n\n##### @CallOnMainThread\n\nWhenever you call this method it will be called on the Android main thread.\nThis allows to run code off the main thread but send events to the UI without dealing with Handlers and Loopers.\n\nRequires to be a `void` method. Works only for `TiView` interfaces implemented by \"Android Views\" (`TiActivity`, `TiFragment`).\n\nEnabled by default, can be disabled with the `TiConfiguration`\n\n##### @DistinctUntilChanged\n\nWhen calling this method the `View` receives no duplicated calls.\nThe View swallows the second call when a method gets called with the same (hashcode) parameters twice.\n\nUsecase:\nThe Presenter binds a huge list to the `View`. The app loses focus (`onDetachView()`) and the exact same Activity instance gains focus again (`onAttachView(view)`).\nThe `Activity` still shows the huge list.\nThe `Presenter` binds the huge list again to the `View`.\nWhen the data has changed the list will be updated.\nWhen the data hasn't changed the call gets swallowed and prevents flickering.\n\nRequires to be a `void` method and has at least one parameter.\n\nEnabled by default, can be disabled with the `TiConfiguration`\n\n\n### View binding interceptors\n\n*View Annotations* only work because ThirtyInch supports interceptors. \nAdd interceptors (`BindViewInterceptor`) to `TiActivity` or `TiFragment` to intercept the binding process from `TiView` to `TiPresenter`.\nInterceptors are public API waiting for other great ideas.\n\n\n```java\npublic class HelloWorldActivity extends TiActivity\u003cHelloWorldPresenter, HelloWorldView\u003e\n        implements HelloWorldView {\n\n    public HelloWorldActivity() {\n        addBindViewInterceptor(new LoggingInterceptor());\n    }\n}\n```\n\n`LoggingInterceptor` is available as module and logs all calls to the view.\n\n### Kotlin\n\nUsing Kotlin these days is a no-brainer.\n`ThirtyInch` provides some extension methods to improve itself even further!  \n\n#### SendToView\n\nWhen using `sendToView`, repeating `it.*` inside the lambda is quite annoying.\nIt's clear that the methods are called *on* the view.\nWith the kotlin extension `deliverToView` the `TiView` will be give over to the lambda as `this`.\n```kotlin\nclass HelloWorldPresenter : TiPresenter\u003cHelloWorldView\u003e {\n\n  override fun onCreate() {\n      // normal java API\n      sendToView {\n          it.showText(\"Hello World\")\n      }\n      \n      // kotlin extension\n      deliverToView {\n          showText(\"Hello World\")\n      }\n  }\n}\n\ninterface HelloWorldView : TiView {\n    fun showText(text: String)\n}\n``` \nBack in the Java days we had to use `it` inside the `sendToView`-lambda.\n\n#### Coroutines\nIf you're using Kotlin's Coroutines we offer a `CoroutineScope` that scopes to a presenter's lifecycle.\n\n```kotlin\nclass HelloWorldPresenter : TiPresenter\u003cHelloWorldView\u003e {\n\n  private val scope = TiCoroutineScope(this, Dispatchers.Default)\n\n  override fun onCreate() {\n      scope.launch { ... }\n  }\n}\n```\nThe created `Job` will automatically be cancelled when the presenter is destroyed.\n\nAlternatively, you can launch jobs that get cancelled when a `TiView` detaches:\n```kotlin\nclass HelloWorldPresenter : TiPresenter\u003cHelloWorldView\u003e {\n\n  private val scope = TiCoroutineScope(this, Dispatchers.Default)\n\n  override fun onAttachView(view: HelloWorldView) {\n      scope.launchUntilViewDetaches { ... }\n  }\n}\n```\nHowever, be careful that `launchUntilViewDetaches` can only be called when there is a view attached!\n\n### [RxJava](https://github.com/ReactiveX/RxJava)\n\nUsing RxJava for networking is very often used.\nObserving a `Model` is another good usecase where Rx can be used inside of a `TiPresenter`.\nThe Rx package provides helper classes to deal with `Subscription` or wait for an attached `TiView`.\n\n```java\npublic class HelloWorldPresenter extends TiPresenter\u003cHelloWorldView\u003e {\n\n    // add the subscription helper to your presenter\n    private RxTiPresenterSubscriptionHandler rxHelper = new RxTiPresenterSubscriptionHandler(this);\n\n    @Override\n    protected void onCreate() {\n        super.onCreate();\n        \n        // automatically unsubscribe in onDestroy()\n        rxHelper.manageSubscription(\n                Observable.interval(0, 1, TimeUnit.SECONDS)\n                    // cache the latest value when no view is attached\n                    // emits when the view got attached\n                    .compose(RxTiPresenterUtils.\u003cLong\u003edeliverLatestToView(this))\n                    .subscribe(uptime -\u003e getView().showPresenterUpTime(uptime))\n        );\n    }\n\n    @Override\n    protected void onAttachView(@NonNull final HelloWorldView view) {\n        super.onAttachView(view);\n        \n        // automatically unsubscribe in onDetachView(view)\n        rxHelper.manageViewSubscription(anotherObservable.subscribe());\n    }\n}\n```\n\nYou can make `Disposable` handling even less intrusive in Kotlin. Just create the following interface and make your presenters implement it:\n\n```kotlin\ninterface DisposableHandler {\n\n    // Initialize with reference to your TiPresenter instance\n    val disposableHandler: RxTiPresenterDisposableHandler\n\n    // Dispose of Disposables dependent on the TiPresenter lifecycle\n    fun Disposable.disposeWhenDestroyed(): Disposable = disposableHandler.manageDisposable(this)\n\n    // Dispose of Disposables dependent on the TiView attached/detached state\n    fun Disposable.disposeWhenViewDetached(): Disposable = disposableHandler.manageViewDisposable(this)\n} \n```\n\nThen just implement the interface in your presenter and you can use created extension functions to manage `Disposable`s:\n\n```kotlin\nclass MyPresenter : TiPresenter\u003cMyView\u003e(), DisposableHandler {\n\n    override val disposableHandler = RxTiPresenterDisposableHandler(this)\n\n    override fun onCreate() {\n        super.onCreate()\n\n        // Presenter lifecycle dependent Disposable\n        myObservable\n            .subscribe()\n            .disposeWhenDestroyed()\n    }\n\n    override fun onAttachView(view: MyView) {\n        super.onAttachView(view)\n\n        // View attached/detached dependent Disposable\n        myViewObservable\n            .subscribe()\n            .disposeWhenViewDetached()\n    }\n}\n```\n\n# License\n\n```\nCopyright 2016 grandcentrix GmbH\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n   http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgcx-hci%2Fthirtyinch","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fgcx-hci%2Fthirtyinch","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgcx-hci%2Fthirtyinch/lists"}