├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE.txt ├── README.md ├── RELEASING.md ├── build.gradle ├── checkstyle.xml ├── gradle-mvn-push.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── mortar-dagger1 ├── build.gradle ├── gradle.properties └── src │ ├── main │ ├── AndroidManifest.xml │ └── java │ │ └── mortar │ │ └── dagger1support │ │ └── ObjectGraphService.java │ └── test │ └── java │ └── mortar │ └── ObjectGraphServiceTest.java ├── mortar-hellodagger2 ├── build.gradle └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── example │ │ └── hellodagger2 │ │ ├── DaggerService.java │ │ ├── HelloDagger2Activity.java │ │ ├── HelloDagger2Application.java │ │ ├── Main.java │ │ └── MainView.java │ └── res │ ├── drawable │ └── mortar_helloworld_icon.png │ └── layout │ └── main_view.xml ├── mortar-helloworld ├── build.gradle └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── example │ │ └── hellomortar │ │ ├── HelloActivity.java │ │ ├── HelloApplication.java │ │ ├── HelloPresenter.java │ │ └── HelloView.java │ └── res │ ├── drawable │ └── mortar_helloworld_icon.png │ └── layout │ └── main_view.xml ├── mortar-sample ├── build.gradle └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── example │ │ └── mortar │ │ ├── android │ │ └── ActionBarOwner.java │ │ ├── core │ │ ├── MortarDemoActivity.java │ │ ├── MortarDemoApplication.java │ │ ├── MortarScreenSwitcherFrame.java │ │ └── RootModule.java │ │ ├── model │ │ ├── Chat.java │ │ ├── Chats.java │ │ ├── Message.java │ │ ├── QuoteService.java │ │ └── User.java │ │ ├── mortarflow │ │ └── MortarContextFactory.java │ │ ├── mortarscreen │ │ ├── ModuleFactory.java │ │ ├── ScreenScoper.java │ │ ├── WithModule.java │ │ └── WithModuleFactory.java │ │ ├── screen │ │ ├── BackSupport.java │ │ ├── ChatListScreen.java │ │ ├── ChatScreen.java │ │ ├── FramePathContainerView.java │ │ ├── FriendListScreen.java │ │ ├── FriendScreen.java │ │ ├── GsonParceler.java │ │ ├── HandlesBack.java │ │ ├── Layout.java │ │ ├── Layouts.java │ │ ├── MessageScreen.java │ │ ├── SimplePathContainer.java │ │ └── Utils.java │ │ └── view │ │ ├── ChatListView.java │ │ ├── ChatView.java │ │ ├── Confirmation.java │ │ ├── ConfirmerPopup.java │ │ ├── FriendListView.java │ │ ├── FriendView.java │ │ └── MessageView.java │ └── res │ ├── anim │ ├── slide_in_left.xml │ ├── slide_in_right.xml │ ├── slide_out_left.xml │ └── slide_out_right.xml │ ├── drawable │ └── mortar_icon.png │ ├── layout │ ├── chat_list_view.xml │ ├── chat_view.xml │ ├── friend_list_view.xml │ ├── friend_view.xml │ ├── message_view.xml │ └── root_layout.xml │ └── values │ └── ids.xml ├── mortar ├── build.gradle ├── gradle.properties └── src │ ├── main │ ├── AndroidManifest.xml │ └── java │ │ └── mortar │ │ ├── MortarContextWrapper.java │ │ ├── MortarScope.java │ │ ├── MortarScopeDevHelper.java │ │ ├── Popup.java │ │ ├── PopupPresenter.java │ │ ├── Presenter.java │ │ ├── Scoped.java │ │ ├── ViewPresenter.java │ │ └── bundler │ │ ├── BundleService.java │ │ ├── BundleServiceComparator.java │ │ ├── BundleServiceRunner.java │ │ └── Bundler.java │ └── test │ └── java │ └── mortar │ ├── MortarScopeDevHelperTest.java │ ├── MortarScopeTest.java │ ├── PopupPresenterTest.java │ ├── PresenterTest.java │ └── bundler │ └── BundleServiceTest.java └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | # IntelliJ IDEA 2 | .idea 3 | *.iml 4 | gen 5 | 6 | # Maven 7 | target/ 8 | pom.xml.versionsBackup 9 | 10 | # Gradle 11 | .gradle 12 | gradlew.bat 13 | build 14 | local.properties 15 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | 3 | language: android 4 | 5 | android: 6 | components: 7 | - tools 8 | - build-tools-23.0.2 9 | - android-23 10 | licenses: 11 | - android-sdk-license-5be876d5 12 | 13 | jdk: 14 | - oraclejdk8 15 | 16 | script: 17 | - ./gradlew clean build check 18 | 19 | notifications: 20 | email: false 21 | 22 | sudo: false 23 | 24 | cache: 25 | directories: 26 | - $HOME/.gradle 27 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Change Log 2 | ========== 3 | 4 | Version 0.20 *(2016-2-01)* 5 | ------------------ 6 | * Detect multi-registered objects in different scopes of the same hierarchy, and throw an IllegalStateException. This ensures that `Scoped#onEnterScope` and `Scoped#onExitScope` calls are paired up. 7 | 8 | Version 0.19 *(2015-08-04)* 9 | ------------------ 10 | * Fixes ambiguous service lookup behavior of destroyed scopes: 11 | * `MortarScope.getScope(context).isDead()` returns true when you'd expect it to. 12 | * `ObjectGraph.inject(context, object)` throws if the backing scope is dead, as opposed to the current behavior where we instead try (and generally fail with a confusing message) to inject from an ancestor scope. 13 | * The behavior of `MortarScope.hasService(String)` is not changed in destroyed scopes. It always says yes if the service is provided by the receiving scope or an ancestor. 14 | 15 | * Deletes deprecated classes and methods: 16 | * `Blueprint` 17 | * `ObjectGraphService#requireActivityScope` 18 | * `ObjectGraphService#requireChild` 19 | 20 | Version 0.18 *(2015-07-14)* 21 | ------------------ 22 | * Destroying a scope recursively destroys its children first, like it used to. 23 | (0.17 API quake incorrectly reversed this.) 24 | 25 | * Now throws (fail fast!) when doing service lookup in a dead scope. 26 | 27 | * Manually falls back to app context when a service is not found, to work 28 | around the interval in a new activity's life where its base context 29 | is not yet set. 30 | 31 | Version 0.17 *(2015-04-27)* 32 | ------------------ 33 | **API Quake!** 34 | 35 | * Mortar is now decoupled from dependency injection in general, and from Dagger in particular. 36 | 37 | * Mortar core is now a service provider, meant to back Context#getSystemService, and handles registration of Scoped objects. 38 | 39 | * MortarActivityScope is gone, replaced by BundleService and BundleServiceRunner. (Presenter is now built on those services, but basically unchanged.) 40 | 41 | * Dagger support has moved to ObjectGraphService. Blueprint moved with it, and is deprecated. 42 | 43 | * Main sample application continues to be overly complicated and confusing, working on it. 44 | 45 | Version 0.16 *(2014-06-02)* 46 | ------------------ 47 | * Repairs idempotence of MortarScope#destroyChild 48 | 49 | * Adds MortarScope#isDetroyed 50 | 51 | Version 0.15 *(2014-05-29)* 52 | ------------------ 53 | * API break: Presenter#onDestroy and Scoped#onDestroy are now onExitScope(MortarScope). 54 | Also adds onEnterScope(MortarScope) to those classes. 55 | 56 | * API break: MortarScope#destroyChild(MortarScope) replaces MortarScope#destroy. 57 | 58 | Version 0.14 *(2014-04-18)* 59 | ------------------ 60 | * Refine deferral of calls to Bundler#onLoad from MortarActivityScope#onRegister. 61 | See onRegister javadoc for details. 62 | 63 | Version 0.13 *(2014-04-17)* 64 | ------------------ 65 | * Fix accidental bundling of dagger-compiler 66 | 67 | Version 0.12 *(2014-04-09)* 68 | ------------------ 69 | * Guarantees that parent scopes will make their onLoad calls before children. 70 | * API break: MortarContext has been removed. Activities must be careful to 71 | override getSystemService(); see the samples. This change allows 72 | Mortar to coexist peacefully with other ContextWrappers. 73 | 74 | Version 0.11 *(2014-04-03)* 75 | ---------------------------- 76 | * Presenter#onDestroy calling dropView was a bad, bad idea. Now it does 77 | nothing. Drop your own damn views. 78 | * MortarScopeDevHelper now dumps in alphabetical order, tests pass under 79 | Java 8 80 | 81 | Version 0.10 *(2014-04-01)* 82 | ---------------------------- 83 | * Fixes PopupPresenter state saving 84 | * Presenter#onDestroy wasn't calling dropView, does now. 85 | 86 | Version 0.9 *(2014-03-28)* 87 | ---------------------------- 88 | * Fixes redundant calls to Presenter#onLoad 89 | * Improved flow owner view in sample code 90 | * Fixes for redundant Bundler#onLoad calls when registering during onCreate 91 | * Better diagnostic dumps 92 | 93 | Version 0.8 *(2014-03-03)* 94 | ---------------------------- 95 | * Fixes bug with bundle key namespacing in presenters. 96 | 97 | Version 0.7 *(2014-01-30)* 98 | ---------------------------- 99 | * API break: MortarActivityScope#onResume is gone, and as you might expect 100 | Bundler#onLoad is not called at resume time. It just wasn't useful. See 101 | ChatScreen in the sample to see how to to handle pausing. 102 | 103 | * API break: Presenter is no longer a Bundler, and its onLoad method 104 | is never called with a null view. 105 | 106 | * New: Mortar#createRootScope(boolean) for simpler root scope creation. 107 | 108 | * New: Hello Mortar sample app. 109 | 110 | Version 0.6 *(2014-01-20)* 111 | ---------------------------- 112 | * API break: Mortar#createRootActivityScope is not practical, dropped 113 | 114 | * API break: Presenter#dropView now takes the dropped view as an argument, 115 | and is expected to be called by outgoing views. Presenter#view 116 | is no longer a weak reference. 117 | 118 | Version 0.5 *(2014-01-21)* 119 | ---------------------------- 120 | * API break: Simplified Presenter API, HasMortarScope renamed MortarContext and need not 121 | be implemented by view classes 122 | 123 | * API break: Root scope can belong to an activity, root ObjectGraph to be passed in. 124 | 125 | Version 0.2 *(2013-11-12)* 126 | ---------------------------- 127 | 128 | Initial release. 129 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributing 2 | ============ 3 | 4 | If you would like to contribute code to Mortar you can do so through GitHub by 5 | forking the repository and sending a pull request. 6 | 7 | When submitting code, please make every effort to follow existing conventions 8 | and style in order to keep the code as readable as possible. Please also make 9 | sure your code compiles by running `./gradlew clean build check`. 10 | 11 | Before your code can be accepted into the project you must also sign the 12 | [Individual Contributor License Agreement (CLA)][1]. 13 | 14 | 15 | [1]: https://spreadsheets.google.com/spreadsheet/viewform?formkey=dDViT2xzUHAwRkI3X3k5Z0lQM091OGc6MQ&ndplr=1 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mortar 2 | 3 | ## Deprecated 4 | 5 | Mortar had a good run and served us well, but new use is strongly discouraged. The app suite at Square that drove its creation is in the process of replacing Mortar with [Square Workflow](https://square.github.io/workflow/). 6 | 7 | ## What's a Mortar? 8 | 9 | Mortar provides a simplified, composable overlay for the Android lifecycle, 10 | to aid in the use of [Views as the modular unit of Android applications][rant]. 11 | It leverages [Context#getSystemService][services] to act as an a la carte supplier 12 | of services like dependency injection, bundle persistence, and whatever else 13 | your app needs to provide itself. 14 | 15 | One of the most useful services Mortar can provide is its [BundleService][bundle-service], 16 | which gives any View (or any object with access to the Activity context) safe access to 17 | the Activity lifecycle's persistence bundle. For fans of the [Model View Presenter][mvp] 18 | pattern, we provide a persisted [Presenter][presenter] class that builds on BundleService. 19 | Presenters are completely isolated from View concerns. They're particularly good at 20 | surviving configuration changes, weathering the storm as Android destroys your portrait 21 | Activity and Views and replaces them with landscape doppelgangers. 22 | 23 | Mortar can similarly make [Dagger][dagger] ObjectGraphs (or [Dagger2][dagger2] 24 | Components) visible as system services. Or not — these services are 25 | completely decoupled. 26 | 27 | Everything is managed by [MortarScope][scope] singletons, typically 28 | backing the top level Application and Activity contexts. You can also spawn 29 | your own shorter lived scopes to manage transient sessions, like the state of 30 | an object being built by a set of wizard screens. 31 | 32 | 35 | 36 | These nested scopes can shadow the services provided by higher level scopes. 37 | For example, a [Dagger extension graph][ogplus] specific to your wizard session 38 | can cover the one normally available, transparently to the wizard Views. 39 | Calls like `ObjectGraphService.inject(getContext(), this)` are now possible 40 | without considering which graph will do the injection. 41 | 42 | ## The Big Picture 43 | 44 | An application will typically have a singleton MortarScope instance. 45 | Its job is to serve as a delegate to the app's `getSystemService` method, something like: 46 | 47 | ```java 48 | public class MyApplication extends Application { 49 | private MortarScope rootScope; 50 | 51 | @Override public Object getSystemService(String name) { 52 | if (rootScope == null) rootScope = MortarScope.buildRootScope().build(getScopeName()); 53 | 54 | return rootScope.hasService(name) ? rootScope.getService(name) : super.getSystemService(name); 55 | } 56 | } 57 | ``` 58 | 59 | This exposes a single, core service, the scope itself. From the scope you can 60 | spawn child scopes, and you can register objects that implement the 61 | [Scoped](https://github.com/square/mortar/blob/master/mortar/src/main/java/mortar/Scoped.java#L18) 62 | interface with it for setup and tear-down calls. 63 | 64 | * `Scoped#onEnterScope(MortarScope)` 65 | * `Scoped#onExitScope(MortarScope)` 66 | 67 | To make a scope provide other services, like a [Dagger ObjectGraph][og], 68 | you register them while building the scope. That would make our Application's 69 | `getSystemService` method look like this: 70 | 71 | ```java 72 | @Override public Object getSystemService(String name) { 73 | if (rootScope == null) { 74 | rootScope = MortarScope.buildRootScope() 75 | .with(ObjectGraphService.SERVICE_NAME, ObjectGraph.create(new RootModule())) 76 | .build(getScopeName()); 77 | } 78 | 79 | return rootScope.hasService(name) ? rootScope.getService(name) : super.getSystemService(name); 80 | } 81 | ``` 82 | 83 | Now any part of our app that has access to a `Context` can inject itself: 84 | 85 | ```java 86 | public class MyView extends LinearLayout { 87 | @Inject SomeService service; 88 | 89 | public MyView(Context context, AttributeSet attrs) { 90 | super(context, attrs); 91 | ObjectGraphService.inject(context, this); 92 | } 93 | } 94 | ``` 95 | 96 | To take advantage of the BundleService describe above, you'll put similar code 97 | into your Activity. If it doesn't exist already, you'll 98 | build a sub-scope to back the Activity's `getSystemService` method, and 99 | while building it set up the `BundleServiceRunner`. You'll also notify 100 | the BundleServiceRunner each time `onCreate` and `onSaveInstanceState` are 101 | called, to make the persistence bundle available to the rest of the app. 102 | 103 | ```java 104 | public class MyActivity extends Activity { 105 | private MortarScope activityScope; 106 | 107 | @Override public Object getSystemService(String name) { 108 | MortarScope activityScope = MortarScope.findChild(getApplicationContext(), getScopeName()); 109 | 110 | if (activityScope == null) { 111 | activityScope = MortarScope.buildChild(getApplicationContext()) // 112 | .withService(BundleServiceRunner.SERVICE_NAME, new BundleServiceRunner()) 113 | .withService(HelloPresenter.class.getName(), new HelloPresenter()) 114 | .build(getScopeName()); 115 | } 116 | 117 | return activityScope.hasService(name) ? activityScope.getService(name) 118 | : super.getSystemService(name); 119 | } 120 | 121 | @Override protected void onCreate(Bundle savedInstanceState) { 122 | super.onCreate(savedInstanceState); 123 | BundleServiceRunner.getBundleServiceRunner(this).onCreate(savedInstanceState); 124 | setContentView(R.layout.main_view); 125 | } 126 | 127 | @Override protected void onSaveInstanceState(Bundle outState) { 128 | super.onSaveInstanceState(outState); 129 | BundleServiceRunner.getBundleServiceRunner(this).onSaveInstanceState(outState); 130 | } 131 | } 132 | ``` 133 | 134 | With that in place, any object in your app can sign up with the `BundleService` 135 | to save and restore its state. This is nice for views, since Bundles are less 136 | of a hassle than the `Parcelable` objects required by `View#onSaveInstanceState`, 137 | and a boon to any business objects in the rest of your app. 138 | 139 | Download 140 | -------- 141 | 142 | Download [the latest JAR][jar] or grab via Maven: 143 | 144 | ```xml 145 | 146 | com.squareup.mortar 147 | mortar 148 | (insert latest version) 149 | 150 | ``` 151 | 152 | Gradle: 153 | 154 | ```groovy 155 | compile 'com.squareup.mortar:mortar:(latest version)' 156 | ``` 157 | 158 | ## Full Disclosure 159 | 160 | This stuff has been in "rapid" development over a pretty long gestation period, 161 | but is finally stabilizing. We don't expect drastic changes before cutting a 162 | 1.0 release, but we still cannot promise a stable API from release to release. 163 | 164 | Mortar is a key component of multiple Square apps, including our flagship 165 | [Square Register][register] app. 166 | 167 | License 168 | -------- 169 | 170 | Copyright 2013 Square, Inc. 171 | 172 | Licensed under the Apache License, Version 2.0 (the "License"); 173 | you may not use this file except in compliance with the License. 174 | You may obtain a copy of the License at 175 | 176 | http://www.apache.org/licenses/LICENSE-2.0 177 | 178 | Unless required by applicable law or agreed to in writing, software 179 | distributed under the License is distributed on an "AS IS" BASIS, 180 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 181 | See the License for the specific language governing permissions and 182 | limitations under the License. 183 | 184 | [bundle-service]: https://github.com/square/mortar/blob/master/mortar/src/main/java/mortar/bundler/BundleService.java 185 | [mvp]: http://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93presenter 186 | [dagger]: http://square.github.io/dagger/ 187 | [dagger2]: http://google.github.io/dagger/ 188 | [jar]: http://repository.sonatype.org/service/local/artifact/maven/redirect?r=central-proxy&g=com.squareup.mortar&a=mortar&v=LATEST 189 | [og]: https://square.github.io/dagger/1.x/dagger/dagger/ObjectGraph.html 190 | [ogplus]: https://github.com/square/dagger/blob/dagger-parent-1.1.0/core/src/main/java/dagger/ObjectGraph.java#L96 191 | [presenter]: https://github.com/square/mortar/blob/master/mortar/src/main/java/mortar/Presenter.java 192 | [rant]: http://corner.squareup.com/2014/10/advocating-against-android-fragments.html 193 | [register]: https://play.google.com/store/apps/details?id=com.squareup 194 | [scope]: https://github.com/square/mortar/blob/master/mortar/src/main/java/mortar/MortarScope.java 195 | [services]: http://developer.android.com/reference/android/content/Context.html#getSystemService(java.lang.String) 196 | -------------------------------------------------------------------------------- /RELEASING.md: -------------------------------------------------------------------------------- 1 | Releasing 2 | ======== 3 | 4 | 1. Change the version in `gradle.properties` to a non-SNAPSHOT verson. 5 | 2. Update the `CHANGELOG.md` for the impending release. 6 | 3. `git commit -am "Prepare for release X.Y."` (where X.Y is the new version) 7 | 4. `git tag -a X.Y -m "Version X.Y"` (where X.Y is the new version) 8 | 5. `./gradlew clean uploadArchives` 9 | 6. Update the `gradle.properties` to the next SNAPSHOT version. 10 | 7. `git commit -am "Prepare next development version."` 11 | 8. `git push && git push --tags` 12 | 9. Visit [Sonatype Nexus](https://oss.sonatype.org/) and promote the artifact. 13 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | ext.deps = [ 2 | android_gradle_plugin : 'com.android.tools.build:gradle:1.2.3', 3 | ] 4 | 5 | ext.minSdkVersion = 14 6 | ext.compileSdkVersion = 23 7 | ext.buildToolsVersion = '23.0.2' 8 | 9 | task wrapper(type: Wrapper) { 10 | gradleVersion = '2.3' 11 | } 12 | 13 | subprojects { 14 | buildscript { 15 | repositories { 16 | mavenCentral() 17 | } 18 | } 19 | 20 | repositories { 21 | mavenCentral() 22 | } 23 | 24 | group = GROUP 25 | version = VERSION_NAME 26 | 27 | apply plugin: 'maven' 28 | } 29 | -------------------------------------------------------------------------------- /checkstyle.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /gradle-mvn-push.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013 Chris Banes 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | apply plugin: 'maven' 18 | apply plugin: 'signing' 19 | 20 | def isReleaseBuild() { 21 | return VERSION_NAME.contains("SNAPSHOT") == false 22 | } 23 | 24 | def getReleaseRepositoryUrl() { 25 | return hasProperty('RELEASE_REPOSITORY_URL') ? RELEASE_REPOSITORY_URL 26 | : "https://oss.sonatype.org/service/local/staging/deploy/maven2/" 27 | } 28 | 29 | def getSnapshotRepositoryUrl() { 30 | return hasProperty('SNAPSHOT_REPOSITORY_URL') ? SNAPSHOT_REPOSITORY_URL 31 | : "https://oss.sonatype.org/content/repositories/snapshots/" 32 | } 33 | 34 | def getRepositoryUsername() { 35 | return hasProperty('NEXUS_USERNAME') ? NEXUS_USERNAME : "" 36 | } 37 | 38 | def getRepositoryPassword() { 39 | return hasProperty('NEXUS_PASSWORD') ? NEXUS_PASSWORD : "" 40 | } 41 | 42 | afterEvaluate { project -> 43 | uploadArchives { 44 | repositories { 45 | mavenDeployer { 46 | beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) } 47 | 48 | pom.groupId = GROUP 49 | pom.artifactId = POM_ARTIFACT_ID 50 | pom.version = VERSION_NAME 51 | 52 | repository(url: getReleaseRepositoryUrl()) { 53 | authentication(userName: getRepositoryUsername(), password: getRepositoryPassword()) 54 | } 55 | snapshotRepository(url: getSnapshotRepositoryUrl()) { 56 | authentication(userName: getRepositoryUsername(), password: getRepositoryPassword()) 57 | } 58 | 59 | pom.project { 60 | name POM_NAME 61 | packaging POM_PACKAGING 62 | description POM_DESCRIPTION 63 | url POM_URL 64 | 65 | scm { 66 | url POM_SCM_URL 67 | connection POM_SCM_CONNECTION 68 | developerConnection POM_SCM_DEV_CONNECTION 69 | } 70 | 71 | licenses { 72 | license { 73 | name POM_LICENCE_NAME 74 | url POM_LICENCE_URL 75 | distribution POM_LICENCE_DIST 76 | } 77 | } 78 | 79 | developers { 80 | developer { 81 | id POM_DEVELOPER_ID 82 | name POM_DEVELOPER_NAME 83 | } 84 | } 85 | } 86 | } 87 | } 88 | } 89 | 90 | signing { 91 | required { isReleaseBuild() && gradle.taskGraph.hasTask("uploadArchives") } 92 | sign configurations.archives 93 | } 94 | 95 | task androidJavadocs(type: Javadoc) { 96 | source = android.sourceSets.main.java.srcDirs 97 | classpath += project.files(android.getBootClasspath().join(File.pathSeparator)) 98 | } 99 | 100 | task androidJavadocsJar(type: Jar, dependsOn: androidJavadocs) { 101 | classifier = 'javadoc' 102 | from androidJavadocs.destinationDir 103 | } 104 | 105 | task androidSourcesJar(type: Jar) { 106 | classifier = 'sources' 107 | from android.sourceSets.main.java.sourceFiles 108 | } 109 | 110 | artifacts { 111 | archives androidSourcesJar 112 | archives androidJavadocsJar 113 | } 114 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | VERSION_NAME=0.21-SNAPSHOT 2 | GROUP=com.squareup.mortar 3 | 4 | POM_DESCRIPTION=Modular Android without Fragments. 5 | POM_URL=https://github.com/square/mortar/ 6 | POM_SCM_URL=https://github.com/square/mortar/ 7 | POM_SCM_CONNECTION=scm:git:git://github.com/square/mortar.git 8 | POM_SCM_DEV_CONNECTION=scm:git:ssh://git@github.com/square/mortar.git 9 | POM_LICENCE_NAME=Apache 2.0 10 | POM_LICENCE_URL=http://www.apache.org/licenses/LICENSE-2.0.txt 11 | POM_LICENCE_DIST=repo 12 | POM_ORGANIZATION_NAME=Square, Inc. 13 | POM_ORGANIZATION_URL=http://squareup.com 14 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/square/mortar/732e2459f6a01afa7270a2798df43fd92b6ca4cf/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sun Feb 22 20:31:17 PST 2015 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-2.3-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # For Cygwin, ensure paths are in UNIX format before anything is touched. 46 | if $cygwin ; then 47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 48 | fi 49 | 50 | # Attempt to set APP_HOME 51 | # Resolve links: $0 may be a link 52 | PRG="$0" 53 | # Need this for relative symlinks. 54 | while [ -h "$PRG" ] ; do 55 | ls=`ls -ld "$PRG"` 56 | link=`expr "$ls" : '.*-> \(.*\)$'` 57 | if expr "$link" : '/.*' > /dev/null; then 58 | PRG="$link" 59 | else 60 | PRG=`dirname "$PRG"`"/$link" 61 | fi 62 | done 63 | SAVED="`pwd`" 64 | cd "`dirname \"$PRG\"`/" >&- 65 | APP_HOME="`pwd -P`" 66 | cd "$SAVED" >&- 67 | 68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 69 | 70 | # Determine the Java command to use to start the JVM. 71 | if [ -n "$JAVA_HOME" ] ; then 72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 73 | # IBM's JDK on AIX uses strange locations for the executables 74 | JAVACMD="$JAVA_HOME/jre/sh/java" 75 | else 76 | JAVACMD="$JAVA_HOME/bin/java" 77 | fi 78 | if [ ! -x "$JAVACMD" ] ; then 79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 80 | 81 | Please set the JAVA_HOME variable in your environment to match the 82 | location of your Java installation." 83 | fi 84 | else 85 | JAVACMD="java" 86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 87 | 88 | Please set the JAVA_HOME variable in your environment to match the 89 | location of your Java installation." 90 | fi 91 | 92 | # Increase the maximum file descriptors if we can. 93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 94 | MAX_FD_LIMIT=`ulimit -H -n` 95 | if [ $? -eq 0 ] ; then 96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 97 | MAX_FD="$MAX_FD_LIMIT" 98 | fi 99 | ulimit -n $MAX_FD 100 | if [ $? -ne 0 ] ; then 101 | warn "Could not set maximum file descriptor limit: $MAX_FD" 102 | fi 103 | else 104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 105 | fi 106 | fi 107 | 108 | # For Darwin, add options to specify how the application appears in the dock 109 | if $darwin; then 110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 111 | fi 112 | 113 | # For Cygwin, switch paths to Windows format before running java 114 | if $cygwin ; then 115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 165 | -------------------------------------------------------------------------------- /mortar-dagger1/build.gradle: -------------------------------------------------------------------------------- 1 | import com.android.builder.core.BuilderConstants; 2 | 3 | buildscript { 4 | dependencies { 5 | classpath deps.android_gradle_plugin 6 | classpath 'com.neenbedankt.gradle.plugins:android-apt:1.4' 7 | } 8 | } 9 | apply plugin: 'com.android.library' 10 | apply plugin: 'com.neenbedankt.android-apt' 11 | 12 | android { 13 | compileSdkVersion rootProject.ext.compileSdkVersion 14 | buildToolsVersion rootProject.ext.buildToolsVersion 15 | 16 | defaultConfig { 17 | minSdkVersion rootProject.ext.minSdkVersion 18 | versionName VERSION_NAME 19 | } 20 | } 21 | 22 | dependencies { 23 | compile project(':mortar') 24 | compile 'com.squareup.dagger:dagger:1.2.1' 25 | apt 'com.squareup.dagger:dagger-compiler:1.2.1' 26 | testCompile 'com.squareup.dagger:dagger-compiler:1.2.1' 27 | testCompile 'junit:junit:4.10' 28 | testCompile 'org.easytesting:fest-assert-core:2.0M10' 29 | testCompile 'org.mockito:mockito-core:1.9.5' 30 | testCompile 'org.robolectric:robolectric:2.2' 31 | } 32 | 33 | 34 | android.libraryVariants.all { variant -> 35 | def name = variant.buildType.name 36 | if (!BuilderConstants.DEBUG.equals(name)) { 37 | def task = project.tasks.create "jar${name.capitalize()}", Jar 38 | task.dependsOn variant.javaCompile 39 | task.from variant.javaCompile.destinationDir 40 | artifacts.add('archives', task); 41 | } 42 | } 43 | 44 | if (hasProperty("POM_DEVELOPER_ID")) { 45 | apply from: '../gradle-mvn-push.gradle' 46 | } 47 | -------------------------------------------------------------------------------- /mortar-dagger1/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_NAME=Mortar Dagger1 Support 2 | POM_ARTIFACT_ID=mortar-dagger1 3 | POM_PACKAGING=jar 4 | -------------------------------------------------------------------------------- /mortar-dagger1/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /mortar-dagger1/src/main/java/mortar/dagger1support/ObjectGraphService.java: -------------------------------------------------------------------------------- 1 | package mortar.dagger1support; 2 | 3 | import android.app.Activity; 4 | import android.content.Context; 5 | import dagger.ObjectGraph; 6 | import mortar.MortarScope; 7 | 8 | /** 9 | * Provides utility methods for using Mortar with Dagger 1. 10 | */ 11 | public class ObjectGraphService { 12 | public static final String SERVICE_NAME = ObjectGraphService.class.getName(); 13 | 14 | /** 15 | * Create a new {@link ObjectGraph} based on the given module. The new graph will extend 16 | * the graph found in the parent scope (via {@link ObjectGraph#plus}), if there is one. 17 | */ 18 | public static ObjectGraph create(MortarScope parent, Object... daggerModules) { 19 | ObjectGraph parentGraph = getObjectGraph(parent); 20 | 21 | return parentGraph == null ? ObjectGraph.create(daggerModules) 22 | : parentGraph.plus(daggerModules); 23 | } 24 | 25 | public static ObjectGraph getObjectGraph(Context context) { 26 | //noinspection ResourceType 27 | return (ObjectGraph) context.getSystemService(ObjectGraphService.SERVICE_NAME); 28 | } 29 | 30 | public static ObjectGraph getObjectGraph(MortarScope scope) { 31 | return scope.getService(ObjectGraphService.SERVICE_NAME); 32 | } 33 | 34 | /** 35 | * A convenience wrapper for {@link ObjectGraphService#getObjectGraph} to simplify dynamic 36 | * injection, typically for {@link Activity} and {@link android.view.View} instances that must be 37 | * instantiated by Android. 38 | */ 39 | public static void inject(Context context, Object object) { 40 | getObjectGraph(context).inject(object); 41 | } 42 | 43 | /** 44 | * A convenience wrapper for {@link ObjectGraphService#getObjectGraph} to simplify dynamic 45 | * injection, typically for {@link Activity} and {@link android.view.View} instances that must be 46 | * instantiated by Android. 47 | */ 48 | public static void inject(MortarScope scope, Object object) { 49 | getObjectGraph(scope).inject(object); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /mortar-hellodagger2/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | dependencies { 3 | classpath deps.android_gradle_plugin 4 | classpath 'com.neenbedankt.gradle.plugins:android-apt:1.4' 5 | } 6 | } 7 | 8 | apply plugin: 'com.android.application' 9 | apply plugin: 'com.neenbedankt.android-apt' 10 | 11 | dependencies { 12 | compile project(':mortar') 13 | compile 'com.google.dagger:dagger:2.0' 14 | apt 'com.google.dagger:dagger-compiler:2.0' 15 | provided 'org.glassfish:javax.annotation:10.0-b28' 16 | } 17 | 18 | android { 19 | compileSdkVersion rootProject.ext.compileSdkVersion 20 | buildToolsVersion rootProject.ext.buildToolsVersion 21 | 22 | defaultConfig { 23 | minSdkVersion rootProject.ext.minSdkVersion 24 | versionName VERSION_NAME 25 | } 26 | 27 | lintOptions { 28 | abortOnError false 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /mortar-hellodagger2/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 22 | 23 | 27 | 28 | 34 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /mortar-hellodagger2/src/main/java/com/example/hellodagger2/DaggerService.java: -------------------------------------------------------------------------------- 1 | package com.example.hellodagger2; 2 | 3 | import android.content.Context; 4 | import java.lang.reflect.Method; 5 | 6 | public class DaggerService { 7 | public static final String SERVICE_NAME = DaggerService.class.getName(); 8 | 9 | /** 10 | * Caller is required to know the type of the component for this context. 11 | */ 12 | @SuppressWarnings("unchecked") // 13 | public static T getDaggerComponent(Context context) { 14 | //noinspection ResourceType 15 | return (T) context.getSystemService(SERVICE_NAME); 16 | } 17 | 18 | /** 19 | * Magic method that creates a component with its dependencies set, by reflection. Relies on 20 | * Dagger2 naming conventions. 21 | */ 22 | public static T createComponent(Class componentClass, Object... dependencies) { 23 | String fqn = componentClass.getName(); 24 | 25 | String packageName = componentClass.getPackage().getName(); 26 | // Accounts for inner classes, ie MyApplication$Component 27 | String simpleName = fqn.substring(packageName.length() + 1); 28 | String generatedName = (packageName + ".Dagger" + simpleName).replace('$', '_'); 29 | 30 | try { 31 | Class generatedClass = Class.forName(generatedName); 32 | Object builder = generatedClass.getMethod("builder").invoke(null); 33 | 34 | for (Method method : builder.getClass().getDeclaredMethods()) { 35 | Class[] params = method.getParameterTypes(); 36 | if (params.length == 1) { 37 | Class dependencyClass = params[0]; 38 | for (Object dependency : dependencies) { 39 | if (dependencyClass.isAssignableFrom(dependency.getClass())) { 40 | method.invoke(builder, dependency); 41 | break; 42 | } 43 | } 44 | } 45 | } 46 | //noinspection unchecked 47 | return (T) builder.getClass().getMethod("build").invoke(builder); 48 | } catch (Exception e) { 49 | throw new RuntimeException(e); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /mortar-hellodagger2/src/main/java/com/example/hellodagger2/HelloDagger2Activity.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.example.hellodagger2; 17 | 18 | import android.app.Activity; 19 | import android.os.Bundle; 20 | import mortar.MortarScope; 21 | import mortar.bundler.BundleServiceRunner; 22 | 23 | import static mortar.MortarScope.buildChild; 24 | import static mortar.MortarScope.findChild; 25 | import static com.example.hellodagger2.DaggerService.createComponent; 26 | 27 | public class HelloDagger2Activity extends Activity { 28 | @Override public Object getSystemService(String name) { 29 | MortarScope activityScope = findChild(getApplicationContext(), getScopeName()); 30 | 31 | if (activityScope == null) { 32 | activityScope = buildChild(getApplicationContext()) // 33 | .withService(BundleServiceRunner.SERVICE_NAME, new BundleServiceRunner()) 34 | .withService(DaggerService.SERVICE_NAME, createComponent(Main.Component.class)) 35 | .build(getScopeName()); 36 | } 37 | 38 | return activityScope.hasService(name) ? activityScope.getService(name) 39 | : super.getSystemService(name); 40 | } 41 | 42 | @Override protected void onCreate(Bundle savedInstanceState) { 43 | super.onCreate(savedInstanceState); 44 | 45 | BundleServiceRunner.getBundleServiceRunner(this).onCreate(savedInstanceState); 46 | setContentView(R.layout.main_view); 47 | } 48 | 49 | @Override protected void onSaveInstanceState(Bundle outState) { 50 | super.onSaveInstanceState(outState); 51 | BundleServiceRunner.getBundleServiceRunner(this).onSaveInstanceState(outState); 52 | } 53 | 54 | @Override protected void onDestroy() { 55 | if (isFinishing()) { 56 | MortarScope activityScope = findChild(getApplicationContext(), getScopeName()); 57 | if (activityScope != null) activityScope.destroy(); 58 | } 59 | 60 | super.onDestroy(); 61 | } 62 | 63 | private String getScopeName() { 64 | return getClass().getName(); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /mortar-hellodagger2/src/main/java/com/example/hellodagger2/HelloDagger2Application.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.example.hellodagger2; 17 | 18 | import android.app.Application; 19 | import mortar.MortarScope; 20 | 21 | public class HelloDagger2Application extends Application { 22 | private MortarScope rootScope; 23 | 24 | @Override public Object getSystemService(String name) { 25 | if (rootScope == null) rootScope = MortarScope.buildRootScope().build("Root"); 26 | 27 | return rootScope.hasService(name) ? rootScope.getService(name) : super.getSystemService(name); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /mortar-hellodagger2/src/main/java/com/example/hellodagger2/Main.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.example.hellodagger2; 17 | 18 | import android.os.Bundle; 19 | import java.text.DateFormat; 20 | import java.text.SimpleDateFormat; 21 | import java.util.Date; 22 | import javax.inject.Inject; 23 | import javax.inject.Singleton; 24 | import mortar.ViewPresenter; 25 | 26 | public class Main { 27 | 28 | @dagger.Component @Singleton interface Component { 29 | void inject(MainView t); 30 | } 31 | 32 | @Singleton 33 | static class Presenter extends ViewPresenter { 34 | private final DateFormat format = new SimpleDateFormat(); 35 | private int serial = -1; 36 | 37 | @Inject Presenter() { 38 | } 39 | 40 | @Override protected void onLoad(Bundle savedInstanceState) { 41 | if (savedInstanceState != null && serial == -1) serial = savedInstanceState.getInt("serial"); 42 | getView().show("Update #" + ++serial + " at " + format.format(new Date())); 43 | } 44 | 45 | @Override protected void onSave(Bundle outState) { 46 | outState.putInt("serial", serial); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /mortar-hellodagger2/src/main/java/com/example/hellodagger2/MainView.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.example.hellodagger2; 17 | 18 | import android.content.Context; 19 | import android.util.AttributeSet; 20 | import android.widget.LinearLayout; 21 | import android.widget.TextView; 22 | import javax.inject.Inject; 23 | 24 | public class MainView extends LinearLayout { 25 | @Inject Main.Presenter presenter; 26 | 27 | private TextView textView; 28 | 29 | public MainView(Context context, AttributeSet attrs) { 30 | super(context, attrs); 31 | DaggerService.getDaggerComponent(context).inject(this); 32 | } 33 | 34 | @Override protected void onFinishInflate() { 35 | super.onFinishInflate(); 36 | textView = (TextView) findViewById(R.id.text); 37 | } 38 | 39 | @Override protected void onAttachedToWindow() { 40 | super.onAttachedToWindow(); 41 | presenter.takeView(this); 42 | } 43 | 44 | @Override protected void onDetachedFromWindow() { 45 | presenter.dropView(this); 46 | super.onDetachedFromWindow(); 47 | } 48 | 49 | public void show(CharSequence stuff) { 50 | textView.setText(stuff); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /mortar-hellodagger2/src/main/res/drawable/mortar_helloworld_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/square/mortar/732e2459f6a01afa7270a2798df43fd92b6ca4cf/mortar-hellodagger2/src/main/res/drawable/mortar_helloworld_icon.png -------------------------------------------------------------------------------- /mortar-hellodagger2/src/main/res/layout/main_view.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 25 | 26 | 34 | 42 | 43 | -------------------------------------------------------------------------------- /mortar-helloworld/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | dependencies { 3 | classpath deps.android_gradle_plugin 4 | } 5 | } 6 | 7 | apply plugin: 'com.android.application' 8 | 9 | dependencies { 10 | compile project(':mortar') 11 | compile project(':mortar-dagger1') 12 | compile 'com.squareup.dagger:dagger:1.2.2' 13 | provided 'com.squareup.dagger:dagger-compiler:1.2.2' 14 | compile 'com.android.support:support-v4:21.0.3' 15 | } 16 | 17 | android { 18 | compileSdkVersion rootProject.ext.compileSdkVersion 19 | buildToolsVersion rootProject.ext.buildToolsVersion 20 | 21 | defaultConfig { 22 | minSdkVersion rootProject.ext.minSdkVersion 23 | versionName VERSION_NAME 24 | } 25 | 26 | lintOptions { 27 | abortOnError false 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /mortar-helloworld/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 22 | 23 | 27 | 28 | 34 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /mortar-helloworld/src/main/java/com/example/hellomortar/HelloActivity.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.example.hellomortar; 17 | 18 | import android.app.Activity; 19 | import android.os.Bundle; 20 | import mortar.MortarScope; 21 | import mortar.bundler.BundleServiceRunner; 22 | 23 | import static mortar.MortarScope.buildChild; 24 | import static mortar.MortarScope.findChild; 25 | 26 | public class HelloActivity extends Activity { 27 | @Override public Object getSystemService(String name) { 28 | MortarScope activityScope = findChild(getApplicationContext(), getScopeName()); 29 | 30 | if (activityScope == null) { 31 | activityScope = buildChild(getApplicationContext()) // 32 | .withService(BundleServiceRunner.SERVICE_NAME, new BundleServiceRunner()) 33 | .withService(HelloPresenter.class.getName(), new HelloPresenter()) 34 | .build(getScopeName()); 35 | } 36 | 37 | return activityScope.hasService(name) ? activityScope.getService(name) 38 | : super.getSystemService(name); 39 | } 40 | 41 | @Override protected void onCreate(Bundle savedInstanceState) { 42 | super.onCreate(savedInstanceState); 43 | 44 | BundleServiceRunner.getBundleServiceRunner(this).onCreate(savedInstanceState); 45 | setContentView(R.layout.main_view); 46 | } 47 | 48 | @Override protected void onSaveInstanceState(Bundle outState) { 49 | super.onSaveInstanceState(outState); 50 | BundleServiceRunner.getBundleServiceRunner(this).onSaveInstanceState(outState); 51 | } 52 | 53 | @Override protected void onDestroy() { 54 | if (isFinishing()) { 55 | MortarScope activityScope = findChild(getApplicationContext(), getScopeName()); 56 | if (activityScope != null) activityScope.destroy(); 57 | } 58 | 59 | super.onDestroy(); 60 | } 61 | 62 | private String getScopeName() { 63 | return getClass().getName(); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /mortar-helloworld/src/main/java/com/example/hellomortar/HelloApplication.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.example.hellomortar; 17 | 18 | import android.app.Application; 19 | import mortar.MortarScope; 20 | 21 | public class HelloApplication extends Application { 22 | private MortarScope rootScope; 23 | 24 | @Override public Object getSystemService(String name) { 25 | if (rootScope == null) rootScope = MortarScope.buildRootScope().build("Root"); 26 | 27 | return rootScope.hasService(name) ? rootScope.getService(name) : super.getSystemService(name); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /mortar-helloworld/src/main/java/com/example/hellomortar/HelloPresenter.java: -------------------------------------------------------------------------------- 1 | package com.example.hellomortar; 2 | 3 | import android.os.Bundle; 4 | import java.text.DateFormat; 5 | import java.text.SimpleDateFormat; 6 | import java.util.Date; 7 | import mortar.ViewPresenter; 8 | 9 | class HelloPresenter extends ViewPresenter { 10 | private final DateFormat format = new SimpleDateFormat(); 11 | private int serial = -1; 12 | 13 | @Override protected void onLoad(Bundle savedInstanceState) { 14 | if (savedInstanceState != null && serial == -1) serial = savedInstanceState.getInt("serial"); 15 | getView().show("Update #" + ++serial + " at " + format.format(new Date())); 16 | } 17 | 18 | @Override protected void onSave(Bundle outState) { 19 | outState.putInt("serial", serial); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /mortar-helloworld/src/main/java/com/example/hellomortar/HelloView.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.example.hellomortar; 17 | 18 | import android.content.Context; 19 | import android.util.AttributeSet; 20 | import android.widget.LinearLayout; 21 | import android.widget.TextView; 22 | 23 | public class HelloView extends LinearLayout { 24 | private final HelloPresenter presenter; 25 | 26 | private TextView textView; 27 | 28 | public HelloView(Context context, AttributeSet attrs) { 29 | super(context, attrs); 30 | presenter = (HelloPresenter) context.getSystemService(HelloPresenter.class.getName()); 31 | } 32 | 33 | @Override protected void onFinishInflate() { 34 | super.onFinishInflate(); 35 | textView = (TextView) findViewById(R.id.text); 36 | } 37 | 38 | @Override protected void onAttachedToWindow() { 39 | super.onAttachedToWindow(); 40 | presenter.takeView(this); 41 | } 42 | 43 | @Override protected void onDetachedFromWindow() { 44 | presenter.dropView(this); 45 | super.onDetachedFromWindow(); 46 | } 47 | 48 | public void show(CharSequence stuff) { 49 | textView.setText(stuff); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /mortar-helloworld/src/main/res/drawable/mortar_helloworld_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/square/mortar/732e2459f6a01afa7270a2798df43fd92b6ca4cf/mortar-helloworld/src/main/res/drawable/mortar_helloworld_icon.png -------------------------------------------------------------------------------- /mortar-helloworld/src/main/res/layout/main_view.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 25 | 26 | 34 | 42 | 43 | -------------------------------------------------------------------------------- /mortar-sample/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | dependencies { 3 | classpath deps.android_gradle_plugin 4 | } 5 | } 6 | 7 | apply plugin: 'com.android.application' 8 | 9 | dependencies { 10 | compile project(':mortar') 11 | compile project(':mortar-dagger1') 12 | compile 'com.squareup.flow:flow:0.9' 13 | compile 'com.squareup.flow:flow-path:0.9' 14 | compile 'com.squareup.retrofit:retrofit:1.6.1' 15 | compile 'com.google.code.gson:gson:2.3' 16 | compile 'com.squareup.dagger:dagger:1.2.2' 17 | provided 'com.squareup.dagger:dagger-compiler:1.2.2' 18 | compile 'com.android.support:support-v4:21.0.3' 19 | compile 'io.reactivex:rxjava:1.0.5' 20 | compile 'io.reactivex:rxandroid:0.24.0' 21 | } 22 | 23 | android { 24 | compileSdkVersion rootProject.ext.compileSdkVersion 25 | buildToolsVersion rootProject.ext.buildToolsVersion 26 | 27 | defaultConfig { 28 | minSdkVersion rootProject.ext.minSdkVersion 29 | versionName VERSION_NAME 30 | } 31 | 32 | lintOptions { 33 | abortOnError false 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /mortar-sample/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 22 | 23 | 27 | 28 | 29 | 30 | 36 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /mortar-sample/src/main/java/com/example/mortar/android/ActionBarOwner.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.example.mortar.android; 17 | 18 | import android.content.Context; 19 | import android.os.Bundle; 20 | import dagger.Module; 21 | import dagger.Provides; 22 | import javax.inject.Singleton; 23 | import mortar.Presenter; 24 | import mortar.bundler.BundleService; 25 | import rx.functions.Action0; 26 | 27 | import static mortar.bundler.BundleService.getBundleService; 28 | 29 | /** Allows shared configuration of the Android ActionBar. */ 30 | public class ActionBarOwner extends Presenter { 31 | public interface Activity { 32 | void setShowHomeEnabled(boolean enabled); 33 | 34 | void setUpButtonEnabled(boolean enabled); 35 | 36 | void setTitle(CharSequence title); 37 | 38 | void setMenu(MenuAction action); 39 | 40 | Context getContext(); 41 | } 42 | 43 | public static class Config { 44 | public final boolean showHomeEnabled; 45 | public final boolean upButtonEnabled; 46 | public final CharSequence title; 47 | public final MenuAction action; 48 | 49 | public Config(boolean showHomeEnabled, boolean upButtonEnabled, CharSequence title, 50 | MenuAction action) { 51 | this.showHomeEnabled = showHomeEnabled; 52 | this.upButtonEnabled = upButtonEnabled; 53 | this.title = title; 54 | this.action = action; 55 | } 56 | 57 | public Config withAction(MenuAction action) { 58 | return new Config(showHomeEnabled, upButtonEnabled, title, action); 59 | } 60 | } 61 | 62 | public static class MenuAction { 63 | public final CharSequence title; 64 | public final Action0 action; 65 | 66 | public MenuAction(CharSequence title, Action0 action) { 67 | this.title = title; 68 | this.action = action; 69 | } 70 | } 71 | 72 | private Config config; 73 | 74 | ActionBarOwner() { 75 | } 76 | 77 | @Override public void onLoad(Bundle savedInstanceState) { 78 | if (config != null) update(); 79 | } 80 | 81 | public void setConfig(Config config) { 82 | this.config = config; 83 | update(); 84 | } 85 | 86 | public Config getConfig() { 87 | return config; 88 | } 89 | 90 | @Override protected BundleService extractBundleService(Activity activity) { 91 | return getBundleService(activity.getContext()); 92 | } 93 | 94 | private void update() { 95 | if (!hasView()) return; 96 | Activity activity = getView(); 97 | 98 | activity.setShowHomeEnabled(config.showHomeEnabled); 99 | activity.setUpButtonEnabled(config.upButtonEnabled); 100 | activity.setTitle(config.title); 101 | activity.setMenu(config.action); 102 | } 103 | 104 | @Module(library = true) 105 | public static class ActionBarModule { 106 | @Provides @Singleton ActionBarOwner provideActionBarOwner() { 107 | return new ActionBarOwner(); 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /mortar-sample/src/main/java/com/example/mortar/core/MortarDemoActivity.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.example.mortar.core; 17 | 18 | import android.app.ActionBar; 19 | import android.content.Context; 20 | import android.content.Intent; 21 | import android.os.Bundle; 22 | import android.util.Log; 23 | import android.view.Menu; 24 | import android.view.MenuItem; 25 | import com.example.mortar.R; 26 | import com.example.mortar.android.ActionBarOwner; 27 | import com.example.mortar.screen.ChatListScreen; 28 | import com.example.mortar.screen.FriendListScreen; 29 | import com.example.mortar.screen.GsonParceler; 30 | import com.example.mortar.screen.HandlesBack; 31 | import com.google.gson.Gson; 32 | import flow.Flow; 33 | import flow.FlowDelegate; 34 | import flow.History; 35 | import flow.path.Path; 36 | import flow.path.PathContainerView; 37 | import javax.inject.Inject; 38 | import mortar.MortarScope; 39 | import mortar.MortarScopeDevHelper; 40 | import mortar.bundler.BundleServiceRunner; 41 | import mortar.dagger1support.ObjectGraphService; 42 | import rx.functions.Action0; 43 | 44 | import static android.view.MenuItem.SHOW_AS_ACTION_ALWAYS; 45 | import static mortar.bundler.BundleServiceRunner.getBundleServiceRunner; 46 | 47 | /** 48 | * A well intentioned but overly complex sample that demonstrates 49 | * the use of Mortar, Flow and Dagger in a single app. 50 | */ 51 | public class MortarDemoActivity extends android.app.Activity 52 | implements ActionBarOwner.Activity, Flow.Dispatcher { 53 | 54 | private MortarScope activityScope; 55 | private ActionBarOwner.MenuAction actionBarMenuAction; 56 | 57 | @Inject ActionBarOwner actionBarOwner; 58 | 59 | private PathContainerView container; 60 | private HandlesBack containerAsHandlesBack; 61 | private FlowDelegate flowDelegate; 62 | 63 | @Override public Context getContext() { 64 | return this; 65 | } 66 | 67 | @Override public void dispatch(Flow.Traversal traversal, Flow.TraversalCallback callback) { 68 | Path newScreen = traversal.destination.top(); 69 | String title = newScreen.getClass().getSimpleName(); 70 | ActionBarOwner.MenuAction menu = new ActionBarOwner.MenuAction("Friends", new Action0() { 71 | @Override public void call() { 72 | Flow.get(MortarDemoActivity.this).set(new FriendListScreen()); 73 | } 74 | }); 75 | actionBarOwner.setConfig( 76 | new ActionBarOwner.Config(false, !(newScreen instanceof ChatListScreen), title, menu)); 77 | 78 | container.dispatch(traversal, callback); 79 | } 80 | 81 | @Override protected void onCreate(Bundle savedInstanceState) { 82 | super.onCreate(savedInstanceState); 83 | 84 | GsonParceler parceler = new GsonParceler(new Gson()); 85 | @SuppressWarnings("deprecation") FlowDelegate.NonConfigurationInstance nonConfig = 86 | (FlowDelegate.NonConfigurationInstance) getLastNonConfigurationInstance(); 87 | 88 | MortarScope parentScope = MortarScope.getScope(getApplication()); 89 | 90 | String scopeName = getLocalClassName() + "-task-" + getTaskId(); 91 | 92 | activityScope = parentScope.findChild(scopeName); 93 | if (activityScope == null) { 94 | activityScope = parentScope.buildChild() 95 | .withService(BundleServiceRunner.SERVICE_NAME, new BundleServiceRunner()) 96 | .build(scopeName); 97 | } 98 | ObjectGraphService.inject(this, this); 99 | 100 | getBundleServiceRunner(activityScope).onCreate(savedInstanceState); 101 | 102 | actionBarOwner.takeView(this); 103 | 104 | setContentView(R.layout.root_layout); 105 | container = (PathContainerView) findViewById(R.id.container); 106 | containerAsHandlesBack = (HandlesBack) container; 107 | flowDelegate = FlowDelegate.onCreate(nonConfig, getIntent(), savedInstanceState, parceler, 108 | History.single(new ChatListScreen()), this); 109 | } 110 | 111 | @Override protected void onNewIntent(Intent intent) { 112 | super.onNewIntent(intent); 113 | flowDelegate.onNewIntent(intent); 114 | } 115 | 116 | @Override protected void onResume() { 117 | super.onResume(); 118 | flowDelegate.onResume(); 119 | } 120 | 121 | @Override protected void onPause() { 122 | flowDelegate.onPause(); 123 | super.onPause(); 124 | } 125 | 126 | @SuppressWarnings("deprecation") // https://code.google.com/p/android/issues/detail?id=151346 127 | @Override public Object onRetainNonConfigurationInstance() { 128 | return flowDelegate.onRetainNonConfigurationInstance(); 129 | } 130 | 131 | @Override public Object getSystemService(String name) { 132 | if (flowDelegate != null) { 133 | Object flowService = flowDelegate.getSystemService(name); 134 | if (flowService != null) return flowService; 135 | } 136 | 137 | return activityScope != null && activityScope.hasService(name) ? activityScope.getService(name) 138 | : super.getSystemService(name); 139 | } 140 | 141 | @Override protected void onSaveInstanceState(Bundle outState) { 142 | super.onSaveInstanceState(outState); 143 | flowDelegate.onSaveInstanceState(outState); 144 | getBundleServiceRunner(this). 145 | onSaveInstanceState(outState); 146 | } 147 | 148 | /** Inform the view about back events. */ 149 | @Override public void onBackPressed() { 150 | if (!containerAsHandlesBack.onBackPressed()) super.onBackPressed(); 151 | } 152 | 153 | /** Inform the view about up events. */ 154 | @Override public boolean onOptionsItemSelected(MenuItem item) { 155 | if (item.getItemId() == android.R.id.home) return containerAsHandlesBack.onBackPressed(); 156 | return super.onOptionsItemSelected(item); 157 | } 158 | 159 | /** Configure the action bar menu as required by {@link ActionBarOwner.Activity}. */ 160 | @Override public boolean onCreateOptionsMenu(Menu menu) { 161 | if (actionBarMenuAction != null) { 162 | menu.add(actionBarMenuAction.title) 163 | .setShowAsActionFlags(SHOW_AS_ACTION_ALWAYS) 164 | .setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() { 165 | @Override public boolean onMenuItemClick(MenuItem menuItem) { 166 | actionBarMenuAction.action.call(); 167 | return true; 168 | } 169 | }); 170 | } 171 | menu.add("Log Scope Hierarchy") 172 | .setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() { 173 | @Override public boolean onMenuItemClick(MenuItem item) { 174 | Log.d("DemoActivity", MortarScopeDevHelper.scopeHierarchyToString(activityScope)); 175 | return true; 176 | } 177 | }); 178 | return true; 179 | } 180 | 181 | @Override protected void onDestroy() { 182 | actionBarOwner.dropView(this); 183 | actionBarOwner.setConfig(null); 184 | 185 | // activityScope may be null in case isWrongInstance() returned true in onCreate() 186 | if (isFinishing() && activityScope != null) { 187 | activityScope.destroy(); 188 | activityScope = null; 189 | } 190 | 191 | super.onDestroy(); 192 | } 193 | 194 | @Override public void setShowHomeEnabled(boolean enabled) { 195 | ActionBar actionBar = getActionBar(); 196 | actionBar.setDisplayShowHomeEnabled(false); 197 | } 198 | 199 | @Override public void setUpButtonEnabled(boolean enabled) { 200 | ActionBar actionBar = getActionBar(); 201 | actionBar.setDisplayHomeAsUpEnabled(enabled); 202 | actionBar.setHomeButtonEnabled(enabled); 203 | } 204 | 205 | @Override public void setTitle(CharSequence title) { 206 | getActionBar().setTitle(title); 207 | } 208 | 209 | @Override public void setMenu(ActionBarOwner.MenuAction action) { 210 | if (action != actionBarMenuAction) { 211 | actionBarMenuAction = action; 212 | invalidateOptionsMenu(); 213 | } 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /mortar-sample/src/main/java/com/example/mortar/core/MortarDemoApplication.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.example.mortar.core; 17 | 18 | import android.app.Application; 19 | import dagger.ObjectGraph; 20 | import mortar.MortarScope; 21 | import mortar.dagger1support.ObjectGraphService; 22 | 23 | public class MortarDemoApplication extends Application { 24 | private MortarScope rootScope; 25 | 26 | @Override public Object getSystemService(String name) { 27 | if (rootScope == null) { 28 | rootScope = MortarScope.buildRootScope() 29 | .withService(ObjectGraphService.SERVICE_NAME, ObjectGraph.create(new RootModule())) 30 | .build("Root"); 31 | } 32 | 33 | if (rootScope.hasService(name)) return rootScope.getService(name); 34 | 35 | return super.getSystemService(name); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /mortar-sample/src/main/java/com/example/mortar/core/MortarScreenSwitcherFrame.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.example.mortar.core; 17 | 18 | import android.content.Context; 19 | import android.util.AttributeSet; 20 | import com.example.mortar.R; 21 | import com.example.mortar.mortarflow.MortarContextFactory; 22 | import com.example.mortar.screen.FramePathContainerView; 23 | import com.example.mortar.screen.SimplePathContainer; 24 | import flow.path.Path; 25 | 26 | public class MortarScreenSwitcherFrame extends FramePathContainerView { 27 | public MortarScreenSwitcherFrame(Context context, AttributeSet attrs) { 28 | super(context, attrs, new SimplePathContainer(R.id.screen_switcher_tag, 29 | Path.contextFactory(new MortarContextFactory()))); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /mortar-sample/src/main/java/com/example/mortar/core/RootModule.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.example.mortar.core; 17 | 18 | import com.example.mortar.android.ActionBarOwner; 19 | import com.example.mortar.model.Chats; 20 | import com.example.mortar.model.QuoteService; 21 | import com.example.mortar.screen.GsonParceler; 22 | import com.google.gson.Gson; 23 | import com.google.gson.GsonBuilder; 24 | import dagger.Module; 25 | import dagger.Provides; 26 | import flow.StateParceler; 27 | import javax.inject.Singleton; 28 | import retrofit.RestAdapter; 29 | import retrofit.converter.GsonConverter; 30 | 31 | /** 32 | * Defines app-wide singletons. 33 | */ 34 | @Module( 35 | includes = { ActionBarOwner.ActionBarModule.class, Chats.Module.class }, 36 | injects = MortarDemoActivity.class, 37 | library = true) 38 | public class RootModule { 39 | @Provides @Singleton Gson provideGson() { 40 | return new GsonBuilder().create(); 41 | } 42 | 43 | @Provides @Singleton StateParceler provideParcer(Gson gson) { 44 | return new GsonParceler(gson); 45 | } 46 | 47 | @Provides @Singleton QuoteService provideQuoteService() { 48 | RestAdapter restAdapter = 49 | new RestAdapter.Builder().setEndpoint("http://www.iheartquotes.com/api/v1/") 50 | .setConverter(new GsonConverter(new Gson())) 51 | .build(); 52 | return restAdapter.create(QuoteService.class); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /mortar-sample/src/main/java/com/example/mortar/model/Chat.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.example.mortar.model; 17 | 18 | import android.text.TextUtils; 19 | import java.util.List; 20 | import java.util.Random; 21 | import java.util.concurrent.CopyOnWriteArrayList; 22 | import retrofit.RetrofitError; 23 | import rx.Observable; 24 | import rx.Subscriber; 25 | import rx.android.schedulers.AndroidSchedulers; 26 | import rx.schedulers.Schedulers; 27 | 28 | public class Chat { 29 | private static final int SLEEP_MILLIS = 500; 30 | private static final int PROBABILITY = 3; 31 | 32 | private final int id; 33 | private final List users; 34 | private final List messages; 35 | 36 | private Chats chats; 37 | 38 | Chat(Chats chats, int id, List users, List seed) { 39 | this.chats = chats; 40 | this.id = id; 41 | this.users = users; 42 | messages = new CopyOnWriteArrayList<>(seed); 43 | } 44 | 45 | public int getId() { 46 | return id; 47 | } 48 | 49 | public Observable getMessage(int index) { 50 | return Observable.just(messages.get(index)); 51 | } 52 | 53 | public Observable getMessages() { 54 | return Observable.create(new Observable.OnSubscribe() { 55 | @Override public void call(final Subscriber subscriber) { 56 | final Random random = new Random(); 57 | while (true) { 58 | if (random.nextInt(PROBABILITY) == 0) { 59 | try { 60 | User from = users.get(random.nextInt(users.size())); 61 | Message next = new Message(from, chats.service.getQuote().quote); 62 | messages.add(next); 63 | if (!subscriber.isUnsubscribed()) { 64 | subscriber.onNext(next); 65 | } 66 | } catch (RetrofitError e) { 67 | if (!subscriber.isUnsubscribed()) { 68 | subscriber.onError(e); 69 | break; 70 | } 71 | } 72 | } 73 | 74 | try { 75 | // Hijacking the thread like this is sleazey, but you get the idea. 76 | Thread.sleep(SLEEP_MILLIS); 77 | } catch (InterruptedException e) { 78 | if (!subscriber.isUnsubscribed()) { 79 | subscriber.onError(e); 80 | } 81 | break; 82 | } 83 | } 84 | } 85 | }) 86 | .startWith(messages) // 87 | .subscribeOn(Schedulers.io()) // 88 | .observeOn(AndroidSchedulers.mainThread()); 89 | } 90 | 91 | @Override public String toString() { 92 | return TextUtils.join(", ", users.toArray(new User[users.size()])); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /mortar-sample/src/main/java/com/example/mortar/model/Chats.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.example.mortar.model; 17 | 18 | import dagger.Provides; 19 | import java.util.Collections; 20 | import java.util.List; 21 | import java.util.concurrent.Executor; 22 | import java.util.concurrent.Executors; 23 | import javax.inject.Inject; 24 | import javax.inject.Singleton; 25 | 26 | import static java.util.Arrays.asList; 27 | 28 | @Singleton 29 | public class Chats { 30 | private final List all; 31 | private final List friends; 32 | 33 | final User me = new User(-1, "Me"); 34 | 35 | final Executor messagePollThread; 36 | final QuoteService service; 37 | 38 | @Inject 39 | Chats(Executor messagePollThread, QuoteService service) { 40 | this.messagePollThread = messagePollThread; 41 | this.service = service; 42 | 43 | User alex = new User(0, "Alex"); 44 | User chris = new User(1, "Chris"); 45 | 46 | friends = asList(alex, chris); 47 | 48 | all = Collections.unmodifiableList(asList(// 49 | new Chat(this, 0, asList(alex, chris), // 50 | asList(new Message(me, "What's up?"), // 51 | new Message(alex, "Not much."), // 52 | new Message(chris, "Wanna hang out?"), // 53 | new Message(me, "Sure."), // 54 | new Message(alex, "Let's do it.") // 55 | )), // 56 | new Chat(this, 1, asList(chris), // 57 | asList(new Message(me, "You there bro?") // 58 | ))) // 59 | ); 60 | } 61 | 62 | public List getFriends() { 63 | return friends; 64 | } 65 | 66 | public User getFriend(int id) { 67 | return friends.get(id); 68 | } 69 | 70 | public List getAll() { 71 | return all; 72 | } 73 | 74 | public Chat getChat(int id) { 75 | return all.get(id); 76 | } 77 | 78 | @dagger.Module(injects = Chats.class, library = true, complete = false) 79 | public static class Module { 80 | 81 | @Provides @Singleton Executor provideMessagePollThread() { 82 | return Executors.newSingleThreadExecutor(); 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /mortar-sample/src/main/java/com/example/mortar/model/Message.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.example.mortar.model; 17 | 18 | public class Message { 19 | public final User from; 20 | public final String body; 21 | 22 | public Message(User from, String body) { 23 | this.from = from; 24 | this.body = body; 25 | } 26 | 27 | @Override public String toString() { 28 | return from + ": " + body; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /mortar-sample/src/main/java/com/example/mortar/model/QuoteService.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.example.mortar.model; 17 | 18 | import retrofit.http.GET; 19 | 20 | public interface QuoteService { 21 | class Quote { 22 | public final String quote; 23 | 24 | public Quote(String quote) { 25 | this.quote = quote; 26 | } 27 | } 28 | 29 | @GET("/random?format=json&source=zippy&show_permalink=false&show_source=false") // 30 | Quote getQuote(); 31 | } 32 | -------------------------------------------------------------------------------- /mortar-sample/src/main/java/com/example/mortar/model/User.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.mortar.model; 18 | 19 | public class User { 20 | public final int id; 21 | public final String name; 22 | 23 | public User(int id, String name) { 24 | this.id = id; 25 | this.name = name; 26 | } 27 | 28 | @Override public String toString() { 29 | return name; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /mortar-sample/src/main/java/com/example/mortar/mortarflow/MortarContextFactory.java: -------------------------------------------------------------------------------- 1 | package com.example.mortar.mortarflow; 2 | 3 | import android.content.Context; 4 | import android.content.ContextWrapper; 5 | import android.view.LayoutInflater; 6 | import com.example.mortar.mortarscreen.ScreenScoper; 7 | import flow.path.Path; 8 | import flow.path.PathContextFactory; 9 | import mortar.MortarScope; 10 | 11 | public final class MortarContextFactory implements PathContextFactory { 12 | private final ScreenScoper screenScoper = new ScreenScoper(); 13 | 14 | public MortarContextFactory() { 15 | } 16 | 17 | @Override public Context setUpContext(Path path, Context parentContext) { 18 | MortarScope screenScope = 19 | screenScoper.getScreenScope(parentContext, path.getClass().getName(), path); 20 | return new TearDownContext(parentContext, screenScope); 21 | } 22 | 23 | @Override public void tearDownContext(Context context) { 24 | TearDownContext.destroyScope(context); 25 | } 26 | 27 | static class TearDownContext extends ContextWrapper { 28 | private static final String SERVICE = "SNEAKY_MORTAR_PARENT_HOOK"; 29 | private final MortarScope parentScope; 30 | private LayoutInflater inflater; 31 | 32 | static void destroyScope(Context context) { 33 | MortarScope.getScope(context).destroy(); 34 | } 35 | 36 | public TearDownContext(Context context, MortarScope scope) { 37 | super(scope.createContext(context)); 38 | this.parentScope = MortarScope.getScope(context); 39 | } 40 | 41 | @Override public Object getSystemService(String name) { 42 | if (LAYOUT_INFLATER_SERVICE.equals(name)) { 43 | if (inflater == null) { 44 | inflater = LayoutInflater.from(getBaseContext()).cloneInContext(this); 45 | } 46 | return inflater; 47 | } 48 | 49 | if (SERVICE.equals(name)) { 50 | return parentScope; 51 | } 52 | 53 | return super.getSystemService(name); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /mortar-sample/src/main/java/com/example/mortar/mortarscreen/ModuleFactory.java: -------------------------------------------------------------------------------- 1 | package com.example.mortar.mortarscreen; 2 | 3 | import android.content.res.Resources; 4 | 5 | /** @see WithModuleFactory */ 6 | public abstract class ModuleFactory { 7 | protected abstract Object createDaggerModule(Resources resources, T screen); 8 | } 9 | -------------------------------------------------------------------------------- /mortar-sample/src/main/java/com/example/mortar/mortarscreen/ScreenScoper.java: -------------------------------------------------------------------------------- 1 | package com.example.mortar.mortarscreen; 2 | 3 | import android.content.Context; 4 | import android.content.res.Resources; 5 | import java.lang.reflect.Constructor; 6 | import java.lang.reflect.InvocationTargetException; 7 | import java.util.LinkedHashMap; 8 | import java.util.Map; 9 | import mortar.MortarScope; 10 | import mortar.dagger1support.ObjectGraphService; 11 | 12 | import static java.lang.String.format; 13 | 14 | /** 15 | * Creates {@link MortarScope}s for screens that may be annotated with {@link WithModuleFactory}, 16 | * {@link WithModule}. 17 | */ 18 | public class ScreenScoper { 19 | private static final ModuleFactory NO_FACTORY = new ModuleFactory() { 20 | @Override protected Object createDaggerModule(Resources resources, Object screen) { 21 | throw new UnsupportedOperationException(); 22 | } 23 | }; 24 | 25 | private final Map moduleFactoryCache = new LinkedHashMap<>(); 26 | 27 | public MortarScope getScreenScope(Context context, String name, Object screen) { 28 | MortarScope parentScope = MortarScope.getScope(context); 29 | return getScreenScope(context.getResources(), parentScope, name, screen); 30 | } 31 | 32 | /** 33 | * Finds or creates the scope for the given screen, honoring its optional {@link 34 | * WithModuleFactory} or {@link WithModule} annotation. Note that scopes are also created 35 | * for unannotated screens. 36 | */ 37 | public MortarScope getScreenScope(Resources resources, MortarScope parentScope, final String name, 38 | final Object screen) { 39 | ModuleFactory moduleFactory = getModuleFactory(screen); 40 | Object[] childModule; 41 | if (moduleFactory != NO_FACTORY) { 42 | childModule = new Object[]{ moduleFactory.createDaggerModule(resources, screen) }; 43 | } else { 44 | // We need every screen to have a scope, so that anything it injects is scoped. We need 45 | // this even if the screen doesn't declare a module, because Dagger allows injection of 46 | // objects that are annotated even if they don't appear in a module. 47 | childModule = new Object[0]; 48 | } 49 | 50 | MortarScope childScope = parentScope.findChild(name); 51 | if (childScope == null) { 52 | childScope = parentScope.buildChild() 53 | .withService(ObjectGraphService.SERVICE_NAME, 54 | ObjectGraphService.create(parentScope, childModule)) 55 | .build(name); 56 | } 57 | 58 | return childScope; 59 | } 60 | 61 | private ModuleFactory getModuleFactory(Object screen) { 62 | Class screenType = screen.getClass(); 63 | ModuleFactory moduleFactory = moduleFactoryCache.get(screenType); 64 | 65 | if (moduleFactory != null) return moduleFactory; 66 | 67 | WithModule withModule = screenType.getAnnotation(WithModule.class); 68 | if (withModule != null) { 69 | Class moduleClass = withModule.value(); 70 | 71 | Constructor[] constructors = moduleClass.getDeclaredConstructors(); 72 | 73 | if (constructors.length != 1) { 74 | throw new IllegalArgumentException( 75 | format("Module %s for screen %s should have exactly one public constructor", 76 | moduleClass.getName(), screen)); 77 | } 78 | 79 | Constructor constructor = constructors[0]; 80 | 81 | Class[] parameters = constructor.getParameterTypes(); 82 | 83 | if (parameters.length > 1) { 84 | throw new IllegalArgumentException( 85 | format("Module %s for screen %s should have 0 or 1 parameter", moduleClass.getName(), 86 | screen)); 87 | } 88 | 89 | Class screenParameter; 90 | if (parameters.length == 1) { 91 | screenParameter = parameters[0]; 92 | if (!screenParameter.isInstance(screen)) { 93 | throw new IllegalArgumentException(format("Module %s for screen %s should have a " 94 | + "constructor parameter that is a super class of %s", moduleClass.getName(), 95 | screen, screen.getClass().getName())); 96 | } 97 | } else { 98 | screenParameter = null; 99 | } 100 | 101 | try { 102 | if (screenParameter == null) { 103 | moduleFactory = new NoArgsFactory(constructor); 104 | } else { 105 | moduleFactory = new SingleArgFactory(constructor); 106 | } 107 | } catch (Exception e) { 108 | throw new RuntimeException( 109 | format("Failed to instantiate module %s for screen %s", moduleClass.getName(), screen), 110 | e); 111 | } 112 | } 113 | 114 | if (moduleFactory == null) { 115 | WithModuleFactory withModuleFactory = screenType.getAnnotation(WithModuleFactory.class); 116 | if (withModuleFactory != null) { 117 | Class mfClass = withModuleFactory.value(); 118 | 119 | try { 120 | moduleFactory = mfClass.newInstance(); 121 | } catch (Exception e) { 122 | throw new RuntimeException(format("Failed to instantiate module factory %s for screen %s", 123 | withModuleFactory.value().getName(), screen), e); 124 | } 125 | } 126 | } 127 | 128 | if (moduleFactory == null) moduleFactory = NO_FACTORY; 129 | 130 | moduleFactoryCache.put(screenType, moduleFactory); 131 | 132 | return moduleFactory; 133 | } 134 | 135 | private static class NoArgsFactory extends ModuleFactory { 136 | final Constructor moduleConstructor; 137 | 138 | private NoArgsFactory(Constructor moduleConstructor) { 139 | this.moduleConstructor = moduleConstructor; 140 | } 141 | 142 | @Override protected Object createDaggerModule(Resources resources, Object ignored) { 143 | try { 144 | return moduleConstructor.newInstance(); 145 | } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) { 146 | throw new RuntimeException(e); 147 | } 148 | } 149 | } 150 | 151 | private static class SingleArgFactory extends ModuleFactory { 152 | final Constructor moduleConstructor; 153 | 154 | public SingleArgFactory(Constructor moduleConstructor) { 155 | this.moduleConstructor = moduleConstructor; 156 | } 157 | 158 | @Override protected Object createDaggerModule(Resources resources, Object screen) { 159 | try { 160 | return moduleConstructor.newInstance(screen); 161 | } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) { 162 | throw new RuntimeException(e); 163 | } 164 | } 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /mortar-sample/src/main/java/com/example/mortar/mortarscreen/WithModule.java: -------------------------------------------------------------------------------- 1 | package com.example.mortar.mortarscreen; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | import mortar.MortarScope; 8 | 9 | /** 10 | * Marks a screen as defining a {@link MortarScope}, with the class of a Dagger module 11 | * to instantiate via reflection. The module must be a static type with a default 12 | * constructor. For more flexibility, use {@link WithModuleFactory}. 13 | * 14 | * @see ScreenScoper 15 | */ 16 | @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) 17 | public @interface WithModule { 18 | Class value(); 19 | } 20 | -------------------------------------------------------------------------------- /mortar-sample/src/main/java/com/example/mortar/mortarscreen/WithModuleFactory.java: -------------------------------------------------------------------------------- 1 | package com.example.mortar.mortarscreen; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | import mortar.MortarScope; 8 | 9 | /** 10 | * Marks a screen as defining a {@link MortarScope}, with a factory class to 11 | * create its Dagger module. 12 | * 13 | * @see WithModule 14 | * @see ScreenScoper 15 | */ 16 | @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) 17 | public @interface WithModuleFactory { 18 | Class value(); 19 | } 20 | -------------------------------------------------------------------------------- /mortar-sample/src/main/java/com/example/mortar/screen/BackSupport.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.mortar.screen; 18 | 19 | import android.view.View; 20 | import flow.Flow; 21 | 22 | /** 23 | * Support for {@link HandlesBack}. 24 | */ 25 | public class BackSupport { 26 | 27 | public static boolean onBackPressed(View childView) { 28 | if (childView instanceof HandlesBack) { 29 | if (((HandlesBack) childView).onBackPressed()) { 30 | return true; 31 | } 32 | } 33 | return Flow.get(childView).goBack(); 34 | } 35 | 36 | private BackSupport() { 37 | } 38 | } -------------------------------------------------------------------------------- /mortar-sample/src/main/java/com/example/mortar/screen/ChatListScreen.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.example.mortar.screen; 17 | 18 | import android.os.Bundle; 19 | import com.example.mortar.R; 20 | import com.example.mortar.core.RootModule; 21 | import com.example.mortar.model.Chat; 22 | import com.example.mortar.model.Chats; 23 | import com.example.mortar.mortarscreen.WithModule; 24 | import com.example.mortar.view.ChatListView; 25 | import dagger.Provides; 26 | import flow.Flow; 27 | import flow.path.Path; 28 | import java.util.List; 29 | import javax.inject.Inject; 30 | import javax.inject.Singleton; 31 | import mortar.ViewPresenter; 32 | 33 | @Layout(R.layout.chat_list_view) @WithModule(ChatListScreen.Module.class) 34 | public class ChatListScreen extends Path { 35 | 36 | @dagger.Module(injects = ChatListView.class, addsTo = RootModule.class) 37 | public static class Module { 38 | @Provides List provideConversations(Chats chats) { 39 | return chats.getAll(); 40 | } 41 | } 42 | 43 | @Singleton 44 | public static class Presenter extends ViewPresenter { 45 | private final List chats; 46 | 47 | @Inject Presenter(List chats) { 48 | this.chats = chats; 49 | } 50 | 51 | @Override public void onLoad(Bundle savedInstanceState) { 52 | super.onLoad(savedInstanceState); 53 | if (!hasView()) return; 54 | getView().showConversations(chats); 55 | } 56 | 57 | public void onConversationSelected(int position) { 58 | Flow.get(getView()).set(new ChatScreen(position)); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /mortar-sample/src/main/java/com/example/mortar/screen/ChatScreen.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.example.mortar.screen; 17 | 18 | import android.os.Bundle; 19 | import android.util.Log; 20 | import com.example.mortar.R; 21 | import com.example.mortar.android.ActionBarOwner; 22 | import com.example.mortar.core.RootModule; 23 | import com.example.mortar.model.Chat; 24 | import com.example.mortar.model.Chats; 25 | import com.example.mortar.model.Message; 26 | import com.example.mortar.mortarscreen.WithModule; 27 | import com.example.mortar.view.ChatView; 28 | import com.example.mortar.view.Confirmation; 29 | import dagger.Provides; 30 | import flow.Flow; 31 | import flow.path.Path; 32 | import javax.inject.Inject; 33 | import javax.inject.Singleton; 34 | import mortar.PopupPresenter; 35 | import mortar.ViewPresenter; 36 | import rx.Observer; 37 | import rx.Subscription; 38 | import rx.functions.Action0; 39 | import rx.subscriptions.Subscriptions; 40 | 41 | @Layout(R.layout.chat_view) @WithModule(ChatScreen.Module.class) 42 | public class ChatScreen extends Path { 43 | private final int conversationIndex; 44 | 45 | public ChatScreen(int conversationIndex) { 46 | this.conversationIndex = conversationIndex; 47 | } 48 | 49 | @dagger.Module(injects = ChatView.class, addsTo = RootModule.class) 50 | public class Module { 51 | @Provides Chat provideConversation(Chats chats) { 52 | return chats.getChat(conversationIndex); 53 | } 54 | } 55 | 56 | @Singleton 57 | public static class Presenter extends ViewPresenter { 58 | private final Chat chat; 59 | private final ActionBarOwner actionBar; 60 | private final PopupPresenter confirmer; 61 | 62 | private Subscription running = Subscriptions.empty(); 63 | 64 | @Inject 65 | public Presenter(Chat chat, ActionBarOwner actionBar) { 66 | this.chat = chat; 67 | this.actionBar = actionBar; 68 | this.confirmer = new PopupPresenter() { 69 | @Override protected void onPopupResult(Boolean confirmed) { 70 | if (confirmed) Presenter.this.getView().toast("Haven't implemented that, friend."); 71 | } 72 | }; 73 | } 74 | 75 | @Override public void dropView(ChatView view) { 76 | confirmer.dropView(view.getConfirmerPopup()); 77 | super.dropView(view); 78 | } 79 | 80 | @Override public void onLoad(Bundle savedInstanceState) { 81 | if (!hasView()) return; 82 | 83 | ActionBarOwner.Config actionBarConfig = actionBar.getConfig(); 84 | 85 | actionBarConfig = 86 | actionBarConfig.withAction(new ActionBarOwner.MenuAction("End", new Action0() { 87 | @Override public void call() { 88 | confirmer.show( 89 | new Confirmation("End Chat", "Do you really want to leave this chat?", "Yes", 90 | "I guess not")); 91 | } 92 | })); 93 | 94 | actionBar.setConfig(actionBarConfig); 95 | 96 | confirmer.takeView(getView().getConfirmerPopup()); 97 | 98 | running = chat.getMessages().subscribe(new Observer() { 99 | @Override public void onCompleted() { 100 | Log.w(getClass().getName(), "That's surprising, never thought this should end."); 101 | running = Subscriptions.empty(); 102 | } 103 | 104 | @Override public void onError(Throwable e) { 105 | Log.w(getClass().getName(), "'sploded, will try again on next config change."); 106 | Log.w(getClass().getName(), e); 107 | running = Subscriptions.empty(); 108 | } 109 | 110 | @Override public void onNext(Message message) { 111 | if (!hasView()) return; 112 | getView().getItems().add(message); 113 | } 114 | }); 115 | } 116 | 117 | @Override protected void onExitScope() { 118 | ensureStopped(); 119 | } 120 | 121 | public void onConversationSelected(int position) { 122 | Flow.get(getView().getContext()).set(new MessageScreen(chat.getId(), position)); 123 | } 124 | 125 | public void visibilityChanged(boolean visible) { 126 | if (!visible) { 127 | ensureStopped(); 128 | } 129 | } 130 | 131 | private void ensureStopped() { 132 | running.unsubscribe(); 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /mortar-sample/src/main/java/com/example/mortar/screen/FramePathContainerView.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.mortar.screen; 18 | 19 | import android.content.Context; 20 | import android.util.AttributeSet; 21 | import android.view.MotionEvent; 22 | import android.view.ViewGroup; 23 | import android.widget.FrameLayout; 24 | import com.example.mortar.R; 25 | import flow.Flow; 26 | import flow.path.Path; 27 | import flow.path.PathContainer; 28 | import flow.path.PathContainerView; 29 | 30 | 31 | /** A FrameLayout that can show screens for a {@link flow.Flow}. */ 32 | public class FramePathContainerView extends FrameLayout 33 | implements HandlesBack, PathContainerView { 34 | private final PathContainer container; 35 | private boolean disabled; 36 | 37 | @SuppressWarnings("UnusedDeclaration") // Used by layout inflation, of course! 38 | public FramePathContainerView(Context context, AttributeSet attrs) { 39 | this(context, attrs, new SimplePathContainer(R.id.screen_switcher_tag, Path.contextFactory())); 40 | } 41 | 42 | /** 43 | * Allows subclasses to use custom {@link flow.path.PathContainer} implementations. Allows the use 44 | * of more sophisticated transition schemes, and customized context wrappers. 45 | */ 46 | protected FramePathContainerView(Context context, AttributeSet attrs, PathContainer container) { 47 | super(context, attrs); 48 | this.container = container; 49 | } 50 | 51 | @Override public boolean dispatchTouchEvent(MotionEvent ev) { 52 | return !disabled && super.dispatchTouchEvent(ev); 53 | } 54 | 55 | @Override public ViewGroup getContainerView() { 56 | return this; 57 | } 58 | 59 | @Override public void dispatch(Flow.Traversal traversal, final Flow.TraversalCallback callback) { 60 | disabled = true; 61 | container.executeTraversal(this, traversal, new Flow.TraversalCallback() { 62 | @Override public void onTraversalCompleted() { 63 | callback.onTraversalCompleted(); 64 | disabled = false; 65 | } 66 | }); 67 | } 68 | 69 | @Override public boolean onBackPressed() { 70 | return BackSupport.onBackPressed(getCurrentChild()); 71 | } 72 | 73 | @Override public ViewGroup getCurrentChild() { 74 | return (ViewGroup) getContainerView().getChildAt(0); 75 | } 76 | } -------------------------------------------------------------------------------- /mortar-sample/src/main/java/com/example/mortar/screen/FriendListScreen.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.example.mortar.screen; 17 | 18 | import android.os.Bundle; 19 | import com.example.mortar.R; 20 | import com.example.mortar.core.RootModule; 21 | import com.example.mortar.model.Chats; 22 | import com.example.mortar.model.User; 23 | import com.example.mortar.mortarscreen.WithModule; 24 | import com.example.mortar.view.FriendListView; 25 | import dagger.Provides; 26 | import flow.Flow; 27 | import flow.path.Path; 28 | import java.util.List; 29 | import javax.inject.Inject; 30 | import javax.inject.Singleton; 31 | import mortar.ViewPresenter; 32 | 33 | @Layout(R.layout.friend_list_view) @WithModule(FriendListScreen.Module.class) 34 | public class FriendListScreen extends Path { 35 | 36 | @dagger.Module(injects = FriendListView.class, addsTo = RootModule.class) 37 | public static class Module { 38 | @Provides List provideFriends(Chats chats) { 39 | return chats.getFriends(); 40 | } 41 | } 42 | 43 | @Singleton 44 | public static class Presenter extends ViewPresenter { 45 | private final List friends; 46 | 47 | @Inject Presenter(List friends) { 48 | this.friends = friends; 49 | } 50 | 51 | @Override public void onLoad(Bundle savedInstanceState) { 52 | super.onLoad(savedInstanceState); 53 | if (!hasView()) return; 54 | getView().showFriends(friends); 55 | } 56 | 57 | public void onFriendSelected(int position) { 58 | Flow.get(getView()).set(new FriendScreen(position)); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /mortar-sample/src/main/java/com/example/mortar/screen/FriendScreen.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.example.mortar.screen; 17 | 18 | import android.os.Bundle; 19 | import com.example.mortar.R; 20 | import com.example.mortar.core.RootModule; 21 | import com.example.mortar.model.Chats; 22 | import com.example.mortar.model.User; 23 | import com.example.mortar.mortarscreen.WithModule; 24 | import com.example.mortar.view.FriendView; 25 | import dagger.Provides; 26 | import flow.path.Path; 27 | import javax.inject.Inject; 28 | import javax.inject.Singleton; 29 | import mortar.ViewPresenter; 30 | 31 | @Layout(R.layout.friend_view) @WithModule(FriendScreen.Module.class) 32 | public class FriendScreen extends Path { 33 | private final int index; 34 | 35 | public FriendScreen(int index) { 36 | this.index = index; 37 | } 38 | 39 | @dagger.Module(injects = FriendView.class, addsTo = RootModule.class) 40 | public class Module { 41 | @Provides User provideFriend(Chats chats) { 42 | return chats.getFriend(index); 43 | } 44 | } 45 | 46 | @Singleton 47 | public static class Presenter extends ViewPresenter { 48 | private final User friend; 49 | 50 | @Inject Presenter(User friend) { 51 | this.friend = friend; 52 | } 53 | 54 | @Override public void onLoad(Bundle savedInstanceState) { 55 | super.onLoad(savedInstanceState); 56 | if (!hasView()) return; 57 | getView().setText(friend.name); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /mortar-sample/src/main/java/com/example/mortar/screen/GsonParceler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.example.mortar.screen; 17 | 18 | import android.os.Parcel; 19 | import android.os.Parcelable; 20 | import com.google.gson.Gson; 21 | import com.google.gson.stream.JsonReader; 22 | import com.google.gson.stream.JsonWriter; 23 | import flow.StateParceler; 24 | import java.io.IOException; 25 | import java.io.StringReader; 26 | import java.io.StringWriter; 27 | 28 | public class GsonParceler implements StateParceler { 29 | private final Gson gson; 30 | 31 | public GsonParceler(Gson gson) { 32 | this.gson = gson; 33 | } 34 | 35 | @Override public Parcelable wrap(Object instance) { 36 | try { 37 | String json = encode(instance); 38 | return new Wrapper(json); 39 | } catch (IOException e) { 40 | throw new RuntimeException(e); 41 | } 42 | } 43 | 44 | @Override public Object unwrap(Parcelable parcelable) { 45 | Wrapper wrapper = (Wrapper) parcelable; 46 | try { 47 | return decode(wrapper.json); 48 | } catch (IOException e) { 49 | throw new RuntimeException(e); 50 | } 51 | } 52 | 53 | private String encode(Object instance) throws IOException { 54 | StringWriter stringWriter = new StringWriter(); 55 | JsonWriter writer = new JsonWriter(stringWriter); 56 | 57 | try { 58 | Class type = instance.getClass(); 59 | 60 | writer.beginObject(); 61 | writer.name(type.getName()); 62 | gson.toJson(instance, type, writer); 63 | writer.endObject(); 64 | 65 | return stringWriter.toString(); 66 | } finally { 67 | writer.close(); 68 | } 69 | } 70 | 71 | private Object decode(String json) throws IOException { 72 | JsonReader reader = new JsonReader(new StringReader(json)); 73 | 74 | try { 75 | reader.beginObject(); 76 | 77 | Class type = Class.forName(reader.nextName()); 78 | return gson.fromJson(reader, type); 79 | } catch (ClassNotFoundException e) { 80 | throw new RuntimeException(e); 81 | } finally { 82 | reader.close(); 83 | } 84 | } 85 | 86 | private static class Wrapper implements Parcelable { 87 | final String json; 88 | 89 | Wrapper(String json) { 90 | this.json = json; 91 | } 92 | 93 | @Override public int describeContents() { 94 | return 0; 95 | } 96 | 97 | @Override public void writeToParcel(Parcel out, int flags) { 98 | out.writeString(json); 99 | } 100 | 101 | public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { 102 | @Override public Wrapper createFromParcel(Parcel in) { 103 | String json = in.readString(); 104 | return new Wrapper(json); 105 | } 106 | 107 | @Override public Wrapper[] newArray(int size) { 108 | return new Wrapper[size]; 109 | } 110 | }; 111 | } 112 | } -------------------------------------------------------------------------------- /mortar-sample/src/main/java/com/example/mortar/screen/HandlesBack.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.mortar.screen; 18 | 19 | /** 20 | * Implemented by views that want the option to intercept back button taps. If a view has subviews 21 | * that implement this interface their {@link #onBackPressed()} method should be invoked before 22 | * any of this view's own logic. 23 | *

24 | * 25 | * The typical flow of back button handling starts in the {@link android.app.Activity#onBackPressed()} 26 | * calling {@link #onBackPressed()} on its content view. Each view in turn delegates to its 27 | * child views to give them first say. 28 | */ 29 | public interface HandlesBack { 30 | /** 31 | * Returns true if back event was handled, false if someone higher in 32 | * the chain should. 33 | */ 34 | boolean onBackPressed(); 35 | } -------------------------------------------------------------------------------- /mortar-sample/src/main/java/com/example/mortar/screen/Layout.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.mortar.screen; 18 | 19 | import java.lang.annotation.Retention; 20 | import java.lang.annotation.Target; 21 | 22 | import static java.lang.annotation.ElementType.TYPE; 23 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 24 | 25 | /** 26 | * Marks a class that designates a screen and specifies its layout. A screen is a distinct part of 27 | * an application containing all information that describes this state. 28 | * 29 | *

For example,


30 |  * {@literal@}Layout(R.layout.conversation_screen_layout)
31 |  * public class ConversationScreen { ... }
32 |  * 
33 | */ 34 | @Retention(RUNTIME) @Target(TYPE) public @interface Layout { 35 | int value(); 36 | } -------------------------------------------------------------------------------- /mortar-sample/src/main/java/com/example/mortar/screen/Layouts.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.example.mortar.screen; 17 | 18 | import android.content.Context; 19 | import android.view.LayoutInflater; 20 | 21 | public final class Layouts { 22 | 23 | /** Create an instance of the view specified in a {@link Layout} annotation. */ 24 | public static android.view.View createView(Context context, Object screen) { 25 | return createView(context, screen.getClass()); 26 | } 27 | 28 | /** Create an instance of the view specified in a {@link Layout} annotation. */ 29 | public static android.view.View createView(Context context, Class screenType) { 30 | Layout screen = screenType.getAnnotation(Layout.class); 31 | if (screen == null) { 32 | throw new IllegalArgumentException( 33 | String.format("@%s annotation not found on class %s", Layout.class.getSimpleName(), 34 | screenType.getName())); 35 | } 36 | 37 | int layout = screen.value(); 38 | return inflateLayout(context, layout); 39 | } 40 | 41 | private static android.view.View inflateLayout(Context context, int layoutId) { 42 | return LayoutInflater.from(context).inflate(layoutId, null); 43 | } 44 | 45 | private Layouts() { 46 | } 47 | } -------------------------------------------------------------------------------- /mortar-sample/src/main/java/com/example/mortar/screen/MessageScreen.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.example.mortar.screen; 17 | 18 | import android.os.Bundle; 19 | import com.example.mortar.R; 20 | import com.example.mortar.core.RootModule; 21 | import com.example.mortar.model.Chats; 22 | import com.example.mortar.model.Message; 23 | import com.example.mortar.mortarscreen.WithModule; 24 | import com.example.mortar.view.MessageView; 25 | import dagger.Provides; 26 | import flow.Flow; 27 | import flow.path.Path; 28 | import javax.inject.Inject; 29 | import javax.inject.Singleton; 30 | import mortar.ViewPresenter; 31 | import rx.Observable; 32 | import rx.functions.Action1; 33 | 34 | @Layout(R.layout.message_view) @WithModule(MessageScreen.Module.class) 35 | public class MessageScreen extends Path { 36 | private final int chatId; 37 | private final int messageId; 38 | 39 | public MessageScreen(int chatId, int messageId) { 40 | this.chatId = chatId; 41 | this.messageId = messageId; 42 | } 43 | 44 | @dagger.Module(injects = MessageView.class, addsTo = RootModule.class) 45 | public class Module { 46 | @Provides Observable provideMessage(Chats chats) { 47 | return chats.getChat(chatId).getMessage(messageId); 48 | } 49 | } 50 | 51 | @Singleton 52 | public static class Presenter extends ViewPresenter { 53 | private final Observable messageSource; 54 | 55 | private Message message; 56 | 57 | @Inject Presenter(Observable messageSource) { 58 | this.messageSource = messageSource; 59 | } 60 | 61 | @Override public void onLoad(Bundle savedInstanceState) { 62 | super.onLoad(savedInstanceState); 63 | if (!hasView()) return; 64 | 65 | messageSource.subscribe(new Action1() { 66 | @Override public void call(Message message) { 67 | if (!hasView()) return; 68 | Presenter.this.message = message; 69 | MessageView view = getView(); 70 | view.setUser(message.from.name); 71 | view.setMessage(message.body); 72 | } 73 | }); 74 | } 75 | 76 | public void onUserSelected() { 77 | if (message == null) return; 78 | int position = message.from.id; 79 | if (position != -1) { 80 | Flow.get(getView()).set(new FriendScreen(position)); 81 | } 82 | } 83 | } 84 | } 85 | 86 | 87 | -------------------------------------------------------------------------------- /mortar-sample/src/main/java/com/example/mortar/screen/SimplePathContainer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.mortar.screen; 18 | 19 | import android.animation.Animator; 20 | import android.animation.AnimatorListenerAdapter; 21 | import android.animation.AnimatorSet; 22 | import android.animation.ObjectAnimator; 23 | import android.view.LayoutInflater; 24 | import android.view.View; 25 | import android.view.ViewGroup; 26 | import flow.Flow; 27 | import flow.path.Path; 28 | import flow.path.PathContainer; 29 | import flow.path.PathContext; 30 | import flow.path.PathContextFactory; 31 | import java.util.LinkedHashMap; 32 | import java.util.Map; 33 | 34 | import static flow.Flow.Direction.REPLACE; 35 | 36 | /** 37 | * Provides basic right-to-left transitions. Saves and restores view state. 38 | * Uses {@link PathContext} to allow customized sub-containers. 39 | */ 40 | public class SimplePathContainer extends PathContainer { 41 | private static final Map PATH_LAYOUT_CACHE = new LinkedHashMap<>(); 42 | private final PathContextFactory contextFactory; 43 | 44 | public SimplePathContainer(int tagKey, PathContextFactory contextFactory) { 45 | super(tagKey); 46 | this.contextFactory = contextFactory; 47 | } 48 | 49 | @Override protected void performTraversal(final ViewGroup containerView, 50 | final TraversalState traversalState, final Flow.Direction direction, 51 | final Flow.TraversalCallback callback) { 52 | 53 | final PathContext context; 54 | final PathContext oldPath; 55 | if (containerView.getChildCount() > 0) { 56 | oldPath = PathContext.get(containerView.getChildAt(0).getContext()); 57 | } else { 58 | oldPath = PathContext.root(containerView.getContext()); 59 | } 60 | 61 | Path to = traversalState.toPath(); 62 | 63 | View newView; 64 | context = PathContext.create(oldPath, to, contextFactory); 65 | int layout = getLayout(to); 66 | newView = LayoutInflater.from(context) 67 | .cloneInContext(context) 68 | .inflate(layout, containerView, false); 69 | 70 | View fromView = null; 71 | if (traversalState.fromPath() != null) { 72 | fromView = containerView.getChildAt(0); 73 | traversalState.saveViewState(fromView); 74 | } 75 | traversalState.restoreViewState(newView); 76 | 77 | if (fromView == null || direction == REPLACE) { 78 | containerView.removeAllViews(); 79 | containerView.addView(newView); 80 | oldPath.destroyNotIn(context, contextFactory); 81 | callback.onTraversalCompleted(); 82 | } else { 83 | containerView.addView(newView); 84 | final View finalFromView = fromView; 85 | Utils.waitForMeasure(newView, new Utils.OnMeasuredCallback() { 86 | @Override public void onMeasured(View view, int width, int height) { 87 | runAnimation(containerView, finalFromView, view, direction, new Flow.TraversalCallback() { 88 | @Override public void onTraversalCompleted() { 89 | containerView.removeView(finalFromView); 90 | oldPath.destroyNotIn(context, contextFactory); 91 | callback.onTraversalCompleted(); 92 | } 93 | }); 94 | } 95 | }); 96 | } 97 | } 98 | 99 | protected int getLayout(Path path) { 100 | Class pathType = path.getClass(); 101 | Integer layoutResId = PATH_LAYOUT_CACHE.get(pathType); 102 | if (layoutResId == null) { 103 | Layout layout = (Layout) pathType.getAnnotation(Layout.class); 104 | if (layout == null) { 105 | throw new IllegalArgumentException( 106 | String.format("@%s annotation not found on class %s", Layout.class.getSimpleName(), 107 | pathType.getName())); 108 | } 109 | layoutResId = layout.value(); 110 | PATH_LAYOUT_CACHE.put(pathType, layoutResId); 111 | } 112 | return layoutResId; 113 | } 114 | 115 | private void runAnimation(final ViewGroup container, final View from, final View to, 116 | Flow.Direction direction, final Flow.TraversalCallback callback) { 117 | Animator animator = createSegue(from, to, direction); 118 | animator.addListener(new AnimatorListenerAdapter() { 119 | @Override public void onAnimationEnd(Animator animation) { 120 | container.removeView(from); 121 | callback.onTraversalCompleted(); 122 | } 123 | }); 124 | animator.start(); 125 | } 126 | 127 | private Animator createSegue(View from, View to, Flow.Direction direction) { 128 | boolean backward = direction == Flow.Direction.BACKWARD; 129 | int fromTranslation = backward ? from.getWidth() : -from.getWidth(); 130 | int toTranslation = backward ? -to.getWidth() : to.getWidth(); 131 | 132 | AnimatorSet set = new AnimatorSet(); 133 | 134 | set.play(ObjectAnimator.ofFloat(from, View.TRANSLATION_X, fromTranslation)); 135 | set.play(ObjectAnimator.ofFloat(to, View.TRANSLATION_X, toTranslation, 0)); 136 | 137 | return set; 138 | } 139 | } -------------------------------------------------------------------------------- /mortar-sample/src/main/java/com/example/mortar/screen/Utils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.mortar.screen; 18 | 19 | import android.view.View; 20 | import android.view.ViewTreeObserver; 21 | 22 | public final class Utils { 23 | public interface OnMeasuredCallback { 24 | void onMeasured(View view, int width, int height); 25 | } 26 | 27 | public static void waitForMeasure(final View view, final OnMeasuredCallback callback) { 28 | int width = view.getWidth(); 29 | int height = view.getHeight(); 30 | 31 | if (width > 0 && height > 0) { 32 | callback.onMeasured(view, width, height); 33 | return; 34 | } 35 | 36 | view.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { 37 | @Override public boolean onPreDraw() { 38 | final ViewTreeObserver observer = view.getViewTreeObserver(); 39 | if (observer.isAlive()) { 40 | observer.removeOnPreDrawListener(this); 41 | } 42 | 43 | callback.onMeasured(view, view.getWidth(), view.getHeight()); 44 | 45 | return true; 46 | } 47 | }); 48 | } 49 | 50 | private Utils() { 51 | } 52 | } -------------------------------------------------------------------------------- /mortar-sample/src/main/java/com/example/mortar/view/ChatListView.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.mortar.view; 18 | 19 | import android.content.Context; 20 | import android.util.AttributeSet; 21 | import android.view.View; 22 | import android.widget.AdapterView; 23 | import android.widget.ArrayAdapter; 24 | import android.widget.ListView; 25 | import mortar.dagger1support.ObjectGraphService; 26 | import com.example.mortar.model.Chat; 27 | import com.example.mortar.screen.ChatListScreen; 28 | import java.util.List; 29 | import javax.inject.Inject; 30 | 31 | public class ChatListView extends ListView { 32 | @Inject ChatListScreen.Presenter presenter; 33 | 34 | public ChatListView(Context context, AttributeSet attrs) { 35 | super(context, attrs); 36 | ObjectGraphService.inject(context, this); 37 | } 38 | 39 | @Override protected void onAttachedToWindow() { 40 | super.onAttachedToWindow(); 41 | presenter.takeView(this); 42 | } 43 | 44 | @Override protected void onDetachedFromWindow() { 45 | super.onDetachedFromWindow(); 46 | presenter.dropView(this); 47 | } 48 | 49 | public void showConversations(List chats) { 50 | Adapter adapter = new Adapter(getContext(), chats); 51 | 52 | setAdapter(adapter); 53 | setOnItemClickListener(new OnItemClickListener() { 54 | @Override public void onItemClick(AdapterView parent, View view, int position, long id) { 55 | presenter.onConversationSelected(position); 56 | } 57 | }); 58 | } 59 | 60 | private static class Adapter extends ArrayAdapter { 61 | public Adapter(Context context, List objects) { 62 | super(context, android.R.layout.simple_list_item_1, objects); 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /mortar-sample/src/main/java/com/example/mortar/view/ChatView.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.mortar.view; 18 | 19 | import android.content.Context; 20 | import android.util.AttributeSet; 21 | import android.view.View; 22 | import android.widget.AdapterView; 23 | import android.widget.ArrayAdapter; 24 | import android.widget.ListView; 25 | import android.widget.Toast; 26 | import mortar.dagger1support.ObjectGraphService; 27 | import com.example.mortar.model.Message; 28 | import com.example.mortar.screen.ChatScreen; 29 | import javax.inject.Inject; 30 | 31 | public class ChatView extends ListView { 32 | @Inject ChatScreen.Presenter presenter; 33 | 34 | private final ConfirmerPopup confirmerPopup; 35 | 36 | public ChatView(Context context, AttributeSet attrs) { 37 | super(context, attrs); 38 | ObjectGraphService.inject(context, this); 39 | confirmerPopup = new ConfirmerPopup(context); 40 | 41 | setTranscriptMode(ListView.TRANSCRIPT_MODE_NORMAL); 42 | } 43 | 44 | @Override protected void onAttachedToWindow() { 45 | super.onAttachedToWindow(); 46 | presenter.takeView(this); 47 | } 48 | 49 | @Override protected void onDetachedFromWindow() { 50 | super.onDetachedFromWindow(); 51 | presenter.dropView(this); 52 | } 53 | 54 | @Override protected void onWindowVisibilityChanged(int visibility) { 55 | super.onWindowVisibilityChanged(visibility); 56 | presenter.visibilityChanged(visibility == VISIBLE); 57 | } 58 | 59 | public ConfirmerPopup getConfirmerPopup() { 60 | return confirmerPopup; 61 | } 62 | 63 | public ArrayAdapter getItems() { 64 | @SuppressWarnings("unchecked") ArrayAdapter adapter = 65 | (ArrayAdapter) getAdapter(); 66 | 67 | if (adapter == null) { 68 | adapter = new ArrayAdapter<>(getContext(), android.R.layout.simple_list_item_1); 69 | setAdapter(adapter); 70 | adapter.setNotifyOnChange(true); 71 | setOnItemClickListener(new OnItemClickListener() { 72 | @Override public void onItemClick(AdapterView parent, View view, int position, long id) { 73 | presenter.onConversationSelected(position); 74 | } 75 | }); 76 | } 77 | 78 | return adapter; 79 | } 80 | 81 | public void toast(String message) { 82 | Toast.makeText(getContext(), message, Toast.LENGTH_LONG).show(); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /mortar-sample/src/main/java/com/example/mortar/view/Confirmation.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.example.mortar.view; 17 | 18 | import android.os.Parcel; 19 | import android.os.Parcelable; 20 | 21 | /** Messages displayed by a {@link ConfirmerPopup}. */ 22 | public class Confirmation implements Parcelable { 23 | public final String title; 24 | public final String body; 25 | public final String confirm; 26 | public final String cancel; 27 | 28 | public Confirmation(String title, String body, String confirm, String cancel) { 29 | this.title = title; 30 | this.body = body; 31 | this.confirm = confirm; 32 | this.cancel = cancel; 33 | } 34 | 35 | @Override 36 | public boolean equals(Object o) { 37 | if (this == o) return true; 38 | if (o == null || getClass() != o.getClass()) return false; 39 | 40 | Confirmation that = (Confirmation) o; 41 | 42 | return body.equals(that.body) 43 | && cancel.equals(that.cancel) 44 | && confirm.equals(that.confirm) 45 | && title.equals(that.title); 46 | } 47 | 48 | private static final int HASH_PRIME = 31; 49 | @Override 50 | public int hashCode() { 51 | int result = title.hashCode(); 52 | result = HASH_PRIME * result + body.hashCode(); 53 | result = HASH_PRIME * result + confirm.hashCode(); 54 | result = HASH_PRIME * result + cancel.hashCode(); 55 | return result; 56 | } 57 | 58 | @Override public int describeContents() { 59 | return 0; 60 | } 61 | 62 | @Override public void writeToParcel(Parcel parcel, int i) { 63 | parcel.writeString(title); 64 | parcel.writeString(body); 65 | parcel.writeString(confirm); 66 | parcel.writeString(cancel); 67 | } 68 | 69 | @SuppressWarnings("UnusedDeclaration") 70 | public static final Creator CREATOR = new Creator() { 71 | @Override public Confirmation createFromParcel(Parcel parcel) { 72 | return new Confirmation(parcel.readString(), parcel.readString(), parcel.readString(), 73 | parcel.readString()); 74 | } 75 | 76 | @Override public Confirmation[] newArray(int size) { 77 | return new Confirmation[size]; 78 | } 79 | }; 80 | } 81 | -------------------------------------------------------------------------------- /mortar-sample/src/main/java/com/example/mortar/view/ConfirmerPopup.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.example.mortar.view; 17 | 18 | import android.app.AlertDialog; 19 | import android.content.Context; 20 | import android.content.DialogInterface; 21 | import mortar.Popup; 22 | import mortar.PopupPresenter; 23 | 24 | public class ConfirmerPopup implements Popup { 25 | private final Context context; 26 | 27 | private AlertDialog dialog; 28 | 29 | public ConfirmerPopup(Context context) { 30 | this.context = context; 31 | } 32 | 33 | @Override public Context getContext() { 34 | return context; 35 | } 36 | 37 | @Override 38 | public void show(Confirmation info, boolean withFlourish, 39 | final PopupPresenter presenter) { 40 | if (dialog != null) throw new IllegalStateException("Already showing, can't show " + info); 41 | 42 | dialog = new AlertDialog.Builder(context).setTitle(info.title) 43 | .setMessage(info.body) 44 | .setPositiveButton(info.confirm, new DialogInterface.OnClickListener() { 45 | @Override public void onClick(DialogInterface d, int which) { 46 | dialog = null; 47 | presenter.onDismissed(true); 48 | } 49 | }) 50 | .setNegativeButton(info.cancel, new DialogInterface.OnClickListener() { 51 | @Override public void onClick(DialogInterface d, int which) { 52 | dialog = null; 53 | presenter.onDismissed(false); 54 | } 55 | }) 56 | .setCancelable(true) 57 | .setOnCancelListener(new DialogInterface.OnCancelListener() { 58 | @Override public void onCancel(DialogInterface d) { 59 | dialog = null; 60 | presenter.onDismissed(false); 61 | } 62 | }) 63 | .show(); 64 | } 65 | 66 | @Override public boolean isShowing() { 67 | return dialog != null; 68 | } 69 | 70 | @Override public void dismiss(boolean withFlourish) { 71 | dialog.dismiss(); 72 | dialog = null; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /mortar-sample/src/main/java/com/example/mortar/view/FriendListView.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.mortar.view; 18 | 19 | import android.content.Context; 20 | import android.util.AttributeSet; 21 | import android.view.View; 22 | import android.widget.AdapterView; 23 | import android.widget.ArrayAdapter; 24 | import android.widget.ListView; 25 | import mortar.dagger1support.ObjectGraphService; 26 | import com.example.mortar.model.User; 27 | import com.example.mortar.screen.FriendListScreen; 28 | import java.util.List; 29 | import javax.inject.Inject; 30 | 31 | public class FriendListView extends ListView { 32 | @Inject FriendListScreen.Presenter presenter; 33 | 34 | public FriendListView(Context context, AttributeSet attrs) { 35 | super(context, attrs); 36 | ObjectGraphService.inject(context, this); 37 | } 38 | 39 | @Override protected void onAttachedToWindow() { 40 | super.onAttachedToWindow(); 41 | presenter.takeView(this); 42 | } 43 | 44 | @Override protected void onDetachedFromWindow() { 45 | super.onDetachedFromWindow(); 46 | presenter.dropView(this); 47 | } 48 | 49 | public void showFriends(List friends) { 50 | Adapter adapter = new Adapter(getContext(), friends); 51 | 52 | setAdapter(adapter); 53 | setOnItemClickListener(new OnItemClickListener() { 54 | @Override public void onItemClick(AdapterView parent, View view, int position, long id) { 55 | presenter.onFriendSelected(position); 56 | } 57 | }); 58 | } 59 | 60 | private static class Adapter extends ArrayAdapter { 61 | public Adapter(Context context, List objects) { 62 | super(context, android.R.layout.simple_list_item_1, objects); 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /mortar-sample/src/main/java/com/example/mortar/view/FriendView.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.mortar.view; 18 | 19 | import android.content.Context; 20 | import android.util.AttributeSet; 21 | import android.widget.TextView; 22 | import mortar.dagger1support.ObjectGraphService; 23 | import com.example.mortar.screen.FriendScreen; 24 | import javax.inject.Inject; 25 | 26 | public class FriendView extends TextView { 27 | @Inject FriendScreen.Presenter presenter; 28 | 29 | public FriendView(Context context, AttributeSet attrs) { 30 | super(context, attrs); 31 | ObjectGraphService.inject(context, this); 32 | } 33 | 34 | @Override protected void onAttachedToWindow() { 35 | super.onAttachedToWindow(); 36 | presenter.takeView(this); 37 | } 38 | 39 | @Override protected void onDetachedFromWindow() { 40 | super.onDetachedFromWindow(); 41 | presenter.dropView(this); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /mortar-sample/src/main/java/com/example/mortar/view/MessageView.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.mortar.view; 18 | 19 | import android.content.Context; 20 | import android.util.AttributeSet; 21 | import android.view.View; 22 | import android.widget.LinearLayout; 23 | import android.widget.TextView; 24 | import mortar.dagger1support.ObjectGraphService; 25 | import com.example.mortar.R; 26 | import com.example.mortar.screen.MessageScreen; 27 | import javax.inject.Inject; 28 | 29 | public class MessageView extends LinearLayout { 30 | @Inject MessageScreen.Presenter presenter; 31 | 32 | private TextView userView; 33 | private TextView messageView; 34 | 35 | public MessageView(Context context, AttributeSet attrs) { 36 | super(context, attrs); 37 | setOrientation(VERTICAL); 38 | ObjectGraphService.inject(context, this); 39 | } 40 | 41 | @Override protected void onFinishInflate() { 42 | super.onFinishInflate(); 43 | messageView = (TextView) findViewById(R.id.message); 44 | userView = (TextView) findViewById(R.id.user); 45 | userView.setOnClickListener(new OnClickListener() { 46 | @Override public void onClick(View v) { 47 | presenter.onUserSelected(); 48 | } 49 | }); 50 | } 51 | 52 | @Override protected void onAttachedToWindow() { 53 | super.onAttachedToWindow(); 54 | presenter.takeView(this); 55 | } 56 | 57 | @Override protected void onDetachedFromWindow() { 58 | super.onDetachedFromWindow(); 59 | presenter.dropView(this); 60 | } 61 | 62 | public void setUser(String user) { 63 | userView.setText(user); 64 | } 65 | 66 | public void setMessage(String message) { 67 | messageView.setText(message); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /mortar-sample/src/main/res/anim/slide_in_left.xml: -------------------------------------------------------------------------------- 1 | 2 | 20 | 21 | 22 | 24 | 26 | -------------------------------------------------------------------------------- /mortar-sample/src/main/res/anim/slide_in_right.xml: -------------------------------------------------------------------------------- 1 | 2 | 20 | 21 | 22 | 24 | 26 | -------------------------------------------------------------------------------- /mortar-sample/src/main/res/anim/slide_out_left.xml: -------------------------------------------------------------------------------- 1 | 2 | 20 | 21 | 22 | 24 | 26 | -------------------------------------------------------------------------------- /mortar-sample/src/main/res/anim/slide_out_right.xml: -------------------------------------------------------------------------------- 1 | 2 | 20 | 21 | 22 | 24 | 26 | -------------------------------------------------------------------------------- /mortar-sample/src/main/res/drawable/mortar_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/square/mortar/732e2459f6a01afa7270a2798df43fd92b6ca4cf/mortar-sample/src/main/res/drawable/mortar_icon.png -------------------------------------------------------------------------------- /mortar-sample/src/main/res/layout/chat_list_view.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 25 | -------------------------------------------------------------------------------- /mortar-sample/src/main/res/layout/chat_view.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 24 | -------------------------------------------------------------------------------- /mortar-sample/src/main/res/layout/friend_list_view.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 24 | -------------------------------------------------------------------------------- /mortar-sample/src/main/res/layout/friend_view.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 24 | -------------------------------------------------------------------------------- /mortar-sample/src/main/res/layout/message_view.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 24 | 25 | 32 | 33 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /mortar-sample/src/main/res/layout/root_layout.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 24 | -------------------------------------------------------------------------------- /mortar-sample/src/main/res/values/ids.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /mortar/build.gradle: -------------------------------------------------------------------------------- 1 | import com.android.builder.core.BuilderConstants; 2 | 3 | buildscript { 4 | dependencies { 5 | classpath deps.android_gradle_plugin 6 | } 7 | } 8 | 9 | apply plugin: 'com.android.library' 10 | android { 11 | compileSdkVersion rootProject.ext.compileSdkVersion 12 | buildToolsVersion rootProject.ext.buildToolsVersion 13 | 14 | defaultConfig { 15 | minSdkVersion rootProject.ext.minSdkVersion 16 | versionName VERSION_NAME 17 | } 18 | } 19 | 20 | dependencies { 21 | testCompile 'junit:junit:4.10' 22 | testCompile 'org.easytesting:fest-assert-core:2.0M10' 23 | testCompile 'org.mockito:mockito-core:1.9.5' 24 | testCompile 'org.robolectric:robolectric:2.2' 25 | } 26 | 27 | 28 | android.libraryVariants.all { variant -> 29 | def name = variant.buildType.name 30 | if (!BuilderConstants.DEBUG.equals(name)) { 31 | def task = project.tasks.create "jar${name.capitalize()}", Jar 32 | task.dependsOn variant.javaCompile 33 | task.from variant.javaCompile.destinationDir 34 | artifacts.add('archives', task); 35 | } 36 | } 37 | 38 | if (hasProperty("POM_DEVELOPER_ID")) { 39 | apply from: '../gradle-mvn-push.gradle' 40 | } 41 | -------------------------------------------------------------------------------- /mortar/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_NAME=Mortar 2 | POM_ARTIFACT_ID=mortar 3 | POM_PACKAGING=jar 4 | -------------------------------------------------------------------------------- /mortar/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 5 | -------------------------------------------------------------------------------- /mortar/src/main/java/mortar/MortarContextWrapper.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package mortar; 17 | 18 | import android.content.Context; 19 | import android.content.ContextWrapper; 20 | import android.view.LayoutInflater; 21 | 22 | class MortarContextWrapper extends ContextWrapper { 23 | private final MortarScope scope; 24 | 25 | private LayoutInflater inflater; 26 | 27 | public MortarContextWrapper(Context context, MortarScope scope) { 28 | super(context); 29 | this.scope = scope; 30 | } 31 | 32 | @Override public Object getSystemService(String name) { 33 | if (LAYOUT_INFLATER_SERVICE.equals(name)) { 34 | if (inflater == null) { 35 | inflater = LayoutInflater.from(getBaseContext()).cloneInContext(this); 36 | } 37 | return inflater; 38 | } 39 | return scope.hasService(name) ? scope.getService(name) : super.getSystemService(name); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /mortar/src/main/java/mortar/MortarScopeDevHelper.java: -------------------------------------------------------------------------------- 1 | package mortar; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Collections; 5 | import java.util.Comparator; 6 | import java.util.List; 7 | 8 | public class MortarScopeDevHelper { 9 | 10 | /** 11 | * Format the scope hierarchy as a multi line string containing the scope names. 12 | * Can be given any scope in the hierarchy, will always print the whole scope hierarchy. 13 | * Also prints the Dagger modules containing entry points (injects). We've only tested this with 14 | * Dagger 1.1.0, please report any bug you may find. 15 | */ 16 | public static String scopeHierarchyToString(MortarScope mortarScope) { 17 | StringBuilder result = new StringBuilder("Mortar Hierarchy:\n"); 18 | MortarScope rootScope = getRootScope(mortarScope); 19 | Node rootNode = new MortarScopeNode(rootScope); 20 | nodeHierarchyToString(result, 0, 0, rootNode); 21 | return result.toString(); 22 | } 23 | 24 | interface Node { 25 | String getName(); 26 | 27 | List getChildNodes(); 28 | } 29 | 30 | static class MortarScopeNode implements Node { 31 | 32 | private final MortarScope mortarScope; 33 | 34 | MortarScopeNode(MortarScope mortarScope) { 35 | this.mortarScope = mortarScope; 36 | } 37 | 38 | @Override public String getName() { 39 | return "SCOPE " + mortarScope.getName(); 40 | } 41 | 42 | @Override public List getChildNodes() { 43 | List childNodes = new ArrayList<>(); 44 | addScopeChildren(childNodes); 45 | return childNodes; 46 | } 47 | 48 | private void addScopeChildren(List childNodes) { 49 | for (MortarScope childScope : mortarScope.children.values()) { 50 | childNodes.add(new MortarScopeNode(childScope)); 51 | } 52 | } 53 | } 54 | 55 | 56 | private static MortarScope getRootScope(MortarScope scope) { 57 | while (scope.parent != null) { 58 | scope = scope.parent; 59 | } 60 | return scope; 61 | } 62 | 63 | private static void nodeHierarchyToString(StringBuilder result, int depth, long lastChildMask, 64 | Node node) { 65 | appendLinePrefix(result, depth, lastChildMask); 66 | result.append(node.getName()).append('\n'); 67 | 68 | List childNodes = node.getChildNodes(); 69 | Collections.sort(childNodes, new NodeSorter()); 70 | 71 | int lastIndex = childNodes.size() - 1; 72 | int index = 0; 73 | for (Node childNode : childNodes) { 74 | if (index == lastIndex) { 75 | lastChildMask = lastChildMask | (1 << depth); 76 | } 77 | nodeHierarchyToString(result, depth + 1, lastChildMask, childNode); 78 | index++; 79 | } 80 | } 81 | 82 | private static void appendLinePrefix(StringBuilder result, int depth, long lastChildMask) { 83 | int lastDepth = depth - 1; 84 | // Add a non-breaking space at the beginning of the line because Logcat eats normal spaces. 85 | result.append('\u00a0'); 86 | for (int parentDepth = 0; parentDepth <= lastDepth; parentDepth++) { 87 | if (parentDepth > 0) { 88 | result.append(' '); 89 | } 90 | boolean lastChild = (lastChildMask & (1 << parentDepth)) != 0; 91 | if (lastChild) { 92 | if (parentDepth == lastDepth) { 93 | result.append('`'); 94 | } else { 95 | result.append(' '); 96 | } 97 | } else { 98 | if (parentDepth == lastDepth) { 99 | result.append('+'); 100 | } else { 101 | result.append('|'); 102 | } 103 | } 104 | } 105 | if (depth > 0) { 106 | result.append("-"); 107 | } 108 | } 109 | 110 | private MortarScopeDevHelper() { 111 | throw new UnsupportedOperationException("This is a helper class"); 112 | } 113 | 114 | private static class NodeSorter implements Comparator { 115 | @Override public int compare(Node lhs, Node rhs) { 116 | return lhs.getName().compareTo(rhs.getName()); 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /mortar/src/main/java/mortar/Popup.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package mortar; 17 | 18 | import android.content.Context; 19 | import android.os.Parcelable; 20 | 21 | /** 22 | * Implemented by classes that run a popup display for a view, typically a dialog. 23 | * 24 | * @see PopupPresenter 25 | * @param info to display 26 | */ 27 | public interface Popup { 28 | /** 29 | * Show the given info. How to handle redundant calls is a decision to be made 30 | * per implementation. Some classes may throw {@link IllegalStateException} 31 | * if the popup is already visible. Others may update a visible display to reflect 32 | * the new info. 33 | */ 34 | void show(D info, boolean withFlourish, PopupPresenter presenter); 35 | 36 | boolean isShowing(); 37 | 38 | void dismiss(boolean withFlourish); 39 | 40 | Context getContext(); 41 | } 42 | -------------------------------------------------------------------------------- /mortar/src/main/java/mortar/PopupPresenter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package mortar; 17 | 18 | import android.os.Bundle; 19 | import android.os.Parcelable; 20 | import mortar.bundler.BundleService; 21 | 22 | /** 23 | * @param the type of info this dialog displays. D must provide value-based implementations 24 | * of {@link #hashCode()} and {@link #equals(Object)} in order for debouncing code in {@link #show} 25 | * to work properly. 26 | * 27 | * When using multiple {@link PopupPresenter}s of the same type in the same view, construct them 28 | * with {@link #PopupPresenter(String)} to give them a name to distinguish them. 29 | */ 30 | public abstract class PopupPresenter extends Presenter> { 31 | private static final boolean WITH_FLOURISH = true; 32 | 33 | private final String whatToShowKey; 34 | 35 | private D whatToShow; 36 | 37 | /** 38 | * @param customStateKey custom key name for saving state, useful when you have multiple instance 39 | * of the same PopupPresenter class tied to a view. 40 | */ 41 | protected PopupPresenter(String customStateKey) { 42 | this.whatToShowKey = getClass().getName() + customStateKey; 43 | } 44 | 45 | protected PopupPresenter() { 46 | this(""); 47 | } 48 | 49 | public D showing() { 50 | return whatToShow; 51 | } 52 | 53 | public void show(D info) { 54 | if (whatToShow == info || whatToShow != null && whatToShow.equals(info)) { 55 | // It's very likely this is a button bounce 56 | // http://stackoverflow.com/questions/2886407/dealing-with-rapid-tapping-on-buttons 57 | return; 58 | } 59 | 60 | whatToShow = info; 61 | if (!hasView()) return; 62 | getView().show(whatToShow, WITH_FLOURISH, this); 63 | } 64 | 65 | public void dismiss() { 66 | if (whatToShow != null) { 67 | whatToShow = null; 68 | if (!hasView()) return; 69 | Popup popUp = getView(); 70 | if (popUp.isShowing()) popUp.dismiss(WITH_FLOURISH); 71 | } 72 | } 73 | 74 | public final void onDismissed(R result) { 75 | whatToShow = null; 76 | onPopupResult(result); 77 | } 78 | 79 | abstract protected void onPopupResult(R result); 80 | 81 | @Override protected BundleService extractBundleService(Popup view) { 82 | return BundleService.getBundleService(view.getContext()); 83 | } 84 | 85 | @Override public void dropView(Popup view) { 86 | Popup oldView = getView(); 87 | if (oldView == view && oldView.isShowing()) oldView.dismiss(!WITH_FLOURISH); 88 | super.dropView(view); 89 | } 90 | 91 | @Override public void onLoad(Bundle savedInstanceState) { 92 | if (whatToShow == null && savedInstanceState != null) { 93 | whatToShow = savedInstanceState.getParcelable(whatToShowKey); 94 | } 95 | 96 | if (whatToShow == null) return; 97 | 98 | if (!hasView()) return; 99 | Popup view = getView(); 100 | 101 | if (!view.isShowing()) { 102 | view.show(whatToShow, !WITH_FLOURISH, this); 103 | } 104 | } 105 | 106 | @Override public void onSave(Bundle outState) { 107 | if (whatToShow != null) { 108 | outState.putParcelable(whatToShowKey, whatToShow); 109 | } 110 | } 111 | 112 | @Override public void onExitScope() { 113 | Popup popUp = getView(); 114 | if (popUp != null && popUp.isShowing()) popUp.dismiss(!WITH_FLOURISH); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /mortar/src/main/java/mortar/Presenter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package mortar; 17 | 18 | import android.os.Bundle; 19 | import mortar.bundler.BundleService; 20 | import mortar.bundler.Bundler; 21 | 22 | public abstract class Presenter { 23 | private V view = null; 24 | 25 | /** Load has been called for the current {@link #view}. */ 26 | private boolean loaded; 27 | 28 | private Bundler registration = new Bundler() { 29 | @Override public String getMortarBundleKey() { 30 | return Presenter.this.getMortarBundleKey(); 31 | } 32 | 33 | @Override public void onLoad(Bundle savedInstanceState) { 34 | if (hasView() && !loaded) { 35 | loaded = true; 36 | Presenter.this.onLoad(savedInstanceState); 37 | } 38 | } 39 | 40 | @Override public void onSave(Bundle outState) { 41 | Presenter.this.onSave(outState); 42 | } 43 | 44 | @Override public void onEnterScope(MortarScope scope) { 45 | Presenter.this.onEnterScope(scope); 46 | } 47 | 48 | @Override public void onExitScope() { 49 | Presenter.this.onExitScope(); 50 | } 51 | }; 52 | 53 | /** 54 | * Called to give this presenter control of a view, typically from 55 | * {@link android.view.View#onAttachedToWindow()}. Sets the 56 | * view that will be returned from {@link #getView()}. 57 | *

58 | * This presenter will be immediately {@link BundleService#register registered} 59 | * (or re-registered) with the given view's scope, leading to an immediate call to {@link 60 | * #onLoad}. 61 | *

62 | * It is expected that {@link #dropView(Object)} will be called with the same argument when the 63 | * view is no longer active, e.g. from {@link android.view.View#onDetachedFromWindow()}. 64 | * 65 | * @see BundleService#register 66 | */ 67 | public final void takeView(V view) { 68 | if (view == null) throw new NullPointerException("new view must not be null"); 69 | 70 | if (this.view != view) { 71 | if (this.view != null) dropView(this.view); 72 | 73 | this.view = view; 74 | extractBundleService(view).register(registration); 75 | } 76 | } 77 | 78 | /** 79 | * Called to surrender control of this view, e.g. when the view is detached. If and only if 80 | * the given view matches the last passed to {@link #takeView}, the reference to the view is 81 | * cleared. 82 | *

83 | * Mismatched views are a no-op, not an error. This is to provide protection in the 84 | * not uncommon case that dropView and takeView are called out of order. For example, an 85 | * activity's views are typically inflated in {@link 86 | * android.app.Activity#onCreate}, but are only detached some time after {@link 87 | * android.app.Activity#onDestroy() onExitScope}. It's possible for a view from one activity 88 | * to be detached well after the window for the next activity has its views inflated—that 89 | * is, after the next activity's onResume call. 90 | */ 91 | public void dropView(V view) { 92 | if (view == null) throw new NullPointerException("dropped view must not be null"); 93 | if (view == this.view) { 94 | loaded = false; 95 | this.view = null; 96 | } 97 | } 98 | 99 | protected String getMortarBundleKey() { 100 | return getClass().getName(); 101 | } 102 | 103 | /** Called by {@link #takeView}. Given a view instance, return its {@link MortarScope}. */ 104 | protected abstract BundleService extractBundleService(V view); 105 | 106 | /** 107 | * Returns the view managed by this presenter, or null if {@link #takeView} has never been 108 | * called, or after {@link #dropView}. 109 | */ 110 | protected final V getView() { 111 | return view; 112 | } 113 | 114 | /** 115 | * @return true if this presenter is currently managing a view, or false if {@link #takeView} has 116 | * never been called, or after {@link #dropView}. 117 | */ 118 | protected final boolean hasView() { 119 | return view != null; 120 | } 121 | 122 | /** Like {@link Bundler#onEnterScope}. */ 123 | protected void onEnterScope(MortarScope scope) { 124 | } 125 | 126 | /** 127 | * Like {@link Bundler#onLoad}, but called only when {@link #getView()} is not 128 | * null, and debounced. That is, this method will be called exactly once for a given view 129 | * instance, at least until that view is {@link #dropView(Object) dropped}. 130 | * 131 | * See {@link #takeView} for details. 132 | */ 133 | protected void onLoad(Bundle savedInstanceState) { 134 | } 135 | 136 | /** Like {@link Bundler#onSave}. */ 137 | protected void onSave(Bundle outState) { 138 | } 139 | 140 | /** 141 | * Like {@link Bundler#onExitScope}. One subtlety to note is that a presenter may be created 142 | * by a higher level scope than the one it is registered with, in which case it may receive 143 | * multiple calls to this method. 144 | */ 145 | protected void onExitScope() { 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /mortar/src/main/java/mortar/Scoped.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package mortar; 17 | 18 | public interface Scoped { 19 | /** 20 | * Called the first time the receiver is registered with the given scope. 21 | */ 22 | void onEnterScope(MortarScope scope); 23 | 24 | /** 25 | * Called when a scope the receiver is registered with is destroyed. 26 | */ 27 | void onExitScope(); 28 | } 29 | -------------------------------------------------------------------------------- /mortar/src/main/java/mortar/ViewPresenter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package mortar; 17 | 18 | import android.view.View; 19 | import mortar.bundler.BundleService; 20 | 21 | public class ViewPresenter extends Presenter { 22 | @Override protected final BundleService extractBundleService(V view) { 23 | return BundleService.getBundleService(view.getContext()); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /mortar/src/main/java/mortar/bundler/BundleService.java: -------------------------------------------------------------------------------- 1 | package mortar.bundler; 2 | 3 | import android.content.Context; 4 | import android.os.Bundle; 5 | import java.util.ArrayList; 6 | import java.util.LinkedHashSet; 7 | import java.util.List; 8 | import java.util.Set; 9 | import mortar.MortarScope; 10 | import mortar.Scoped; 11 | 12 | import static java.lang.String.format; 13 | 14 | public class BundleService { 15 | final BundleServiceRunner runner; 16 | final MortarScope scope; 17 | final Set bundlers = new LinkedHashSet<>(); 18 | 19 | Bundle scopeBundle; 20 | private List toBeLoaded = new ArrayList<>(); 21 | 22 | BundleService(BundleServiceRunner runner, MortarScope scope) { 23 | this.runner = runner; 24 | this.scope = scope; 25 | scopeBundle = findScopeBundle(runner.rootBundle); 26 | } 27 | 28 | public static BundleService getBundleService(Context context) { 29 | BundleServiceRunner runner = BundleServiceRunner.getBundleServiceRunner(context); 30 | if (runner == null) { 31 | throw new IllegalStateException( 32 | "You forgot to set up a " + BundleServiceRunner.class.getName() + " in your activity"); 33 | } 34 | return runner.requireBundleService(MortarScope.getScope(context)); 35 | } 36 | 37 | public static BundleService getBundleService(MortarScope scope) { 38 | BundleServiceRunner runner = BundleServiceRunner.getBundleServiceRunner(scope); 39 | if (runner == null) { 40 | throw new IllegalStateException( 41 | "You forgot to set up a " + BundleServiceRunner.class.getName() + " in your activity"); 42 | } 43 | return runner.requireBundleService(scope); 44 | } 45 | 46 | /** 47 | *

Registers {@link Bundler} instances with this service. See that interface for details. 48 | */ 49 | public void register(Bundler bundler) { 50 | if (bundler == null) throw new NullPointerException("Cannot register null bundler."); 51 | 52 | if (runner.state == BundleServiceRunner.State.SAVING) { 53 | throw new IllegalStateException("Cannot register during onSave"); 54 | } 55 | 56 | if (bundlers.add(bundler)) bundler.onEnterScope(scope); 57 | String mortarBundleKey = bundler.getMortarBundleKey(); 58 | if (mortarBundleKey == null || mortarBundleKey.trim().equals("")) { 59 | throw new IllegalArgumentException(format("%s has null or empty bundle key", bundler)); 60 | } 61 | 62 | switch (runner.state) { 63 | case IDLE: 64 | toBeLoaded.add(bundler); 65 | runner.servicesToBeLoaded.add(this); 66 | runner.finishLoading(); 67 | break; 68 | case LOADING: 69 | if (!toBeLoaded.contains(bundler)) { 70 | toBeLoaded.add(bundler); 71 | runner.servicesToBeLoaded.add(this); 72 | } 73 | break; 74 | 75 | default: 76 | throw new AssertionError("Unexpected state " + runner.state); 77 | } 78 | } 79 | 80 | void init() { 81 | scope.register(new Scoped() { 82 | @Override public void onEnterScope(MortarScope scope) { 83 | runner.scopedServices.put(runner.bundleKey(scope), BundleService.this); 84 | } 85 | 86 | @Override public void onExitScope() { 87 | if (runner.rootBundle != null) runner.rootBundle.remove(runner.bundleKey(scope)); 88 | for (Bundler b : bundlers) b.onExitScope(); 89 | runner.scopedServices.remove(runner.bundleKey(scope)); 90 | runner.servicesToBeLoaded.remove(BundleService.this); 91 | } 92 | }); 93 | } 94 | 95 | boolean needsLoading() { 96 | return !toBeLoaded.isEmpty(); 97 | } 98 | 99 | void loadOne() { 100 | if (toBeLoaded.isEmpty()) return; 101 | 102 | Bundler next = toBeLoaded.remove(0); 103 | Bundle leafBundle = 104 | scopeBundle == null ? null : scopeBundle.getBundle(next.getMortarBundleKey()); 105 | next.onLoad(leafBundle); 106 | } 107 | 108 | /** @return true if we have clients that now need to be loaded */ 109 | boolean updateScopedBundleOnCreate(Bundle rootBundle) { 110 | scopeBundle = findScopeBundle(rootBundle); 111 | toBeLoaded.addAll(bundlers); 112 | return !toBeLoaded.isEmpty(); 113 | } 114 | 115 | private Bundle findScopeBundle(Bundle root) { 116 | return root == null ? null : root.getBundle(runner.bundleKey(scope)); 117 | } 118 | 119 | void saveToRootBundle(Bundle rootBundle) { 120 | String key = runner.bundleKey(scope); 121 | scopeBundle = rootBundle.getBundle(key); 122 | 123 | if (scopeBundle == null) { 124 | scopeBundle = new Bundle(); 125 | rootBundle.putBundle(key, scopeBundle); 126 | } 127 | 128 | for (Bundler bundler : bundlers) { 129 | Bundle childBundle = scopeBundle.getBundle(bundler.getMortarBundleKey()); 130 | if (childBundle == null) { 131 | childBundle = new Bundle(); 132 | scopeBundle.putBundle(bundler.getMortarBundleKey(), childBundle); 133 | } 134 | 135 | bundler.onSave(childBundle); 136 | 137 | // Short circuit if the scope was destroyed by the save call. 138 | if (scope.isDestroyed()) return; 139 | } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /mortar/src/main/java/mortar/bundler/BundleServiceComparator.java: -------------------------------------------------------------------------------- 1 | package mortar.bundler; 2 | 3 | import java.util.Comparator; 4 | import mortar.MortarScope; 5 | 6 | class BundleServiceComparator implements Comparator { 7 | @Override public int compare(BundleService left, BundleService right) { 8 | String[] leftPath = left.scope.getPath().split(MortarScope.DIVIDER); 9 | String[] rightPath = right.scope.getPath().split(MortarScope.DIVIDER); 10 | 11 | if (leftPath.length != rightPath.length) { 12 | return leftPath.length < rightPath.length ? -1 : 1; 13 | } 14 | 15 | int segments = leftPath.length; 16 | for (int i = 0; i < segments; i++) { 17 | int result = leftPath[i].compareTo(rightPath[i]); 18 | if (result != 0) return result; 19 | } 20 | 21 | return 0; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /mortar/src/main/java/mortar/bundler/BundleServiceRunner.java: -------------------------------------------------------------------------------- 1 | package mortar.bundler; 2 | 3 | import android.content.Context; 4 | import android.os.Bundle; 5 | import java.util.ArrayList; 6 | import java.util.LinkedHashMap; 7 | import java.util.List; 8 | import java.util.Map; 9 | import java.util.NavigableSet; 10 | import java.util.TreeSet; 11 | import mortar.MortarScope; 12 | import mortar.Presenter; 13 | import mortar.Scoped; 14 | 15 | public class BundleServiceRunner implements Scoped { 16 | public static final String SERVICE_NAME = BundleServiceRunner.class.getName(); 17 | 18 | public static BundleServiceRunner getBundleServiceRunner(Context context) { 19 | return (BundleServiceRunner) context.getSystemService(SERVICE_NAME); 20 | } 21 | 22 | public static BundleServiceRunner getBundleServiceRunner(MortarScope scope) { 23 | return scope.getService(SERVICE_NAME); 24 | } 25 | 26 | final Map scopedServices = new LinkedHashMap<>(); 27 | final NavigableSet servicesToBeLoaded = 28 | new TreeSet<>(new BundleServiceComparator()); 29 | 30 | Bundle rootBundle; 31 | 32 | enum State { 33 | IDLE, LOADING, SAVING 34 | } 35 | 36 | State state = State.IDLE; 37 | 38 | private String rootScopePath; 39 | 40 | BundleService requireBundleService(MortarScope scope) { 41 | BundleService service = scopedServices.get(bundleKey(scope)); 42 | if (service == null) { 43 | service = new BundleService(this, scope); 44 | service.init(); 45 | } 46 | return service; 47 | } 48 | 49 | @Override public void onEnterScope(MortarScope scope) { 50 | if (rootScopePath != null) throw new IllegalStateException("Cannot double register"); 51 | rootScopePath = scope.getPath(); 52 | } 53 | 54 | @Override public void onExitScope() { 55 | // Nothing to do. 56 | } 57 | 58 | /** 59 | * To be called from the host {@link android.app.Activity}'s {@link 60 | * android.app.Activity#onCreate}. Calls the registered {@link Bundler}'s {@link Bundler#onLoad} 61 | * methods. To avoid redundant calls to {@link Presenter#onLoad} it's best to call this before 62 | * {@link android.app.Activity#setContentView}. 63 | */ 64 | public void onCreate(Bundle savedInstanceState) { 65 | rootBundle = savedInstanceState; 66 | 67 | for (Map.Entry entry : scopedServices.entrySet()) { 68 | BundleService scopedService = entry.getValue(); 69 | if (scopedService.updateScopedBundleOnCreate(rootBundle)) { 70 | servicesToBeLoaded.add(scopedService); 71 | } 72 | } 73 | finishLoading(); 74 | } 75 | 76 | /** 77 | * To be called from the host {@link android.app.Activity}'s {@link 78 | * android.app.Activity#onSaveInstanceState}. Calls the registrants' {@link Bundler#onSave} 79 | * methods. 80 | */ 81 | public void onSaveInstanceState(Bundle outState) { 82 | if (state != State.IDLE) { 83 | throw new IllegalStateException("Cannot handle onSaveInstanceState while " + state); 84 | } 85 | rootBundle = outState; 86 | 87 | state = State.SAVING; 88 | 89 | // Make a dwindling copy of the services, in case one is deleted as a side effect 90 | // of another's onSave. 91 | List> servicesToBeSaved = 92 | new ArrayList<>(scopedServices.entrySet()); 93 | 94 | while (!servicesToBeSaved.isEmpty()) { 95 | Map.Entry entry = servicesToBeSaved.remove(0); 96 | if (scopedServices.containsKey(entry.getKey())) entry.getValue().saveToRootBundle(rootBundle); 97 | } 98 | 99 | state = State.IDLE; 100 | } 101 | 102 | void finishLoading() { 103 | if (state != State.IDLE) throw new AssertionError("Unexpected state " + state); 104 | state = State.LOADING; 105 | 106 | while (!servicesToBeLoaded.isEmpty()) { 107 | BundleService next = servicesToBeLoaded.first(); 108 | next.loadOne(); 109 | if (!next.needsLoading()) servicesToBeLoaded.remove(next); 110 | } 111 | 112 | state = State.IDLE; 113 | } 114 | 115 | String bundleKey(MortarScope scope) { 116 | if (rootScopePath == null) throw new IllegalStateException("Was this service not registered?"); 117 | String path = scope.getPath(); 118 | if (!path.startsWith(rootScopePath)) { 119 | throw new IllegalArgumentException(String.format("\"%s\" is not under \"%s\"", scope, 120 | rootScopePath)); 121 | } 122 | 123 | return path.substring(rootScopePath.length()); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /mortar/src/main/java/mortar/bundler/Bundler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package mortar.bundler; 17 | 18 | import android.os.Bundle; 19 | import mortar.MortarScope; 20 | 21 | /** Implemented by objects that want to persist via the bundle. */ 22 | public interface Bundler { 23 | /** 24 | * Like {@link mortar.Scoped#onEnterScope}, called synchronously when a bundler 25 | * is {@link BundleService#register registered} with a {@link BundleService}. 26 | */ 27 | void onEnterScope(MortarScope scope); 28 | 29 | /** 30 | * The key that will identify the bundles passed to this instance via {@link #onLoad} 31 | * and {@link #onSave}. 32 | */ 33 | String getMortarBundleKey(); 34 | 35 | /** 36 | * Called when this object is {@link BundleService#register registered}, and each time 37 | * {@link BundleServiceRunner#onCreate} is called (e.g. after a configuration change like 38 | * rotation, or after the app process is respawned). Callers should assume that the initial 39 | * call to this method is made asynchronously, but be prepared for a synchronous call. 40 | * 41 | *

Note that receivers are likely to outlive multiple activity instances, and so receive 42 | * multiple calls of this method. Implementations should be prepared to ignore saved state if 43 | * they are already initialized. 44 | * 45 | * @param savedInstanceState the state written by the most recent call to {@link #onSave}, or 46 | * null if that has never happened. 47 | */ 48 | void onLoad(Bundle savedInstanceState); 49 | 50 | /** 51 | * Called from the {@link BundleServiceRunner#onSaveInstanceState}, to allow the receiver 52 | * to save state before the process is killed. Note that receivers are likely to outlive multiple 53 | * activity instances, and so receive multiple calls of this method. Any state required to revive 54 | * a new instance of the receiver in a new process should be written out each time, as there is 55 | * no way to know if the app is about to hibernate. 56 | * 57 | * @param outState a bundle to write any state that needs to be restored if the plugin is 58 | * revived 59 | */ 60 | void onSave(Bundle outState); 61 | 62 | void onExitScope(); 63 | } 64 | -------------------------------------------------------------------------------- /mortar/src/test/java/mortar/MortarScopeDevHelperTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package mortar; 17 | 18 | import org.junit.Test; 19 | 20 | import static mortar.MortarScopeDevHelper.scopeHierarchyToString; 21 | import static org.fest.assertions.api.Assertions.assertThat; 22 | 23 | public class MortarScopeDevHelperTest { 24 | private static final char BLANK = '\u00a0'; 25 | 26 | @Test public void nestedScopeHierarchyToString() { 27 | MortarScope root = MortarScope.buildRootScope().build("Root"); 28 | root.buildChild().build("Cadet"); 29 | 30 | MortarScope colonel = root.buildChild().build("Colonel"); 31 | colonel.buildChild().build("ElderColonel"); 32 | colonel.buildChild().build("ZeElderColonel"); 33 | 34 | MortarScope elder = root.buildChild().build("Elder"); 35 | elder.buildChild().build("ElderCadet"); 36 | elder.buildChild().build("ZeElderCadet"); 37 | elder.buildChild().build("ElderElder"); 38 | elder.buildChild().build("AnElderCadet"); 39 | 40 | String hierarchy = scopeHierarchyToString(root); 41 | assertThat(hierarchy).isEqualTo("" // 42 | + "Mortar Hierarchy:\n" // 43 | + BLANK + "SCOPE Root\n" // 44 | + BLANK + "+-SCOPE Cadet\n" // 45 | + BLANK + "+-SCOPE Colonel\n" // 46 | + BLANK + "| +-SCOPE ElderColonel\n" // 47 | + BLANK + "| `-SCOPE ZeElderColonel\n" // 48 | + BLANK + "`-SCOPE Elder\n" // 49 | + BLANK + " +-SCOPE AnElderCadet\n" // 50 | + BLANK + " +-SCOPE ElderCadet\n" // 51 | + BLANK + " +-SCOPE ElderElder\n" // 52 | + BLANK + " `-SCOPE ZeElderCadet\n" // 53 | ); 54 | } 55 | 56 | @Test public void startsFromMortarScope() { 57 | MortarScope root = MortarScope.buildRootScope().build("Root"); 58 | MortarScope child = root.buildChild().build("Child"); 59 | 60 | String hierarchy = scopeHierarchyToString(child); 61 | 62 | assertThat(hierarchy).isEqualTo("" // 63 | + "Mortar Hierarchy:\n" // 64 | + BLANK + "SCOPE Root\n" // 65 | + BLANK + "`-SCOPE Child\n" // 66 | ); 67 | } 68 | 69 | @Test public void noSpaceAtLineBeginnings() { 70 | MortarScope root = MortarScope.buildRootScope().build("Root"); 71 | MortarScope child = root.buildChild().build("Child"); 72 | child.buildChild().build("Grand Child"); 73 | 74 | String hierarchy = scopeHierarchyToString(root); 75 | 76 | assertThat(hierarchy).isEqualTo("" // 77 | + "Mortar Hierarchy:\n" // 78 | + BLANK + "SCOPE Root\n" // 79 | + BLANK + "`-SCOPE Child\n" // 80 | + BLANK + " `-SCOPE Grand Child\n" // 81 | ); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /mortar/src/test/java/mortar/PopupPresenterTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package mortar; 17 | 18 | import android.content.Context; 19 | import android.os.Bundle; 20 | import android.os.Parcelable; 21 | import mortar.bundler.BundleServiceRunner; 22 | import org.junit.Before; 23 | import org.junit.Test; 24 | import org.junit.runner.RunWith; 25 | import org.mockito.Mock; 26 | import org.mockito.invocation.InvocationOnMock; 27 | import org.mockito.stubbing.Answer; 28 | import org.robolectric.RobolectricTestRunner; 29 | import org.robolectric.annotation.Config; 30 | 31 | import static mortar.bundler.BundleServiceRunner.getBundleServiceRunner; 32 | import static org.fest.assertions.api.Assertions.assertThat; 33 | import static org.mockito.Matchers.any; 34 | import static org.mockito.Matchers.anyBoolean; 35 | import static org.mockito.Matchers.anyString; 36 | import static org.mockito.Matchers.eq; 37 | import static org.mockito.Matchers.same; 38 | import static org.mockito.Mockito.mock; 39 | import static org.mockito.Mockito.never; 40 | import static org.mockito.Mockito.verify; 41 | import static org.mockito.Mockito.when; 42 | import static org.mockito.MockitoAnnotations.initMocks; 43 | 44 | // Robolectric allows us to use Bundles. 45 | @RunWith(RobolectricTestRunner.class) 46 | @Config(manifest = Config.NONE) 47 | public class PopupPresenterTest { 48 | 49 | static class TestPopupPresenter extends PopupPresenter { 50 | String result; 51 | 52 | TestPopupPresenter() { 53 | } 54 | 55 | TestPopupPresenter(String customStateKey) { 56 | super(customStateKey); 57 | } 58 | 59 | @Override protected void onPopupResult(String result) { 60 | this.result = result; 61 | } 62 | } 63 | 64 | static final boolean WITH_FLOURISH = true; 65 | static final boolean WITHOUT_FLOURISH = false; 66 | 67 | @Mock Popup view; 68 | @Mock Context context; 69 | 70 | MortarScope root; 71 | MortarScope activityScope; 72 | TestPopupPresenter presenter; 73 | 74 | @Before public void setUp() { 75 | initMocks(this); 76 | when(view.getContext()).thenReturn(context); 77 | when((context).getSystemService(anyString())).then(returnScopedService()); 78 | 79 | newProcess(); 80 | getBundleServiceRunner(activityScope).onCreate(null); 81 | presenter = new TestPopupPresenter(); 82 | } 83 | 84 | /** Simulate a new proecess by creating brand new scope instances. */ 85 | private void newProcess() { 86 | root = MortarScope.buildRootScope().build("Root"); 87 | activityScope = root.buildChild() 88 | .withService(BundleServiceRunner.SERVICE_NAME, new BundleServiceRunner()) 89 | .build("activity"); 90 | } 91 | 92 | private Answer returnScopedService() { 93 | return new Answer() { 94 | @Override public Object answer(InvocationOnMock invocation) throws Throwable { 95 | return activityScope.getService((String) invocation.getArguments()[0]); 96 | } 97 | }; 98 | } 99 | 100 | @Test public void takeViewDoesNotShowView() { 101 | presenter.takeView(view); 102 | verify(view, never()).show(any(Parcelable.class), anyBoolean(), any(TestPopupPresenter.class)); 103 | } 104 | 105 | @Test public void showAfterTakeViewShowsView() { 106 | presenter.takeView(view); 107 | Parcelable info = mock(Parcelable.class); 108 | presenter.show(info); 109 | verify(view).show(same(info), eq(WITH_FLOURISH), same(presenter)); 110 | } 111 | 112 | @Test public void dismissAfterShowDismissesView() { 113 | presenter.takeView(view); 114 | presenter.show(mock(Parcelable.class)); 115 | when(view.isShowing()).thenReturn(true); 116 | presenter.dismiss(); 117 | verify(view).dismiss(eq(WITH_FLOURISH)); 118 | } 119 | 120 | @Test public void dismissWithViewNotShowingDoesNotDismissView() { 121 | presenter.takeView(view); 122 | presenter.show(mock(Parcelable.class)); 123 | when(view.isShowing()).thenReturn(false); 124 | presenter.dismiss(); 125 | verify(view, never()).dismiss(anyBoolean()); 126 | } 127 | 128 | @Test public void dismissWithoutShowDoesNotDismissView() { 129 | presenter.takeView(view); 130 | presenter.dismiss(); 131 | verify(view, never()).dismiss(anyBoolean()); 132 | } 133 | 134 | @Test public void showingReturnsInfo() { 135 | Parcelable info = mock(Parcelable.class); 136 | presenter.show(info); 137 | assertThat(presenter.showing()).isSameAs(info); 138 | } 139 | 140 | @Test public void dismissClearsInfo() { 141 | presenter.show(mock(Parcelable.class)); 142 | presenter.dismiss(); 143 | assertThat(presenter.showing()).isNull(); 144 | } 145 | 146 | @Test public void showTwiceWithSameInfoDebounces() { 147 | presenter.takeView(view); 148 | Parcelable info = mock(Parcelable.class); 149 | presenter.show(info); 150 | presenter.show(info); 151 | verify(view).show(same(info), anyBoolean(), same(presenter)); 152 | } 153 | 154 | @Test public void destroyDismissesWithoutFlourish() { 155 | presenter.takeView(view); 156 | when(view.isShowing()).thenReturn(true); 157 | activityScope.destroy(); 158 | verify(view).dismiss(eq(WITHOUT_FLOURISH)); 159 | } 160 | 161 | @Test public void takeViewRestoresPopup() { 162 | presenter.takeView(view); 163 | Parcelable info = mock(Parcelable.class); 164 | presenter.show(info); 165 | 166 | Bundle state = new Bundle(); 167 | getBundleServiceRunner(activityScope).onSaveInstanceState(state); 168 | 169 | newProcess(); 170 | getBundleServiceRunner(activityScope).onCreate(state); 171 | 172 | presenter = new TestPopupPresenter(); 173 | presenter.takeView(view); 174 | verify(view).show(same(info), eq(WITHOUT_FLOURISH), same(presenter)); 175 | } 176 | 177 | @Test public void customStateKeyAvoidsStateMixing() { 178 | String customStateKey1 = "presenter1"; 179 | TestPopupPresenter presenter1 = new TestPopupPresenter(customStateKey1); 180 | presenter1.takeView(view); 181 | Bundle info1 = new Bundle(); 182 | info1.putString("key", "data1"); 183 | presenter1.show(info1); 184 | 185 | String customStateKey2 = "presenter2"; 186 | TestPopupPresenter presenter2 = new TestPopupPresenter(customStateKey2); 187 | presenter2.takeView(view); 188 | Bundle info2 = new Bundle(); 189 | info2.putString("key", "data2"); 190 | presenter2.show(info2); 191 | 192 | Bundle state = new Bundle(); 193 | getBundleServiceRunner(activityScope).onSaveInstanceState(state); 194 | newProcess(); 195 | getBundleServiceRunner(activityScope).onCreate(state); 196 | 197 | presenter1 = new TestPopupPresenter(customStateKey1); 198 | presenter1.takeView(view); 199 | assertThat(presenter1.showing()).isEqualTo(info1).isNotEqualTo(info2); 200 | 201 | presenter2 = new TestPopupPresenter(customStateKey2); 202 | presenter2.takeView(view); 203 | assertThat(presenter2.showing()).isEqualTo(info2).isNotEqualTo(info1); 204 | } 205 | 206 | @Test public void onDismissedClearsInfo() { 207 | presenter.show(mock(Parcelable.class)); 208 | presenter.onDismissed(""); 209 | assertThat(presenter.showing()).isNull(); 210 | } 211 | 212 | @Test public void onDismissedDeliversResult() { 213 | presenter.show(mock(Parcelable.class)); 214 | presenter.onDismissed("result"); 215 | assertThat(presenter.result).isEqualTo("result"); 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /mortar/src/test/java/mortar/PresenterTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package mortar; 17 | 18 | import android.os.Bundle; 19 | import mortar.bundler.BundleService; 20 | import mortar.bundler.BundleServiceRunner; 21 | import org.junit.Before; 22 | import org.junit.Test; 23 | import org.junit.runner.RunWith; 24 | import org.robolectric.RobolectricTestRunner; 25 | import org.robolectric.annotation.Config; 26 | 27 | import static org.fest.assertions.api.Assertions.assertThat; 28 | 29 | // Robolectric allows us to use Bundles. 30 | @RunWith(RobolectricTestRunner.class) 31 | @Config(manifest = Config.NONE) 32 | public class PresenterTest { 33 | static class SomeView { 34 | } 35 | 36 | MortarScope root; 37 | MortarScope activityScope; 38 | 39 | @Before public void setUp() { 40 | root = MortarScope.buildRootScope().build("Root"); 41 | activityScope = root.buildChild() 42 | .withService(BundleServiceRunner.SERVICE_NAME, new BundleServiceRunner()) 43 | .build("name"); 44 | } 45 | 46 | class ChildPresenter extends Presenter { 47 | final String payload; 48 | boolean loaded; 49 | 50 | ChildPresenter(String payload) { 51 | this.payload = payload; 52 | } 53 | 54 | @Override protected BundleService extractBundleService(SomeView view) { 55 | return BundleService.getBundleService(activityScope); 56 | } 57 | 58 | @Override protected void onSave(Bundle savedInstanceState) { 59 | savedInstanceState.putString("key", payload); 60 | } 61 | 62 | @Override protected void onLoad(Bundle savedInstanceState) { 63 | if (savedInstanceState != null) { 64 | assertThat(savedInstanceState.getString("key")).isEqualTo(payload); 65 | loaded = true; 66 | } 67 | } 68 | } 69 | 70 | class ParentPresenter extends Presenter { 71 | @Override protected BundleService extractBundleService(SomeView view) { 72 | return BundleService.getBundleService(activityScope); 73 | } 74 | 75 | // The child presenters are anonymous inner classes but of the same 76 | // type. This is like the case where a presenter manages two 77 | // popup presenters for two different dialogs. 78 | 79 | ChildPresenter childOne = new ChildPresenter("one") { 80 | }; 81 | ChildPresenter childTwo = new ChildPresenter("two") { 82 | }; 83 | 84 | @Override protected void onLoad(Bundle savedInstanceState) { 85 | childOne.takeView(getView()); 86 | childTwo.takeView(getView()); 87 | } 88 | 89 | @Override public void dropView(SomeView view) { 90 | childTwo.dropView(view); 91 | childOne.dropView(view); 92 | super.dropView(view); 93 | } 94 | } 95 | 96 | @Test public void childPresentersGetTheirOwnBundles() { 97 | BundleServiceRunner bundleServiceRunner = 98 | BundleServiceRunner.getBundleServiceRunner(activityScope); 99 | bundleServiceRunner.onCreate(null); 100 | 101 | ParentPresenter presenter = new ParentPresenter(); 102 | SomeView view = new SomeView(); 103 | 104 | presenter.takeView(view); 105 | 106 | Bundle bundle = new Bundle(); 107 | bundleServiceRunner.onSaveInstanceState(bundle); 108 | presenter.dropView(view); 109 | 110 | bundleServiceRunner.onCreate(bundle); 111 | presenter.takeView(view); 112 | 113 | /** 114 | * Assertions in {@link ChildPresenter#onLoad(android.os.Bundle)} are the real test, 115 | * but let's check that the were run 116 | */ 117 | 118 | assertThat(presenter.childOne.loaded).isTrue(); 119 | assertThat(presenter.childTwo.loaded).isTrue(); 120 | } 121 | 122 | class SimplePresenter extends Presenter { 123 | MortarScope registered; 124 | MortarScope destroyed; 125 | boolean loaded; 126 | Object droppedView; 127 | 128 | @Override protected void onEnterScope(MortarScope scope) { 129 | registered = scope; 130 | } 131 | 132 | @Override protected BundleService extractBundleService(SomeView view) { 133 | return BundleService.getBundleService(activityScope); 134 | } 135 | 136 | @Override protected void onLoad(Bundle savedInstanceState) { 137 | loaded = true; 138 | } 139 | 140 | @Override public void dropView(SomeView view) { 141 | droppedView = view; 142 | super.dropView(view); 143 | } 144 | 145 | @Override protected void onExitScope() { 146 | destroyed = activityScope; 147 | } 148 | } 149 | 150 | /** https://github.com/square/mortar/issues/59 */ 151 | @Test public void onLoadOnlyOncePerView() { 152 | SimplePresenter presenter = new SimplePresenter(); 153 | SomeView view = new SomeView(); 154 | 155 | presenter.takeView(view); 156 | assertThat(presenter.loaded).isTrue(); 157 | 158 | presenter.loaded = false; 159 | BundleServiceRunner.getBundleServiceRunner(activityScope).onCreate(null); 160 | assertThat(presenter.loaded).isFalse(); 161 | } 162 | 163 | @Test public void newViewNewLoad() { 164 | SimplePresenter presenter = new SimplePresenter(); 165 | SomeView viewOne = new SomeView(); 166 | 167 | presenter.takeView(viewOne); 168 | assertThat(presenter.loaded).isTrue(); 169 | 170 | presenter.loaded = false; 171 | SomeView viewTwo = new SomeView(); 172 | presenter.takeView(viewTwo); 173 | assertThat(presenter.loaded).isTrue(); 174 | } 175 | 176 | @Test public void dropRetakeReload() { 177 | SimplePresenter presenter = new SimplePresenter(); 178 | SomeView view = new SomeView(); 179 | 180 | presenter.takeView(view); 181 | assertThat(presenter.loaded).isTrue(); 182 | 183 | presenter.dropView(view); 184 | 185 | presenter.loaded = false; 186 | presenter.takeView(view); 187 | assertThat(presenter.loaded).isTrue(); 188 | } 189 | 190 | /** 191 | * When takeView clobbers an existing view, dropView should be called. (We could 192 | * drop this requirement if dropView were final, see https://github.com/square/mortar/issues/52) 193 | */ 194 | @Test public void autoDropCallsDrop() { 195 | SimplePresenter presenter = new SimplePresenter(); 196 | SomeView viewOne = new SomeView(); 197 | SomeView viewTwo = new SomeView(); 198 | 199 | presenter.takeView(viewOne); 200 | presenter.takeView(viewTwo); 201 | assertThat(presenter.droppedView).isSameAs(viewOne); 202 | } 203 | 204 | @Test public void onRegisteredIsFired() { 205 | SimplePresenter presenter = new SimplePresenter(); 206 | SomeView viewOne = new SomeView(); 207 | 208 | presenter.takeView(viewOne); 209 | assertThat(presenter.registered).isSameAs(activityScope); 210 | } 211 | 212 | @Test public void onRegisteredIsDebounced() { 213 | SimplePresenter presenter = new SimplePresenter(); 214 | SomeView viewOne = new SomeView(); 215 | 216 | presenter.takeView(viewOne); 217 | presenter.dropView(viewOne); 218 | presenter.registered = null; 219 | 220 | presenter.takeView(viewOne); 221 | assertThat(presenter.registered).isNull(); 222 | } 223 | 224 | @Test public void onExitIsFired() { 225 | SimplePresenter presenter = new SimplePresenter(); 226 | SomeView viewOne = new SomeView(); 227 | 228 | presenter.takeView(viewOne); 229 | activityScope.destroy(); 230 | 231 | assertThat(presenter.destroyed).isSameAs(activityScope); 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':mortar' 2 | include ':mortar-dagger1' 3 | include ':mortar-hellodagger2' 4 | include ':mortar-helloworld' 5 | include ':mortar-sample' 6 | --------------------------------------------------------------------------------