├── .gitignore ├── .idea └── runConfigurations.xml ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── bignerdranch │ │ └── android │ │ └── testfriendlyarchitecture │ │ ├── TestReminderApplication.java │ │ ├── controller │ │ └── AddReminderTest.java │ │ ├── fakes │ │ └── FakeReminderStore.java │ │ └── inject │ │ ├── FakeStoreModule.java │ │ ├── ReminderTestRunner.java │ │ └── TestComponent.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── bignerdranch │ │ │ └── android │ │ │ └── testfriendlyarchitecture │ │ │ ├── controller │ │ │ ├── DrawerActivity.java │ │ │ ├── ReminderApplication.java │ │ │ ├── create │ │ │ │ ├── CreateReminderActivity.java │ │ │ │ └── DateDialogFragment.java │ │ │ └── list │ │ │ │ ├── ReminderAdapter.java │ │ │ │ ├── ReminderListFragment.java │ │ │ │ └── ReminderViewHolder.java │ │ │ ├── inject │ │ │ ├── AdapterModule.java │ │ │ ├── AppComponent.java │ │ │ ├── AppModule.java │ │ │ ├── ModelModule.java │ │ │ ├── StoreModule.java │ │ │ └── ViewModelModule.java │ │ │ ├── model │ │ │ ├── Reminder.java │ │ │ ├── date │ │ │ │ ├── DateFormatter.java │ │ │ │ ├── DateRange.java │ │ │ │ ├── DateSelectionListener.java │ │ │ │ └── DateUtils.java │ │ │ └── store │ │ │ │ ├── LiveReminderStore.java │ │ │ │ ├── ReminderContract.java │ │ │ │ ├── ReminderCursorWrapper.java │ │ │ │ ├── ReminderDatabase.java │ │ │ │ └── ReminderStore.java │ │ │ └── viewmodel │ │ │ ├── CreateReminderViewModel.java │ │ │ └── ReminderItemViewModel.java │ └── res │ │ ├── drawable │ │ ├── ic_add_reminder.xml │ │ └── ic_launcher_background.xml │ │ ├── layout │ │ ├── activity_create_reminder.xml │ │ ├── activity_drawer.xml │ │ ├── fragment_date_dialog.xml │ │ ├── fragment_reminder_list.xml │ │ ├── item_reminder.xml │ │ └── navigation_drawer_header.xml │ │ ├── menu │ │ └── navigation_drawer_items.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── values-v21 │ │ └── styles.xml │ │ └── values │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── bignerdranch │ └── android │ └── testfriendlyarchitecture │ ├── model │ └── date │ │ ├── DateFormatterTest.java │ │ └── DateUtilsTest.java │ └── viewmodel │ └── CreateReminderViewModelTest.java ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/android,androidstudio 3 | 4 | ### Android ### 5 | # Built application files 6 | *.apk 7 | *.ap_ 8 | 9 | # Files for the ART/Dalvik VM 10 | *.dex 11 | 12 | # Java class files 13 | *.class 14 | 15 | # Generated files 16 | bin/ 17 | gen/ 18 | out/ 19 | 20 | # Gradle files 21 | .gradle/ 22 | build/ 23 | 24 | # Local configuration file (sdk path, etc) 25 | local.properties 26 | 27 | # Proguard folder generated by Eclipse 28 | proguard/ 29 | 30 | # Log Files 31 | *.log 32 | 33 | # Android Studio Navigation editor temp files 34 | .navigation/ 35 | 36 | # Android Studio captures folder 37 | captures/ 38 | 39 | # Intellij 40 | *.iml 41 | .idea/workspace.xml 42 | .idea/tasks.xml 43 | .idea/gradle.xml 44 | .idea/dictionaries 45 | .idea/libraries 46 | 47 | # Keystore files 48 | *.jks 49 | 50 | # External native build folder generated in Android Studio 2.2 and later 51 | .externalNativeBuild 52 | 53 | # Google Services (e.g. APIs or Firebase) 54 | google-services.json 55 | 56 | # Freeline 57 | freeline.py 58 | freeline/ 59 | freeline_project_description.json 60 | 61 | ### Android Patch ### 62 | gen-external-apklibs 63 | 64 | ### AndroidStudio ### 65 | # Covers files to be ignored for android development using Android Studio. 66 | 67 | # Built application files 68 | 69 | # Files for the ART/Dalvik VM 70 | 71 | # Java class files 72 | 73 | # Generated files 74 | 75 | # Gradle files 76 | .gradle 77 | 78 | # Signing files 79 | .signing/ 80 | 81 | # Local configuration file (sdk path, etc) 82 | 83 | # Proguard folder generated by Eclipse 84 | 85 | # Log Files 86 | 87 | # Android Studio 88 | /*/build/ 89 | /*/local.properties 90 | /*/out 91 | /*/*/build 92 | /*/*/production 93 | *.ipr 94 | *~ 95 | *.swp 96 | 97 | # Android Patch 98 | 99 | # External native build folder generated in Android Studio 2.2 and later 100 | 101 | # NDK 102 | obj/ 103 | 104 | # IntelliJ IDEA 105 | *.iws 106 | /out/ 107 | 108 | # User-specific configurations 109 | .idea/libraries/ 110 | .idea/.name 111 | .idea/compiler.xml 112 | .idea/copyright/profiles_settings.xml 113 | .idea/encodings.xml 114 | .idea/misc.xml 115 | .idea/modules.xml 116 | .idea/scopes/scope_settings.xml 117 | .idea/vcs.xml 118 | .idea/jsLibraryMappings.xml 119 | .idea/datasources.xml 120 | .idea/dataSources.ids 121 | .idea/sqlDataSources.xml 122 | .idea/dynamic.xml 123 | .idea/uiDesigner.xml 124 | 125 | # Keystore files 126 | 127 | # OS-specific files 128 | .DS_Store 129 | .DS_Store? 130 | ._* 131 | .Spotlight-V100 132 | .Trashes 133 | ehthumbs.db 134 | Thumbs.db 135 | 136 | # Legacy Eclipse project files 137 | .classpath 138 | .project 139 | 140 | # Mobile Tools for Java (J2ME) 141 | .mtj.tmp/ 142 | 143 | # Package Files # 144 | *.jar 145 | *.war 146 | *.ear 147 | 148 | # virtual machine crash logs (Reference: http://www.java.com/en/download/help/error_hotspot.xml) 149 | hs_err_pid* 150 | 151 | ## Plugin-specific files: 152 | 153 | # mpeltonen/sbt-idea plugin 154 | .idea_modules/ 155 | 156 | # JIRA plugin 157 | atlassian-ide-plugin.xml 158 | 159 | # Mongo Explorer plugin 160 | .idea/mongoSettings.xml 161 | 162 | # Crashlytics plugin (for Android Studio and IntelliJ) 163 | com_crashlytics_export_strings.xml 164 | crashlytics.properties 165 | crashlytics-build.properties 166 | fabric.properties 167 | 168 | ### AndroidStudio Patch ### 169 | # Google Services plugin 170 | 171 | !/gradle/wrapper/gradle-wrapper.jar 172 | 173 | # End of https://www.gitignore.io/api/android,androidstudio 174 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Test Friendly Architecture 2 | 3 | Sample application I used for the examples in my conference talk at 360AnDev in 4 | Denver. It uses Dagger 2 to inject dependencies in production and uses a Fake 5 | store object in the integration tests. 6 | 7 | 8 | ### Useful Articles and Talks 9 | 10 | [What makes Android apps testable](https://www.philosophicalhacker.com/post/what-makes-android-apps-testable/) 11 | 12 | [Espresso: Beyond the Basics](https://news.realm.io/news/mobilization-inaki-villar-espresso-beyond-the-basics/) 13 | 14 | [Test Driving away Coupling in Activities](https://www.philosophicalhacker.com/post/test-driving-away-coupling-in-activities/) 15 | 16 | [Testing MVP using Espresso and Mockito](https://josiassena.com/testing-mvp-using-espresso-and-mockito/) 17 | 18 | [MVC vs MVP vs MVVM on Android](https://news.realm.io/news/eric-maxwell-mvc-mvp-and-mvvm-on-android) 19 | 20 | [Model-View-Presenter: Android guidelines](https://medium.com/@cervonefrancesco/model-view-presenter-android-guidelines-94970b430ddf) 21 | 22 | [Shades of MVVM](https://www.bignerdranch.com/blog/shades-of-mvvm/) 23 | 24 | [Android Architecture Patterns: Model-View-Presenter](https://upday.github.io/blog/model-view-presenter/) 25 | 26 | [Effective Android Architecture](https://news.realm.io/news/360andev-richa-khandelwal-effective-android-architecture-patterns-java/) 27 | 28 | [Dependency Injection in Android with Dagger 2](https://www.raywenderlich.com/146804/dependency-injection-dagger-2) 29 | 30 | [Dagger 2: Android Modules](https://proandroiddev.com/dagger-2-android-modules-e168821cfc57) 31 | 32 | [Activities Subcomponents Multibinding in Dagger 2](https://medium.com/azimolabs/activities-subcomponents-multibinding-in-dagger-2-85d6053d6a95) 33 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 25 5 | buildToolsVersion "26.0.0" 6 | defaultConfig { 7 | applicationId "com.bignerdranch.android.testfriendlyarchitecture" 8 | minSdkVersion 19 9 | targetSdkVersion 25 10 | versionCode 1 11 | versionName "1.0" 12 | testInstrumentationRunner "com.bignerdranch.android.testfriendlyarchitecture.inject.ReminderTestRunner" 13 | } 14 | buildTypes { 15 | release { 16 | minifyEnabled false 17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 18 | } 19 | } 20 | dataBinding { 21 | enabled = true 22 | } 23 | compileOptions { 24 | sourceCompatibility 1.8 25 | targetCompatibility 1.8 26 | } 27 | } 28 | 29 | dependencies { 30 | implementation fileTree(dir: 'libs', include: ['*.jar']) 31 | implementation 'com.android.support:appcompat-v7:25.4.0' 32 | implementation 'com.android.support:recyclerview-v7:25.4.0' 33 | implementation 'com.android.support:design:25.4.0' 34 | 35 | // Dagger Dependencies 36 | implementation 'com.google.dagger:dagger:2.11' 37 | annotationProcessor 'com.google.dagger:dagger-compiler:2.11' 38 | 39 | // RxJava 40 | implementation 'io.reactivex.rxjava2:rxandroid:2.0.1' 41 | implementation 'io.reactivex.rxjava2:rxjava:2.1.1' 42 | 43 | // Joda Time 44 | implementation 'joda-time:joda-time:2.9.9' 45 | 46 | testImplementation 'junit:junit:4.12' 47 | testImplementation 'org.mockito:mockito-core:2.8.47' 48 | 49 | androidTestImplementation('com.android.support.test.espresso:espresso-core:2.2.2', { 50 | exclude group: 'com.android.support', module: 'support-annotations' 51 | }) 52 | androidTestImplementation('com.android.support.test.espresso:espresso-contrib:2.2.2', { 53 | exclude group: 'com.android.support', module: 'appcompat' 54 | exclude module: 'support-annotations' 55 | exclude module: 'support-v4' 56 | exclude module: 'support-v13' 57 | exclude module: 'recyclerview-v7' 58 | exclude module: 'appcompat-v7' 59 | }) 60 | androidTestAnnotationProcessor 'com.google.dagger:dagger-compiler:2.11' 61 | } 62 | -------------------------------------------------------------------------------- /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 /Users/bgardner/Library/Android/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 | 19 | # Uncomment this to preserve the line number information for 20 | # debugging stack traces. 21 | #-keepattributes SourceFile,LineNumberTable 22 | 23 | # If you keep the line number information, uncomment this to 24 | # hide the original source file name. 25 | #-renamesourcefileattribute SourceFile 26 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/bignerdranch/android/testfriendlyarchitecture/TestReminderApplication.java: -------------------------------------------------------------------------------- 1 | package com.bignerdranch.android.testfriendlyarchitecture; 2 | 3 | import android.content.Context; 4 | 5 | import com.bignerdranch.android.testfriendlyarchitecture.controller.ReminderApplication; 6 | import com.bignerdranch.android.testfriendlyarchitecture.inject.AppModule; 7 | import com.bignerdranch.android.testfriendlyarchitecture.inject.DaggerTestComponent; 8 | 9 | 10 | public class TestReminderApplication extends ReminderApplication { 11 | 12 | public static TestReminderApplication get(Context context) { 13 | return (TestReminderApplication) context.getApplicationContext(); 14 | } 15 | 16 | @Override 17 | protected void initializeDependencyInjection() { 18 | appComponent = DaggerTestComponent.builder() 19 | .appModule(new AppModule(this)) 20 | .build(); 21 | } 22 | 23 | @Override 24 | public DaggerTestComponent getAppComponent() { 25 | return (DaggerTestComponent) appComponent; 26 | } 27 | 28 | 29 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/com/bignerdranch/android/testfriendlyarchitecture/controller/AddReminderTest.java: -------------------------------------------------------------------------------- 1 | package com.bignerdranch.android.testfriendlyarchitecture.controller; 2 | 3 | import android.support.test.espresso.ViewInteraction; 4 | import android.support.test.espresso.contrib.PickerActions; 5 | import android.support.test.rule.ActivityTestRule; 6 | import android.support.test.runner.AndroidJUnit4; 7 | import android.test.suitebuilder.annotation.LargeTest; 8 | import android.view.View; 9 | import android.view.ViewGroup; 10 | import android.view.ViewParent; 11 | import android.widget.DatePicker; 12 | 13 | import com.bignerdranch.android.testfriendlyarchitecture.R; 14 | import com.bignerdranch.android.testfriendlyarchitecture.TestReminderApplication; 15 | import com.bignerdranch.android.testfriendlyarchitecture.model.store.ReminderStore; 16 | 17 | import org.hamcrest.Description; 18 | import org.hamcrest.Matcher; 19 | import org.hamcrest.Matchers; 20 | import org.hamcrest.TypeSafeMatcher; 21 | import org.junit.After; 22 | import org.junit.Before; 23 | import org.junit.Rule; 24 | import org.junit.Test; 25 | import org.junit.runner.RunWith; 26 | 27 | import javax.inject.Inject; 28 | 29 | import static android.support.test.espresso.Espresso.onView; 30 | import static android.support.test.espresso.action.ViewActions.click; 31 | import static android.support.test.espresso.action.ViewActions.closeSoftKeyboard; 32 | import static android.support.test.espresso.action.ViewActions.replaceText; 33 | import static android.support.test.espresso.assertion.ViewAssertions.matches; 34 | import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed; 35 | import static android.support.test.espresso.matcher.ViewMatchers.withClassName; 36 | import static android.support.test.espresso.matcher.ViewMatchers.withId; 37 | import static android.support.test.espresso.matcher.ViewMatchers.withText; 38 | import static org.hamcrest.Matchers.allOf; 39 | import static org.hamcrest.Matchers.is; 40 | 41 | @LargeTest 42 | @RunWith(AndroidJUnit4.class) 43 | public class AddReminderTest { 44 | @Rule 45 | public ActivityTestRule mActivityTestRule = 46 | new ActivityTestRule<>(DrawerActivity.class); 47 | @Inject 48 | ReminderStore reminderStore; 49 | 50 | @Before 51 | public void setup() { 52 | TestReminderApplication.get(mActivityTestRule.getActivity()) 53 | .getAppComponent().inject(this); 54 | } 55 | 56 | @After 57 | public void tearDown() { 58 | reminderStore.clearReminders(); 59 | } 60 | 61 | @Test 62 | public void addReminderTest() { 63 | // Click on FAB to add new reminder 64 | ViewInteraction floatingActionButton = onView( 65 | allOf(withId(R.id.add_reminder), isDisplayed())); 66 | floatingActionButton.perform(click()); 67 | 68 | // Input the new reminder title 69 | ViewInteraction reminderTitleInput = onView( 70 | allOf(childAtPosition( 71 | childAtPosition( 72 | withClassName(is("android.support.design.widget.TextInputLayout")), 73 | 0), 74 | 0), 75 | isDisplayed())); 76 | reminderTitleInput.perform(replaceText("test reminder"), closeSoftKeyboard()); 77 | 78 | // Click on date set button to open date picker dialog 79 | ViewInteraction dateSelectionButton = onView( 80 | allOf(withId(R.id.set_date), isDisplayed())); 81 | dateSelectionButton.perform(click()); 82 | 83 | // Set the date in date picker using PickerActions from espresso contrib library 84 | onView(withClassName(Matchers.equalTo(DatePicker.class.getName()))) 85 | .perform(PickerActions.setDate(2017, 7, 13)); // July 13th 2017 86 | 87 | // click the ok button to dismiss the dialog 88 | ViewInteraction appCompatButton2 = onView( 89 | allOf(withId(R.id.confirm), isDisplayed())); 90 | appCompatButton2.perform(click()); 91 | 92 | // click save reminder button to go back to list screen 93 | ViewInteraction appCompatButton3 = onView( 94 | allOf(withId(R.id.save_reminder), withText("Save Reminder"), isDisplayed())); 95 | appCompatButton3.perform(click()); 96 | 97 | // verify that the reminder name is correct 98 | ViewInteraction textView = onView( 99 | allOf(withText("test reminder"), 100 | childAtPosition( 101 | childAtPosition( 102 | withId(R.id.reminder_list), 103 | 0), 104 | 0), 105 | isDisplayed())); 106 | textView.check(matches(withText("test reminder"))); 107 | 108 | // verify that the reminder date is correct 109 | ViewInteraction textView2 = onView( 110 | allOf(withText("07/13/2017"), 111 | childAtPosition( 112 | childAtPosition( 113 | withId(R.id.reminder_list), 114 | 0), 115 | 1), 116 | isDisplayed())); 117 | textView2.check(matches(withText("07/13/2017"))); 118 | } 119 | 120 | private static Matcher childAtPosition( 121 | final Matcher parentMatcher, final int position) { 122 | 123 | return new TypeSafeMatcher() { 124 | @Override 125 | public void describeTo(Description description) { 126 | description.appendText("Child at position " + position + " in parent "); 127 | parentMatcher.describeTo(description); 128 | } 129 | 130 | @Override 131 | public boolean matchesSafely(View view) { 132 | ViewParent parent = view.getParent(); 133 | return parent instanceof ViewGroup && parentMatcher.matches(parent) 134 | && view.equals(((ViewGroup) parent).getChildAt(position)); 135 | } 136 | }; 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/bignerdranch/android/testfriendlyarchitecture/fakes/FakeReminderStore.java: -------------------------------------------------------------------------------- 1 | package com.bignerdranch.android.testfriendlyarchitecture.fakes; 2 | 3 | import com.bignerdranch.android.testfriendlyarchitecture.model.Reminder; 4 | import com.bignerdranch.android.testfriendlyarchitecture.model.store.ReminderStore; 5 | 6 | import java.util.ArrayList; 7 | import java.util.List; 8 | 9 | public class FakeReminderStore implements ReminderStore { 10 | 11 | private List reminders; 12 | 13 | public FakeReminderStore() { 14 | reminders = new ArrayList<>(); 15 | } 16 | 17 | @Override 18 | public List getReminders() { 19 | return reminders; 20 | } 21 | 22 | @Override 23 | public void addReminder(Reminder reminder) { 24 | reminders.add(reminder); 25 | } 26 | 27 | @Override 28 | public void clearReminders() { 29 | reminders = new ArrayList<>(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/bignerdranch/android/testfriendlyarchitecture/inject/FakeStoreModule.java: -------------------------------------------------------------------------------- 1 | package com.bignerdranch.android.testfriendlyarchitecture.inject; 2 | 3 | import com.bignerdranch.android.testfriendlyarchitecture.fakes.FakeReminderStore; 4 | import com.bignerdranch.android.testfriendlyarchitecture.model.store.ReminderStore; 5 | 6 | import javax.inject.Singleton; 7 | 8 | import dagger.Module; 9 | import dagger.Provides; 10 | 11 | @Module 12 | @Singleton 13 | public class FakeStoreModule { 14 | 15 | @Provides 16 | @Singleton 17 | ReminderStore providesReminderStore() { 18 | return new FakeReminderStore(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/bignerdranch/android/testfriendlyarchitecture/inject/ReminderTestRunner.java: -------------------------------------------------------------------------------- 1 | package com.bignerdranch.android.testfriendlyarchitecture.inject; 2 | 3 | import android.app.Application; 4 | import android.content.Context; 5 | import android.support.test.runner.AndroidJUnitRunner; 6 | 7 | import com.bignerdranch.android.testfriendlyarchitecture.TestReminderApplication; 8 | 9 | public class ReminderTestRunner extends AndroidJUnitRunner { 10 | @Override 11 | public Application newApplication(ClassLoader cl, 12 | String className, 13 | Context context) 14 | throws InstantiationException, IllegalAccessException, ClassNotFoundException { 15 | return super.newApplication(cl, TestReminderApplication.class.getName(), context); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/bignerdranch/android/testfriendlyarchitecture/inject/TestComponent.java: -------------------------------------------------------------------------------- 1 | package com.bignerdranch.android.testfriendlyarchitecture.inject; 2 | 3 | import com.bignerdranch.android.testfriendlyarchitecture.controller.AddReminderTest; 4 | 5 | import javax.inject.Singleton; 6 | 7 | import dagger.Component; 8 | 9 | @Component(modules = {AppModule.class, ViewModelModule.class, 10 | FakeStoreModule.class, AdapterModule.class, ModelModule.class}) 11 | @Singleton 12 | interface TestComponent extends AppComponent { 13 | void inject(AddReminderTest test); 14 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 12 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /app/src/main/java/com/bignerdranch/android/testfriendlyarchitecture/controller/DrawerActivity.java: -------------------------------------------------------------------------------- 1 | package com.bignerdranch.android.testfriendlyarchitecture.controller; 2 | 3 | import android.databinding.DataBindingUtil; 4 | import android.os.Bundle; 5 | import android.support.design.widget.NavigationView; 6 | import android.support.v4.app.Fragment; 7 | import android.support.v7.app.ActionBarDrawerToggle; 8 | import android.support.v7.app.AppCompatActivity; 9 | import android.view.View; 10 | 11 | import com.bignerdranch.android.testfriendlyarchitecture.R; 12 | import com.bignerdranch.android.testfriendlyarchitecture.controller.create.CreateReminderActivity; 13 | import com.bignerdranch.android.testfriendlyarchitecture.controller.list.ReminderListFragment; 14 | import com.bignerdranch.android.testfriendlyarchitecture.databinding.ActivityDrawerBinding; 15 | import com.bignerdranch.android.testfriendlyarchitecture.model.date.DateRange; 16 | 17 | public class DrawerActivity extends AppCompatActivity { 18 | private ActivityDrawerBinding binding; 19 | 20 | @Override 21 | protected void onCreate(Bundle savedInstanceState) { 22 | super.onCreate(savedInstanceState); 23 | binding = DataBindingUtil.setContentView(this, R.layout.activity_drawer); 24 | // setup toolbar from layout 25 | setSupportActionBar(binding.toolbar); 26 | // setup hamburger menu 27 | ActionBarDrawerToggle toggle = createDrawerToggle(); 28 | binding.drawerLayout.addDrawerListener(toggle); 29 | toggle.syncState(); 30 | // setup navigation item selection listener 31 | binding.navigation.setNavigationItemSelectedListener(listener); 32 | // set click listener on FAB 33 | binding.addReminder.setOnClickListener(addReminderListener); 34 | // setup initial screen on first launch 35 | if (savedInstanceState == null) { 36 | updateFragment(ReminderListFragment.newInstance(DateRange.ALL)); 37 | } 38 | } 39 | 40 | private ActionBarDrawerToggle createDrawerToggle() { 41 | return new ActionBarDrawerToggle(this, binding.drawerLayout, binding.toolbar, 42 | R.string.content_description_open_drawer, 43 | R.string.content_description_close_drawer); 44 | } 45 | 46 | private NavigationView.OnNavigationItemSelectedListener listener = menuItem -> { 47 | binding.drawerLayout.closeDrawers(); 48 | ReminderListFragment fragment; 49 | switch (menuItem.getItemId()) { 50 | case R.id.reminders_all: 51 | fragment = ReminderListFragment.newInstance(DateRange.ALL); 52 | break; 53 | case R.id.reminders_today: 54 | fragment = ReminderListFragment.newInstance(DateRange.TODAY); 55 | break; 56 | case R.id.reminders_week: 57 | fragment = ReminderListFragment.newInstance(DateRange.WEEK); 58 | break; 59 | default: 60 | throw new IllegalArgumentException("Unknown navigation item selected!"); 61 | } 62 | getSupportFragmentManager().beginTransaction() 63 | .replace(binding.fragmentContainer.getId(), fragment) 64 | .commit(); 65 | return true; 66 | }; 67 | 68 | private View.OnClickListener addReminderListener = view -> { 69 | // start the create activity 70 | startActivity(CreateReminderActivity.newIntent(this)); 71 | }; 72 | 73 | private void updateFragment(Fragment fragment) { 74 | getSupportFragmentManager().beginTransaction() 75 | .replace(binding.fragmentContainer.getId(), fragment) 76 | .commit(); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /app/src/main/java/com/bignerdranch/android/testfriendlyarchitecture/controller/ReminderApplication.java: -------------------------------------------------------------------------------- 1 | package com.bignerdranch.android.testfriendlyarchitecture.controller; 2 | 3 | import android.app.Application; 4 | import android.content.Context; 5 | 6 | import com.bignerdranch.android.testfriendlyarchitecture.inject.AdapterModule; 7 | import com.bignerdranch.android.testfriendlyarchitecture.inject.AppComponent; 8 | import com.bignerdranch.android.testfriendlyarchitecture.inject.AppModule; 9 | import com.bignerdranch.android.testfriendlyarchitecture.inject.DaggerAppComponent; 10 | import com.bignerdranch.android.testfriendlyarchitecture.inject.StoreModule; 11 | import com.bignerdranch.android.testfriendlyarchitecture.inject.ViewModelModule; 12 | 13 | public class ReminderApplication extends Application { 14 | 15 | protected AppComponent appComponent; 16 | 17 | public static ReminderApplication get(Context context) { 18 | return (ReminderApplication) context.getApplicationContext(); 19 | } 20 | 21 | @Override 22 | public void onCreate() { 23 | super.onCreate(); 24 | initializeDependencyInjection(); 25 | } 26 | 27 | public AppComponent getAppComponent() { 28 | return appComponent; 29 | } 30 | 31 | protected void initializeDependencyInjection() { 32 | appComponent = DaggerAppComponent.builder() 33 | .appModule(new AppModule(this)) 34 | .viewModelModule(new ViewModelModule()) 35 | .storeModule(new StoreModule()) 36 | .adapterModule(new AdapterModule()) 37 | .build(); 38 | } 39 | } -------------------------------------------------------------------------------- /app/src/main/java/com/bignerdranch/android/testfriendlyarchitecture/controller/create/CreateReminderActivity.java: -------------------------------------------------------------------------------- 1 | package com.bignerdranch.android.testfriendlyarchitecture.controller.create; 2 | 3 | import android.content.Context; 4 | import android.content.Intent; 5 | import android.databinding.DataBindingUtil; 6 | import android.os.Bundle; 7 | import android.support.v7.app.AppCompatActivity; 8 | import android.view.View; 9 | 10 | import com.bignerdranch.android.testfriendlyarchitecture.R; 11 | import com.bignerdranch.android.testfriendlyarchitecture.controller.ReminderApplication; 12 | import com.bignerdranch.android.testfriendlyarchitecture.databinding.ActivityCreateReminderBinding; 13 | import com.bignerdranch.android.testfriendlyarchitecture.model.date.DateSelectionListener; 14 | import com.bignerdranch.android.testfriendlyarchitecture.model.store.ReminderStore; 15 | import com.bignerdranch.android.testfriendlyarchitecture.viewmodel.CreateReminderViewModel; 16 | 17 | import org.joda.time.LocalDate; 18 | 19 | import javax.inject.Inject; 20 | 21 | public class CreateReminderActivity extends AppCompatActivity 22 | implements DateSelectionListener { 23 | private static final String TAG_DATE_FRAGMENT = "tag_date_fragment"; 24 | 25 | private ActivityCreateReminderBinding binding; 26 | @Inject 27 | protected CreateReminderViewModel viewModel; 28 | @Inject 29 | protected ReminderStore reminderStore; 30 | 31 | public static Intent newIntent(Context context) { 32 | return new Intent(context, CreateReminderActivity.class); 33 | } 34 | 35 | @Override 36 | protected void onCreate(Bundle savedInstanceState) { 37 | super.onCreate(savedInstanceState); 38 | // Inject dependencies 39 | ReminderApplication.get(this).getAppComponent().inject(this); 40 | // Setup view 41 | binding = DataBindingUtil.setContentView(this, R.layout.activity_create_reminder); 42 | binding.setReminder(viewModel); 43 | binding.setDate.setOnClickListener(setDateListener); 44 | binding.saveReminder.setOnClickListener(saveReminderListener); 45 | } 46 | 47 | private View.OnClickListener setDateListener = view -> { 48 | DateDialogFragment fragment = DateDialogFragment.newInstance(viewModel.getReminder().getDate()); 49 | getSupportFragmentManager().beginTransaction() 50 | .add(fragment, TAG_DATE_FRAGMENT) 51 | .commit(); 52 | }; 53 | 54 | private View.OnClickListener saveReminderListener = view -> { 55 | reminderStore.addReminder(viewModel.getReminder()); 56 | finish(); 57 | }; 58 | 59 | @Override 60 | public void onDateSelected(LocalDate date) { 61 | viewModel.setDate(date); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /app/src/main/java/com/bignerdranch/android/testfriendlyarchitecture/controller/create/DateDialogFragment.java: -------------------------------------------------------------------------------- 1 | package com.bignerdranch.android.testfriendlyarchitecture.controller.create; 2 | 3 | import android.content.Context; 4 | import android.databinding.DataBindingUtil; 5 | import android.os.Bundle; 6 | import android.support.annotation.Nullable; 7 | import android.support.v4.app.DialogFragment; 8 | import android.view.LayoutInflater; 9 | import android.view.View; 10 | import android.view.ViewGroup; 11 | import android.widget.DatePicker; 12 | 13 | import com.bignerdranch.android.testfriendlyarchitecture.R; 14 | import com.bignerdranch.android.testfriendlyarchitecture.databinding.FragmentDateDialogBinding; 15 | import com.bignerdranch.android.testfriendlyarchitecture.model.date.DateSelectionListener; 16 | 17 | import org.joda.time.LocalDate; 18 | 19 | public class DateDialogFragment extends DialogFragment { 20 | private static final String ARG_DATE = "arg_date"; 21 | 22 | private FragmentDateDialogBinding binding; 23 | private DateSelectionListener listener; 24 | private LocalDate currentDate; 25 | 26 | 27 | public static DateDialogFragment newInstance(LocalDate date) { 28 | Bundle args = new Bundle(); 29 | args.putSerializable(ARG_DATE, date); 30 | DateDialogFragment fragment = new DateDialogFragment(); 31 | fragment.setArguments(args); 32 | return fragment; 33 | } 34 | 35 | @Override 36 | public void onCreate(@Nullable Bundle savedInstanceState) { 37 | super.onCreate(savedInstanceState); 38 | currentDate = (LocalDate) getArguments().getSerializable(ARG_DATE); 39 | if (currentDate == null) { 40 | currentDate = new LocalDate(); 41 | } 42 | } 43 | 44 | @Nullable 45 | @Override 46 | public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { 47 | binding = DataBindingUtil.inflate(inflater, R.layout.fragment_date_dialog, container, false); 48 | binding.confirm.setOnClickListener(confirmListener); 49 | binding.datePicker.init( 50 | currentDate.getYear(), 51 | currentDate.getMonthOfYear() - 1, // date picker expects 0-11 month, jodatime supplies 1-12 52 | currentDate.getDayOfMonth(), 53 | dateChangedListener); 54 | 55 | return binding.getRoot(); 56 | } 57 | 58 | @Override 59 | public void onAttach(Context context) { 60 | super.onAttach(context); 61 | listener = (DateSelectionListener) context; 62 | } 63 | 64 | @Override 65 | public void onDetach() { 66 | super.onDetach(); 67 | listener = null; 68 | } 69 | 70 | private View.OnClickListener confirmListener = view -> { 71 | listener.onDateSelected(currentDate); 72 | dismiss(); 73 | }; 74 | 75 | private DatePicker.OnDateChangedListener dateChangedListener = new DatePicker.OnDateChangedListener() { 76 | @Override 77 | public void onDateChanged(DatePicker view, int year, int monthOfYear, int dayOfMonth) { 78 | int jodaMonth = monthOfYear + 1; // date picker expects 0-11 month, jodatime supplies 1-12 79 | currentDate = new LocalDate(year, jodaMonth, dayOfMonth); 80 | } 81 | }; 82 | } 83 | -------------------------------------------------------------------------------- /app/src/main/java/com/bignerdranch/android/testfriendlyarchitecture/controller/list/ReminderAdapter.java: -------------------------------------------------------------------------------- 1 | package com.bignerdranch.android.testfriendlyarchitecture.controller.list; 2 | 3 | import android.content.Context; 4 | import android.databinding.DataBindingUtil; 5 | import android.support.v7.widget.RecyclerView; 6 | import android.view.LayoutInflater; 7 | import android.view.ViewGroup; 8 | 9 | import com.bignerdranch.android.testfriendlyarchitecture.R; 10 | import com.bignerdranch.android.testfriendlyarchitecture.databinding.ItemReminderBinding; 11 | import com.bignerdranch.android.testfriendlyarchitecture.model.Reminder; 12 | 13 | import java.util.ArrayList; 14 | import java.util.List; 15 | 16 | public class ReminderAdapter extends RecyclerView.Adapter { 17 | 18 | private Context context; 19 | private List reminders; 20 | 21 | public ReminderAdapter(Context context) { 22 | this.context = context; 23 | this.reminders = new ArrayList<>(); 24 | } 25 | 26 | @Override 27 | public ReminderViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 28 | LayoutInflater inflater = LayoutInflater.from(context); 29 | ItemReminderBinding binding = DataBindingUtil.inflate(inflater, R.layout.item_reminder, parent, false); 30 | return new ReminderViewHolder(binding, context); 31 | } 32 | 33 | @Override 34 | public void onBindViewHolder(ReminderViewHolder holder, int position) { 35 | holder.bindReminder(reminders.get(position)); 36 | } 37 | 38 | @Override 39 | public int getItemCount() { 40 | return reminders.size(); 41 | } 42 | 43 | public void setReminders(List reminders) { 44 | this.reminders = reminders; 45 | notifyDataSetChanged(); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /app/src/main/java/com/bignerdranch/android/testfriendlyarchitecture/controller/list/ReminderListFragment.java: -------------------------------------------------------------------------------- 1 | package com.bignerdranch.android.testfriendlyarchitecture.controller.list; 2 | 3 | import android.databinding.DataBindingUtil; 4 | import android.os.Bundle; 5 | import android.support.annotation.Nullable; 6 | import android.support.v4.app.Fragment; 7 | import android.support.v7.widget.LinearLayoutManager; 8 | import android.util.Log; 9 | import android.view.LayoutInflater; 10 | import android.view.View; 11 | import android.view.ViewGroup; 12 | 13 | import com.bignerdranch.android.testfriendlyarchitecture.R; 14 | import com.bignerdranch.android.testfriendlyarchitecture.controller.ReminderApplication; 15 | import com.bignerdranch.android.testfriendlyarchitecture.databinding.FragmentReminderListBinding; 16 | import com.bignerdranch.android.testfriendlyarchitecture.model.Reminder; 17 | import com.bignerdranch.android.testfriendlyarchitecture.model.date.DateRange; 18 | import com.bignerdranch.android.testfriendlyarchitecture.model.date.DateUtils; 19 | import com.bignerdranch.android.testfriendlyarchitecture.model.store.ReminderStore; 20 | 21 | import java.util.List; 22 | 23 | import javax.inject.Inject; 24 | 25 | import io.reactivex.Observable; 26 | import io.reactivex.SingleObserver; 27 | import io.reactivex.disposables.Disposable; 28 | 29 | public class ReminderListFragment extends Fragment { 30 | private static final String TAG = "ReminderListFragment"; 31 | private static final String ARG_DATE_RANGE = "arg_date_range"; 32 | 33 | private FragmentReminderListBinding binding; 34 | private DateRange range; 35 | @Inject 36 | ReminderAdapter adapter; 37 | @Inject 38 | ReminderStore reminderStore; 39 | @Inject 40 | DateUtils dateUtils; 41 | 42 | public static ReminderListFragment newInstance(DateRange range) { 43 | Bundle args = new Bundle(); 44 | args.putSerializable(ARG_DATE_RANGE, range); 45 | ReminderListFragment fragment = new ReminderListFragment(); 46 | fragment.setArguments(args); 47 | return fragment; 48 | } 49 | 50 | @Override 51 | public void onCreate(@Nullable Bundle savedInstanceState) { 52 | super.onCreate(savedInstanceState); 53 | ReminderApplication.get(getContext()).getAppComponent().inject(this); 54 | range = (DateRange) getArguments().getSerializable(ARG_DATE_RANGE); 55 | } 56 | 57 | @Nullable 58 | @Override 59 | public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { 60 | binding = DataBindingUtil.inflate(inflater, R.layout.fragment_reminder_list, container, false); 61 | binding.reminderList.setLayoutManager(new LinearLayoutManager(getContext())); 62 | binding.reminderList.setAdapter(adapter); 63 | return binding.getRoot(); 64 | } 65 | 66 | @Override 67 | public void onResume() { 68 | super.onResume(); 69 | setupAdapter(); 70 | } 71 | 72 | private void setupAdapter() { 73 | Observable.fromIterable(reminderStore.getReminders()) 74 | .filter(this::isReminderInRange) 75 | .toList() 76 | .subscribe(new SingleObserver>() { 77 | @Override 78 | public void onSubscribe(Disposable d) { 79 | } 80 | 81 | @Override 82 | public void onSuccess(List reminders) { 83 | adapter.setReminders(reminders); 84 | } 85 | 86 | @Override 87 | public void onError(Throwable e) { 88 | Log.e(TAG, "onError: could not setup adapter", e); 89 | } 90 | }); 91 | } 92 | 93 | private boolean isReminderInRange(Reminder reminder) { 94 | switch (range) { 95 | case ALL: 96 | return true; 97 | case TODAY: 98 | return dateUtils.isToday(reminder.getDate()); 99 | case WEEK: 100 | return dateUtils.isThisWeek(reminder.getDate()); 101 | default: 102 | throw new IllegalArgumentException("Unknown date range specified"); 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /app/src/main/java/com/bignerdranch/android/testfriendlyarchitecture/controller/list/ReminderViewHolder.java: -------------------------------------------------------------------------------- 1 | package com.bignerdranch.android.testfriendlyarchitecture.controller.list; 2 | 3 | import android.content.Context; 4 | import android.support.v7.widget.RecyclerView; 5 | 6 | import com.bignerdranch.android.testfriendlyarchitecture.controller.ReminderApplication; 7 | import com.bignerdranch.android.testfriendlyarchitecture.databinding.ItemReminderBinding; 8 | import com.bignerdranch.android.testfriendlyarchitecture.model.Reminder; 9 | import com.bignerdranch.android.testfriendlyarchitecture.viewmodel.ReminderItemViewModel; 10 | 11 | import javax.inject.Inject; 12 | 13 | public class ReminderViewHolder extends RecyclerView.ViewHolder { 14 | 15 | private ItemReminderBinding binding; 16 | @Inject 17 | ReminderItemViewModel viewModel; 18 | 19 | public ReminderViewHolder(ItemReminderBinding binding, Context context) { 20 | super(binding.getRoot()); 21 | ReminderApplication.get(context).getAppComponent().inject(this); 22 | this.binding = binding; 23 | binding.setReminder(viewModel); 24 | } 25 | 26 | public void bindReminder(Reminder reminder) { 27 | viewModel.setReminder(reminder); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/src/main/java/com/bignerdranch/android/testfriendlyarchitecture/inject/AdapterModule.java: -------------------------------------------------------------------------------- 1 | package com.bignerdranch.android.testfriendlyarchitecture.inject; 2 | 3 | import android.content.Context; 4 | 5 | import com.bignerdranch.android.testfriendlyarchitecture.controller.list.ReminderAdapter; 6 | 7 | import javax.inject.Singleton; 8 | 9 | import dagger.Module; 10 | import dagger.Provides; 11 | 12 | @Module 13 | @Singleton 14 | public class AdapterModule { 15 | 16 | @Provides 17 | ReminderAdapter providesReminderAdapter(Context context) { 18 | return new ReminderAdapter(context); 19 | } 20 | } -------------------------------------------------------------------------------- /app/src/main/java/com/bignerdranch/android/testfriendlyarchitecture/inject/AppComponent.java: -------------------------------------------------------------------------------- 1 | package com.bignerdranch.android.testfriendlyarchitecture.inject; 2 | 3 | import com.bignerdranch.android.testfriendlyarchitecture.controller.create.CreateReminderActivity; 4 | import com.bignerdranch.android.testfriendlyarchitecture.controller.list.ReminderListFragment; 5 | import com.bignerdranch.android.testfriendlyarchitecture.controller.list.ReminderViewHolder; 6 | 7 | import javax.inject.Singleton; 8 | 9 | import dagger.Component; 10 | 11 | @Component(modules = {AppModule.class, ViewModelModule.class, 12 | StoreModule.class, AdapterModule.class, ModelModule.class}) 13 | @Singleton 14 | public interface AppComponent { 15 | // Activity injection 16 | void inject(CreateReminderActivity activity); 17 | 18 | // Fragment injection 19 | void inject(ReminderListFragment fragment); 20 | 21 | // View holder injection 22 | void inject(ReminderViewHolder viewHolder); 23 | } 24 | -------------------------------------------------------------------------------- /app/src/main/java/com/bignerdranch/android/testfriendlyarchitecture/inject/AppModule.java: -------------------------------------------------------------------------------- 1 | package com.bignerdranch.android.testfriendlyarchitecture.inject; 2 | 3 | import android.content.Context; 4 | 5 | import com.bignerdranch.android.testfriendlyarchitecture.model.date.DateFormatter; 6 | import com.bignerdranch.android.testfriendlyarchitecture.model.date.DateUtils; 7 | 8 | import javax.inject.Singleton; 9 | 10 | import dagger.Module; 11 | import dagger.Provides; 12 | 13 | @Module 14 | @Singleton 15 | public class AppModule { 16 | 17 | private Context appContext; 18 | 19 | public AppModule(Context context) { 20 | appContext = context.getApplicationContext(); 21 | } 22 | 23 | @Provides 24 | Context providesAppContext() { 25 | return appContext; 26 | } 27 | 28 | @Provides 29 | DateFormatter providesDateFormatter() { 30 | return new DateFormatter(); 31 | } 32 | 33 | @Provides 34 | DateUtils providesDateUtils() { 35 | return new DateUtils(); 36 | } 37 | } -------------------------------------------------------------------------------- /app/src/main/java/com/bignerdranch/android/testfriendlyarchitecture/inject/ModelModule.java: -------------------------------------------------------------------------------- 1 | package com.bignerdranch.android.testfriendlyarchitecture.inject; 2 | 3 | import com.bignerdranch.android.testfriendlyarchitecture.model.Reminder; 4 | 5 | import javax.inject.Singleton; 6 | 7 | import dagger.Module; 8 | import dagger.Provides; 9 | 10 | @Module 11 | @Singleton 12 | public class ModelModule { 13 | 14 | @Provides 15 | Reminder providesReminder() { 16 | return new Reminder(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/java/com/bignerdranch/android/testfriendlyarchitecture/inject/StoreModule.java: -------------------------------------------------------------------------------- 1 | package com.bignerdranch.android.testfriendlyarchitecture.inject; 2 | 3 | import android.content.Context; 4 | 5 | import com.bignerdranch.android.testfriendlyarchitecture.model.store.LiveReminderStore; 6 | import com.bignerdranch.android.testfriendlyarchitecture.model.store.ReminderDatabase; 7 | import com.bignerdranch.android.testfriendlyarchitecture.model.store.ReminderStore; 8 | 9 | import javax.inject.Named; 10 | import javax.inject.Singleton; 11 | 12 | import dagger.Module; 13 | import dagger.Provides; 14 | 15 | @Module 16 | @Singleton 17 | public class StoreModule { 18 | private static final String DATABASE_FILE_NAME = "reminders.db"; 19 | private static final int DATABASE_VERSION_NUMBER = 1; 20 | 21 | @Provides 22 | @Singleton 23 | ReminderStore providesReminderStore(ReminderDatabase database) { 24 | return new LiveReminderStore(database); 25 | } 26 | 27 | /** 28 | * The provides methods below are internal to this module. 29 | * They are only used to supply the ReminderDatabase parameter to the ReminderStore 30 | */ 31 | @Provides 32 | ReminderDatabase providesReminderDatabase(Context context, 33 | @Named("database_file") String filename, 34 | @Named("database_version") int version) { 35 | return new ReminderDatabase(context, filename, null, version); 36 | } 37 | 38 | @Provides 39 | @Named("database_file") 40 | String providesDatabaseFileName() { 41 | return DATABASE_FILE_NAME; 42 | } 43 | 44 | @Provides 45 | @Named("database_version") 46 | int providesDatabaseVersion() { 47 | return DATABASE_VERSION_NUMBER; 48 | } 49 | } 50 | 51 | -------------------------------------------------------------------------------- /app/src/main/java/com/bignerdranch/android/testfriendlyarchitecture/inject/ViewModelModule.java: -------------------------------------------------------------------------------- 1 | package com.bignerdranch.android.testfriendlyarchitecture.inject; 2 | 3 | import android.content.Context; 4 | 5 | import com.bignerdranch.android.testfriendlyarchitecture.model.Reminder; 6 | import com.bignerdranch.android.testfriendlyarchitecture.model.date.DateFormatter; 7 | import com.bignerdranch.android.testfriendlyarchitecture.viewmodel.CreateReminderViewModel; 8 | import com.bignerdranch.android.testfriendlyarchitecture.viewmodel.ReminderItemViewModel; 9 | 10 | import javax.inject.Singleton; 11 | 12 | import dagger.Module; 13 | import dagger.Provides; 14 | 15 | @Module 16 | @Singleton 17 | public class ViewModelModule { 18 | @Provides 19 | CreateReminderViewModel providesCreateReminderViewModel(Context context, 20 | DateFormatter dateFormatter, 21 | Reminder reminder) { 22 | return new CreateReminderViewModel(context, dateFormatter, reminder); 23 | } 24 | 25 | @Provides 26 | ReminderItemViewModel providesReminderItemViewModel(DateFormatter dateFormatter) { 27 | return new ReminderItemViewModel(dateFormatter); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/src/main/java/com/bignerdranch/android/testfriendlyarchitecture/model/Reminder.java: -------------------------------------------------------------------------------- 1 | package com.bignerdranch.android.testfriendlyarchitecture.model; 2 | 3 | import org.joda.time.LocalDate; 4 | 5 | public class Reminder { 6 | 7 | private int id; 8 | private String title; 9 | private LocalDate date; 10 | 11 | public Reminder() { 12 | this(null, new LocalDate()); 13 | } 14 | 15 | public Reminder(String title, LocalDate date) { 16 | this(0, title, date); 17 | } 18 | 19 | public Reminder(int id, String title, LocalDate date) { 20 | this.id = id; 21 | this.title = title; 22 | this.date = date; 23 | } 24 | 25 | public int getId() { 26 | return id; 27 | } 28 | 29 | public String getTitle() { 30 | return title; 31 | } 32 | 33 | public void setTitle(String title) { 34 | this.title = title; 35 | } 36 | 37 | public LocalDate getDate() { 38 | return date; 39 | } 40 | 41 | public void setDate(LocalDate date) { 42 | this.date = date; 43 | } 44 | } -------------------------------------------------------------------------------- /app/src/main/java/com/bignerdranch/android/testfriendlyarchitecture/model/date/DateFormatter.java: -------------------------------------------------------------------------------- 1 | package com.bignerdranch.android.testfriendlyarchitecture.model.date; 2 | 3 | import org.joda.time.LocalDate; 4 | import org.joda.time.format.DateTimeFormat; 5 | import org.joda.time.format.DateTimeFormatter; 6 | 7 | public class DateFormatter { 8 | private static final String FORMAT_STRING = "MM/dd/yyyy"; 9 | private static final String NULL_RETURN = ""; 10 | 11 | public String format(LocalDate date) { 12 | if (date == null) { 13 | return NULL_RETURN; 14 | } 15 | DateTimeFormatter formatter = DateTimeFormat.forPattern(FORMAT_STRING); 16 | return formatter.print(date); 17 | } 18 | } -------------------------------------------------------------------------------- /app/src/main/java/com/bignerdranch/android/testfriendlyarchitecture/model/date/DateRange.java: -------------------------------------------------------------------------------- 1 | package com.bignerdranch.android.testfriendlyarchitecture.model.date; 2 | 3 | import java.io.Serializable; 4 | 5 | public enum DateRange implements Serializable { 6 | ALL, 7 | TODAY, 8 | WEEK 9 | } -------------------------------------------------------------------------------- /app/src/main/java/com/bignerdranch/android/testfriendlyarchitecture/model/date/DateSelectionListener.java: -------------------------------------------------------------------------------- 1 | package com.bignerdranch.android.testfriendlyarchitecture.model.date; 2 | 3 | import org.joda.time.LocalDate; 4 | 5 | public interface DateSelectionListener { 6 | void onDateSelected(LocalDate date); 7 | } 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/java/com/bignerdranch/android/testfriendlyarchitecture/model/date/DateUtils.java: -------------------------------------------------------------------------------- 1 | package com.bignerdranch.android.testfriendlyarchitecture.model.date; 2 | 3 | import org.joda.time.LocalDate; 4 | 5 | public class DateUtils { 6 | 7 | public boolean isToday(LocalDate date) { 8 | LocalDate today = new LocalDate(); 9 | return date.equals(today); 10 | } 11 | 12 | public boolean isThisWeek(LocalDate date) { 13 | LocalDate today = new LocalDate(); 14 | LocalDate nextWeek = today.plusDays(7); 15 | return !date.isBefore(today) && !date.isAfter(nextWeek); 16 | } 17 | } -------------------------------------------------------------------------------- /app/src/main/java/com/bignerdranch/android/testfriendlyarchitecture/model/store/LiveReminderStore.java: -------------------------------------------------------------------------------- 1 | package com.bignerdranch.android.testfriendlyarchitecture.model.store; 2 | 3 | import android.content.ContentValues; 4 | import android.database.Cursor; 5 | 6 | import com.bignerdranch.android.testfriendlyarchitecture.model.Reminder; 7 | 8 | import java.util.ArrayList; 9 | import java.util.List; 10 | 11 | public class LiveReminderStore implements ReminderStore { 12 | 13 | private ReminderDatabase database; 14 | private List reminders; 15 | 16 | public LiveReminderStore(ReminderDatabase database) { 17 | this.database = database; 18 | } 19 | 20 | @Override 21 | public List getReminders() { 22 | if (reminders == null) { 23 | reminders = loadReminders(); 24 | } 25 | return reminders; 26 | } 27 | 28 | @Override 29 | public void addReminder(Reminder reminder) { 30 | // add to in memory cache 31 | reminders.add(reminder); 32 | // write to database 33 | ContentValues values = getContentValues(reminder); 34 | database.getWritableDatabase() 35 | .insert(ReminderContract.TABLE_NAME, null, values); 36 | } 37 | 38 | @Override 39 | public void clearReminders() { 40 | reminders = new ArrayList<>(); 41 | database.getWritableDatabase().execSQL("DELETE FROM " + ReminderContract.TABLE_NAME); 42 | } 43 | 44 | private List loadReminders() { 45 | Cursor cursor = database.getReadableDatabase() 46 | .query(ReminderContract.TABLE_NAME, null, null, null, null, null, null); 47 | ReminderCursorWrapper wrapper = new ReminderCursorWrapper(cursor); 48 | List reminders = new ArrayList<>(); 49 | try { 50 | wrapper.moveToFirst(); 51 | while (!wrapper.isAfterLast()) { 52 | reminders.add(wrapper.getReminder()); 53 | wrapper.moveToNext(); 54 | } 55 | } finally { 56 | wrapper.close(); 57 | cursor.close(); 58 | } 59 | return reminders; 60 | } 61 | 62 | private ContentValues getContentValues(Reminder reminder) { 63 | ContentValues values = new ContentValues(); 64 | values.put(ReminderContract.Cols.TITLE, reminder.getTitle()); 65 | Long date; 66 | if (reminder.getDate() != null) { 67 | date = reminder.getDate().toDate().getTime(); 68 | } else { 69 | date = null; 70 | } 71 | values.put(ReminderContract.Cols.DATE, date); 72 | return values; 73 | } 74 | } 75 | 76 | -------------------------------------------------------------------------------- /app/src/main/java/com/bignerdranch/android/testfriendlyarchitecture/model/store/ReminderContract.java: -------------------------------------------------------------------------------- 1 | package com.bignerdranch.android.testfriendlyarchitecture.model.store; 2 | 3 | public class ReminderContract { 4 | 5 | public static final String TABLE_NAME = "reminders"; 6 | 7 | public static class Cols { 8 | public static final String ID = "_id"; 9 | public static final String TITLE = "title"; 10 | public static final String DATE = "date"; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /app/src/main/java/com/bignerdranch/android/testfriendlyarchitecture/model/store/ReminderCursorWrapper.java: -------------------------------------------------------------------------------- 1 | package com.bignerdranch.android.testfriendlyarchitecture.model.store; 2 | 3 | import android.database.Cursor; 4 | import android.database.CursorWrapper; 5 | 6 | import com.bignerdranch.android.testfriendlyarchitecture.model.Reminder; 7 | 8 | import org.joda.time.LocalDate; 9 | 10 | public class ReminderCursorWrapper extends CursorWrapper { 11 | public ReminderCursorWrapper(Cursor cursor) { 12 | super(cursor); 13 | } 14 | 15 | public Reminder getReminder() { 16 | int id = getInt(getColumnIndex(ReminderContract.Cols.ID)); 17 | String title = getString(getColumnIndex(ReminderContract.Cols.TITLE)); 18 | long date = getLong(getColumnIndex(ReminderContract.Cols.DATE)); 19 | 20 | return new Reminder(id, title, new LocalDate(date)); 21 | } 22 | } -------------------------------------------------------------------------------- /app/src/main/java/com/bignerdranch/android/testfriendlyarchitecture/model/store/ReminderDatabase.java: -------------------------------------------------------------------------------- 1 | package com.bignerdranch.android.testfriendlyarchitecture.model.store; 2 | 3 | import android.content.Context; 4 | import android.database.sqlite.SQLiteDatabase; 5 | import android.database.sqlite.SQLiteOpenHelper; 6 | 7 | public class ReminderDatabase extends SQLiteOpenHelper { 8 | 9 | public ReminderDatabase(Context context, String name, SQLiteDatabase.CursorFactory factory, int version) { 10 | super(context, name, factory, version); 11 | } 12 | 13 | @Override 14 | public void onCreate(SQLiteDatabase db) { 15 | db.execSQL("CREATE TABLE " + ReminderContract.TABLE_NAME + "(" + 16 | ReminderContract.Cols.ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + 17 | ReminderContract.Cols.TITLE + " TEXT," + 18 | ReminderContract.Cols.DATE + " INT)"); 19 | } 20 | 21 | @Override 22 | public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/java/com/bignerdranch/android/testfriendlyarchitecture/model/store/ReminderStore.java: -------------------------------------------------------------------------------- 1 | package com.bignerdranch.android.testfriendlyarchitecture.model.store; 2 | 3 | import com.bignerdranch.android.testfriendlyarchitecture.model.Reminder; 4 | 5 | import java.util.List; 6 | 7 | public interface ReminderStore { 8 | List getReminders(); 9 | void addReminder(Reminder reminder); 10 | void clearReminders(); 11 | } 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/java/com/bignerdranch/android/testfriendlyarchitecture/viewmodel/CreateReminderViewModel.java: -------------------------------------------------------------------------------- 1 | package com.bignerdranch.android.testfriendlyarchitecture.viewmodel; 2 | 3 | import android.content.Context; 4 | import android.databinding.BaseObservable; 5 | import android.databinding.Bindable; 6 | 7 | import com.bignerdranch.android.testfriendlyarchitecture.BR; 8 | import com.bignerdranch.android.testfriendlyarchitecture.R; 9 | import com.bignerdranch.android.testfriendlyarchitecture.model.Reminder; 10 | import com.bignerdranch.android.testfriendlyarchitecture.model.date.DateFormatter; 11 | 12 | import org.joda.time.LocalDate; 13 | 14 | public class CreateReminderViewModel extends BaseObservable { 15 | private Context context; 16 | private DateFormatter dateFormatter; 17 | private Reminder reminder; 18 | 19 | public CreateReminderViewModel(Context context, 20 | DateFormatter dateFormatter, 21 | Reminder reminder) { 22 | this.context = context; 23 | this.dateFormatter = dateFormatter; 24 | this.reminder = reminder; 25 | } 26 | 27 | public Reminder getReminder() { 28 | return reminder; 29 | } 30 | 31 | public String getTitle() { 32 | return reminder.getTitle(); 33 | } 34 | 35 | public void setTitle(String title) { 36 | reminder.setTitle(title); 37 | } 38 | 39 | @Bindable 40 | public String getDate() { 41 | LocalDate date = reminder.getDate(); 42 | if (date != null) { 43 | return dateFormatter.format(date); 44 | } 45 | return context.getString(R.string.reminder_set_date); 46 | } 47 | 48 | public void setDate(LocalDate date) { 49 | reminder.setDate(date); 50 | notifyPropertyChanged(BR.date); 51 | } 52 | } 53 | 54 | -------------------------------------------------------------------------------- /app/src/main/java/com/bignerdranch/android/testfriendlyarchitecture/viewmodel/ReminderItemViewModel.java: -------------------------------------------------------------------------------- 1 | package com.bignerdranch.android.testfriendlyarchitecture.viewmodel; 2 | 3 | import android.databinding.BaseObservable; 4 | import android.databinding.Bindable; 5 | 6 | import com.bignerdranch.android.testfriendlyarchitecture.model.Reminder; 7 | import com.bignerdranch.android.testfriendlyarchitecture.model.date.DateFormatter; 8 | 9 | public class ReminderItemViewModel extends BaseObservable { 10 | 11 | private DateFormatter dateFormatter; 12 | private Reminder reminder; 13 | 14 | public ReminderItemViewModel(DateFormatter dateFormatter) { 15 | this.dateFormatter = dateFormatter; 16 | } 17 | 18 | public void setReminder(Reminder reminder) { 19 | this.reminder = reminder; 20 | notifyChange(); 21 | } 22 | 23 | @Bindable 24 | public String getTitle() { 25 | return reminder.getTitle(); 26 | } 27 | 28 | @Bindable 29 | public String getDate() { 30 | return dateFormatter.format(reminder.getDate()); 31 | } 32 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_add_reminder.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 12 | 17 | 22 | 27 | 32 | 37 | 42 | 47 | 52 | 57 | 62 | 67 | 72 | 77 | 82 | 87 | 92 | 97 | 102 | 107 | 112 | 113 | 114 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_create_reminder.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 9 | 10 | 15 | 16 | 19 | 20 | 25 | 26 | 27 |