├── .gitignore ├── LICENSE ├── README.md ├── app ├── .gitignore ├── app.iml ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── corneliudascalu │ │ └── mvpnotes │ │ └── tests │ │ ├── MainInstrumentationTest.java │ │ ├── MockNoteInteractor.java │ │ └── MockNoteInteractorModule.java │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── corneliudascalu │ │ └── mvpnotes │ │ ├── AppModule.java │ │ ├── MVPNotesApp.java │ │ ├── common │ │ ├── BaseInjectedActivity.java │ │ ├── InjectedDialogFragment.java │ │ ├── ObjectGraphCreator.java │ │ └── ObjectGraphHolder.java │ │ ├── data │ │ ├── interactor │ │ │ ├── InteractorsModule.java │ │ │ ├── NoteInteractor.java │ │ │ └── impl │ │ │ │ └── NoteInteractorImpl.java │ │ └── model │ │ │ ├── DatabaseModule.java │ │ │ ├── Note.java │ │ │ └── SimpleDatabase.java │ │ ├── ui │ │ └── view │ │ │ ├── details │ │ │ ├── NoteDetailsDialogFragment.java │ │ │ ├── NoteDetailsModule.java │ │ │ ├── SimpleNoteDetailsPresenter.java │ │ │ └── interfaces │ │ │ │ ├── NoteDetailsPresenter.java │ │ │ │ └── NoteDetailsView.java │ │ │ └── main │ │ │ ├── NotesActivity.java │ │ │ ├── NotesModule.java │ │ │ ├── NotesPresenter.java │ │ │ ├── NotesView.java │ │ │ ├── OnNoteOperationListener.java │ │ │ └── SimpleNotesPresenter.java │ │ └── util │ │ ├── DateTimeDeserializer.java │ │ └── DateTimeSerializer.java │ └── res │ ├── drawable-hdpi │ └── ic_launcher.png │ ├── drawable-mdpi │ └── ic_launcher.png │ ├── drawable-xhdpi │ └── ic_launcher.png │ ├── drawable-xxhdpi │ └── ic_launcher.png │ ├── layout │ ├── activity_notes.xml │ └── note_details.xml │ ├── menu │ ├── delete_note.xml │ └── notes.xml │ ├── values-w820dp │ └── dimens.xml │ └── values │ ├── dimens.xml │ ├── strings.xml │ └── styles.xml ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── mvp-notes.iml └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.apk 3 | *.ap_ 4 | 5 | # Files for the Dalvik VM 6 | *.dex 7 | 8 | # Java class files 9 | *.class 10 | 11 | # Generated files 12 | bin/ 13 | gen/ 14 | .idea/ 15 | 16 | # Gradle files 17 | .gradle/ 18 | build/ 19 | 20 | # Local configuration file (sdk path, etc) 21 | local.properties 22 | 23 | # Proguard folder generated by Eclipse 24 | proguard/ 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | mvp-notes 2 | ========= 3 | 4 | Sample Android project implementing the MVP paradigm, using Dagger and Espresso 5 | 6 | The code is commented, and the related blog post is [here]. 7 | 8 | [here]: http://corneliudascalu.github.io/model-view-presenter-in-android/ 9 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/app.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | 10 | 11 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 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 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | def versionMajor = 0 4 | def versionMinor = 1 5 | def versionPatch = 0 6 | def versionBuild = 0 // bump for dogfood builds, public betas, etc. 7 | 8 | android { 9 | compileSdkVersion 19 10 | buildToolsVersion "19.1.0" 11 | 12 | defaultConfig { 13 | applicationId "com.corneliudascalu.mvpnotes" 14 | minSdkVersion 15 15 | targetSdkVersion 19 16 | versionCode versionMajor * 10000 + versionMinor * 1000 + versionPatch * 100 + versionBuild 17 | versionName "${versionMajor}.${versionMinor}.${versionPatch}" 18 | testInstrumentationRunner "com.google.android.apps.common.testing.testrunner.GoogleInstrumentationTestRunner" 19 | } 20 | buildTypes { 21 | release { 22 | runProguard false 23 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 24 | } 25 | } 26 | 27 | packagingOptions { 28 | exclude 'META-INF/services/javax.annotation.processing.Processor' 29 | exclude 'LICENSE.txt' 30 | } 31 | } 32 | 33 | dependencies { 34 | compile fileTree(dir: 'libs', include: ['*.jar']) 35 | compile 'com.github.gabrielemariotti.cards:library:1.7.3' 36 | compile 'com.github.gabrielemariotti.cards:library-extra:1.7.3' 37 | compile 'com.jakewharton:butterknife:5.1.1' 38 | compile 'com.jakewharton.timber:timber:2.4.1' 39 | compile 'com.squareup.dagger:dagger:1.2.1' 40 | compile 'com.squareup.dagger:dagger-compiler:1.2.1' 41 | compile 'joda-time:joda-time:2.3' 42 | compile 'com.google.code.gson:gson:2.2.4' 43 | 44 | androidTestCompile('com.jakewharton.espresso:espresso:1.1-r3') { 45 | // avoid duplicating the dagger dependencies (it's included in Jake Wharton's Espresso build) 46 | exclude group: 'com.squareup.dagger' 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /home/corneliu/programs/android-studio/sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/corneliudascalu/mvpnotes/tests/MainInstrumentationTest.java: -------------------------------------------------------------------------------- 1 | package com.corneliudascalu.mvpnotes.tests; 2 | 3 | import android.app.Application; 4 | import android.test.ActivityInstrumentationTestCase2; 5 | import com.corneliudascalu.mvpnotes.AppModule; 6 | import com.corneliudascalu.mvpnotes.MVPNotesApp; 7 | import com.corneliudascalu.mvpnotes.R; 8 | import com.corneliudascalu.mvpnotes.common.ObjectGraphCreator; 9 | import com.corneliudascalu.mvpnotes.common.ObjectGraphHolder; 10 | import com.corneliudascalu.mvpnotes.ui.view.main.NotesActivity; 11 | import com.google.android.apps.common.testing.ui.espresso.ViewInteraction; 12 | import dagger.ObjectGraph; 13 | 14 | import static com.google.android.apps.common.testing.ui.espresso.Espresso.onView; 15 | import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.click; 16 | import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.typeText; 17 | import static com.google.android.apps.common.testing.ui.espresso.assertion.ViewAssertions.matches; 18 | import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.withId; 19 | import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.withText; 20 | 21 | /** 22 | * @author Corneliu Dascalu 23 | */ 24 | public class MainInstrumentationTest extends ActivityInstrumentationTestCase2 { 25 | private NotesActivity notesActivity; 26 | 27 | public MainInstrumentationTest() { 28 | super(NotesActivity.class); 29 | } 30 | 31 | @Override 32 | protected void setUp() throws Exception { 33 | super.setUp(); 34 | 35 | // set a new graph creator to be used by the activity 36 | // we could use a different AppModule here if we want, but the point is to demonstrate overriding a module 37 | ObjectGraphHolder.forceObjectGraphCreator(new ObjectGraphCreator() { 38 | @Override 39 | public ObjectGraph create(Application application) { 40 | return ObjectGraph.create(new AppModule((MVPNotesApp) application), new MockNoteInteractorModule()); 41 | } 42 | }); 43 | 44 | notesActivity = getActivity(); 45 | } 46 | 47 | public void testSimpleExistence() { 48 | onView(withId(R.id.submitNoteButton)).check(matches(withText(R.string.create))); 49 | } 50 | 51 | public void testSubmitNote() { 52 | 53 | ViewInteraction viewInteraction = onView(withId(R.id.noteText)); 54 | viewInteraction.perform(click()); 55 | viewInteraction.perform(typeText("test note")); 56 | onView(withId(R.id.submitNoteButton)).perform(click()); 57 | 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/corneliudascalu/mvpnotes/tests/MockNoteInteractor.java: -------------------------------------------------------------------------------- 1 | package com.corneliudascalu.mvpnotes.tests; 2 | 3 | import com.corneliudascalu.mvpnotes.data.interactor.NoteInteractor; 4 | import com.corneliudascalu.mvpnotes.data.model.Note; 5 | import com.corneliudascalu.mvpnotes.ui.view.main.OnNoteOperationListener; 6 | 7 | import timber.log.Timber; 8 | 9 | import java.util.Random; 10 | 11 | /** 12 | * A different implementation of a {@link com.corneliudascalu.mvpnotes.data.interactor.NoteInteractor}. There's not 13 | * much difference to {@link com.corneliudascalu.mvpnotes.data.interactor.impl.NoteInteractorImpl} used in the app, 14 | * but assume that the one from the app is doing real work 15 | * 16 | * @author Corneliu Dascalu 17 | */ 18 | public class MockNoteInteractor implements NoteInteractor { 19 | @Override 20 | public void storeNote(Note note, OnNoteOperationListener listener) { 21 | if (new Random().nextInt(10) > 3) { 22 | Timber.d("MockNote", "Adding note"); 23 | listener.onNoteAdded(note); 24 | } else { 25 | Timber.d("MockNote", "Generating note error"); 26 | listener.onNoteAddError(new Note.Error(note, "Mock error")); 27 | } 28 | } 29 | 30 | @Override 31 | public void deleteNote(Note note) { 32 | 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/corneliudascalu/mvpnotes/tests/MockNoteInteractorModule.java: -------------------------------------------------------------------------------- 1 | package com.corneliudascalu.mvpnotes.tests; 2 | 3 | import com.corneliudascalu.mvpnotes.data.interactor.NoteInteractor; 4 | import dagger.Module; 5 | import dagger.Provides; 6 | 7 | /** 8 | * An interactor module that supplies a different {@link com.corneliudascalu.mvpnotes.data.interactor 9 | * .NoteInteractor}, by overriding the one from the {@link com.corneliudascalu.mvpnotes.data.interactor 10 | * .InteractorsModule} 11 | * 12 | * @author Corneliu Dascalu 13 | */ 14 | @Module(overrides = true, library = true) 15 | public class MockNoteInteractorModule { 16 | 17 | @Provides 18 | NoteInteractor provideNoteInteractor() { 19 | return new MockNoteInteractor(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 8 | 9 | 15 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /app/src/main/java/com/corneliudascalu/mvpnotes/AppModule.java: -------------------------------------------------------------------------------- 1 | package com.corneliudascalu.mvpnotes; 2 | 3 | import com.corneliudascalu.mvpnotes.data.interactor.InteractorsModule; 4 | 5 | import javax.inject.Singleton; 6 | 7 | import dagger.Module; 8 | import dagger.Provides; 9 | 10 | /** 11 | * @author Corneliu Dascalu 12 | */ 13 | @Module(injects = {MVPNotesApp.class}, 14 | includes = {InteractorsModule.class} 15 | ) 16 | public class AppModule { 17 | private MVPNotesApp app; 18 | 19 | public AppModule(MVPNotesApp app) { 20 | this.app = app; 21 | } 22 | 23 | @Provides 24 | @Singleton 25 | public MVPNotesApp provideApplication() { 26 | return app; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/src/main/java/com/corneliudascalu/mvpnotes/MVPNotesApp.java: -------------------------------------------------------------------------------- 1 | package com.corneliudascalu.mvpnotes; 2 | 3 | import com.corneliudascalu.mvpnotes.common.ObjectGraphCreator; 4 | import com.corneliudascalu.mvpnotes.common.ObjectGraphHolder; 5 | 6 | import android.app.Application; 7 | 8 | import java.util.Arrays; 9 | import java.util.List; 10 | 11 | import dagger.ObjectGraph; 12 | 13 | /** 14 | * @author Corneliu Dascalu 15 | */ 16 | public class MVPNotesApp extends Application { 17 | 18 | @Override 19 | public void onCreate() { 20 | super.onCreate(); 21 | ObjectGraphHolder.setObjectGraphCreator(new ObjectGraphCreator() { 22 | @Override 23 | public ObjectGraph create(Application application) { 24 | return ObjectGraph.create(getModules().toArray()); 25 | } 26 | }); 27 | } 28 | 29 | /** 30 | * The list of modules containing application-level stuff 31 | */ 32 | private List getModules() { 33 | return Arrays.asList(new AppModule(this)); 34 | } 35 | 36 | /** 37 | * Create a scoped object graph by adding some modules to the app modules 38 | */ 39 | public ObjectGraph createScopedObjectGraph(Object... modules) { 40 | return ObjectGraphHolder.getObjectGraph(this).plus(modules); 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /app/src/main/java/com/corneliudascalu/mvpnotes/common/BaseInjectedActivity.java: -------------------------------------------------------------------------------- 1 | package com.corneliudascalu.mvpnotes.common; 2 | 3 | import android.app.Activity; 4 | import android.os.Bundle; 5 | import dagger.ObjectGraph; 6 | 7 | import java.util.List; 8 | 9 | /** 10 | * Base class for all activities, which creates a scoped object graph and injects is in the activity. Extended 11 | * classes will implement the {@link #getModules()} method to specify the needed modules 12 | * 13 | * @author Corneliu Dascalu 14 | */ 15 | public abstract class BaseInjectedActivity extends Activity { 16 | 17 | private ObjectGraph objectGraph; 18 | 19 | @Override 20 | protected void onCreate(Bundle savedInstanceState) { 21 | super.onCreate(savedInstanceState); 22 | objectGraph = ObjectGraphHolder.createScopedObjectGraph(getApplication(), getModules().toArray()); 23 | objectGraph.inject(this); 24 | } 25 | 26 | protected abstract List getModules(); 27 | 28 | @Override 29 | protected void onDestroy() { 30 | super.onDestroy(); 31 | objectGraph = null; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/src/main/java/com/corneliudascalu/mvpnotes/common/InjectedDialogFragment.java: -------------------------------------------------------------------------------- 1 | package com.corneliudascalu.mvpnotes.common; 2 | 3 | import android.app.DialogFragment; 4 | import android.os.Bundle; 5 | 6 | import java.util.List; 7 | 8 | import dagger.ObjectGraph; 9 | 10 | /** 11 | * @author Corneliu Dascalu 12 | */ 13 | public abstract class InjectedDialogFragment extends DialogFragment { 14 | 15 | private ObjectGraph objectGraph; 16 | 17 | @Override 18 | public void onCreate(Bundle savedInstanceState) { 19 | super.onCreate(savedInstanceState); 20 | objectGraph = ObjectGraphHolder 21 | .createScopedObjectGraph(getActivity().getApplication(), getModules().toArray()); 22 | objectGraph.inject(this); 23 | } 24 | 25 | protected abstract List getModules(); 26 | 27 | @Override 28 | public void onDestroy() { 29 | super.onDestroy(); 30 | objectGraph = null; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/src/main/java/com/corneliudascalu/mvpnotes/common/ObjectGraphCreator.java: -------------------------------------------------------------------------------- 1 | package com.corneliudascalu.mvpnotes.common; 2 | 3 | import android.app.Application; 4 | import dagger.ObjectGraph; 5 | 6 | /** 7 | * Defines the structure of the object graph creator. There's no reason to have more than one, 8 | * but it's useful when testing 9 | * 10 | * @author Corneliu Dascalu 11 | */ 12 | public interface ObjectGraphCreator { 13 | ObjectGraph create(Application application); 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/java/com/corneliudascalu/mvpnotes/common/ObjectGraphHolder.java: -------------------------------------------------------------------------------- 1 | package com.corneliudascalu.mvpnotes.common; 2 | 3 | import android.app.Application; 4 | import dagger.ObjectGraph; 5 | 6 | /** 7 | * Holds the app object graph in a static variable, which will only be destroyed if the app is destroyed 8 | * 9 | * @author Corneliu Dascalu 10 | */ 11 | public class ObjectGraphHolder { 12 | private static ObjectGraph objectGraph; 13 | private static ObjectGraphCreator objectGraphCreator; 14 | 15 | public static ObjectGraph getObjectGraph(Application application) { 16 | if (objectGraph == null) { 17 | objectGraph = objectGraphCreator.create(application); 18 | } 19 | return objectGraph; 20 | } 21 | 22 | public static void setObjectGraphCreator(ObjectGraphCreator objectGraphCreator) { 23 | if (ObjectGraphHolder.objectGraphCreator == null) { 24 | ObjectGraphHolder.objectGraphCreator = objectGraphCreator; 25 | } 26 | } 27 | 28 | public static void forceObjectGraphCreator(ObjectGraphCreator objectGraphCreator) { 29 | objectGraph = null; 30 | ObjectGraphHolder.objectGraphCreator = objectGraphCreator; 31 | } 32 | 33 | public static void inject(Application application, Object object) { 34 | getObjectGraph(application).inject(object); 35 | } 36 | 37 | public static ObjectGraph createScopedObjectGraph(Application application, Object... modules) { 38 | return ObjectGraphHolder.getObjectGraph(application).plus(modules); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /app/src/main/java/com/corneliudascalu/mvpnotes/data/interactor/InteractorsModule.java: -------------------------------------------------------------------------------- 1 | package com.corneliudascalu.mvpnotes.data.interactor; 2 | 3 | import com.corneliudascalu.mvpnotes.MVPNotesApp; 4 | import com.corneliudascalu.mvpnotes.data.interactor.impl.NoteInteractorImpl; 5 | 6 | import dagger.Module; 7 | import dagger.Provides; 8 | 9 | /** 10 | * Provides the list of interactors used throughout the app. Handy to have them all in the same 11 | * file, 12 | * and the {@code library} annotation means that only needed interactors will be injected in the 13 | * target object 14 | * 15 | * @author Corneliu Dascalu 16 | */ 17 | @Module(library = true, complete = false) 18 | public class InteractorsModule { 19 | 20 | @Provides 21 | public NoteInteractor provideNotesInteractor(MVPNotesApp app) { 22 | return new NoteInteractorImpl(app); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/java/com/corneliudascalu/mvpnotes/data/interactor/NoteInteractor.java: -------------------------------------------------------------------------------- 1 | package com.corneliudascalu.mvpnotes.data.interactor; 2 | 3 | import com.corneliudascalu.mvpnotes.data.model.Note; 4 | import com.corneliudascalu.mvpnotes.ui.view.main.OnNoteOperationListener; 5 | 6 | /** 7 | * Defines the notes interactor, which takes care of storing and deleting notes, either directly or 8 | * by communicating 9 | * with another layer. 10 | * 11 | * @author Corneliu Dascalu 12 | */ 13 | public interface NoteInteractor { 14 | 15 | void storeNote(Note note, OnNoteOperationListener listener); 16 | 17 | void deleteNote(Note note, OnNoteOperationListener listener); 18 | 19 | void getNote(long id, OnNoteOperationListener listener); 20 | 21 | void getAllNotes(OnNoteOperationListener listener); 22 | } 23 | -------------------------------------------------------------------------------- /app/src/main/java/com/corneliudascalu/mvpnotes/data/interactor/impl/NoteInteractorImpl.java: -------------------------------------------------------------------------------- 1 | package com.corneliudascalu.mvpnotes.data.interactor.impl; 2 | 3 | import com.corneliudascalu.mvpnotes.MVPNotesApp; 4 | import com.corneliudascalu.mvpnotes.common.ObjectGraphHolder; 5 | import com.corneliudascalu.mvpnotes.data.interactor.NoteInteractor; 6 | import com.corneliudascalu.mvpnotes.data.model.DatabaseModule; 7 | import com.corneliudascalu.mvpnotes.data.model.Note; 8 | import com.corneliudascalu.mvpnotes.data.model.SimpleDatabase; 9 | import com.corneliudascalu.mvpnotes.ui.view.main.OnNoteOperationListener; 10 | 11 | import android.os.Handler; 12 | 13 | import java.util.List; 14 | import java.util.Random; 15 | 16 | import javax.inject.Inject; 17 | 18 | import dagger.ObjectGraph; 19 | 20 | /** 21 | * A very simple implementation of a NoteInteractor, which simulates delayed operations. 22 | * Because this is just a sample, I'm using the SimpleDatabase class directly, not an interface. 23 | * Normally, there should be a ContentProvider there, or something similar. 24 | * 25 | * @author Corneliu Dascalu 26 | */ 27 | public class NoteInteractorImpl implements NoteInteractor { 28 | 29 | private Handler mHandler; 30 | 31 | private Random mRandom; 32 | 33 | public NoteInteractorImpl(MVPNotesApp app) { 34 | mHandler = new Handler(); 35 | // inject the SimpleDatabase only here, because I don't need it anywhere else 36 | ObjectGraph objectGraph = ObjectGraphHolder.createScopedObjectGraph(app) 37 | .plus(DatabaseModule.class); 38 | objectGraph.inject(this); 39 | mRandom = new Random(); 40 | } 41 | 42 | @Inject 43 | SimpleDatabase mSimpleDatabase; 44 | 45 | private void simulateExecuteInBackground(Runnable runnable) { 46 | mHandler.postDelayed(runnable, 1000); 47 | } 48 | 49 | @Override 50 | public void storeNote(final Note note, final OnNoteOperationListener listener) { 51 | simulateExecuteInBackground(new Runnable() { 52 | @Override 53 | public void run() { 54 | if (mRandom.nextInt(10) > 0) { 55 | long id = mSimpleDatabase.addOrReplace(note); 56 | if (id > 0) { 57 | note.id = id; 58 | listener.onNoteAdded(note); 59 | } else { 60 | listener.onNoteAddError(new Note.Error(note, "Note not inserted in db")); 61 | } 62 | } else { 63 | listener.onNoteAddError(new Note.Error(note, "Generic error")); 64 | } 65 | } 66 | }); 67 | } 68 | 69 | @Override 70 | public void deleteNote(final Note note, final OnNoteOperationListener listener) { 71 | simulateExecuteInBackground(new Runnable() { 72 | @Override 73 | public void run() { 74 | // generate random error 75 | if (mRandom.nextInt(10) > 0) { 76 | listener.onNoteDeleteError(new Note.Error(note, "Randomly generated error")); 77 | return; 78 | } 79 | int deleted = mSimpleDatabase.delete(note.id); 80 | if (deleted > 0) { 81 | listener.onNoteDeleted(note); 82 | } else { 83 | listener.onNoteDeleteError(new Note.Error(note, "Note not deleted")); 84 | } 85 | } 86 | }); 87 | 88 | } 89 | 90 | @Override 91 | public void getNote(final long id, final OnNoteOperationListener listener) { 92 | simulateExecuteInBackground(new Runnable() { 93 | @Override 94 | public void run() { 95 | Note note = mSimpleDatabase.get(id); 96 | if (note != null) { 97 | listener.onNoteRetrieved(note); 98 | } else { 99 | listener.onRetrieveError(new Note.Error(id, "Couldn't retrieve note")); 100 | } 101 | } 102 | }); 103 | 104 | } 105 | 106 | @Override 107 | public void getAllNotes(final OnNoteOperationListener listener) { 108 | simulateExecuteInBackground(new Runnable() { 109 | @Override 110 | public void run() { 111 | List all = mSimpleDatabase.getAll(); 112 | listener.onNoteListRetrieved(all); 113 | } 114 | }); 115 | 116 | } 117 | 118 | } 119 | -------------------------------------------------------------------------------- /app/src/main/java/com/corneliudascalu/mvpnotes/data/model/DatabaseModule.java: -------------------------------------------------------------------------------- 1 | package com.corneliudascalu.mvpnotes.data.model; 2 | 3 | import com.corneliudascalu.mvpnotes.AppModule; 4 | import com.corneliudascalu.mvpnotes.MVPNotesApp; 5 | import com.corneliudascalu.mvpnotes.data.interactor.impl.NoteInteractorImpl; 6 | 7 | import android.content.Context; 8 | import android.content.SharedPreferences; 9 | 10 | import javax.inject.Singleton; 11 | 12 | import dagger.Module; 13 | import dagger.Provides; 14 | 15 | /** 16 | * A module that provides a SimpleDatabase instance. It adds to AppModule to 17 | * get the MVPNotesApp instance from there 18 | * 19 | * @author Corneliu Dascalu 20 | */ 21 | @Module(injects = NoteInteractorImpl.class, addsTo = AppModule.class) 22 | public class DatabaseModule { 23 | 24 | @Provides 25 | @Singleton 26 | public SimpleDatabase provideSimpleDatabase(MVPNotesApp app) { 27 | SharedPreferences sharedPreferences = app 28 | .getSharedPreferences("notes_db", Context.MODE_PRIVATE); 29 | return new SimpleDatabase(sharedPreferences); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/java/com/corneliudascalu/mvpnotes/data/model/Note.java: -------------------------------------------------------------------------------- 1 | package com.corneliudascalu.mvpnotes.data.model; 2 | 3 | import org.joda.time.DateTime; 4 | 5 | import android.os.Parcel; 6 | import android.os.Parcelable; 7 | 8 | /** 9 | * Simple model of a note 10 | * 11 | * @author Corneliu Dascalu 12 | */ 13 | public class Note implements Parcelable { 14 | 15 | public static final String EXTRA_NOTE = Note.class.getSimpleName() + "extraNote"; 16 | 17 | public long id; 18 | 19 | public String title; 20 | 21 | public String text; 22 | 23 | public DateTime createdDate; 24 | 25 | /** 26 | * A note-related error. I find it useful to define exceptions this way, to make it easy and 27 | * clear when using an 28 | * event bus. 29 | */ 30 | public static class Error extends Exception { 31 | 32 | private long id; 33 | 34 | public Note note; 35 | 36 | public Error(Note note, String s) { 37 | super(s); 38 | this.note = note; 39 | } 40 | 41 | public Error(long id, String s) { 42 | super(); 43 | this.id = id; 44 | } 45 | } 46 | 47 | @Override 48 | public int describeContents() { 49 | return 0; 50 | } 51 | 52 | @Override 53 | public void writeToParcel(Parcel dest, int flags) { 54 | dest.writeLong(this.id); 55 | dest.writeString(this.title); 56 | dest.writeString(this.text); 57 | dest.writeString(this.createdDate.toString()); 58 | } 59 | 60 | public Note() { 61 | } 62 | 63 | private Note(Parcel in) { 64 | this.id = in.readLong(); 65 | this.title = in.readString(); 66 | this.text = in.readString(); 67 | this.createdDate = new DateTime(in.readString()); 68 | } 69 | 70 | public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { 71 | public Note createFromParcel(Parcel source) { 72 | return new Note(source); 73 | } 74 | 75 | public Note[] newArray(int size) { 76 | return new Note[size]; 77 | } 78 | }; 79 | } 80 | -------------------------------------------------------------------------------- /app/src/main/java/com/corneliudascalu/mvpnotes/data/model/SimpleDatabase.java: -------------------------------------------------------------------------------- 1 | package com.corneliudascalu.mvpnotes.data.model; 2 | 3 | import com.google.gson.Gson; 4 | import com.google.gson.GsonBuilder; 5 | 6 | import com.corneliudascalu.mvpnotes.util.DateTimeDeserializer; 7 | import com.corneliudascalu.mvpnotes.util.DateTimeSerializer; 8 | 9 | import org.joda.time.DateTime; 10 | 11 | import android.content.SharedPreferences; 12 | 13 | import java.util.ArrayList; 14 | import java.util.Collection; 15 | import java.util.List; 16 | import java.util.Map; 17 | 18 | /** 19 | * Very simple database, that stores notes as JSON strings, with the ids as keys. 20 | * It uses a SharedPreferences instance. 21 | * 22 | * @author Corneliu Dascalu 23 | */ 24 | public class SimpleDatabase { 25 | 26 | private final SharedPreferences mDatabase; 27 | 28 | 29 | public SimpleDatabase(SharedPreferences sharedPreferences) { 30 | mDatabase = sharedPreferences; 31 | } 32 | 33 | /** 34 | * Add/replace a note to the database, setting an id if it's zero. 35 | * 36 | * @return The id of the note 37 | */ 38 | public long addOrReplace(Note note) { 39 | if (note.id == 0) { 40 | note.id = System.currentTimeMillis(); 41 | } 42 | Gson gson = new GsonBuilder() 43 | .registerTypeAdapter(DateTime.class, new DateTimeSerializer()) 44 | .registerTypeAdapter(DateTime.class, new DateTimeDeserializer()) 45 | .create(); 46 | String json = gson.toJson(note); 47 | mDatabase.edit().putString(String.valueOf(note.id), json).apply(); 48 | return note.id; 49 | } 50 | 51 | /** 52 | * Retrieve a note from the database 53 | * 54 | * @param id The id of the wanted note 55 | * @return The note, if found, or null otherwise 56 | */ 57 | public Note get(long id) { 58 | String json = mDatabase.getString(String.valueOf(id), null); 59 | if (json != null) { 60 | return new GsonBuilder() 61 | .registerTypeAdapter(DateTime.class, new DateTimeSerializer()) 62 | .registerTypeAdapter(DateTime.class, new DateTimeDeserializer()) 63 | .create().fromJson(json, Note.class); 64 | } 65 | return null; 66 | } 67 | 68 | /** 69 | * Return all the notes in the database 70 | */ 71 | public List getAll() { 72 | Map all = (Map) mDatabase.getAll(); 73 | Collection values = all.values(); 74 | if (values.size() == 0) { 75 | return new ArrayList(); 76 | } 77 | 78 | ArrayList notes = new ArrayList(values.size()); 79 | Gson gson = new GsonBuilder() 80 | .registerTypeAdapter(DateTime.class, new DateTimeSerializer()) 81 | .registerTypeAdapter(DateTime.class, new DateTimeDeserializer()) 82 | .create(); 83 | 84 | for (String value : values) { 85 | notes.add(gson.fromJson(value, Note.class)); 86 | } 87 | return notes; 88 | } 89 | 90 | /** 91 | * Delete the note with the supplied id from the database 92 | * 93 | * @return The number of deleted notes 94 | */ 95 | public int delete(long id) { 96 | if (mDatabase.contains(String.valueOf(id))) { 97 | mDatabase.edit().remove(String.valueOf(id)).apply(); 98 | return 1; 99 | } 100 | return 0; 101 | } 102 | 103 | } 104 | -------------------------------------------------------------------------------- /app/src/main/java/com/corneliudascalu/mvpnotes/ui/view/details/NoteDetailsDialogFragment.java: -------------------------------------------------------------------------------- 1 | package com.corneliudascalu.mvpnotes.ui.view.details; 2 | 3 | import com.corneliudascalu.mvpnotes.R; 4 | import com.corneliudascalu.mvpnotes.common.InjectedDialogFragment; 5 | import com.corneliudascalu.mvpnotes.data.model.Note; 6 | import com.corneliudascalu.mvpnotes.ui.view.details.interfaces.NoteDetailsPresenter; 7 | import com.corneliudascalu.mvpnotes.ui.view.details.interfaces.NoteDetailsView; 8 | 9 | import org.joda.time.format.DateTimeFormatter; 10 | import org.joda.time.format.DateTimeFormatterBuilder; 11 | 12 | import android.os.Bundle; 13 | import android.view.LayoutInflater; 14 | import android.view.View; 15 | import android.view.ViewGroup; 16 | import android.widget.ImageButton; 17 | import android.widget.TextView; 18 | import android.widget.Toast; 19 | 20 | import java.util.Arrays; 21 | import java.util.List; 22 | 23 | import javax.inject.Inject; 24 | 25 | import butterknife.ButterKnife; 26 | import butterknife.InjectView; 27 | 28 | /** 29 | * @author Corneliu Dascalu 30 | */ 31 | public class NoteDetailsDialogFragment extends InjectedDialogFragment implements NoteDetailsView { 32 | 33 | public static final String TAG = "noteDetailsTag"; 34 | 35 | @InjectView(R.id.noteText) 36 | TextView noteText; 37 | 38 | @InjectView(R.id.noteDate) 39 | TextView noteDateText; 40 | 41 | @InjectView(R.id.deleteNoteButton) 42 | ImageButton deleteButton; 43 | 44 | @Inject 45 | NoteDetailsPresenter mPresenter; 46 | 47 | private DateTimeFormatter mFormatter; 48 | 49 | public static NoteDetailsDialogFragment newInstance(Note note) { 50 | Bundle args = new Bundle(); 51 | args.putParcelable(Note.EXTRA_NOTE, note); 52 | NoteDetailsDialogFragment fragment = new NoteDetailsDialogFragment(); 53 | fragment.setArguments(args); 54 | return fragment; 55 | } 56 | 57 | @Override 58 | public void onCreate(Bundle savedInstanceState) { 59 | super.onCreate(savedInstanceState); 60 | 61 | if (getArguments() != null) { 62 | setData(getArguments()); 63 | } 64 | 65 | DateTimeFormatterBuilder builder = new DateTimeFormatterBuilder(); 66 | mFormatter = builder 67 | .appendYear(4, 4).appendLiteral("-") 68 | .appendMonthOfYear(2).appendLiteral("-") 69 | .appendDayOfMonth(2).appendLiteral(" at ") 70 | .appendHourOfDay(2).appendLiteral(":") 71 | .appendMinuteOfHour(2) 72 | .toFormatter(); 73 | } 74 | 75 | private void setData(Bundle arguments) { 76 | if (arguments != null) { 77 | Note note = arguments.getParcelable(Note.EXTRA_NOTE); 78 | mPresenter.setData(note); 79 | } 80 | } 81 | 82 | @Override 83 | protected List getModules() { 84 | return Arrays.asList(new NoteDetailsModule(this)); 85 | } 86 | 87 | @Override 88 | public View onCreateView(LayoutInflater inflater, ViewGroup container, 89 | Bundle savedInstanceState) { 90 | View view = inflater.inflate(R.layout.note_details, container, false); 91 | ButterKnife.inject(this, view); 92 | 93 | deleteButton.setOnClickListener(new View.OnClickListener() { 94 | @Override 95 | public void onClick(View v) { 96 | mPresenter.deleteNote(); 97 | } 98 | }); 99 | 100 | return view; 101 | } 102 | 103 | @Override 104 | public void onViewCreated(View view, Bundle savedInstanceState) { 105 | super.onViewCreated(view, savedInstanceState); 106 | mPresenter.viewReady(); 107 | } 108 | 109 | @Override 110 | public void setNote(Note note) { 111 | getDialog().setTitle(note.title); 112 | noteText.setText(note.text); 113 | noteDateText.setText(note.createdDate.toString(mFormatter)); 114 | } 115 | 116 | @Override 117 | public void close() { 118 | dismiss(); 119 | } 120 | 121 | @Override 122 | public void showError(String error) { 123 | Toast.makeText(getActivity(), error, Toast.LENGTH_SHORT).show(); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /app/src/main/java/com/corneliudascalu/mvpnotes/ui/view/details/NoteDetailsModule.java: -------------------------------------------------------------------------------- 1 | package com.corneliudascalu.mvpnotes.ui.view.details; 2 | 3 | import com.corneliudascalu.mvpnotes.AppModule; 4 | import com.corneliudascalu.mvpnotes.data.interactor.NoteInteractor; 5 | import com.corneliudascalu.mvpnotes.ui.view.details.interfaces.NoteDetailsPresenter; 6 | import com.corneliudascalu.mvpnotes.ui.view.details.interfaces.NoteDetailsView; 7 | 8 | import javax.inject.Singleton; 9 | 10 | import dagger.Module; 11 | import dagger.Provides; 12 | 13 | /** 14 | * @author Corneliu Dascalu 15 | */ 16 | @Module 17 | (injects = NoteDetailsDialogFragment.class, 18 | addsTo = AppModule.class) 19 | public class NoteDetailsModule { 20 | 21 | private final NoteDetailsView view; 22 | 23 | public NoteDetailsModule(NoteDetailsView view) { 24 | this.view = view; 25 | } 26 | 27 | @Provides 28 | public NoteDetailsView provideNoteDetailsView() { 29 | return view; 30 | } 31 | 32 | @Provides 33 | @Singleton 34 | public NoteDetailsPresenter provideNoteDetailsPresenter(NoteDetailsView view, 35 | NoteInteractor noteInteractor) { 36 | return new SimpleNoteDetailsPresenter(view, noteInteractor); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/src/main/java/com/corneliudascalu/mvpnotes/ui/view/details/SimpleNoteDetailsPresenter.java: -------------------------------------------------------------------------------- 1 | package com.corneliudascalu.mvpnotes.ui.view.details; 2 | 3 | import com.corneliudascalu.mvpnotes.data.interactor.NoteInteractor; 4 | import com.corneliudascalu.mvpnotes.data.model.Note; 5 | import com.corneliudascalu.mvpnotes.ui.view.details.interfaces.NoteDetailsPresenter; 6 | import com.corneliudascalu.mvpnotes.ui.view.details.interfaces.NoteDetailsView; 7 | import com.corneliudascalu.mvpnotes.ui.view.main.OnNoteOperationListener; 8 | 9 | import java.util.List; 10 | 11 | /** 12 | * @author Corneliu Dascalu 13 | */ 14 | public class SimpleNoteDetailsPresenter implements NoteDetailsPresenter { 15 | 16 | private final NoteDetailsView view; 17 | 18 | private final NoteInteractor interactor; 19 | 20 | private Note note; 21 | 22 | public SimpleNoteDetailsPresenter(NoteDetailsView view, NoteInteractor interactor) { 23 | this.view = view; 24 | this.interactor = interactor; 25 | } 26 | 27 | @Override 28 | public void setData(Note note) { 29 | this.note = note; 30 | } 31 | 32 | @Override 33 | public void viewReady() { 34 | view.setNote(note); 35 | } 36 | 37 | @Override 38 | public void deleteNote() { 39 | interactor.deleteNote(note, new OnNoteOperationListener() { 40 | @Override 41 | public void onNoteAdded(Note note) { 42 | 43 | } 44 | 45 | @Override 46 | public void onNoteAddError(Note.Error error) { 47 | 48 | } 49 | 50 | @Override 51 | public void onNoteDeleted(Note note) { 52 | view.close(); 53 | } 54 | 55 | @Override 56 | public void onNoteDeleteError(Note.Error error) { 57 | view.showError(error.getMessage()); 58 | view.close(); 59 | } 60 | 61 | @Override 62 | public void onNoteRetrieved(Note note) { 63 | 64 | } 65 | 66 | @Override 67 | public void onNoteListRetrieved(List notes) { 68 | 69 | } 70 | 71 | @Override 72 | public void onRetrieveError(Note.Error error) { 73 | 74 | } 75 | }); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /app/src/main/java/com/corneliudascalu/mvpnotes/ui/view/details/interfaces/NoteDetailsPresenter.java: -------------------------------------------------------------------------------- 1 | package com.corneliudascalu.mvpnotes.ui.view.details.interfaces; 2 | 3 | import com.corneliudascalu.mvpnotes.data.model.Note; 4 | 5 | /** 6 | * @author Corneliu Dascalu 7 | */ 8 | public interface NoteDetailsPresenter { 9 | 10 | void setData(Note note); 11 | 12 | void viewReady(); 13 | 14 | void deleteNote(); 15 | } 16 | -------------------------------------------------------------------------------- /app/src/main/java/com/corneliudascalu/mvpnotes/ui/view/details/interfaces/NoteDetailsView.java: -------------------------------------------------------------------------------- 1 | package com.corneliudascalu.mvpnotes.ui.view.details.interfaces; 2 | 3 | import com.corneliudascalu.mvpnotes.data.model.Note; 4 | 5 | /** 6 | * @author Corneliu Dascalu 7 | */ 8 | public interface NoteDetailsView { 9 | 10 | void setNote(Note note); 11 | 12 | void close(); 13 | 14 | void showError(String error); 15 | } 16 | -------------------------------------------------------------------------------- /app/src/main/java/com/corneliudascalu/mvpnotes/ui/view/main/NotesActivity.java: -------------------------------------------------------------------------------- 1 | package com.corneliudascalu.mvpnotes.ui.view.main; 2 | 3 | import com.corneliudascalu.mvpnotes.R; 4 | import com.corneliudascalu.mvpnotes.common.BaseInjectedActivity; 5 | import com.corneliudascalu.mvpnotes.data.model.Note; 6 | import com.corneliudascalu.mvpnotes.ui.view.details.NoteDetailsDialogFragment; 7 | 8 | import org.joda.time.format.DateTimeFormatter; 9 | import org.joda.time.format.DateTimeFormatterBuilder; 10 | 11 | import android.os.Bundle; 12 | import android.view.ActionMode; 13 | import android.view.LayoutInflater; 14 | import android.view.Menu; 15 | import android.view.MenuItem; 16 | import android.view.View; 17 | import android.view.ViewGroup; 18 | import android.widget.AdapterView; 19 | import android.widget.ArrayAdapter; 20 | import android.widget.Button; 21 | import android.widget.ListView; 22 | import android.widget.TextView; 23 | 24 | import java.util.ArrayList; 25 | import java.util.Arrays; 26 | import java.util.Collections; 27 | import java.util.List; 28 | 29 | import javax.inject.Inject; 30 | 31 | import butterknife.ButterKnife; 32 | import butterknife.InjectView; 33 | import butterknife.OnClick; 34 | 35 | 36 | public class NotesActivity extends BaseInjectedActivity implements NotesView { 37 | 38 | @InjectView(R.id.noteText) 39 | TextView noteText; 40 | 41 | @InjectView(R.id.submitNoteButton) 42 | Button submitNote; 43 | 44 | @InjectView(R.id.notesList) 45 | ListView notesList; 46 | 47 | @Inject 48 | NotesPresenter notesPresenter; 49 | 50 | DateTimeFormatter dateTimeFormatter; 51 | 52 | private ArrayAdapter mAdapter; 53 | 54 | private ArrayList mNotes; 55 | 56 | private ActionMode.Callback noteSelectedCallback = new ActionMode.Callback() { 57 | @Override 58 | public boolean onCreateActionMode(ActionMode mode, Menu menu) { 59 | mode.getMenuInflater().inflate(R.menu.delete_note, menu); 60 | return true; 61 | } 62 | 63 | @Override 64 | public boolean onPrepareActionMode(ActionMode mode, Menu menu) { 65 | return false; 66 | } 67 | 68 | @Override 69 | public boolean onActionItemClicked(ActionMode mode, MenuItem item) { 70 | switch (item.getItemId()) { 71 | case R.id.action_delete: 72 | notesPresenter.deleteNote(mPendingNote); 73 | mode.finish(); 74 | return true; 75 | } 76 | return false; 77 | } 78 | 79 | @Override 80 | public void onDestroyActionMode(ActionMode mode) { 81 | 82 | } 83 | }; 84 | 85 | private Note mPendingNote; 86 | 87 | @Override 88 | protected void onCreate(Bundle savedInstanceState) { 89 | super.onCreate(savedInstanceState); 90 | setContentView(R.layout.activity_notes); 91 | ButterKnife.inject(this); 92 | DateTimeFormatterBuilder builder = new DateTimeFormatterBuilder(); 93 | builder.appendHourOfDay(2).appendLiteral(":").appendMinuteOfHour(2).appendLiteral(":") 94 | .appendSecondOfMinute(2); 95 | dateTimeFormatter = builder.toFormatter(); 96 | 97 | mNotes = new ArrayList(); 98 | mAdapter = new ArrayAdapter(this, 99 | android.R.layout.simple_list_item_2, mNotes) { 100 | @Override 101 | public View getView(int position, View convertView, ViewGroup parent) { 102 | if (convertView == null) { 103 | convertView = LayoutInflater.from(parent.getContext()) 104 | .inflate(android.R.layout.simple_list_item_2, parent, false); 105 | } 106 | 107 | Note note = getItem(position); 108 | ((TextView) convertView.findViewById(android.R.id.text1)).setText(note.title); 109 | ((TextView) convertView.findViewById(android.R.id.text2)).setText(note.text); 110 | 111 | return convertView; 112 | } 113 | }; 114 | 115 | notesList.setAdapter(mAdapter); 116 | notesList.setOnItemClickListener(new AdapterView.OnItemClickListener() { 117 | @Override 118 | public void onItemClick(AdapterView parent, View view, int position, long id) { 119 | Note note = mAdapter.getItem(position); 120 | NoteDetailsDialogFragment dialog = NoteDetailsDialogFragment.newInstance(note); 121 | dialog.show(getFragmentManager(), NoteDetailsDialogFragment.TAG); 122 | } 123 | }); 124 | notesList.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() { 125 | @Override 126 | public boolean onItemLongClick(AdapterView parent, View view, int position, 127 | long id) { 128 | mPendingNote = mAdapter.getItem(position); 129 | NotesActivity.this.startActionMode(noteSelectedCallback); 130 | return true; 131 | } 132 | }); 133 | notesPresenter.requestNotes(); 134 | } 135 | 136 | /** 137 | * We'll need the {@link com.corneliudascalu.mvpnotes.ui.view.main.NotesModule} to supply the 138 | * {@link com 139 | * .corneliudascalu.mvpnotes.ui.view.main.NotesPresenter} (and maybe other stuff, in the 140 | * future) 141 | */ 142 | @Override 143 | protected List getModules() { 144 | return Arrays.asList(new NotesModule(this)); 145 | } 146 | 147 | @OnClick(R.id.submitNoteButton) 148 | public void submitNote() { 149 | notesPresenter.submitNewNote("Note", noteText.getText().toString()); 150 | noteText.setText(null); 151 | } 152 | 153 | @Override 154 | public boolean onCreateOptionsMenu(Menu menu) { 155 | // Inflate the menu; this adds items to the action bar if it is present. 156 | getMenuInflater().inflate(R.menu.notes, menu); 157 | return true; 158 | } 159 | 160 | @Override 161 | public boolean onOptionsItemSelected(MenuItem item) { 162 | // Handle action bar item clicks here. The action bar will 163 | // automatically handle clicks on the Home/Up button, so long 164 | // as you specify a parent activity in AndroidManifest.xml. 165 | int id = item.getItemId(); 166 | if (id == R.id.action_settings) { 167 | return true; 168 | } 169 | return super.onOptionsItemSelected(item); 170 | } 171 | 172 | @Override 173 | public void setNoteError(String error) { 174 | noteText.setError(error); 175 | } 176 | 177 | @Override 178 | public void addNotes(Note... notes) { 179 | Collections.addAll(mNotes, notes); 180 | mAdapter.notifyDataSetChanged(); 181 | } 182 | 183 | @Override 184 | public void removeNote(Note note) { 185 | mNotes.remove(note); 186 | mAdapter.notifyDataSetChanged(); 187 | } 188 | 189 | } 190 | -------------------------------------------------------------------------------- /app/src/main/java/com/corneliudascalu/mvpnotes/ui/view/main/NotesModule.java: -------------------------------------------------------------------------------- 1 | package com.corneliudascalu.mvpnotes.ui.view.main; 2 | 3 | import com.corneliudascalu.mvpnotes.AppModule; 4 | import com.corneliudascalu.mvpnotes.data.interactor.NoteInteractor; 5 | import dagger.Module; 6 | import dagger.Provides; 7 | 8 | import javax.inject.Singleton; 9 | 10 | /** 11 | * Provides the stuff needed by the activity. In this case, the {@link com.corneliudascalu.mvpnotes.ui.view.main 12 | * .NotesPresenter}. Interesting to note, the {@link com.corneliudascalu.mvpnotes.data.interactor.NoteInteractor} 13 | * from the {@link #provideNotesPresenter(NotesView, com.corneliudascalu.mvpnotes.data.interactor.NoteInteractor)} 14 | * method definition is provided by the {@link com.corneliudascalu.mvpnotes.AppModule}, 15 | * through the {@link com.corneliudascalu.mvpnotes.data.interactor.InteractorsModule InteractorsModule} 16 | * 17 | * @author Corneliu Dascalu 18 | */ 19 | @Module( 20 | injects = {NotesActivity.class}, 21 | addsTo = AppModule.class 22 | ) 23 | public class NotesModule { 24 | private NotesView notesView; 25 | 26 | public NotesModule(NotesView notesView) { 27 | this.notesView = notesView; 28 | } 29 | 30 | @Provides 31 | @Singleton 32 | public NotesView provideNotesView() { 33 | return notesView; 34 | } 35 | 36 | @Provides 37 | @Singleton 38 | public NotesPresenter provideNotesPresenter(NotesView notesView, NoteInteractor noteInteractor) { 39 | return new SimpleNotesPresenter(notesView, noteInteractor); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/src/main/java/com/corneliudascalu/mvpnotes/ui/view/main/NotesPresenter.java: -------------------------------------------------------------------------------- 1 | package com.corneliudascalu.mvpnotes.ui.view.main; 2 | 3 | import com.corneliudascalu.mvpnotes.data.model.Note; 4 | 5 | /** 6 | * Defines the methods of a NotesPresenter implementation 7 | * 8 | * @author Corneliu Dascalu 9 | */ 10 | public interface NotesPresenter { 11 | 12 | /** 13 | * Request a list of notes from the presenter 14 | */ 15 | void requestNotes(); 16 | 17 | /** 18 | * Send a note to the presenter, to store it 19 | */ 20 | void submitNewNote(String title, String text); 21 | 22 | /** 23 | * Request to the presenter to delete this note 24 | */ 25 | void deleteNote(Note note); 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/java/com/corneliudascalu/mvpnotes/ui/view/main/NotesView.java: -------------------------------------------------------------------------------- 1 | package com.corneliudascalu.mvpnotes.ui.view.main; 2 | 3 | import com.corneliudascalu.mvpnotes.data.model.Note; 4 | 5 | /** 6 | * Defines the methods of a NotesView. In our case, the interface will be implemented by an activity, 7 | * but it could just as well be implemented by a fragment or a simple view. 8 | * 9 | * @author Corneliu Dascalu 10 | */ 11 | public interface NotesView { 12 | void setNoteError(String error); 13 | 14 | void addNotes(Note... note); 15 | 16 | void removeNote(Note note); 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/java/com/corneliudascalu/mvpnotes/ui/view/main/OnNoteOperationListener.java: -------------------------------------------------------------------------------- 1 | package com.corneliudascalu.mvpnotes.ui.view.main; 2 | 3 | import com.corneliudascalu.mvpnotes.data.model.Note; 4 | 5 | import java.util.List; 6 | 7 | /** 8 | * @author Corneliu Dascalu 9 | */ 10 | public interface OnNoteOperationListener { 11 | 12 | void onNoteAdded(Note note); 13 | 14 | void onNoteAddError(Note.Error error); 15 | 16 | void onNoteDeleted(Note note); 17 | 18 | void onNoteDeleteError(Note.Error error); 19 | 20 | void onNoteRetrieved(Note note); 21 | 22 | void onNoteListRetrieved(List notes); 23 | 24 | void onRetrieveError(Note.Error error); 25 | } 26 | -------------------------------------------------------------------------------- /app/src/main/java/com/corneliudascalu/mvpnotes/ui/view/main/SimpleNotesPresenter.java: -------------------------------------------------------------------------------- 1 | package com.corneliudascalu.mvpnotes.ui.view.main; 2 | 3 | import com.corneliudascalu.mvpnotes.data.interactor.NoteInteractor; 4 | import com.corneliudascalu.mvpnotes.data.model.Note; 5 | 6 | import org.joda.time.DateTime; 7 | 8 | import java.util.List; 9 | 10 | /** 11 | * Implements a simple note presenter, that handles the data input from the {@link 12 | * com.corneliudascalu.mvpnotes.ui 13 | * .view.main.NotesView} and communicates with the {@link com.corneliudascalu.mvpnotes.data.interactor.NoteInteractor} 14 | * 15 | * @author Corneliu Dascalu 16 | */ 17 | public class SimpleNotesPresenter implements NotesPresenter, OnNoteOperationListener { 18 | 19 | private NotesView notesView; 20 | 21 | private NoteInteractor noteInteractor; 22 | 23 | public SimpleNotesPresenter(NotesView notesView, NoteInteractor noteInteractor) { 24 | this.notesView = notesView; 25 | this.noteInteractor = noteInteractor; 26 | } 27 | 28 | @Override 29 | public void requestNotes() { 30 | noteInteractor.getAllNotes(this); 31 | } 32 | 33 | @Override 34 | public void submitNewNote(String title, String text) { 35 | Note note = new Note(); 36 | note.createdDate = new DateTime(); 37 | note.text = text; 38 | note.title = title; 39 | noteInteractor.storeNote(note, this); 40 | } 41 | 42 | @Override 43 | public void deleteNote(Note note) { 44 | noteInteractor.deleteNote(note, this); 45 | } 46 | 47 | @Override 48 | public void onNoteAdded(Note note) { 49 | notesView.addNotes(note); 50 | } 51 | 52 | @Override 53 | public void onNoteAddError(Note.Error error) { 54 | notesView.setNoteError(error.getMessage()); 55 | } 56 | 57 | @Override 58 | public void onNoteDeleted(Note note) {notesView.removeNote(note); 59 | } 60 | 61 | @Override 62 | public void onNoteDeleteError(Note.Error error) { 63 | notesView.setNoteError(error.getMessage()); 64 | } 65 | 66 | @Override 67 | public void onNoteRetrieved(Note note) { 68 | notesView.addNotes(note); 69 | } 70 | 71 | @Override 72 | public void onNoteListRetrieved(List notes) { 73 | notesView.addNotes(notes.toArray(new Note[notes.size()])); 74 | } 75 | 76 | @Override 77 | public void onRetrieveError(Note.Error error) { 78 | notesView.setNoteError(error.getMessage()); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /app/src/main/java/com/corneliudascalu/mvpnotes/util/DateTimeDeserializer.java: -------------------------------------------------------------------------------- 1 | package com.corneliudascalu.mvpnotes.util; 2 | 3 | import com.google.gson.JsonDeserializationContext; 4 | import com.google.gson.JsonDeserializer; 5 | import com.google.gson.JsonElement; 6 | import com.google.gson.JsonParseException; 7 | 8 | import org.joda.time.DateTime; 9 | 10 | import java.lang.reflect.Type; 11 | 12 | /** 13 | * @author Corneliu Dascalu 14 | */ 15 | public class DateTimeDeserializer implements JsonDeserializer { 16 | 17 | @Override 18 | public DateTime deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) 19 | throws JsonParseException { 20 | return new DateTime(json.getAsJsonPrimitive().getAsString()); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/src/main/java/com/corneliudascalu/mvpnotes/util/DateTimeSerializer.java: -------------------------------------------------------------------------------- 1 | package com.corneliudascalu.mvpnotes.util; 2 | 3 | import com.google.gson.JsonElement; 4 | import com.google.gson.JsonPrimitive; 5 | import com.google.gson.JsonSerializationContext; 6 | import com.google.gson.JsonSerializer; 7 | 8 | import org.joda.time.DateTime; 9 | 10 | import java.lang.reflect.Type; 11 | 12 | /** 13 | * @author Corneliu Dascalu 14 | */ 15 | public class DateTimeSerializer implements JsonSerializer { 16 | 17 | @Override 18 | public JsonElement serialize(DateTime src, Type typeOfSrc, JsonSerializationContext context) { 19 | return new JsonPrimitive(src.toString()); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/corneliudascalu/mvp-notes/8ec370dd635280380de7b60f4b2dd2874878d4dd/app/src/main/res/drawable-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/corneliudascalu/mvp-notes/8ec370dd635280380de7b60f4b2dd2874878d4dd/app/src/main/res/drawable-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/corneliudascalu/mvp-notes/8ec370dd635280380de7b60f4b2dd2874878d4dd/app/src/main/res/drawable-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/corneliudascalu/mvp-notes/8ec370dd635280380de7b60f4b2dd2874878d4dd/app/src/main/res/drawable-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_notes.xml: -------------------------------------------------------------------------------- 1 | 11 | 12 | 17 | 18 |