├── .firebaserc
├── .gitignore
├── .idea
├── .name
├── codeStyles
│ ├── Project.xml
│ └── codeStyleConfig.xml
├── compiler.xml
├── deploymentTargetDropDown.xml
├── gradle.xml
├── jarRepositories.xml
├── misc.xml
└── vcs.xml
├── README.md
├── Task
├── There
├── app
├── .gitignore
├── build.gradle
├── google-services.json
├── proguard-rules.pro
└── src
│ ├── androidTest
│ └── java
│ │ └── com
│ │ └── codingwithmitch
│ │ └── cleannotes
│ │ ├── BaseTest.kt
│ │ ├── InstrumentationTestSuite.kt
│ │ ├── di
│ │ ├── TestAppComponent.kt
│ │ ├── TestModule.kt
│ │ └── TestNoteFragmentFactoryModule.kt
│ │ ├── framework
│ │ ├── MockTestRunner.kt
│ │ ├── datasource
│ │ │ ├── cache
│ │ │ │ └── NoteDaoServiceTests.kt
│ │ │ ├── data
│ │ │ │ └── NoteDataFactory.kt
│ │ │ └── network
│ │ │ │ └── NoteFirestoreServiceTests.kt
│ │ └── presentation
│ │ │ ├── TestBaseApplication.kt
│ │ │ ├── TestNoteFragmentFactory.kt
│ │ │ ├── end_to_end
│ │ │ └── NotesFeatureTest.kt
│ │ │ ├── notedetail
│ │ │ └── NoteDetailFragmentTests.kt
│ │ │ └── notelist
│ │ │ └── NoteListFragmentTests.kt
│ │ └── util
│ │ ├── EspressoIdlingResourceRule.kt
│ │ └── ViewShownIdlingResource.kt
│ ├── debug
│ ├── assets
│ │ └── note_list.json
│ └── java
│ │ └── com
│ │ └── codingwithmitch
│ │ └── cleannotes
│ │ └── util
│ │ └── EspressoIdlingResource.kt
│ ├── main
│ ├── AndroidManifest.xml
│ ├── java
│ │ └── com
│ │ │ └── codingwithmitch
│ │ │ └── cleannotes
│ │ │ ├── business
│ │ │ ├── data
│ │ │ │ ├── cache
│ │ │ │ │ ├── CacheConstants.kt
│ │ │ │ │ ├── CacheErrors.kt
│ │ │ │ │ ├── CacheResponseHandler.kt
│ │ │ │ │ ├── CacheResult.kt
│ │ │ │ │ ├── abstraction
│ │ │ │ │ │ └── NoteCacheDataSource.kt
│ │ │ │ │ └── implementation
│ │ │ │ │ │ └── NoteCacheDataSourceImpl.kt
│ │ │ │ ├── network
│ │ │ │ │ ├── ApiResponseHandler.kt
│ │ │ │ │ ├── ApiResult.kt
│ │ │ │ │ ├── NetworkConstants.kt
│ │ │ │ │ ├── NetworkErrors.kt
│ │ │ │ │ ├── abstraction
│ │ │ │ │ │ └── NoteNetworkDataSource.kt
│ │ │ │ │ └── implementation
│ │ │ │ │ │ └── NoteNetworkDataSourceImpl.kt
│ │ │ │ └── util
│ │ │ │ │ ├── GenericErrors.kt
│ │ │ │ │ └── RepositoryExtensions.kt
│ │ │ ├── domain
│ │ │ │ ├── model
│ │ │ │ │ ├── Note.kt
│ │ │ │ │ └── NoteFactory.kt
│ │ │ │ ├── state
│ │ │ │ │ ├── DataChannelManager.kt
│ │ │ │ │ ├── DataState.kt
│ │ │ │ │ ├── MessageStack.kt
│ │ │ │ │ ├── StateEvent.kt
│ │ │ │ │ ├── StateEventManager.kt
│ │ │ │ │ ├── StateResource.kt
│ │ │ │ │ └── ViewState.kt
│ │ │ │ └── util
│ │ │ │ │ ├── DateUtil.kt
│ │ │ │ │ └── EntityMapper.kt
│ │ │ └── interactors
│ │ │ │ ├── common
│ │ │ │ └── DeleteNote.kt
│ │ │ │ ├── notedetail
│ │ │ │ ├── NoteDetailInteractors.kt
│ │ │ │ └── UpdateNote.kt
│ │ │ │ ├── notelist
│ │ │ │ ├── DeleteMultipleNotes.kt
│ │ │ │ ├── GetNumNotes.kt
│ │ │ │ ├── InsertMultipleNotes.kt
│ │ │ │ ├── InsertNewNote.kt
│ │ │ │ ├── NoteListInteractors.kt
│ │ │ │ ├── RestoreDeletedNote.kt
│ │ │ │ └── SearchNotes.kt
│ │ │ │ └── splash
│ │ │ │ ├── SyncDeletedNotes.kt
│ │ │ │ └── SyncNotes.kt
│ │ │ ├── di
│ │ │ ├── AppComponent.kt
│ │ │ ├── AppModule.kt
│ │ │ ├── NoteFragmentFactoryModule.kt
│ │ │ ├── NoteViewModelModule.kt
│ │ │ └── ProductionModule.kt
│ │ │ ├── framework
│ │ │ ├── datasource
│ │ │ │ ├── cache
│ │ │ │ │ ├── abstraction
│ │ │ │ │ │ └── NoteDaoService.kt
│ │ │ │ │ ├── database
│ │ │ │ │ │ ├── NoteDao.kt
│ │ │ │ │ │ └── NoteDatabase.kt
│ │ │ │ │ ├── implementation
│ │ │ │ │ │ └── NoteDaoServiceImpl.kt
│ │ │ │ │ ├── mappers
│ │ │ │ │ │ └── CacheMapper.kt
│ │ │ │ │ └── model
│ │ │ │ │ │ └── NoteCacheEntity.kt
│ │ │ │ ├── network
│ │ │ │ │ ├── abstraction
│ │ │ │ │ │ └── NoteFirestoreService.kt
│ │ │ │ │ ├── implementation
│ │ │ │ │ │ └── NoteFirestoreServiceImpl.kt
│ │ │ │ │ ├── mappers
│ │ │ │ │ │ └── NetworkMapper.kt
│ │ │ │ │ └── model
│ │ │ │ │ │ └── NoteNetworkEntity.kt
│ │ │ │ └── preferences
│ │ │ │ │ └── PreferenceKeys.kt
│ │ │ └── presentation
│ │ │ │ ├── BaseApplication.kt
│ │ │ │ ├── MainActivity.kt
│ │ │ │ ├── UIController.kt
│ │ │ │ ├── common
│ │ │ │ ├── BaseNoteFragment.kt
│ │ │ │ ├── BaseViewModel.kt
│ │ │ │ ├── KeyboardExtensions.kt
│ │ │ │ ├── NoteFragmentFactory.kt
│ │ │ │ ├── NoteViewModelFactory.kt
│ │ │ │ ├── TopSpacingItemDecoration.kt
│ │ │ │ └── ViewExtensions.kt
│ │ │ │ ├── notedetail
│ │ │ │ ├── NoteDetailFragment.kt
│ │ │ │ ├── NoteDetailViewModel.kt
│ │ │ │ └── state
│ │ │ │ │ ├── CollapsingToolbarState.kt
│ │ │ │ │ ├── NoteDetailStateEvent.kt
│ │ │ │ │ ├── NoteDetailViewState.kt
│ │ │ │ │ ├── NoteInteractionManager.kt
│ │ │ │ │ └── NoteInteractionState.kt
│ │ │ │ ├── notelist
│ │ │ │ ├── NoteItemTouchHelperCallback.kt
│ │ │ │ ├── NoteListAdapter.kt
│ │ │ │ ├── NoteListFragment.kt
│ │ │ │ ├── NoteListViewModel.kt
│ │ │ │ └── state
│ │ │ │ │ ├── NoteListInteractionManager.kt
│ │ │ │ │ ├── NoteListStateEvent.kt
│ │ │ │ │ ├── NoteListToolbarState.kt
│ │ │ │ │ └── NoteListViewState.kt
│ │ │ │ └── splash
│ │ │ │ ├── NoteNetworkSyncManager.kt
│ │ │ │ ├── SplashFragment.kt
│ │ │ │ └── SplashViewModel.kt
│ │ │ └── util
│ │ │ ├── AndroidTestUtils.kt
│ │ │ ├── Constants.kt
│ │ │ ├── Logger.kt
│ │ │ └── TodoCallback.kt
│ └── res
│ │ ├── anim
│ │ ├── fade_in.xml
│ │ ├── fade_out.xml
│ │ ├── slide_in_left.xml
│ │ ├── slide_in_right.xml
│ │ ├── slide_out_left.xml
│ │ └── slide_out_right.xml
│ │ ├── color
│ │ └── bottom_nav_selector.xml
│ │ ├── drawable-v24
│ │ └── ic_launcher_foreground.xml
│ │ ├── drawable
│ │ ├── ic_add_white_24dp.xml
│ │ ├── ic_arrow_back_grey_24dp.xml
│ │ ├── ic_close_grey_24dp.xml
│ │ ├── ic_date_range_black_24dp.xml
│ │ ├── ic_delete.xml
│ │ ├── ic_done_grey_24dp.xml
│ │ ├── ic_event_note_white_24dp.xml
│ │ ├── ic_filter_list_grey_24dp.xml
│ │ ├── ic_folder_white_24dp.xml
│ │ ├── ic_launcher_background.xml
│ │ ├── list_item_selector.xml
│ │ ├── logo_250_250.png
│ │ └── red_button_drawable.xml
│ │ ├── layout
│ │ ├── activity_main.xml
│ │ ├── fragment_note_detail.xml
│ │ ├── fragment_note_list.xml
│ │ ├── fragment_splash.xml
│ │ ├── layout_filter.xml
│ │ ├── layout_multiselection_toolbar.xml
│ │ ├── layout_note_detail_toolbar.xml
│ │ ├── layout_note_list_item.xml
│ │ └── layout_searchview_toolbar.xml
│ │ ├── mipmap-anydpi-v26
│ │ ├── ic_launcher.xml
│ │ └── ic_launcher_round.xml
│ │ ├── mipmap-hdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-mdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxxhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── navigation
│ │ └── nav_app_graph.xml
│ │ └── values
│ │ ├── colors.xml
│ │ ├── dimen.xml
│ │ ├── strings.xml
│ │ └── styles.xml
│ ├── release
│ └── java
│ │ └── com
│ │ └── codingwithmitch
│ │ └── cleannotes
│ │ └── util
│ │ └── EspressoIdlingResource.kt
│ └── test
│ ├── java
│ └── com
│ │ └── codingwithmitch
│ │ └── cleannotes
│ │ ├── business
│ │ ├── data
│ │ │ ├── NoteDataFactory.kt
│ │ │ ├── cache
│ │ │ │ └── FakeNoteCacheDataSourceImpl.kt
│ │ │ └── network
│ │ │ │ └── FakeNoteNetworkDataSourceImpl.kt
│ │ └── interactors
│ │ │ ├── common
│ │ │ └── DeleteNoteTest.kt
│ │ │ ├── notedetail
│ │ │ └── UpdateNoteTest.kt
│ │ │ ├── notelist
│ │ │ ├── DeleteMultipleNotesTest.kt
│ │ │ ├── GetNumNotesTest.kt
│ │ │ ├── InsertNewNoteTest.kt
│ │ │ ├── RestoreDeletedNoteTest.kt
│ │ │ └── SearchNotesTest.kt
│ │ │ └── splash
│ │ │ ├── SyncDeletedNotesTest.kt
│ │ │ └── SyncNotesTest.kt
│ │ └── di
│ │ └── DependencyContainer.kt
│ └── res
│ └── note_list.json
├── build.gradle
├── buildSrc
├── build.gradle.kts
├── build
│ ├── classes
│ │ └── kotlin
│ │ │ └── main
│ │ │ ├── META-INF
│ │ │ └── buildSrc.kotlin_module
│ │ │ └── dependencies
│ │ │ ├── AnnotationProcessing.class
│ │ │ ├── Application.class
│ │ │ ├── Build.class
│ │ │ ├── Java.class
│ │ │ ├── Repositories.class
│ │ │ ├── Versions.class
│ │ │ └── dependencies
│ │ │ ├── AndroidTestDependencies.class
│ │ │ ├── Dependencies.class
│ │ │ ├── SupportDependencies.class
│ │ │ └── TestDependencies.class
│ ├── kotlin
│ │ ├── buildSrcjar-classes.txt
│ │ └── compileKotlin
│ │ │ ├── build-history.bin
│ │ │ ├── caches-jvm
│ │ │ ├── inputs
│ │ │ │ ├── source-to-output.tab
│ │ │ │ └── source-to-output.tab_i
│ │ │ ├── jvm
│ │ │ │ └── kotlin
│ │ │ │ │ ├── class-fq-name-to-source.tab
│ │ │ │ │ ├── class-fq-name-to-source.tab_i
│ │ │ │ │ ├── internal-name-to-source.tab
│ │ │ │ │ ├── internal-name-to-source.tab_i
│ │ │ │ │ ├── proto.tab
│ │ │ │ │ ├── proto.tab_i
│ │ │ │ │ └── source-to-classes.tab
│ │ │ └── lookups
│ │ │ │ ├── file-to-id.tab
│ │ │ │ ├── file-to-id.tab_i
│ │ │ │ ├── id-to-file.tab
│ │ │ │ ├── lookups.tab
│ │ │ │ └── lookups.tab_i
│ │ │ └── last-build.bin
│ ├── libs
│ │ └── buildSrc.jar
│ ├── pluginUnderTestMetadata
│ │ └── plugin-under-test-metadata.properties
│ ├── reports
│ │ └── task-properties
│ │ │ └── report.txt
│ └── tmp
│ │ └── jar
│ │ └── MANIFEST.MF
└── src
│ └── main
│ └── java
│ ├── AnnotationProcessing.kt
│ ├── Application.kt
│ ├── Build.kt
│ ├── Java.kt
│ ├── Repositories.kt
│ ├── Versions.kt
│ └── dependencies
│ ├── AndroidTestDependencies.kt
│ ├── Dependencies.kt
│ ├── SupportDependencies.kt
│ └── TestDependencies.kt
├── firebase-debug.log
├── firebase.json
├── firestore-debug.log
├── firestore.indexes.json
├── firestore.rules
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── settings.gradle
├── tests
├── firestore-debug.log
├── run_tests.sh
└── ui_and_unit_tests.sh
└── ui-debug.log
/.firebaserc:
--------------------------------------------------------------------------------
1 | {
2 | "projects": {
3 | "default": "clean-notes-11971"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/caches
5 | /.idea/libraries
6 | /.idea/modules.xml
7 | /.idea/workspace.xml
8 | /.idea/navEditor.xml
9 | /.idea/assetWizardSettings.xml
10 | .DS_Store
11 | /build
12 | /captures
13 | .externalNativeBuild
14 | .cxx
15 |
--------------------------------------------------------------------------------
/.idea/.name:
--------------------------------------------------------------------------------
1 | Clean Notes
--------------------------------------------------------------------------------
/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/.idea/compiler.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/deploymentTargetDropDown.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/.idea/gradle.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
22 |
23 |
--------------------------------------------------------------------------------
/.idea/jarRepositories.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # [Clean Architecture](https://codingwithmitch.com/courses/android-clean-architecture/)
2 |
3 | Android example with Clean Architecture by layer.
4 |
5 | **Watch the course here:** [Clean Architecture](https://codingwithmitch.com/courses/android-clean-architecture/)
6 |
7 | **NOTE** I got rid of dynamic feature modules because you cannot write tests currently. See bug: [google issue tracker](https://issuetracker.google.com/issues/145191501).
8 |
9 | In the future I will make another course on Dynamic Feature Modules.
10 |
11 | 
12 |
13 | # Running this app
14 |
15 | To run this app you will need to create a firebase project and hook it up with the project. I password protected the login of mine so you won't be able to get into the app.
16 |
17 | # Running the Tests
18 | 1. cd into /tests/
19 | 2. type in terminal: `run_tests.sh`
20 | This will run all the unit tests and instrumentation tests. It will also start the firebase emulator to simulate firestore db.
21 | **The test results** are in `/app/build/reports/`.
22 |
23 | # Credits
24 | 1. https://proandroiddev.com/gradle-dependency-management-with-kotlin-94eed4df9a28
25 | 2. https://proandroiddev.com/intro-to-app-modularization-42411e4c421e
26 | 3. https://www.droidcon.com/media-detail?video=380845032
27 | 4. https://proandroiddev.com/kotlin-clean-architecture-1ad42fcd97fa
28 | 5. https://www.raywenderlich.com/3595916-clean-architecture-tutorial-for-android-getting-started
29 | 6. http://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html
30 | 7. https://developer.android.com/guide/navigation/navigation-dynamic
31 | 8. https://proandroiddev.com/navigation-with-dynamic-feature-modules-48ee7645488
32 | 9. https://medium.com/androiddevelopers/patterns-for-accessing-code-from-dynamic-feature-modules-7e5dca6f9123
33 | 10. https://hackernoon.com/android-components-architecture-in-a-modular-word-d0k32i6
34 |
--------------------------------------------------------------------------------
/Task:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mitchtabian/Clean-Notes/ea8f6c95c57685aed42b3b1286aecb33cc2bbf77/Task
--------------------------------------------------------------------------------
/There:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mitchtabian/Clean-Notes/ea8f6c95c57685aed42b3b1286aecb33cc2bbf77/There
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/app/google-services.json:
--------------------------------------------------------------------------------
1 | {
2 | "project_info": {
3 | "project_number": "907934723655",
4 | "firebase_url": "https://clean-notes-11971.firebaseio.com",
5 | "project_id": "clean-notes-11971",
6 | "storage_bucket": "clean-notes-11971.appspot.com"
7 | },
8 | "client": [
9 | {
10 | "client_info": {
11 | "mobilesdk_app_id": "1:907934723655:android:64933e0389125b1c834b62",
12 | "android_client_info": {
13 | "package_name": "com.codingwithmitch.cleannotes"
14 | }
15 | },
16 | "oauth_client": [
17 | {
18 | "client_id": "907934723655-qh5n32pf7j3b7assa9epvvisoturj5hf.apps.googleusercontent.com",
19 | "client_type": 3
20 | }
21 | ],
22 | "api_key": [
23 | {
24 | "current_key": "AIzaSyAHu39nDajD4_6aXppfVQAClShHNl7DXIE"
25 | }
26 | ],
27 | "services": {
28 | "appinvite_service": {
29 | "other_platform_oauth_client": [
30 | {
31 | "client_id": "907934723655-qh5n32pf7j3b7assa9epvvisoturj5hf.apps.googleusercontent.com",
32 | "client_type": 3
33 | }
34 | ]
35 | }
36 | }
37 | }
38 | ],
39 | "configuration_version": "1"
40 | }
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/codingwithmitch/cleannotes/BaseTest.kt:
--------------------------------------------------------------------------------
1 | package com.codingwithmitch.cleannotes
2 |
3 | import android.content.Context
4 | import android.view.View
5 | import androidx.test.core.app.ApplicationProvider
6 | import androidx.test.espresso.Espresso.onView
7 | import androidx.test.espresso.IdlingRegistry
8 | import androidx.test.espresso.IdlingResource
9 | import androidx.test.espresso.assertion.ViewAssertions.doesNotExist
10 | import androidx.test.espresso.assertion.ViewAssertions.matches
11 | import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
12 | import androidx.test.espresso.matcher.ViewMatchers.withId
13 | import com.codingwithmitch.cleannotes.framework.presentation.TestBaseApplication
14 | import com.codingwithmitch.cleannotes.util.ViewShownIdlingResource
15 | import kotlinx.coroutines.ExperimentalCoroutinesApi
16 | import kotlinx.coroutines.FlowPreview
17 | import org.hamcrest.Matcher
18 |
19 |
20 | @ExperimentalCoroutinesApi
21 | @FlowPreview
22 | abstract class BaseTest {
23 |
24 | val application: TestBaseApplication
25 | = ApplicationProvider.getApplicationContext() as TestBaseApplication
26 |
27 | abstract fun injectTest()
28 |
29 | // wait for a certain view to be shown.
30 | // ex: waiting for splash screen to transition to NoteListFragment
31 | fun waitViewShown(matcher: Matcher) {
32 | val idlingResource: IdlingResource = ViewShownIdlingResource(matcher, isDisplayed())
33 | try {
34 | IdlingRegistry.getInstance().register(idlingResource)
35 | onView(withId(0)).check(doesNotExist())
36 | } finally {
37 | IdlingRegistry.getInstance().unregister(idlingResource)
38 | }
39 | }
40 | }
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/codingwithmitch/cleannotes/InstrumentationTestSuite.kt:
--------------------------------------------------------------------------------
1 | package com.codingwithmitch.cleannotes
2 |
3 | import com.codingwithmitch.cleannotes.framework.datasource.cache.NoteDaoServiceTests
4 | import com.codingwithmitch.cleannotes.framework.datasource.network.NoteFirestoreServiceTests
5 | import com.codingwithmitch.cleannotes.framework.presentation.end_to_end.NotesFeatureTest
6 | import com.codingwithmitch.cleannotes.framework.presentation.notedetail.NoteDetailFragmentTests
7 | import com.codingwithmitch.cleannotes.framework.presentation.notelist.NoteListFragmentTests
8 | import kotlinx.coroutines.ExperimentalCoroutinesApi
9 | import kotlinx.coroutines.FlowPreview
10 | import kotlinx.coroutines.InternalCoroutinesApi
11 | import org.junit.runner.RunWith
12 | import org.junit.runners.Suite
13 |
14 |
15 | @FlowPreview
16 | @ExperimentalCoroutinesApi
17 | @InternalCoroutinesApi
18 | @RunWith(Suite::class)
19 | @Suite.SuiteClasses(
20 | NoteDaoServiceTests::class,
21 | NoteFirestoreServiceTests::class,
22 | NoteDetailFragmentTests::class,
23 | NoteListFragmentTests::class,
24 | NotesFeatureTest::class
25 | )
26 | class InstrumentationTestSuite
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 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/codingwithmitch/cleannotes/di/TestAppComponent.kt:
--------------------------------------------------------------------------------
1 | package com.codingwithmitch.cleannotes.di
2 |
3 | import com.codingwithmitch.cleannotes.framework.datasource.cache.NoteDaoServiceTests
4 | import com.codingwithmitch.cleannotes.framework.datasource.network.NoteFirestoreServiceTests
5 | import com.codingwithmitch.cleannotes.framework.presentation.TestBaseApplication
6 | import com.codingwithmitch.cleannotes.framework.presentation.end_to_end.NotesFeatureTest
7 | import com.codingwithmitch.cleannotes.framework.presentation.notedetail.NoteDetailFragmentTests
8 | import com.codingwithmitch.cleannotes.framework.presentation.notelist.NoteListFragmentTests
9 | import com.codingwithmitch.cleannotes.notes.di.NoteViewModelModule
10 | import dagger.BindsInstance
11 | import dagger.Component
12 | import kotlinx.coroutines.ExperimentalCoroutinesApi
13 | import kotlinx.coroutines.FlowPreview
14 | import javax.inject.Singleton
15 |
16 | @ExperimentalCoroutinesApi
17 | @FlowPreview
18 | @Singleton
19 | @Component(
20 | modules = [
21 | TestModule::class,
22 | AppModule::class,
23 | TestNoteFragmentFactoryModule::class,
24 | NoteViewModelModule::class
25 | ]
26 | )
27 | interface TestAppComponent: AppComponent {
28 |
29 | @Component.Factory
30 | interface Factory{
31 |
32 | fun create(@BindsInstance app: TestBaseApplication): TestAppComponent
33 | }
34 |
35 | fun inject(noteDaoServiceTests: NoteDaoServiceTests)
36 |
37 | fun inject(firestoreServiceTests: NoteFirestoreServiceTests)
38 |
39 | fun inject(noteListFragmentTests: NoteListFragmentTests)
40 |
41 | fun inject(noteDetailFragmentTests: NoteDetailFragmentTests)
42 |
43 | fun inject(notesFeatureTest: NotesFeatureTest)
44 | }
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/codingwithmitch/cleannotes/di/TestModule.kt:
--------------------------------------------------------------------------------
1 | package com.codingwithmitch.cleannotes.di
2 |
3 | import android.content.Context
4 | import android.content.SharedPreferences
5 | import androidx.room.Room
6 | import com.codingwithmitch.cleannotes.business.domain.model.NoteFactory
7 | import com.codingwithmitch.cleannotes.framework.datasource.cache.database.NoteDatabase
8 | import com.codingwithmitch.cleannotes.framework.datasource.data.NoteDataFactory
9 | import com.codingwithmitch.cleannotes.framework.datasource.preferences.PreferenceKeys
10 | import com.codingwithmitch.cleannotes.framework.presentation.TestBaseApplication
11 | import com.codingwithmitch.cleannotes.util.AndroidTestUtils
12 | import com.google.firebase.firestore.FirebaseFirestore
13 | import com.google.firebase.firestore.FirebaseFirestoreSettings
14 | import dagger.Module
15 | import dagger.Provides
16 | import kotlinx.coroutines.ExperimentalCoroutinesApi
17 | import kotlinx.coroutines.FlowPreview
18 | import javax.inject.Singleton
19 |
20 | @ExperimentalCoroutinesApi
21 | @FlowPreview
22 | @Module
23 | object TestModule {
24 |
25 | @JvmStatic
26 | @Singleton
27 | @Provides
28 | fun provideAndroidTestUtils(): AndroidTestUtils {
29 | return AndroidTestUtils(true)
30 | }
31 |
32 | @JvmStatic
33 | @Singleton
34 | @Provides
35 | fun provideSharedPreferences(
36 | application: TestBaseApplication
37 | ): SharedPreferences {
38 | return application
39 | .getSharedPreferences(
40 | PreferenceKeys.NOTE_PREFERENCES,
41 | Context.MODE_PRIVATE
42 | )
43 | }
44 |
45 | @JvmStatic
46 | @Singleton
47 | @Provides
48 | fun provideNoteDb(app: TestBaseApplication): NoteDatabase {
49 | return Room
50 | .inMemoryDatabaseBuilder(app, NoteDatabase::class.java)
51 | .fallbackToDestructiveMigration()
52 | .build()
53 | }
54 |
55 | @JvmStatic
56 | @Singleton
57 | @Provides
58 | fun provideFirestoreSettings(): FirebaseFirestoreSettings {
59 | return FirebaseFirestoreSettings.Builder()
60 | .setHost("10.0.2.2:8080")
61 | .setSslEnabled(false)
62 | .setPersistenceEnabled(false)
63 | .build()
64 | }
65 |
66 | @JvmStatic
67 | @Singleton
68 | @Provides
69 | fun provideFirebaseFirestore(
70 | firestoreSettings: FirebaseFirestoreSettings
71 | ): FirebaseFirestore {
72 | val firestore = FirebaseFirestore.getInstance()
73 | firestore.firestoreSettings = firestoreSettings
74 | return firestore
75 | }
76 |
77 | @JvmStatic
78 | @Singleton
79 | @Provides
80 | fun provideNoteDataFactory(
81 | application: TestBaseApplication,
82 | noteFactory: NoteFactory
83 | ): NoteDataFactory{
84 | return NoteDataFactory(application, noteFactory)
85 | }
86 | }
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/codingwithmitch/cleannotes/di/TestNoteFragmentFactoryModule.kt:
--------------------------------------------------------------------------------
1 | package com.codingwithmitch.cleannotes.di
2 |
3 | import androidx.fragment.app.FragmentFactory
4 | import androidx.lifecycle.ViewModelProvider
5 | import com.codingwithmitch.cleannotes.business.domain.util.DateUtil
6 | import com.codingwithmitch.cleannotes.framework.presentation.TestNoteFragmentFactory
7 | import dagger.Module
8 | import dagger.Provides
9 | import kotlinx.coroutines.ExperimentalCoroutinesApi
10 | import kotlinx.coroutines.FlowPreview
11 | import javax.inject.Singleton
12 |
13 | @FlowPreview
14 | @ExperimentalCoroutinesApi
15 | @Module
16 | object TestNoteFragmentFactoryModule {
17 |
18 | @Singleton
19 | @JvmStatic
20 | @Provides
21 | fun provideNoteFragmentFactory(
22 | viewModelFactory: ViewModelProvider.Factory,
23 | dateUtil: DateUtil
24 | ): FragmentFactory {
25 | return TestNoteFragmentFactory(viewModelFactory, dateUtil)
26 | }
27 | }
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/codingwithmitch/cleannotes/framework/MockTestRunner.kt:
--------------------------------------------------------------------------------
1 | package com.codingwithmitch.cleannotes.framework
2 |
3 | import android.app.Application
4 | import android.content.Context
5 | import androidx.test.runner.AndroidJUnitRunner
6 | import com.codingwithmitch.cleannotes.framework.presentation.TestBaseApplication
7 | import kotlinx.coroutines.ExperimentalCoroutinesApi
8 | import kotlinx.coroutines.FlowPreview
9 |
10 | @FlowPreview
11 | @ExperimentalCoroutinesApi
12 | class MockTestRunner: AndroidJUnitRunner(){
13 |
14 | override fun newApplication(
15 | cl: ClassLoader?,
16 | className: String?,
17 | context: Context?
18 | ) : Application {
19 | return super.newApplication(cl, TestBaseApplication::class.java.name, context)
20 | }
21 | }
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/codingwithmitch/cleannotes/framework/datasource/data/NoteDataFactory.kt:
--------------------------------------------------------------------------------
1 | package com.codingwithmitch.cleannotes.framework.datasource.data
2 |
3 | import android.app.Application
4 | import android.content.res.AssetManager
5 | import com.codingwithmitch.cleannotes.business.domain.model.Note
6 | import com.codingwithmitch.cleannotes.business.domain.model.NoteFactory
7 | import com.google.common.reflect.TypeToken
8 | import com.google.gson.Gson
9 | import java.io.IOException
10 | import java.io.InputStream
11 | import java.util.*
12 | import javax.inject.Inject
13 | import javax.inject.Singleton
14 | import kotlin.collections.ArrayList
15 |
16 | @Singleton
17 | class NoteDataFactory
18 | @Inject
19 | constructor(
20 | private val application: Application,
21 | private val noteFactory: NoteFactory
22 | ){
23 |
24 | fun produceListOfNotes(): List{
25 | val notes: List = Gson()
26 | .fromJson(
27 | getNotesFromFile("note_list.json"),
28 | object: TypeToken>() {}.type
29 | )
30 | return notes
31 | }
32 |
33 | fun produceEmptyListOfNotes(): List{
34 | return ArrayList()
35 | }
36 |
37 | fun getNotesFromFile(fileName: String): String? {
38 | return readJSONFromAsset(fileName)
39 | }
40 |
41 | private fun readJSONFromAsset(fileName: String): String? {
42 | var json: String? = null
43 | json = try {
44 | val inputStream: InputStream = (application.assets as AssetManager).open(fileName)
45 | inputStream.bufferedReader().use{it.readText()}
46 | } catch (ex: IOException) {
47 | ex.printStackTrace()
48 | return null
49 | }
50 | return json
51 | }
52 |
53 | fun createSingleNote(
54 | id: String? = null,
55 | title: String,
56 | body: String? = null
57 | ) = noteFactory.createSingleNote(id, title, body)
58 |
59 | fun createNoteList(numNotes: Int) = noteFactory.createNoteList(numNotes)
60 | }
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/codingwithmitch/cleannotes/framework/presentation/TestBaseApplication.kt:
--------------------------------------------------------------------------------
1 | package com.codingwithmitch.cleannotes.framework.presentation
2 |
3 | import com.codingwithmitch.cleannotes.di.DaggerTestAppComponent
4 | import kotlinx.coroutines.ExperimentalCoroutinesApi
5 | import kotlinx.coroutines.FlowPreview
6 |
7 | @FlowPreview
8 | @ExperimentalCoroutinesApi
9 | class TestBaseApplication : BaseApplication(){
10 |
11 | override fun initAppComponent() {
12 | appComponent = DaggerTestAppComponent
13 | .factory()
14 | .create(this)
15 | }
16 |
17 | }
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/codingwithmitch/cleannotes/framework/presentation/TestNoteFragmentFactory.kt:
--------------------------------------------------------------------------------
1 | package com.codingwithmitch.cleannotes.framework.presentation
2 |
3 | import androidx.fragment.app.FragmentFactory
4 | import androidx.lifecycle.ViewModelProvider
5 | import com.codingwithmitch.cleannotes.business.domain.util.DateUtil
6 | import com.codingwithmitch.cleannotes.framework.presentation.notedetail.NoteDetailFragment
7 | import com.codingwithmitch.cleannotes.framework.presentation.notelist.NoteListFragment
8 | import com.codingwithmitch.cleannotes.framework.presentation.splash.SplashFragment
9 | import kotlinx.coroutines.ExperimentalCoroutinesApi
10 | import kotlinx.coroutines.FlowPreview
11 | import javax.inject.Inject
12 | import javax.inject.Singleton
13 |
14 | @ExperimentalCoroutinesApi
15 | @FlowPreview
16 | @Singleton
17 | class TestNoteFragmentFactory
18 | @Inject
19 | constructor(
20 | private val viewModelFactory: ViewModelProvider.Factory,
21 | private val dateUtil: DateUtil
22 | ): FragmentFactory(){
23 |
24 | lateinit var uiController: UIController
25 |
26 | override fun instantiate(classLoader: ClassLoader, className: String) =
27 |
28 | when(className){
29 |
30 | NoteListFragment::class.java.name -> {
31 | val fragment = NoteListFragment(viewModelFactory, dateUtil)
32 | if(::uiController.isInitialized){
33 | fragment.setUIController(uiController)
34 | }
35 | fragment
36 | }
37 |
38 | NoteDetailFragment::class.java.name -> {
39 | val fragment = NoteDetailFragment(viewModelFactory)
40 | if(::uiController.isInitialized){
41 | fragment.setUIController(uiController)
42 | }
43 | fragment
44 | }
45 |
46 | SplashFragment::class.java.name -> {
47 | val fragment = SplashFragment(viewModelFactory)
48 | if(::uiController.isInitialized){
49 | fragment.setUIController(uiController)
50 | }
51 | fragment
52 | }
53 |
54 | else -> {
55 | super.instantiate(classLoader, className)
56 | }
57 | }
58 |
59 | }
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/codingwithmitch/cleannotes/util/EspressoIdlingResourceRule.kt:
--------------------------------------------------------------------------------
1 | package com.codingwithmitch.cleannotes.util
2 |
3 | import androidx.test.espresso.IdlingRegistry
4 | import org.junit.rules.TestWatcher
5 | import org.junit.runner.Description
6 |
7 | class EspressoIdlingResourceRule : TestWatcher(){
8 |
9 | private val CLASS_NAME = "EspressoIdlingResourceRule"
10 |
11 | private val idlingResource = EspressoIdlingResource.countingIdlingResource
12 |
13 | override fun finished(description: Description?) {
14 | printLogD(CLASS_NAME, "FINISHED")
15 | IdlingRegistry.getInstance().unregister(idlingResource)
16 | super.finished(description)
17 | }
18 |
19 | override fun starting(description: Description?) {
20 | printLogD(CLASS_NAME, "STARTING")
21 | IdlingRegistry.getInstance().register(idlingResource)
22 | super.starting(description)
23 | }
24 |
25 |
26 | }
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/codingwithmitch/cleannotes/util/ViewShownIdlingResource.kt:
--------------------------------------------------------------------------------
1 | package com.codingwithmitch.cleannotes.util
2 |
3 | import android.view.View
4 | import androidx.test.espresso.Espresso
5 | import androidx.test.espresso.IdlingResource
6 | import androidx.test.espresso.IdlingResource.ResourceCallback
7 | import androidx.test.espresso.ViewFinder
8 | import org.hamcrest.Matcher
9 |
10 | /*
11 | Author: https://stackoverflow.com/questions/50628219/is-it-possible-to-use-espressos-idlingresource-to-wait-until-a-certain-view-app
12 | */
13 | class ViewShownIdlingResource(
14 | private val viewMatcher: Matcher,
15 | private val idlerMatcher: Matcher
16 | ) : IdlingResource {
17 |
18 | private var resourceCallback: ResourceCallback? = null
19 |
20 | override fun isIdleNow(): Boolean {
21 | val view: View? = getView(viewMatcher)
22 | val idle = idlerMatcher.matches(view)
23 | if (idle && resourceCallback != null) {
24 | resourceCallback!!.onTransitionToIdle()
25 | }
26 | return idle
27 | }
28 |
29 | override fun registerIdleTransitionCallback(resourceCallback: ResourceCallback) {
30 | this.resourceCallback = resourceCallback
31 | }
32 |
33 | override fun getName(): String {
34 | return this.toString() + viewMatcher.toString()
35 | }
36 |
37 |
38 | companion object {
39 |
40 | private fun getView(viewMatcher: Matcher): View? {
41 | return try {
42 | val viewInteraction = Espresso.onView(viewMatcher)
43 | val finderField =
44 | viewInteraction.javaClass.getDeclaredField("viewFinder")
45 | finderField.isAccessible = true
46 | val finder = finderField[viewInteraction] as ViewFinder
47 | finder.view
48 | } catch (e: Exception) {
49 | null
50 | }
51 | }
52 |
53 | }
54 |
55 |
56 | }
--------------------------------------------------------------------------------
/app/src/debug/assets/note_list.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "id": "2474abea-7584-486b-9f88-87a21870b0ec",
4 | "title": "Vancouver PNE 2019",
5 | "body": "Here is Jess and I at the Vancouver PNE. We ate a lot of food.",
6 | "updated_at": "2019-04-14 08:41:22 AM",
7 | "created_at": "2019-04-14 07:05:11 AM"
8 | },
9 | {
10 | "id": "2474fbaa-7884-4h6b-9b8z-87a21670b0ec",
11 | "title": "Ready for a Walk",
12 | "body": "Here I am at the park with my dogs Kiba and Maizy. Maizy is the smaller one and Kiba is the larger one.",
13 | "updated_at": "2019-04-17 11:05:24 PM",
14 | "created_at": "2019-04-15 04:44:57 AM"
15 | },
16 | {
17 | "id": "2474mbaa-7884-4htb-9baz-87a216a0b0ec",
18 | "title": "Maizy Sleeping",
19 | "body": "I took this picture while Maizy was sleeping on the couch. She's very cute.",
20 | "updated_at": "2019-02-01 01:55:53 AM",
21 | "created_at": "2019-01-24 12:19:35 PM"
22 | },
23 | {
24 | "id": "2474fpaa-k884-4u6b-9biz-87am1670b0ec",
25 | "title": "My Brother Blake",
26 | "body": "This is a picture of my brother Blake and I. We were taking some pictures for his website.",
27 | "updated_at": "2019-12-14 03:05:16 PM",
28 | "created_at": "2019-12-13 07:05:17 AM"
29 | },
30 | {
31 | "id": "2474abaa-788a-4a6b-948z-87a2167hb0ec",
32 | "title": "Lounging Dogs",
33 | "body": "Kiba and Maizy are laying in the sun relaxing.",
34 | "updated_at": "2019-11-14 06:12:44 AM",
35 | "created_at": "2019-10-14 02:47:13 PM"
36 | },
37 | {
38 | "id": "24742baa-78j4-4z6b-9b8l-87a11670b0ec",
39 | "title": "Mountains in Washington",
40 | "body": "This is an image I found somewhere on the internert. I love pictures like this. I believe it's in Washington, U.S.A.",
41 | "updated_at": "2019-05-19 11:34:16 PM",
42 | "created_at": "2019-04-25 05:16:36 AM"
43 | },
44 | {
45 | "id": "2g74fbaa-78h4-4hab-9b85-87l21670b0ec",
46 | "title": "France Mountain Range",
47 | "body": "Another beautiful picture of nature. You can find more pictures like this one on Reddit.com, in the subreddit: '/r/earthporn'.",
48 | "updated_at": "2019-10-01 12:22:46 AM",
49 | "created_at": "2019-09-19 09:36:57 PM"
50 | },
51 | {
52 | "id": "2477fbaa-7b84-4hjb-9bkl-87a2a670b0ec",
53 | "title": "Aldergrove Park",
54 | "body": "I walk Kiba and Maizy pretty much every day. Usually we go to a park in Aldergrove. It takes about 1 hour, 15 minutes to walk around the entire park.",
55 | "updated_at": "2019-06-12 12:58:55 AM",
56 | "created_at": "2019-03-19 08:49:41 PM"
57 | },
58 | {
59 | "id": "2477fbbb-7b84-g5jb-9bkl-8741a670b0ec",
60 | "title": "My Computer",
61 | "body": "I sit on my computer all day.",
62 | "updated_at": "2019-03-11 12:58:55 AM",
63 | "created_at": "2019-01-15 11:49:41 PM"
64 | },
65 | {
66 | "id": "247aaabb-7564-g5jb-9bkl-8741ah70b0ec",
67 | "title": "Courses",
68 | "body": "I make Android dev courses for a living.",
69 | "updated_at": "2019-05-04 09:44:55 AM",
70 | "created_at": "2019-01-15 10:11:41 PM"
71 | }
72 | ]
--------------------------------------------------------------------------------
/app/src/debug/java/com/codingwithmitch/cleannotes/util/EspressoIdlingResource.kt:
--------------------------------------------------------------------------------
1 | package com.codingwithmitch.cleannotes.util
2 |
3 | import androidx.test.espresso.idling.CountingIdlingResource
4 |
5 |
6 | object EspressoIdlingResource {
7 |
8 | private val CLASS_NAME = "EspressoIdlingResource"
9 |
10 | private const val RESOURCE = "GLOBAL"
11 |
12 | @JvmField val countingIdlingResource = CountingIdlingResource(RESOURCE)
13 |
14 | fun increment() {
15 | printLogD(CLASS_NAME, "INCREMENTING.")
16 | countingIdlingResource.increment()
17 | }
18 |
19 | fun decrement() {
20 | if (!countingIdlingResource.isIdleNow) {
21 | printLogD(CLASS_NAME, "DECREMENTING.")
22 | countingIdlingResource.decrement()
23 | }
24 | }
25 |
26 | fun clear() {
27 | if (!countingIdlingResource.isIdleNow) {
28 | decrement()
29 | clear()
30 | }
31 | }
32 | }
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
16 |
17 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/app/src/main/java/com/codingwithmitch/cleannotes/business/data/cache/CacheConstants.kt:
--------------------------------------------------------------------------------
1 | package com.codingwithmitch.cleannotes.business.data.cache
2 |
3 | object CacheConstants {
4 |
5 | const val CACHE_TIMEOUT = 2000L
6 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/codingwithmitch/cleannotes/business/data/cache/CacheErrors.kt:
--------------------------------------------------------------------------------
1 | package com.codingwithmitch.cleannotes.business.data.cache
2 |
3 | object CacheErrors {
4 |
5 | const val CACHE_ERROR_UNKNOWN = "Unknown cache error"
6 | const val CACHE_ERROR = "Cache error"
7 | const val CACHE_ERROR_TIMEOUT = "Cache timeout"
8 | const val CACHE_DATA_NULL = "Cache data is null"
9 |
10 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/codingwithmitch/cleannotes/business/data/cache/CacheResponseHandler.kt:
--------------------------------------------------------------------------------
1 | package com.codingwithmitch.cleannotes.business.data.cache
2 |
3 | import com.codingwithmitch.cleannotes.business.data.cache.CacheErrors.CACHE_DATA_NULL
4 | import com.codingwithmitch.cleannotes.business.domain.state.*
5 |
6 |
7 | abstract class CacheResponseHandler (
8 | private val response: CacheResult,
9 | private val stateEvent: StateEvent?
10 | ){
11 | suspend fun getResult(): DataState? {
12 |
13 | return when(response){
14 |
15 | is CacheResult.GenericError -> {
16 | DataState.error(
17 | response = Response(
18 | message = "${stateEvent?.errorInfo()}\n\nReason: ${response.errorMessage}",
19 | uiComponentType = UIComponentType.Dialog(),
20 | messageType = MessageType.Error()
21 | ),
22 | stateEvent = stateEvent
23 | )
24 | }
25 |
26 | is CacheResult.Success -> {
27 | if(response.value == null){
28 | DataState.error(
29 | response = Response(
30 | message = "${stateEvent?.errorInfo()}\n\nReason: ${CACHE_DATA_NULL}.",
31 | uiComponentType = UIComponentType.Dialog(),
32 | messageType = MessageType.Error()
33 | ),
34 | stateEvent = stateEvent
35 | )
36 | }
37 | else{
38 | handleSuccess(resultObj = response.value)
39 | }
40 | }
41 |
42 | }
43 | }
44 |
45 | abstract suspend fun handleSuccess(resultObj: Data): DataState?
46 |
47 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/codingwithmitch/cleannotes/business/data/cache/CacheResult.kt:
--------------------------------------------------------------------------------
1 | package com.codingwithmitch.cleannotes.business.data.cache
2 |
3 | sealed class CacheResult {
4 |
5 | data class Success(val value: T): CacheResult()
6 |
7 | data class GenericError(
8 | val errorMessage: String? = null
9 | ): CacheResult()
10 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/codingwithmitch/cleannotes/business/data/cache/abstraction/NoteCacheDataSource.kt:
--------------------------------------------------------------------------------
1 | package com.codingwithmitch.cleannotes.business.data.cache.abstraction
2 |
3 | import com.codingwithmitch.cleannotes.business.domain.model.Note
4 |
5 | interface NoteCacheDataSource{
6 |
7 | suspend fun insertNote(note: Note): Long
8 |
9 | suspend fun deleteNote(primaryKey: String): Int
10 |
11 | suspend fun deleteNotes(notes: List): Int
12 |
13 | suspend fun updateNote(
14 | primaryKey: String,
15 | newTitle: String,
16 | newBody: String?,
17 | timestamp: String?
18 | ): Int
19 |
20 | suspend fun searchNotes(
21 | query: String,
22 | filterAndOrder: String,
23 | page: Int
24 | ): List
25 |
26 | suspend fun getAllNotes(): List
27 |
28 | suspend fun searchNoteById(id: String): Note?
29 |
30 | suspend fun getNumNotes(): Int
31 |
32 | suspend fun insertNotes(notes: List): LongArray
33 | }
34 |
35 |
36 |
37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/app/src/main/java/com/codingwithmitch/cleannotes/business/data/cache/implementation/NoteCacheDataSourceImpl.kt:
--------------------------------------------------------------------------------
1 | package com.codingwithmitch.cleannotes.business.data.cache.implementation
2 |
3 | import com.codingwithmitch.cleannotes.business.data.cache.abstraction.NoteCacheDataSource
4 | import com.codingwithmitch.cleannotes.business.domain.model.Note
5 | import com.codingwithmitch.cleannotes.framework.datasource.cache.abstraction.NoteDaoService
6 | import javax.inject.Inject
7 | import javax.inject.Singleton
8 |
9 | @Singleton
10 | class NoteCacheDataSourceImpl
11 | @Inject
12 | constructor(
13 | private val noteDaoService: NoteDaoService
14 | ): NoteCacheDataSource {
15 |
16 | override suspend fun insertNote(note: Note): Long {
17 | return noteDaoService.insertNote(note)
18 | }
19 |
20 | override suspend fun deleteNote(primaryKey: String): Int {
21 | return noteDaoService.deleteNote(primaryKey)
22 | }
23 |
24 | override suspend fun deleteNotes(notes: List): Int {
25 | return noteDaoService.deleteNotes(notes)
26 | }
27 |
28 | override suspend fun updateNote(
29 | primaryKey: String,
30 | newTitle: String,
31 | newBody: String?,
32 | timestamp: String?
33 | ): Int {
34 | return noteDaoService.updateNote(
35 | primaryKey,
36 | newTitle,
37 | newBody,
38 | timestamp
39 | )
40 | }
41 |
42 | override suspend fun searchNotes(
43 | query: String,
44 | filterAndOrder: String,
45 | page: Int
46 | ): List {
47 | return noteDaoService.returnOrderedQuery(
48 | query, filterAndOrder, page
49 | )
50 | }
51 |
52 | override suspend fun getAllNotes(): List {
53 | return noteDaoService.getAllNotes()
54 | }
55 |
56 | override suspend fun searchNoteById(id: String): Note? {
57 | return noteDaoService.searchNoteById(id)
58 | }
59 |
60 | override suspend fun getNumNotes(): Int {
61 | return noteDaoService.getNumNotes()
62 | }
63 |
64 | override suspend fun insertNotes(notes: List): LongArray{
65 | return noteDaoService.insertNotes(notes)
66 | }
67 | }
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
--------------------------------------------------------------------------------
/app/src/main/java/com/codingwithmitch/cleannotes/business/data/network/ApiResponseHandler.kt:
--------------------------------------------------------------------------------
1 | package com.codingwithmitch.cleannotes.business.data.network
2 |
3 | import com.codingwithmitch.cleannotes.business.data.network.NetworkErrors.NETWORK_DATA_NULL
4 | import com.codingwithmitch.cleannotes.business.data.network.NetworkErrors.NETWORK_ERROR
5 | import com.codingwithmitch.cleannotes.business.domain.state.*
6 |
7 |
8 | abstract class ApiResponseHandler (
9 | private val response: ApiResult,
10 | private val stateEvent: StateEvent?
11 | ){
12 |
13 | suspend fun getResult(): DataState? {
14 |
15 | return when(response){
16 |
17 | is ApiResult.GenericError -> {
18 | DataState.error(
19 | response = Response(
20 | message = "${stateEvent?.errorInfo()}\n\nReason: ${response.errorMessage.toString()}",
21 | uiComponentType = UIComponentType.Dialog(),
22 | messageType = MessageType.Error()
23 | ),
24 | stateEvent = stateEvent
25 | )
26 | }
27 |
28 | is ApiResult.NetworkError -> {
29 | DataState.error(
30 | response = Response(
31 | message = "${stateEvent?.errorInfo()}\n\nReason: ${NETWORK_ERROR}",
32 | uiComponentType = UIComponentType.Dialog(),
33 | messageType = MessageType.Error()
34 | ),
35 | stateEvent = stateEvent
36 | )
37 | }
38 |
39 | is ApiResult.Success -> {
40 | if(response.value == null){
41 | DataState.error(
42 | response = Response(
43 | message = "${stateEvent?.errorInfo()}\n\nReason: ${NETWORK_DATA_NULL}.",
44 | uiComponentType = UIComponentType.Dialog(),
45 | messageType = MessageType.Error()
46 | ),
47 | stateEvent = stateEvent
48 | )
49 | }
50 | else{
51 | handleSuccess(resultObj = response.value)
52 | }
53 | }
54 |
55 | }
56 | }
57 |
58 | abstract suspend fun handleSuccess(resultObj: Data): DataState?
59 |
60 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/codingwithmitch/cleannotes/business/data/network/ApiResult.kt:
--------------------------------------------------------------------------------
1 | package com.codingwithmitch.cleannotes.business.data.network
2 |
3 | sealed class ApiResult {
4 |
5 | data class Success(val value: T): ApiResult()
6 |
7 | data class GenericError(
8 | val code: Int? = null,
9 | val errorMessage: String? = null
10 | ): ApiResult()
11 |
12 | object NetworkError: ApiResult()
13 | }
14 |
--------------------------------------------------------------------------------
/app/src/main/java/com/codingwithmitch/cleannotes/business/data/network/NetworkConstants.kt:
--------------------------------------------------------------------------------
1 | package com.codingwithmitch.cleannotes.business.data.network
2 |
3 | object NetworkConstants {
4 |
5 | const val NETWORK_TIMEOUT = 6000L
6 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/codingwithmitch/cleannotes/business/data/network/NetworkErrors.kt:
--------------------------------------------------------------------------------
1 | package com.codingwithmitch.cleannotes.business.data.network
2 |
3 | object NetworkErrors {
4 |
5 | const val UNABLE_TO_RESOLVE_HOST = "Unable to resolve host"
6 | const val UNABLE_TODO_OPERATION_WO_INTERNET = "Can't do that operation without an internet connection"
7 | const val ERROR_CHECK_NETWORK_CONNECTION = "Check network connection."
8 | const val NETWORK_ERROR_UNKNOWN = "Unknown network error"
9 | const val NETWORK_ERROR = "Network error"
10 | const val NETWORK_ERROR_TIMEOUT = "Network timeout"
11 | const val NETWORK_DATA_NULL = "Network data is null"
12 |
13 |
14 | fun isNetworkError(msg: String): Boolean{
15 | when{
16 | msg.contains(UNABLE_TO_RESOLVE_HOST) -> return true
17 | else-> return false
18 | }
19 | }
20 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/codingwithmitch/cleannotes/business/data/network/abstraction/NoteNetworkDataSource.kt:
--------------------------------------------------------------------------------
1 | package com.codingwithmitch.cleannotes.business.data.network.abstraction
2 |
3 | import com.codingwithmitch.cleannotes.business.domain.model.Note
4 |
5 |
6 | interface NoteNetworkDataSource{
7 |
8 | suspend fun insertOrUpdateNote(note: Note)
9 |
10 | suspend fun deleteNote(primaryKey: String)
11 |
12 | suspend fun insertDeletedNote(note: Note)
13 |
14 | suspend fun insertDeletedNotes(notes: List)
15 |
16 | suspend fun deleteDeletedNote(note: Note)
17 |
18 | suspend fun getDeletedNotes(): List
19 |
20 | suspend fun deleteAllNotes()
21 |
22 | suspend fun searchNote(note: Note): Note?
23 |
24 | suspend fun getAllNotes(): List
25 |
26 | suspend fun insertOrUpdateNotes(notes: List)
27 |
28 | }
29 |
--------------------------------------------------------------------------------
/app/src/main/java/com/codingwithmitch/cleannotes/business/data/network/implementation/NoteNetworkDataSourceImpl.kt:
--------------------------------------------------------------------------------
1 | package com.codingwithmitch.cleannotes.business.data.network.implementation
2 |
3 | import com.codingwithmitch.cleannotes.business.data.network.abstraction.NoteNetworkDataSource
4 | import com.codingwithmitch.cleannotes.business.domain.model.Note
5 | import com.codingwithmitch.cleannotes.framework.datasource.network.abstraction.NoteFirestoreService
6 | import javax.inject.Inject
7 | import javax.inject.Singleton
8 |
9 |
10 | @Singleton
11 | class NoteNetworkDataSourceImpl
12 | @Inject
13 | constructor(
14 | private val firestoreService: NoteFirestoreService
15 | ): NoteNetworkDataSource {
16 |
17 | override suspend fun insertOrUpdateNote(note: Note) {
18 | return firestoreService.insertOrUpdateNote(note)
19 | }
20 |
21 | override suspend fun deleteNote(primaryKey: String) {
22 | return firestoreService.deleteNote(primaryKey)
23 | }
24 |
25 | override suspend fun insertDeletedNote(note: Note) {
26 | return firestoreService.insertDeletedNote(note)
27 | }
28 |
29 | override suspend fun insertDeletedNotes(notes: List) {
30 | return firestoreService.insertDeletedNotes(notes)
31 | }
32 |
33 | override suspend fun deleteDeletedNote(note: Note) {
34 | return firestoreService.deleteDeletedNote(note)
35 | }
36 |
37 | override suspend fun getDeletedNotes(): List {
38 | return firestoreService.getDeletedNotes()
39 | }
40 |
41 | override suspend fun deleteAllNotes() {
42 | firestoreService.deleteAllNotes()
43 | }
44 |
45 | override suspend fun searchNote(note: Note): Note? {
46 | return firestoreService.searchNote(note)
47 | }
48 |
49 | override suspend fun getAllNotes(): List {
50 | return firestoreService.getAllNotes()
51 | }
52 |
53 | override suspend fun insertOrUpdateNotes(notes: List) {
54 | return firestoreService.insertOrUpdateNotes(notes)
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 |
--------------------------------------------------------------------------------
/app/src/main/java/com/codingwithmitch/cleannotes/business/data/util/GenericErrors.kt:
--------------------------------------------------------------------------------
1 | package com.codingwithmitch.cleannotes.business.data.util
2 |
3 | object GenericErrors {
4 |
5 | const val ERROR_UNKNOWN = "Unknown error"
6 | const val INVALID_STATE_EVENT = "Invalid state event"
7 |
8 | }
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/app/src/main/java/com/codingwithmitch/cleannotes/business/domain/model/Note.kt:
--------------------------------------------------------------------------------
1 | package com.codingwithmitch.cleannotes.business.domain.model
2 |
3 | import android.os.Parcelable
4 | import kotlinx.android.parcel.Parcelize
5 |
6 | @Parcelize
7 | data class Note(
8 | val id: String,
9 | val title: String,
10 | val body: String,
11 | val updated_at: String,
12 | val created_at: String
13 | ) : Parcelable{
14 |
15 | override fun equals(other: Any?): Boolean {
16 | if (this === other) return true
17 | if (javaClass != other?.javaClass) return false
18 |
19 | other as Note
20 |
21 | if (id != other.id) return false
22 | if (title != other.title) return false
23 | if (body != other.body) return false
24 | if (created_at != other.created_at) return false
25 |
26 | return true
27 | }
28 |
29 | override fun hashCode(): Int {
30 | var result = id.hashCode()
31 | result = 31 * result + title.hashCode()
32 | result = 31 * result + body.hashCode()
33 | result = 31 * result + created_at.hashCode()
34 | return result
35 | }
36 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/codingwithmitch/cleannotes/business/domain/model/NoteFactory.kt:
--------------------------------------------------------------------------------
1 | package com.codingwithmitch.cleannotes.business.domain.model
2 |
3 | import com.codingwithmitch.cleannotes.business.domain.util.DateUtil
4 | import java.util.*
5 | import javax.inject.Inject
6 | import javax.inject.Singleton
7 | import kotlin.collections.ArrayList
8 |
9 | @Singleton
10 | class NoteFactory
11 | @Inject
12 | constructor(
13 | private val dateUtil: DateUtil
14 | ) {
15 |
16 | fun createSingleNote(
17 | id: String? = null,
18 | title: String,
19 | body: String? = null
20 | ): Note {
21 | return Note(
22 | id = id ?: UUID.randomUUID().toString(),
23 | title = title,
24 | body = body ?: "",
25 | created_at = dateUtil.getCurrentTimestamp(),
26 | updated_at = dateUtil.getCurrentTimestamp()
27 | )
28 | }
29 |
30 | fun createNoteList(numNotes: Int): List {
31 | val list: ArrayList = ArrayList()
32 | for(i in 0 until numNotes){ // exclusive on upper bound
33 | list.add(
34 | createSingleNote(
35 | id = UUID.randomUUID().toString(),
36 | title = UUID.randomUUID().toString(),
37 | body = UUID.randomUUID().toString()
38 | )
39 | )
40 | }
41 | return list
42 | }
43 |
44 |
45 | }
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
--------------------------------------------------------------------------------
/app/src/main/java/com/codingwithmitch/cleannotes/business/domain/state/DataState.kt:
--------------------------------------------------------------------------------
1 | package com.codingwithmitch.cleannotes.business.domain.state
2 |
3 |
4 | data class DataState(
5 | var stateMessage: StateMessage? = null,
6 | var data: T? = null,
7 | var stateEvent: StateEvent? = null
8 | ) {
9 |
10 | companion object {
11 |
12 | fun error(
13 | response: Response,
14 | stateEvent: StateEvent?
15 | ): DataState {
16 | return DataState(
17 | stateMessage = StateMessage(
18 | response
19 | ),
20 | data = null,
21 | stateEvent = stateEvent
22 | )
23 | }
24 |
25 | fun data(
26 | response: Response?,
27 | data: T? = null,
28 | stateEvent: StateEvent?
29 | ): DataState {
30 | return DataState(
31 | stateMessage = response?.let {
32 | StateMessage(
33 | it
34 | )
35 | },
36 | data = data,
37 | stateEvent = stateEvent
38 | )
39 | }
40 | }
41 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/codingwithmitch/cleannotes/business/domain/state/MessageStack.kt:
--------------------------------------------------------------------------------
1 | package com.codingwithmitch.cleannotes.business.domain.state
2 |
3 |
4 | import androidx.lifecycle.LiveData
5 | import androidx.lifecycle.MutableLiveData
6 | import com.codingwithmitch.cleannotes.util.printLogD
7 | import kotlinx.android.parcel.IgnoredOnParcel
8 | import java.lang.IndexOutOfBoundsException
9 |
10 | const val MESSAGE_STACK_BUNDLE_KEY = "com.codingwithmitch.openapi.util.MessageStack"
11 |
12 | class MessageStack: ArrayList() {
13 |
14 | @IgnoredOnParcel
15 | private val _stateMessage: MutableLiveData = MutableLiveData()
16 |
17 | @IgnoredOnParcel
18 | val stateMessage: LiveData
19 | get() = _stateMessage
20 |
21 | fun isStackEmpty(): Boolean{
22 | return size == 0
23 | }
24 |
25 | override fun addAll(elements: Collection): Boolean {
26 | for(element in elements){
27 | add(element)
28 | }
29 | return true // always return true. We don't care about result bool.
30 | }
31 |
32 | override fun add(element: StateMessage): Boolean {
33 | if(this.contains(element)){ // prevent duplicate errors added to stack
34 | return false
35 | }
36 | val transaction = super.add(element)
37 | if(this.size == 1){
38 | setStateMessage(stateMessage = element)
39 | }
40 | return transaction
41 | }
42 |
43 | override fun removeAt(index: Int): StateMessage {
44 | try{
45 | val transaction = super.removeAt(index)
46 | if(this.size > 0){
47 | setStateMessage(stateMessage = this[0])
48 | }
49 | else{
50 | printLogD("MessageStack", "stack is empty: ")
51 | setStateMessage(null)
52 | }
53 | return transaction
54 | }catch (e: IndexOutOfBoundsException){
55 | setStateMessage(null)
56 | e.printStackTrace()
57 | }
58 | return StateMessage( // this does nothing
59 | Response(
60 | message = "does nothing",
61 | uiComponentType = UIComponentType.None(),
62 | messageType = MessageType.None()
63 | )
64 | )
65 | }
66 |
67 | private fun setStateMessage(stateMessage: StateMessage?){
68 | _stateMessage.value = stateMessage
69 | }
70 | }
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
--------------------------------------------------------------------------------
/app/src/main/java/com/codingwithmitch/cleannotes/business/domain/state/StateEvent.kt:
--------------------------------------------------------------------------------
1 | package com.codingwithmitch.cleannotes.business.domain.state
2 |
3 | interface StateEvent {
4 |
5 | fun errorInfo(): String
6 |
7 | fun eventName(): String
8 |
9 | fun shouldDisplayProgressBar(): Boolean
10 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/codingwithmitch/cleannotes/business/domain/state/StateEventManager.kt:
--------------------------------------------------------------------------------
1 | package com.codingwithmitch.cleannotes.business.domain.state
2 |
3 | import androidx.lifecycle.LiveData
4 | import androidx.lifecycle.MutableLiveData
5 | import com.codingwithmitch.cleannotes.util.EspressoIdlingResource
6 | import com.codingwithmitch.cleannotes.util.printLogD
7 |
8 | /**
9 | * - Keeps track of active StateEvents in DataChannelManager
10 | * - Keeps track of whether the progress bar should show or not based on a boolean
11 | * value in each StateEvent (shouldDisplayProgressBar)
12 | */
13 | class StateEventManager {
14 |
15 | private val activeStateEvents: HashMap = HashMap()
16 |
17 | private val _shouldDisplayProgressBar: MutableLiveData = MutableLiveData()
18 |
19 | val shouldDisplayProgressBar: LiveData
20 | get() = _shouldDisplayProgressBar
21 |
22 | fun getActiveJobNames(): MutableSet{
23 | return activeStateEvents.keys
24 | }
25 |
26 | fun clearActiveStateEventCounter(){
27 | printLogD("DCM", "Clear active state events")
28 | EspressoIdlingResource.clear()
29 | activeStateEvents.clear()
30 | syncNumActiveStateEvents()
31 | }
32 |
33 | fun addStateEvent(stateEvent: StateEvent){
34 | EspressoIdlingResource.increment()
35 | activeStateEvents.put(stateEvent.eventName(), stateEvent)
36 | syncNumActiveStateEvents()
37 | }
38 |
39 | fun removeStateEvent(stateEvent: StateEvent?){
40 | printLogD("DCM sem", "remove state event: ${stateEvent?.eventName()}")
41 | stateEvent?.let {
42 | EspressoIdlingResource.decrement()
43 | }
44 | activeStateEvents.remove(stateEvent?.eventName())
45 | syncNumActiveStateEvents()
46 | }
47 |
48 | fun isStateEventActive(stateEvent: StateEvent): Boolean{
49 | printLogD("DCM sem", "is state event active? " +
50 | "${activeStateEvents.containsKey(stateEvent.eventName())}")
51 | return activeStateEvents.containsKey(stateEvent.eventName())
52 | }
53 |
54 | private fun syncNumActiveStateEvents(){
55 | var shouldDisplayProgressBar = false
56 | for(stateEvent in activeStateEvents.values){
57 | if(stateEvent.shouldDisplayProgressBar()){
58 | shouldDisplayProgressBar = true
59 | }
60 | }
61 | _shouldDisplayProgressBar.value = shouldDisplayProgressBar
62 | }
63 | }
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
--------------------------------------------------------------------------------
/app/src/main/java/com/codingwithmitch/cleannotes/business/domain/state/StateResource.kt:
--------------------------------------------------------------------------------
1 | package com.codingwithmitch.cleannotes.business.domain.state
2 |
3 | import android.view.View
4 | import com.codingwithmitch.cleannotes.util.TodoCallback
5 |
6 |
7 | data class StateMessage(val response: Response)
8 |
9 | data class Response(
10 | val message: String?,
11 | val uiComponentType: UIComponentType,
12 | val messageType: MessageType
13 | )
14 |
15 | sealed class UIComponentType{
16 |
17 | class Toast: UIComponentType()
18 |
19 | class Dialog: UIComponentType()
20 |
21 | class AreYouSureDialog(
22 | val callback: AreYouSureCallback
23 | ): UIComponentType()
24 |
25 | class SnackBar(
26 | val undoCallback: SnackbarUndoCallback? = null,
27 | val onDismissCallback: TodoCallback? = null
28 | ): UIComponentType()
29 |
30 | class None: UIComponentType()
31 | }
32 |
33 | sealed class MessageType{
34 |
35 | class Success: MessageType()
36 |
37 | class Error: MessageType()
38 |
39 | class Info: MessageType()
40 |
41 | class None: MessageType()
42 | }
43 |
44 |
45 | interface StateMessageCallback{
46 |
47 | fun removeMessageFromStack()
48 | }
49 |
50 |
51 | interface AreYouSureCallback {
52 |
53 | fun proceed()
54 |
55 | fun cancel()
56 | }
57 |
58 | interface SnackbarUndoCallback {
59 |
60 | fun undo()
61 | }
62 |
63 | class SnackbarUndoListener
64 | constructor(
65 | private val snackbarUndoCallback: SnackbarUndoCallback?
66 | ): View.OnClickListener {
67 |
68 | override fun onClick(v: View?) {
69 | snackbarUndoCallback?.undo()
70 | }
71 |
72 | }
73 |
74 |
75 | interface DialogInputCaptureCallback {
76 |
77 | fun onTextCaptured(text: String)
78 | }
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
--------------------------------------------------------------------------------
/app/src/main/java/com/codingwithmitch/cleannotes/business/domain/state/ViewState.kt:
--------------------------------------------------------------------------------
1 | package com.codingwithmitch.cleannotes.business.domain.state
2 |
3 | interface ViewState {
4 |
5 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/codingwithmitch/cleannotes/business/domain/util/DateUtil.kt:
--------------------------------------------------------------------------------
1 | package com.codingwithmitch.cleannotes.business.domain.util
2 |
3 | import com.google.firebase.Timestamp
4 | import java.text.SimpleDateFormat
5 | import java.util.*
6 | import javax.inject.Singleton
7 |
8 | @Singleton
9 | class DateUtil
10 | constructor(
11 | private val dateFormat: SimpleDateFormat
12 | )
13 | {
14 |
15 | fun removeTimeFromDateString(sd: String): String{
16 | return sd.substring(0, sd.indexOf(" "))
17 | }
18 |
19 | fun convertFirebaseTimestampToStringData(timestamp: Timestamp): String{
20 | return dateFormat.format(timestamp.toDate())
21 | }
22 |
23 | // Date format: "2019-07-23 HH:mm:ss"
24 | fun convertStringDateToFirebaseTimestamp(date: String): Timestamp{
25 | return Timestamp(dateFormat.parse(date))
26 | }
27 |
28 | // dates format looks like this: "2019-07-23 HH:mm:ss"
29 | fun getCurrentTimestamp(): String {
30 | return dateFormat.format(Date())
31 | }
32 |
33 | }
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/app/src/main/java/com/codingwithmitch/cleannotes/business/domain/util/EntityMapper.kt:
--------------------------------------------------------------------------------
1 | package com.codingwithmitch.cleannotes.business.domain.util
2 |
3 | interface EntityMapper {
4 |
5 | fun mapFromEntity(entity: Entity): DomainModel
6 |
7 | fun mapToEntity(domainModel: DomainModel): Entity
8 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/codingwithmitch/cleannotes/business/interactors/common/DeleteNote.kt:
--------------------------------------------------------------------------------
1 | package com.codingwithmitch.cleannotes.business.interactors.common
2 |
3 | import com.codingwithmitch.cleannotes.business.data.cache.CacheResponseHandler
4 | import com.codingwithmitch.cleannotes.business.data.cache.abstraction.NoteCacheDataSource
5 | import com.codingwithmitch.cleannotes.business.data.network.abstraction.NoteNetworkDataSource
6 | import com.codingwithmitch.cleannotes.business.domain.model.Note
7 | import com.codingwithmitch.cleannotes.business.domain.state.*
8 | import com.codingwithmitch.cleannotes.business.data.util.safeApiCall
9 | import com.codingwithmitch.cleannotes.business.data.util.safeCacheCall
10 | import kotlinx.coroutines.Dispatchers.IO
11 | import kotlinx.coroutines.flow.Flow
12 | import kotlinx.coroutines.flow.flow
13 |
14 | class DeleteNote(
15 | private val noteCacheDataSource: NoteCacheDataSource,
16 | private val noteNetworkDataSource: NoteNetworkDataSource
17 | ){
18 |
19 | fun deleteNote(
20 | note: Note,
21 | stateEvent: StateEvent
22 | ): Flow?> = flow {
23 |
24 | val cacheResult = safeCacheCall(IO){
25 | noteCacheDataSource.deleteNote(note.id)
26 | }
27 |
28 | val response = object: CacheResponseHandler(
29 | response = cacheResult,
30 | stateEvent = stateEvent
31 | ){
32 | override suspend fun handleSuccess(resultObj: Int): DataState? {
33 | return if(resultObj > 0){
34 | DataState.data(
35 | response = Response(
36 | message = DELETE_NOTE_SUCCESS,
37 | uiComponentType = UIComponentType.None(),
38 | messageType = MessageType.Success()
39 | ),
40 | data = null,
41 | stateEvent = stateEvent
42 | )
43 | }
44 | else{
45 | DataState.data(
46 | response = Response(
47 | message = DELETE_NOTE_FAILED,
48 | uiComponentType = UIComponentType.Toast(),
49 | messageType = MessageType.Error()
50 | ),
51 | data = null,
52 | stateEvent = stateEvent
53 | )
54 | }
55 | }
56 | }.getResult()
57 |
58 | emit(response)
59 |
60 | // update network
61 | if(response?.stateMessage?.response?.message.equals(DELETE_NOTE_SUCCESS)){
62 |
63 | // delete from 'notes' node
64 | safeApiCall(IO){
65 | noteNetworkDataSource.deleteNote(note.id)
66 | }
67 |
68 | // insert into 'deletes' node
69 | safeApiCall(IO){
70 | noteNetworkDataSource.insertDeletedNote(note)
71 | }
72 |
73 | }
74 | }
75 |
76 | companion object{
77 | val DELETE_NOTE_SUCCESS = "Successfully deleted note."
78 | val DELETE_NOTE_PENDING = "Delete pending..."
79 | val DELETE_NOTE_FAILED = "Failed to delete note."
80 | val DELETE_ARE_YOU_SURE = "Are you sure you want to delete this?"
81 | }
82 | }
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
--------------------------------------------------------------------------------
/app/src/main/java/com/codingwithmitch/cleannotes/business/interactors/notedetail/NoteDetailInteractors.kt:
--------------------------------------------------------------------------------
1 | package com.codingwithmitch.cleannotes.business.interactors.notedetail
2 |
3 | import com.codingwithmitch.cleannotes.business.interactors.common.DeleteNote
4 | import com.codingwithmitch.cleannotes.framework.presentation.notedetail.state.NoteDetailViewState
5 |
6 | // Use cases
7 | class NoteDetailInteractors (
8 | val deleteNote: DeleteNote,
9 | val updateNote: UpdateNote
10 | )
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/app/src/main/java/com/codingwithmitch/cleannotes/business/interactors/notedetail/UpdateNote.kt:
--------------------------------------------------------------------------------
1 | package com.codingwithmitch.cleannotes.business.interactors.notedetail
2 |
3 | import com.codingwithmitch.cleannotes.business.data.cache.CacheResponseHandler
4 | import com.codingwithmitch.cleannotes.business.data.cache.abstraction.NoteCacheDataSource
5 | import com.codingwithmitch.cleannotes.business.data.network.abstraction.NoteNetworkDataSource
6 | import com.codingwithmitch.cleannotes.business.domain.model.Note
7 | import com.codingwithmitch.cleannotes.business.domain.state.*
8 | import com.codingwithmitch.cleannotes.business.data.util.safeApiCall
9 | import com.codingwithmitch.cleannotes.business.data.util.safeCacheCall
10 | import com.codingwithmitch.cleannotes.framework.presentation.notedetail.state.NoteDetailViewState
11 | import kotlinx.coroutines.Dispatchers
12 | import kotlinx.coroutines.flow.Flow
13 | import kotlinx.coroutines.flow.flow
14 |
15 | class UpdateNote(
16 | private val noteCacheDataSource: NoteCacheDataSource,
17 | private val noteNetworkDataSource: NoteNetworkDataSource
18 | ){
19 |
20 | fun updateNote(
21 | note: Note,
22 | stateEvent: StateEvent
23 | ): Flow?> = flow {
24 |
25 | val cacheResult = safeCacheCall(Dispatchers.IO){
26 | noteCacheDataSource.updateNote(
27 | primaryKey = note.id,
28 | newTitle = note.title,
29 | newBody = note.body,
30 | timestamp = null // generate new timestamp
31 | )
32 | }
33 |
34 | val response = object: CacheResponseHandler(
35 | response = cacheResult,
36 | stateEvent = stateEvent
37 | ){
38 | override suspend fun handleSuccess(resultObj: Int): DataState? {
39 | return if(resultObj > 0){
40 | DataState.data(
41 | response = Response(
42 | message = UPDATE_NOTE_SUCCESS,
43 | uiComponentType = UIComponentType.Toast(),
44 | messageType = MessageType.Success()
45 | ),
46 | data = null,
47 | stateEvent = stateEvent
48 | )
49 | }
50 | else{
51 | DataState.data(
52 | response = Response(
53 | message = UPDATE_NOTE_FAILED,
54 | uiComponentType = UIComponentType.Toast(),
55 | messageType = MessageType.Error()
56 | ),
57 | data = null,
58 | stateEvent = stateEvent
59 | )
60 | }
61 | }
62 | }.getResult()
63 |
64 | emit(response)
65 |
66 | updateNetwork(response?.stateMessage?.response?.message, note)
67 | }
68 |
69 | private suspend fun updateNetwork(response: String?, note: Note) {
70 | if(response.equals(UPDATE_NOTE_SUCCESS)){
71 |
72 | safeApiCall(Dispatchers.IO){
73 | noteNetworkDataSource.insertOrUpdateNote(note)
74 | }
75 | }
76 | }
77 |
78 | companion object{
79 | val UPDATE_NOTE_SUCCESS = "Successfully updated note."
80 | val UPDATE_NOTE_FAILED = "Failed to update note."
81 | val UPDATE_NOTE_FAILED_PK = "Update failed. Note is missing primary key."
82 |
83 | }
84 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/codingwithmitch/cleannotes/business/interactors/notelist/GetNumNotes.kt:
--------------------------------------------------------------------------------
1 | package com.codingwithmitch.cleannotes.business.interactors.notelist
2 |
3 | import com.codingwithmitch.cleannotes.business.data.cache.CacheResponseHandler
4 | import com.codingwithmitch.cleannotes.business.data.cache.abstraction.NoteCacheDataSource
5 | import com.codingwithmitch.cleannotes.business.domain.state.*
6 | import com.codingwithmitch.cleannotes.business.data.util.safeCacheCall
7 | import com.codingwithmitch.cleannotes.framework.presentation.notelist.state.NoteListViewState
8 | import kotlinx.coroutines.Dispatchers.IO
9 | import kotlinx.coroutines.flow.Flow
10 | import kotlinx.coroutines.flow.flow
11 |
12 | class GetNumNotes(
13 | private val noteCacheDataSource: NoteCacheDataSource
14 | ){
15 |
16 | fun getNumNotes(
17 | stateEvent: StateEvent
18 | ): Flow?> = flow {
19 |
20 | val cacheResult = safeCacheCall(IO){
21 | noteCacheDataSource.getNumNotes()
22 | }
23 | val response = object: CacheResponseHandler(
24 | response = cacheResult,
25 | stateEvent = stateEvent
26 | ){
27 | override suspend fun handleSuccess(resultObj: Int): DataState? {
28 | val viewState = NoteListViewState(
29 | numNotesInCache = resultObj
30 | )
31 | return DataState.data(
32 | response = Response(
33 | message = GET_NUM_NOTES_SUCCESS,
34 | uiComponentType = UIComponentType.None(),
35 | messageType = MessageType.Success()
36 | ),
37 | data = viewState,
38 | stateEvent = stateEvent
39 | )
40 | }
41 | }.getResult()
42 |
43 | emit(response)
44 | }
45 |
46 | companion object{
47 | val GET_NUM_NOTES_SUCCESS = "Successfully retrieved the number of notes from the cache."
48 | val GET_NUM_NOTES_FAILED = "Failed to get the number of notes from the cache."
49 | }
50 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/codingwithmitch/cleannotes/business/interactors/notelist/InsertMultipleNotes.kt:
--------------------------------------------------------------------------------
1 | package com.codingwithmitch.cleannotes.business.interactors.notelist
2 |
3 | import com.codingwithmitch.cleannotes.business.data.cache.abstraction.NoteCacheDataSource
4 | import com.codingwithmitch.cleannotes.business.data.network.abstraction.NoteNetworkDataSource
5 | import com.codingwithmitch.cleannotes.business.domain.model.Note
6 | import com.codingwithmitch.cleannotes.business.domain.state.*
7 | import com.codingwithmitch.cleannotes.business.domain.util.DateUtil
8 | import com.codingwithmitch.cleannotes.business.data.util.safeApiCall
9 | import com.codingwithmitch.cleannotes.business.data.util.safeCacheCall
10 | import com.codingwithmitch.cleannotes.framework.presentation.notelist.state.NoteListViewState
11 | import kotlinx.coroutines.Dispatchers.IO
12 | import kotlinx.coroutines.flow.Flow
13 | import kotlinx.coroutines.flow.flow
14 | import java.text.SimpleDateFormat
15 | import java.util.*
16 | import kotlin.collections.ArrayList
17 |
18 | // For testing
19 | class InsertMultipleNotes(
20 | private val noteCacheDataSource: NoteCacheDataSource,
21 | private val noteNetworkDataSource: NoteNetworkDataSource
22 | ){
23 |
24 | fun insertNotes(
25 | numNotes: Int,
26 | stateEvent: StateEvent
27 | ): Flow?> = flow {
28 |
29 | val noteList = NoteListTester.generateNoteList(numNotes)
30 | safeCacheCall(IO){
31 | noteCacheDataSource.insertNotes(noteList)
32 | }
33 |
34 | emit(
35 | DataState.data(
36 | response = Response(
37 | message = "success",
38 | uiComponentType = UIComponentType.None(),
39 | messageType = MessageType.None()
40 | ),
41 | data = null,
42 | stateEvent = stateEvent
43 | )
44 | )
45 |
46 | updateNetwork(noteList)
47 | }
48 |
49 | private suspend fun updateNetwork(noteList: List){
50 | safeApiCall(IO){
51 | noteNetworkDataSource.insertOrUpdateNotes(noteList)
52 | }
53 | }
54 |
55 | }
56 |
57 |
58 | private object NoteListTester {
59 |
60 | private val dateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH)
61 | private val dateUtil =
62 | DateUtil(dateFormat)
63 |
64 | fun generateNoteList(numNotes: Int): List{
65 | val list: ArrayList = ArrayList()
66 | for(id in 0..numNotes){
67 | list.add(generateNote())
68 | }
69 | return list
70 | }
71 |
72 | fun generateNote(): Note {
73 | val note = Note(
74 | id = UUID.randomUUID().toString(),
75 | title = UUID.randomUUID().toString(),
76 | body = UUID.randomUUID().toString(),
77 | created_at = dateUtil.getCurrentTimestamp(),
78 | updated_at = dateUtil.getCurrentTimestamp()
79 | )
80 | return note
81 | }
82 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/codingwithmitch/cleannotes/business/interactors/notelist/NoteListInteractors.kt:
--------------------------------------------------------------------------------
1 | package com.codingwithmitch.cleannotes.business.interactors.notelist
2 |
3 | import com.codingwithmitch.cleannotes.business.interactors.common.DeleteNote
4 | import com.codingwithmitch.cleannotes.framework.presentation.notelist.state.NoteListViewState
5 |
6 |
7 | // Use cases
8 | class NoteListInteractors (
9 | val insertNewNote: InsertNewNote,
10 | val deleteNote: DeleteNote,
11 | val searchNotes: SearchNotes,
12 | val getNumNotes: GetNumNotes,
13 | val restoreDeletedNote: RestoreDeletedNote,
14 | val deleteMultipleNotes: DeleteMultipleNotes,
15 | val insertMultipleNotes: InsertMultipleNotes // for testing
16 | )
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/app/src/main/java/com/codingwithmitch/cleannotes/business/interactors/notelist/SearchNotes.kt:
--------------------------------------------------------------------------------
1 | package com.codingwithmitch.cleannotes.business.interactors.notelist
2 |
3 | import com.codingwithmitch.cleannotes.business.data.cache.CacheResponseHandler
4 | import com.codingwithmitch.cleannotes.business.data.cache.abstraction.NoteCacheDataSource
5 | import com.codingwithmitch.cleannotes.business.domain.model.Note
6 | import com.codingwithmitch.cleannotes.business.domain.state.*
7 | import com.codingwithmitch.cleannotes.business.data.util.safeCacheCall
8 | import com.codingwithmitch.cleannotes.framework.presentation.notelist.state.NoteListViewState
9 | import kotlinx.coroutines.Dispatchers
10 | import kotlinx.coroutines.flow.Flow
11 | import kotlinx.coroutines.flow.flow
12 |
13 | class SearchNotes(
14 | private val noteCacheDataSource: NoteCacheDataSource
15 | ){
16 |
17 | fun searchNotes(
18 | query: String,
19 | filterAndOrder: String,
20 | page: Int,
21 | stateEvent: StateEvent
22 | ): Flow?> = flow {
23 | var updatedPage = page
24 | if(page <= 0){
25 | updatedPage = 1
26 | }
27 | val cacheResult = safeCacheCall(Dispatchers.IO){
28 | noteCacheDataSource.searchNotes(
29 | query = query,
30 | filterAndOrder = filterAndOrder,
31 | page = updatedPage
32 | )
33 | }
34 |
35 | val response = object: CacheResponseHandler>(
36 | response = cacheResult,
37 | stateEvent = stateEvent
38 | ){
39 | override suspend fun handleSuccess(resultObj: List): DataState? {
40 | var message: String? =
41 | SEARCH_NOTES_SUCCESS
42 | var uiComponentType: UIComponentType? = UIComponentType.None()
43 | if(resultObj.size == 0){
44 | message =
45 | SEARCH_NOTES_NO_MATCHING_RESULTS
46 | uiComponentType = UIComponentType.Toast()
47 | }
48 | return DataState.data(
49 | response = Response(
50 | message = message,
51 | uiComponentType = uiComponentType as UIComponentType,
52 | messageType = MessageType.Success()
53 | ),
54 | data = NoteListViewState(
55 | noteList = ArrayList(resultObj)
56 | ),
57 | stateEvent = stateEvent
58 | )
59 | }
60 | }.getResult()
61 |
62 | emit(response)
63 | }
64 |
65 | companion object{
66 | val SEARCH_NOTES_SUCCESS = "Successfully retrieved list of notes."
67 | val SEARCH_NOTES_NO_MATCHING_RESULTS = "There are no notes that match that query."
68 | val SEARCH_NOTES_FAILED = "Failed to retrieve the list of notes."
69 |
70 | }
71 | }
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
--------------------------------------------------------------------------------
/app/src/main/java/com/codingwithmitch/cleannotes/business/interactors/splash/SyncDeletedNotes.kt:
--------------------------------------------------------------------------------
1 | package com.codingwithmitch.cleannotes.business.interactors.splash
2 |
3 | import com.codingwithmitch.cleannotes.business.data.cache.CacheResponseHandler
4 | import com.codingwithmitch.cleannotes.business.data.cache.abstraction.NoteCacheDataSource
5 | import com.codingwithmitch.cleannotes.business.data.network.ApiResponseHandler
6 | import com.codingwithmitch.cleannotes.business.data.network.abstraction.NoteNetworkDataSource
7 | import com.codingwithmitch.cleannotes.business.domain.model.Note
8 | import com.codingwithmitch.cleannotes.business.domain.state.DataState
9 | import com.codingwithmitch.cleannotes.business.data.util.safeApiCall
10 | import com.codingwithmitch.cleannotes.business.data.util.safeCacheCall
11 | import com.codingwithmitch.cleannotes.util.printLogD
12 | import kotlinx.coroutines.Dispatchers.IO
13 |
14 | /*
15 | Search firestore for all notes in the "deleted" node.
16 | It will then search the cache for notes matching those deleted notes.
17 | If a match is found, it is deleted from the cache.
18 | */
19 | class SyncDeletedNotes(
20 | private val noteCacheDataSource: NoteCacheDataSource,
21 | private val noteNetworkDataSource: NoteNetworkDataSource
22 | ){
23 |
24 | suspend fun syncDeletedNotes(){
25 |
26 | val apiResult = safeApiCall(IO){
27 | noteNetworkDataSource.getDeletedNotes()
28 | }
29 | val response = object: ApiResponseHandler, List>(
30 | response = apiResult,
31 | stateEvent = null
32 | ){
33 | override suspend fun handleSuccess(resultObj: List): DataState>? {
34 | return DataState.data(
35 | response = null,
36 | data = resultObj,
37 | stateEvent = null
38 | )
39 | }
40 | }
41 |
42 | val notes = response.getResult()?.data?: ArrayList()
43 |
44 | val cacheResult = safeCacheCall(IO){
45 | noteCacheDataSource.deleteNotes(notes)
46 | }
47 |
48 | object: CacheResponseHandler(
49 | response = cacheResult,
50 | stateEvent = null
51 | ){
52 | override suspend fun handleSuccess(resultObj: Int): DataState? {
53 | printLogD("SyncNotes",
54 | "num deleted notes: ${resultObj}")
55 | return DataState.data(
56 | response = null,
57 | data = resultObj,
58 | stateEvent = null
59 | )
60 | }
61 | }.getResult()
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/src/main/java/com/codingwithmitch/cleannotes/di/AppComponent.kt:
--------------------------------------------------------------------------------
1 | package com.codingwithmitch.cleannotes.di
2 |
3 | import com.codingwithmitch.cleannotes.framework.presentation.BaseApplication
4 | import com.codingwithmitch.cleannotes.framework.presentation.MainActivity
5 | import com.codingwithmitch.cleannotes.framework.presentation.splash.NoteNetworkSyncManager
6 | import com.codingwithmitch.cleannotes.framework.presentation.notedetail.NoteDetailFragment
7 | import com.codingwithmitch.cleannotes.framework.presentation.notelist.NoteListFragment
8 | import com.codingwithmitch.cleannotes.framework.presentation.splash.SplashFragment
9 | import com.codingwithmitch.cleannotes.notes.di.NoteViewModelModule
10 | import dagger.BindsInstance
11 | import dagger.Component
12 | import kotlinx.coroutines.ExperimentalCoroutinesApi
13 | import kotlinx.coroutines.FlowPreview
14 | import javax.inject.Singleton
15 |
16 | @ExperimentalCoroutinesApi
17 | @FlowPreview
18 | @Singleton
19 | @Component(
20 | modules = [
21 | ProductionModule::class,
22 | AppModule::class,
23 | NoteViewModelModule::class,
24 | NoteFragmentFactoryModule::class
25 | ]
26 | )
27 | interface AppComponent {
28 |
29 | val noteNetworkSync: NoteNetworkSyncManager
30 |
31 | @Component.Factory
32 | interface Factory{
33 |
34 | fun create(@BindsInstance app: BaseApplication): AppComponent
35 | }
36 |
37 | fun inject(mainActivity: MainActivity)
38 |
39 | fun inject(splashFragment: SplashFragment)
40 |
41 | fun inject(noteListFragment: NoteListFragment)
42 |
43 | fun inject(noteDetailFragment: NoteDetailFragment)
44 | }
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
--------------------------------------------------------------------------------
/app/src/main/java/com/codingwithmitch/cleannotes/di/NoteFragmentFactoryModule.kt:
--------------------------------------------------------------------------------
1 | package com.codingwithmitch.cleannotes.di
2 |
3 | import androidx.fragment.app.FragmentFactory
4 | import androidx.lifecycle.ViewModelProvider
5 | import com.codingwithmitch.cleannotes.business.domain.util.DateUtil
6 | import com.codingwithmitch.cleannotes.framework.presentation.common.NoteFragmentFactory
7 | import dagger.Module
8 | import dagger.Provides
9 | import kotlinx.coroutines.ExperimentalCoroutinesApi
10 | import kotlinx.coroutines.FlowPreview
11 | import javax.inject.Singleton
12 |
13 | @FlowPreview
14 | @ExperimentalCoroutinesApi
15 | @Module
16 | object NoteFragmentFactoryModule {
17 |
18 | @JvmStatic
19 | @Singleton
20 | @Provides
21 | fun provideNoteFragmentFactory(
22 | viewModelFactory: ViewModelProvider.Factory,
23 | dateUtil: DateUtil
24 | ): FragmentFactory {
25 | return NoteFragmentFactory(
26 | viewModelFactory,
27 | dateUtil
28 | )
29 | }
30 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/codingwithmitch/cleannotes/di/NoteViewModelModule.kt:
--------------------------------------------------------------------------------
1 | package com.codingwithmitch.cleannotes.notes.di
2 |
3 | import android.content.SharedPreferences
4 | import androidx.lifecycle.ViewModelProvider
5 | import com.codingwithmitch.cleannotes.business.domain.model.NoteFactory
6 | import com.codingwithmitch.cleannotes.business.interactors.notedetail.NoteDetailInteractors
7 | import com.codingwithmitch.cleannotes.business.interactors.notelist.NoteListInteractors
8 | import com.codingwithmitch.cleannotes.framework.presentation.common.NoteViewModelFactory
9 | import com.codingwithmitch.cleannotes.framework.presentation.splash.NoteNetworkSyncManager
10 | import dagger.Module
11 | import dagger.Provides
12 | import kotlinx.coroutines.ExperimentalCoroutinesApi
13 | import kotlinx.coroutines.FlowPreview
14 | import javax.inject.Singleton
15 |
16 | @ExperimentalCoroutinesApi
17 | @FlowPreview
18 | @Module
19 | object NoteViewModelModule {
20 |
21 | @Singleton
22 | @JvmStatic
23 | @Provides
24 | fun provideNoteViewModelFactory(
25 | noteListInteractors: NoteListInteractors,
26 | noteDetailInteractors: NoteDetailInteractors,
27 | noteNetworkSyncManager: NoteNetworkSyncManager,
28 | noteFactory: NoteFactory,
29 | editor: SharedPreferences.Editor,
30 | sharedPreferences: SharedPreferences
31 | ): ViewModelProvider.Factory{
32 | return NoteViewModelFactory(
33 | noteListInteractors = noteListInteractors,
34 | noteDetailInteractors = noteDetailInteractors,
35 | noteNetworkSyncManager = noteNetworkSyncManager,
36 | noteFactory = noteFactory,
37 | editor = editor,
38 | sharedPreferences = sharedPreferences
39 | )
40 | }
41 |
42 | }
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
--------------------------------------------------------------------------------
/app/src/main/java/com/codingwithmitch/cleannotes/di/ProductionModule.kt:
--------------------------------------------------------------------------------
1 | package com.codingwithmitch.cleannotes.di
2 |
3 | import android.content.Context
4 | import android.content.SharedPreferences
5 | import androidx.room.Room
6 | import com.codingwithmitch.cleannotes.framework.datasource.cache.database.NoteDatabase
7 | import com.codingwithmitch.cleannotes.framework.datasource.preferences.PreferenceKeys
8 | import com.codingwithmitch.cleannotes.framework.presentation.BaseApplication
9 | import com.codingwithmitch.cleannotes.util.AndroidTestUtils
10 | import com.google.firebase.firestore.FirebaseFirestore
11 | import dagger.Module
12 | import dagger.Provides
13 | import kotlinx.coroutines.ExperimentalCoroutinesApi
14 | import kotlinx.coroutines.FlowPreview
15 | import javax.inject.Singleton
16 |
17 |
18 | /*
19 | Dependencies in this class have test fakes for ui tests. See "TestModule.kt" in
20 | androidTest dir
21 | */
22 | @ExperimentalCoroutinesApi
23 | @FlowPreview
24 | @Module
25 | object ProductionModule {
26 |
27 | @JvmStatic
28 | @Singleton
29 | @Provides
30 | fun provideAndroidTestUtils(): AndroidTestUtils {
31 | return AndroidTestUtils(false)
32 | }
33 |
34 | @JvmStatic
35 | @Singleton
36 | @Provides
37 | fun provideSharedPreferences(
38 | application: BaseApplication
39 | ): SharedPreferences {
40 | return application
41 | .getSharedPreferences(
42 | PreferenceKeys.NOTE_PREFERENCES,
43 | Context.MODE_PRIVATE
44 | )
45 | }
46 |
47 | @JvmStatic
48 | @Singleton
49 | @Provides
50 | fun provideNoteDb(app: BaseApplication): NoteDatabase {
51 | return Room
52 | .databaseBuilder(app, NoteDatabase::class.java, NoteDatabase.DATABASE_NAME)
53 | .fallbackToDestructiveMigration()
54 | .build()
55 | }
56 |
57 | @JvmStatic
58 | @Singleton
59 | @Provides
60 | fun provideFirebaseFirestore(): FirebaseFirestore {
61 | return FirebaseFirestore.getInstance()
62 | }
63 |
64 |
65 | }
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
--------------------------------------------------------------------------------
/app/src/main/java/com/codingwithmitch/cleannotes/framework/datasource/cache/abstraction/NoteDaoService.kt:
--------------------------------------------------------------------------------
1 | package com.codingwithmitch.cleannotes.framework.datasource.cache.abstraction
2 |
3 | import com.codingwithmitch.cleannotes.business.domain.model.Note
4 | import com.codingwithmitch.cleannotes.framework.datasource.cache.database.NOTE_PAGINATION_PAGE_SIZE
5 |
6 | interface NoteDaoService {
7 |
8 | suspend fun insertNote(note: Note): Long
9 |
10 | suspend fun insertNotes(notes: List): LongArray
11 |
12 | suspend fun searchNoteById(id: String): Note?
13 |
14 | suspend fun updateNote(
15 | primaryKey: String,
16 | title: String,
17 | body: String?,
18 | timestamp: String?
19 | ): Int
20 |
21 | suspend fun deleteNote(primaryKey: String): Int
22 |
23 | suspend fun deleteNotes(notes: List): Int
24 |
25 | suspend fun searchNotes(): List
26 |
27 | suspend fun getAllNotes(): List
28 |
29 | suspend fun searchNotesOrderByDateDESC(
30 | query: String,
31 | page: Int,
32 | pageSize: Int = NOTE_PAGINATION_PAGE_SIZE
33 | ): List
34 |
35 | suspend fun searchNotesOrderByDateASC(
36 | query: String,
37 | page: Int,
38 | pageSize: Int = NOTE_PAGINATION_PAGE_SIZE
39 | ): List
40 |
41 | suspend fun searchNotesOrderByTitleDESC(
42 | query: String,
43 | page: Int,
44 | pageSize: Int = NOTE_PAGINATION_PAGE_SIZE
45 | ): List
46 |
47 | suspend fun searchNotesOrderByTitleASC(
48 | query: String,
49 | page: Int,
50 | pageSize: Int = NOTE_PAGINATION_PAGE_SIZE
51 | ): List
52 |
53 | suspend fun getNumNotes(): Int
54 |
55 | suspend fun returnOrderedQuery(
56 | query: String,
57 | filterAndOrder: String,
58 | page: Int
59 | ): List
60 | }
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
--------------------------------------------------------------------------------
/app/src/main/java/com/codingwithmitch/cleannotes/framework/datasource/cache/database/NoteDatabase.kt:
--------------------------------------------------------------------------------
1 | package com.codingwithmitch.cleannotes.framework.datasource.cache.database
2 |
3 | import androidx.room.Database
4 | import androidx.room.RoomDatabase
5 | import com.codingwithmitch.cleannotes.framework.datasource.cache.model.NoteCacheEntity
6 |
7 | @Database(entities = [NoteCacheEntity::class ], version = 1)
8 | abstract class NoteDatabase: RoomDatabase() {
9 |
10 | abstract fun noteDao(): NoteDao
11 |
12 | companion object{
13 | val DATABASE_NAME: String = "note_db"
14 | }
15 |
16 |
17 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/codingwithmitch/cleannotes/framework/datasource/cache/mappers/CacheMapper.kt:
--------------------------------------------------------------------------------
1 | package com.codingwithmitch.cleannotes.framework.datasource.cache.mappers
2 |
3 | import com.codingwithmitch.cleannotes.business.domain.model.Note
4 | import com.codingwithmitch.cleannotes.business.domain.util.DateUtil
5 | import com.codingwithmitch.cleannotes.business.domain.util.EntityMapper
6 | import com.codingwithmitch.cleannotes.framework.datasource.cache.model.NoteCacheEntity
7 | import javax.inject.Inject
8 |
9 | /**
10 | * Maps Note to NoteCacheEntity or NoteCacheEntity to Note.
11 | */
12 | class CacheMapper
13 | @Inject
14 | constructor(
15 | private val dateUtil: DateUtil
16 | ): EntityMapper
17 | {
18 |
19 | fun entityListToNoteList(entities: List): List{
20 | val list: ArrayList = ArrayList()
21 | for(entity in entities){
22 | list.add(mapFromEntity(entity))
23 | }
24 | return list
25 | }
26 |
27 | fun noteListToEntityList(notes: List): List{
28 | val entities: ArrayList = ArrayList()
29 | for(note in notes){
30 | entities.add(mapToEntity(note))
31 | }
32 | return entities
33 | }
34 |
35 | override fun mapFromEntity(entity: NoteCacheEntity): Note {
36 | return Note(
37 | id = entity.id,
38 | title = entity.title,
39 | body = entity.body,
40 | updated_at = entity.updated_at,
41 | created_at = entity.created_at
42 | )
43 | }
44 |
45 | override fun mapToEntity(domainModel: Note): NoteCacheEntity {
46 | return NoteCacheEntity(
47 | id = domainModel.id,
48 | title = domainModel.title,
49 | body = domainModel.body,
50 | updated_at = domainModel.updated_at,
51 | created_at = domainModel.created_at
52 | )
53 | }
54 | }
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
--------------------------------------------------------------------------------
/app/src/main/java/com/codingwithmitch/cleannotes/framework/datasource/cache/model/NoteCacheEntity.kt:
--------------------------------------------------------------------------------
1 | package com.codingwithmitch.cleannotes.framework.datasource.cache.model
2 |
3 | import androidx.room.ColumnInfo
4 | import androidx.room.Entity
5 | import androidx.room.PrimaryKey
6 |
7 | @Entity(tableName = "notes")
8 | data class NoteCacheEntity(
9 |
10 | @PrimaryKey(autoGenerate = false)
11 | @ColumnInfo(name = "id")
12 | var id: String,
13 |
14 | @ColumnInfo(name = "title")
15 | var title: String,
16 |
17 | @ColumnInfo(name = "body")
18 | var body: String,
19 |
20 | @ColumnInfo(name = "updated_at")
21 | var updated_at: String,
22 |
23 | @ColumnInfo(name = "created_at")
24 | var created_at: String
25 |
26 | ){
27 |
28 |
29 |
30 | companion object{
31 |
32 | fun nullTitleError(): String{
33 | return "You must enter a title."
34 | }
35 |
36 | fun nullIdError(): String{
37 | return "NoteEntity object has a null id. This should not be possible. Check local database."
38 | }
39 | }
40 |
41 | override fun equals(other: Any?): Boolean {
42 | if (this === other) return true
43 | if (javaClass != other?.javaClass) return false
44 |
45 | other as NoteCacheEntity
46 |
47 | if (id != other.id) return false
48 | if (title != other.title) return false
49 | if (body != other.body) return false
50 | // if (updated_at != other.updated_at) return false // ignore this
51 | if (created_at != other.created_at) return false
52 |
53 | return true
54 | }
55 |
56 | override fun hashCode(): Int {
57 | var result = id.hashCode()
58 | result = 31 * result + title.hashCode()
59 | result = 31 * result + body.hashCode()
60 | result = 31 * result + updated_at.hashCode()
61 | result = 31 * result + created_at.hashCode()
62 | return result
63 | }
64 | }
65 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/app/src/main/java/com/codingwithmitch/cleannotes/framework/datasource/network/abstraction/NoteFirestoreService.kt:
--------------------------------------------------------------------------------
1 | package com.codingwithmitch.cleannotes.framework.datasource.network.abstraction
2 |
3 | import com.codingwithmitch.cleannotes.business.domain.model.Note
4 |
5 | interface NoteFirestoreService {
6 |
7 | suspend fun insertOrUpdateNote(note: Note)
8 |
9 | suspend fun insertOrUpdateNotes(notes: List)
10 |
11 | suspend fun deleteNote(primaryKey: String)
12 |
13 | suspend fun insertDeletedNote(note: Note)
14 |
15 | suspend fun insertDeletedNotes(notes: List)
16 |
17 | suspend fun deleteDeletedNote(note: Note)
18 |
19 | suspend fun deleteAllNotes()
20 |
21 | suspend fun getDeletedNotes(): List
22 |
23 | suspend fun searchNote(note: Note): Note?
24 |
25 | suspend fun getAllNotes(): List
26 |
27 |
28 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/codingwithmitch/cleannotes/framework/datasource/network/mappers/NetworkMapper.kt:
--------------------------------------------------------------------------------
1 | package com.codingwithmitch.cleannotes.framework.datasource.network.mappers
2 |
3 | import com.codingwithmitch.cleannotes.business.domain.model.Note
4 | import com.codingwithmitch.cleannotes.business.domain.util.DateUtil
5 | import com.codingwithmitch.cleannotes.business.domain.util.EntityMapper
6 | import com.codingwithmitch.cleannotes.framework.datasource.network.model.NoteNetworkEntity
7 | import javax.inject.Inject
8 |
9 | /**
10 | * Maps Note to NoteNetworkEntity or NoteNetworkEntity to Note.
11 | */
12 | class NetworkMapper
13 | @Inject
14 | constructor(
15 | private val dateUtil: DateUtil
16 | ): EntityMapper
17 | {
18 |
19 | fun entityListToNoteList(entities: List): List{
20 | val list: ArrayList = ArrayList()
21 | for(entity in entities){
22 | list.add(mapFromEntity(entity))
23 | }
24 | return list
25 | }
26 |
27 | fun noteListToEntityList(notes: List): List{
28 | val entities: ArrayList = ArrayList()
29 | for(note in notes){
30 | entities.add(mapToEntity(note))
31 | }
32 | return entities
33 | }
34 |
35 | override fun mapFromEntity(entity: NoteNetworkEntity): Note {
36 | return Note(
37 | id = entity.id,
38 | title = entity.title,
39 | body = entity.body,
40 | updated_at = dateUtil.convertFirebaseTimestampToStringData(entity.updated_at),
41 | created_at = dateUtil.convertFirebaseTimestampToStringData(entity.created_at)
42 | )
43 | }
44 |
45 | override fun mapToEntity(domainModel: Note): NoteNetworkEntity {
46 | return NoteNetworkEntity(
47 | id = domainModel.id,
48 | title = domainModel.title,
49 | body = domainModel.body,
50 | updated_at = dateUtil.convertStringDateToFirebaseTimestamp(domainModel.updated_at),
51 | created_at = dateUtil.convertStringDateToFirebaseTimestamp(domainModel.created_at)
52 | )
53 | }
54 |
55 |
56 | }
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
--------------------------------------------------------------------------------
/app/src/main/java/com/codingwithmitch/cleannotes/framework/datasource/network/model/NoteNetworkEntity.kt:
--------------------------------------------------------------------------------
1 | package com.codingwithmitch.cleannotes.framework.datasource.network.model
2 |
3 | import com.google.firebase.Timestamp
4 |
5 |
6 | data class NoteNetworkEntity(
7 |
8 | var id: String,
9 |
10 | var title: String,
11 |
12 | var body: String,
13 |
14 | var updated_at: Timestamp,
15 |
16 | var created_at: Timestamp
17 |
18 | ){
19 |
20 | // no arg constructor for firestore
21 | constructor(): this(
22 | "",
23 | "",
24 | "",
25 | Timestamp.now(),
26 | Timestamp.now()
27 | )
28 |
29 |
30 |
31 | companion object{
32 |
33 | const val UPDATED_AT_FIELD = "updated_at"
34 | const val TITLE_FIELD = "title"
35 | const val BODY_FIELD = "body"
36 | }
37 |
38 | override fun equals(other: Any?): Boolean {
39 | if (this === other) return true
40 | if (javaClass != other?.javaClass) return false
41 |
42 | other as NoteNetworkEntity
43 |
44 | if (id != other.id) return false
45 | if (title != other.title) return false
46 | if (body != other.body) return false
47 | // if (updated_at != other.updated_at) return false // ignore
48 | if (created_at != other.created_at) return false
49 |
50 | return true
51 | }
52 |
53 | override fun hashCode(): Int {
54 | var result = id.hashCode()
55 | result = 31 * result + title.hashCode()
56 | result = 31 * result + body.hashCode()
57 | result = 31 * result + updated_at.hashCode()
58 | result = 31 * result + created_at.hashCode()
59 | return result
60 | }
61 | }
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
--------------------------------------------------------------------------------
/app/src/main/java/com/codingwithmitch/cleannotes/framework/datasource/preferences/PreferenceKeys.kt:
--------------------------------------------------------------------------------
1 | package com.codingwithmitch.cleannotes.framework.datasource.preferences
2 |
3 | class PreferenceKeys {
4 |
5 | companion object{
6 |
7 | // Shared Preference Files:
8 | const val NOTE_PREFERENCES: String = "com.codingwithmitch.cleannotes.notes"
9 |
10 | // Shared Preference Keys
11 | val NOTE_FILTER: String = "${NOTE_PREFERENCES}.NOTE_FILTER"
12 | val NOTE_ORDER: String = "${NOTE_PREFERENCES}.NOTE_ORDER"
13 |
14 | }
15 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/codingwithmitch/cleannotes/framework/presentation/BaseApplication.kt:
--------------------------------------------------------------------------------
1 | package com.codingwithmitch.cleannotes.framework.presentation
2 |
3 | import android.app.Application
4 | import com.codingwithmitch.cleannotes.di.AppComponent
5 | import com.codingwithmitch.cleannotes.di.DaggerAppComponent
6 | import kotlinx.coroutines.*
7 |
8 | @FlowPreview
9 | @ExperimentalCoroutinesApi
10 | open class BaseApplication : Application(){
11 |
12 | lateinit var appComponent: AppComponent
13 |
14 | override fun onCreate() {
15 | super.onCreate()
16 | initAppComponent()
17 | }
18 |
19 | open fun initAppComponent(){
20 | appComponent = DaggerAppComponent
21 | .factory()
22 | .create(this)
23 | }
24 |
25 |
26 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/codingwithmitch/cleannotes/framework/presentation/UIController.kt:
--------------------------------------------------------------------------------
1 | package com.codingwithmitch.cleannotes.framework.presentation
2 |
3 | import com.codingwithmitch.cleannotes.business.domain.state.DialogInputCaptureCallback
4 | import com.codingwithmitch.cleannotes.business.domain.state.Response
5 | import com.codingwithmitch.cleannotes.business.domain.state.StateMessageCallback
6 |
7 |
8 | interface UIController {
9 |
10 | fun displayProgressBar(isDisplayed: Boolean)
11 |
12 | fun hideSoftKeyboard()
13 |
14 | fun displayInputCaptureDialog(title: String, callback: DialogInputCaptureCallback)
15 |
16 | fun onResponseReceived(
17 | response: Response,
18 | stateMessageCallback: StateMessageCallback
19 | )
20 |
21 | }
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/app/src/main/java/com/codingwithmitch/cleannotes/framework/presentation/common/BaseNoteFragment.kt:
--------------------------------------------------------------------------------
1 | package com.codingwithmitch.cleannotes.framework.presentation.common
2 |
3 | import android.content.Context
4 | import android.os.Bundle
5 | import android.view.LayoutInflater
6 | import android.view.View
7 | import android.view.ViewGroup
8 | import android.widget.TextView
9 | import androidx.annotation.LayoutRes
10 | import androidx.fragment.app.Fragment
11 | import com.codingwithmitch.cleannotes.di.AppComponent
12 | import com.codingwithmitch.cleannotes.framework.presentation.BaseApplication
13 | import com.codingwithmitch.cleannotes.framework.presentation.MainActivity
14 | import com.codingwithmitch.cleannotes.framework.presentation.UIController
15 | import com.codingwithmitch.cleannotes.util.TodoCallback
16 | import kotlinx.coroutines.ExperimentalCoroutinesApi
17 | import kotlinx.coroutines.FlowPreview
18 | import java.lang.ClassCastException
19 |
20 | @FlowPreview
21 | @ExperimentalCoroutinesApi
22 | abstract class BaseNoteFragment
23 | constructor(
24 | private @LayoutRes val layoutRes: Int
25 | ): Fragment() {
26 |
27 | lateinit var uiController: UIController
28 |
29 | override fun onCreateView(
30 | inflater: LayoutInflater,
31 | container: ViewGroup?,
32 | savedInstanceState: Bundle?
33 | ): View? {
34 | return inflater.inflate(layoutRes, container, false)
35 | }
36 |
37 | fun displayToolbarTitle(textView: TextView, title: String?, useAnimation: Boolean) {
38 | if(title != null){
39 | showToolbarTitle(textView, title, useAnimation)
40 | }
41 | else{
42 | hideToolbarTitle(textView, useAnimation)
43 | }
44 | }
45 |
46 | private fun hideToolbarTitle(textView: TextView, animation: Boolean){
47 | if(animation){
48 | textView.fadeOut(
49 | object: TodoCallback {
50 | override fun execute() {
51 | textView.text = ""
52 | }
53 | }
54 | )
55 | }
56 | else{
57 | textView.text = ""
58 | textView.gone()
59 | }
60 | }
61 |
62 | private fun showToolbarTitle(
63 | textView: TextView,
64 | title: String,
65 | animation: Boolean
66 | ){
67 | textView.text = title
68 | if(animation){
69 | textView.fadeIn()
70 | }
71 | else{
72 | textView.visible()
73 | }
74 | }
75 |
76 | abstract fun inject()
77 |
78 | fun getAppComponent(): AppComponent{
79 | return activity?.run {
80 | (application as BaseApplication).appComponent
81 | }?: throw Exception("AppComponent is null.")
82 | }
83 |
84 | override fun onAttach(context: Context) {
85 | inject()
86 | super.onAttach(context)
87 | setUIController(null) // null in production
88 | }
89 |
90 | fun setUIController(mockController: UIController?){
91 |
92 | // TEST: Set interface from mock
93 | if(mockController != null){
94 | this.uiController = mockController
95 | }
96 | else{ // PRODUCTION: if no mock, get from context
97 | activity?.let {
98 | if(it is MainActivity){
99 | try{
100 | uiController = context as UIController
101 | }catch (e: ClassCastException){
102 | e.printStackTrace()
103 | }
104 | }
105 | }
106 | }
107 | }
108 | }
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
--------------------------------------------------------------------------------
/app/src/main/java/com/codingwithmitch/cleannotes/framework/presentation/common/BaseViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.codingwithmitch.cleannotes.framework.presentation.common
2 |
3 | import androidx.lifecycle.*
4 | import com.codingwithmitch.cleannotes.business.domain.state.*
5 | import com.codingwithmitch.cleannotes.business.data.util.GenericErrors
6 | import com.codingwithmitch.cleannotes.util.printLogD
7 | import kotlinx.coroutines.*
8 | import kotlinx.coroutines.flow.Flow
9 | import kotlinx.coroutines.flow.flow
10 |
11 |
12 | @FlowPreview
13 | @ExperimentalCoroutinesApi
14 | abstract class BaseViewModel : ViewModel()
15 | {
16 | private val _viewState: MutableLiveData = MutableLiveData()
17 |
18 | val dataChannelManager: DataChannelManager
19 | = object: DataChannelManager(){
20 |
21 | override fun handleNewData(data: ViewState) {
22 | this@BaseViewModel.handleNewData(data)
23 | }
24 | }
25 |
26 | val viewState: LiveData
27 | get() = _viewState
28 |
29 | val shouldDisplayProgressBar: LiveData
30 | = dataChannelManager.shouldDisplayProgressBar
31 |
32 | val stateMessage: LiveData
33 | get() = dataChannelManager.messageStack.stateMessage
34 |
35 | // FOR DEBUGGING
36 | fun getMessageStackSize(): Int{
37 | return dataChannelManager.messageStack.size
38 | }
39 |
40 | fun setupChannel() = dataChannelManager.setupChannel()
41 |
42 | abstract fun handleNewData(data: ViewState)
43 |
44 | abstract fun setStateEvent(stateEvent: StateEvent)
45 |
46 | fun emitStateMessageEvent(
47 | stateMessage: StateMessage,
48 | stateEvent: StateEvent
49 | ) = flow{
50 | emit(
51 | DataState.error(
52 | response = stateMessage.response,
53 | stateEvent = stateEvent
54 | )
55 | )
56 | }
57 |
58 | fun emitInvalidStateEvent(stateEvent: StateEvent) = flow {
59 | emit(
60 | DataState.error(
61 | response = Response(
62 | message = GenericErrors.INVALID_STATE_EVENT,
63 | uiComponentType = UIComponentType.None(),
64 | messageType = MessageType.Error()
65 | ),
66 | stateEvent = stateEvent
67 | )
68 | )
69 | }
70 |
71 | fun launchJob(
72 | stateEvent: StateEvent,
73 | jobFunction: Flow?>
74 | ) = dataChannelManager.launchJob(stateEvent, jobFunction)
75 |
76 | fun getCurrentViewStateOrNew(): ViewState{
77 | return viewState.value ?: initNewViewState()
78 | }
79 |
80 | fun setViewState(viewState: ViewState){
81 | _viewState.value = viewState
82 | }
83 |
84 | fun clearStateMessage(index: Int = 0){
85 | printLogD("BaseViewModel", "clearStateMessage")
86 | dataChannelManager.clearStateMessage(index)
87 | }
88 |
89 | fun clearActiveStateEvents() = dataChannelManager.clearActiveStateEventCounter()
90 |
91 | fun clearAllStateMessages() = dataChannelManager.clearAllStateMessages()
92 |
93 | fun printStateMessages() = dataChannelManager.printStateMessages()
94 |
95 | fun cancelActiveJobs() = dataChannelManager.cancelJobs()
96 |
97 | abstract fun initNewViewState(): ViewState
98 |
99 | }
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
--------------------------------------------------------------------------------
/app/src/main/java/com/codingwithmitch/cleannotes/framework/presentation/common/KeyboardExtensions.kt:
--------------------------------------------------------------------------------
1 | package com.codingwithmitch.cleannotes.framework.presentation.common
2 |
3 | import android.app.Activity
4 | import android.content.Context
5 | import android.view.View
6 | import android.view.inputmethod.InputMethodManager
7 |
8 | // Author: https://github.com/sanogueralorenzo/Android-Kotlin-Clean-Architecture
9 | /**
10 | * Use only from Activities, don't use from Fragment (with getActivity) or from Dialog/DialogFragment
11 | */
12 | fun Activity.hideKeyboard() {
13 | val imm = getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager
14 | val view = currentFocus ?: View(this)
15 | imm.hideSoftInputFromWindow(view.windowToken, 0)
16 | window.decorView
17 | }
18 |
19 | fun View.showKeyboard() {
20 | val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
21 | imm.toggleSoftInput(InputMethodManager.SHOW_FORCED, InputMethodManager.HIDE_IMPLICIT_ONLY)
22 | }
23 |
24 | /**
25 | * Use everywhere except from Activity (Custom View, Fragment, Dialogs, DialogFragments).
26 | */
27 | fun View.hideKeyboard() {
28 | val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
29 | imm.hideSoftInputFromWindow(windowToken, 0)
30 | }
31 |
--------------------------------------------------------------------------------
/app/src/main/java/com/codingwithmitch/cleannotes/framework/presentation/common/NoteFragmentFactory.kt:
--------------------------------------------------------------------------------
1 | package com.codingwithmitch.cleannotes.framework.presentation.common
2 |
3 | import androidx.fragment.app.FragmentFactory
4 | import androidx.lifecycle.ViewModelProvider
5 | import com.codingwithmitch.cleannotes.business.domain.util.DateUtil
6 | import com.codingwithmitch.cleannotes.framework.presentation.notedetail.NoteDetailFragment
7 | import com.codingwithmitch.cleannotes.framework.presentation.notelist.NoteListFragment
8 | import com.codingwithmitch.cleannotes.framework.presentation.splash.SplashFragment
9 | import kotlinx.coroutines.ExperimentalCoroutinesApi
10 | import kotlinx.coroutines.FlowPreview
11 | import javax.inject.Inject
12 |
13 | @ExperimentalCoroutinesApi
14 | @FlowPreview
15 | class NoteFragmentFactory
16 | @Inject
17 | constructor(
18 | private val viewModelFactory: ViewModelProvider.Factory,
19 | private val dateUtil: DateUtil
20 | ): FragmentFactory(){
21 |
22 | override fun instantiate(classLoader: ClassLoader, className: String) =
23 |
24 | when(className){
25 |
26 | NoteListFragment::class.java.name -> {
27 | val fragment = NoteListFragment(viewModelFactory, dateUtil)
28 | fragment
29 | }
30 |
31 | NoteDetailFragment::class.java.name -> {
32 | val fragment = NoteDetailFragment(viewModelFactory)
33 | fragment
34 | }
35 |
36 | SplashFragment::class.java.name -> {
37 | val fragment = SplashFragment(viewModelFactory)
38 | fragment
39 | }
40 |
41 | else -> {
42 | super.instantiate(classLoader, className)
43 | }
44 | }
45 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/codingwithmitch/cleannotes/framework/presentation/common/NoteViewModelFactory.kt:
--------------------------------------------------------------------------------
1 | package com.codingwithmitch.cleannotes.framework.presentation.common
2 |
3 | import android.content.SharedPreferences
4 | import androidx.lifecycle.ViewModel
5 | import androidx.lifecycle.ViewModelProvider
6 | import com.codingwithmitch.cleannotes.business.domain.model.NoteFactory
7 | import com.codingwithmitch.cleannotes.business.interactors.notedetail.NoteDetailInteractors
8 | import com.codingwithmitch.cleannotes.business.interactors.notelist.NoteListInteractors
9 | import com.codingwithmitch.cleannotes.framework.presentation.notedetail.NoteDetailViewModel
10 | import com.codingwithmitch.cleannotes.framework.presentation.notelist.NoteListViewModel
11 | import com.codingwithmitch.cleannotes.framework.presentation.splash.NoteNetworkSyncManager
12 | import com.codingwithmitch.cleannotes.framework.presentation.splash.SplashViewModel
13 | import kotlinx.coroutines.ExperimentalCoroutinesApi
14 | import kotlinx.coroutines.FlowPreview
15 | import javax.inject.Inject
16 | import javax.inject.Singleton
17 |
18 |
19 | @FlowPreview
20 | @ExperimentalCoroutinesApi
21 | class NoteViewModelFactory
22 | constructor(
23 | private val noteListInteractors: NoteListInteractors,
24 | private val noteDetailInteractors: NoteDetailInteractors,
25 | private val noteNetworkSyncManager: NoteNetworkSyncManager,
26 | private val noteFactory: NoteFactory,
27 | private val editor: SharedPreferences.Editor,
28 | private val sharedPreferences: SharedPreferences
29 | ) : ViewModelProvider.Factory {
30 |
31 | override fun create(modelClass: Class): T {
32 | return when(modelClass){
33 |
34 | NoteListViewModel::class.java -> {
35 | NoteListViewModel(
36 | noteInteractors = noteListInteractors,
37 | noteFactory = noteFactory,
38 | editor = editor,
39 | sharedPreferences = sharedPreferences
40 | ) as T
41 | }
42 |
43 | NoteDetailViewModel::class.java -> {
44 | NoteDetailViewModel(
45 | noteInteractors = noteDetailInteractors
46 | ) as T
47 | }
48 |
49 | SplashViewModel::class.java -> {
50 | SplashViewModel(
51 | noteNetworkSyncManager = noteNetworkSyncManager
52 | ) as T
53 | }
54 |
55 | else -> {
56 | throw IllegalArgumentException("unknown model class $modelClass")
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 |
--------------------------------------------------------------------------------
/app/src/main/java/com/codingwithmitch/cleannotes/framework/presentation/common/TopSpacingItemDecoration.kt:
--------------------------------------------------------------------------------
1 | package com.codingwithmitch.cleannotes.framework.presentation.common
2 |
3 | import androidx.recyclerview.widget.RecyclerView
4 | import android.graphics.Rect
5 | import android.view.View
6 |
7 |
8 | class TopSpacingItemDecoration(private val padding: Int) : RecyclerView.ItemDecoration() {
9 |
10 | override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
11 | super.getItemOffsets(outRect, view, parent, state)
12 | outRect.top = padding
13 | }
14 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/codingwithmitch/cleannotes/framework/presentation/notedetail/state/CollapsingToolbarState.kt:
--------------------------------------------------------------------------------
1 | package com.codingwithmitch.cleannotes.framework.presentation.notedetail.state
2 |
3 |
4 | sealed class CollapsingToolbarState{
5 |
6 | class Collapsed: CollapsingToolbarState(){
7 |
8 | override fun toString(): String {
9 | return "Collapsed"
10 | }
11 | }
12 |
13 | class Expanded: CollapsingToolbarState(){
14 |
15 | override fun toString(): String {
16 | return "Expanded"
17 | }
18 | }
19 | }
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/app/src/main/java/com/codingwithmitch/cleannotes/framework/presentation/notedetail/state/NoteDetailStateEvent.kt:
--------------------------------------------------------------------------------
1 | package com.codingwithmitch.cleannotes.framework.presentation.notedetail.state
2 |
3 | import com.codingwithmitch.cleannotes.business.domain.model.Note
4 | import com.codingwithmitch.cleannotes.business.domain.state.StateEvent
5 | import com.codingwithmitch.cleannotes.business.domain.state.StateMessage
6 |
7 |
8 | sealed class NoteDetailStateEvent: StateEvent {
9 |
10 |
11 | class UpdateNoteEvent: NoteDetailStateEvent(){
12 |
13 | override fun errorInfo(): String {
14 | return "Error updating note."
15 | }
16 |
17 | override fun eventName(): String {
18 | return "UpdateNoteEvent"
19 | }
20 |
21 | override fun shouldDisplayProgressBar() = true
22 | }
23 |
24 | class DeleteNoteEvent(
25 | val note: Note
26 | ): NoteDetailStateEvent(){
27 |
28 | override fun errorInfo(): String {
29 | return "Error deleting note."
30 | }
31 |
32 | override fun eventName(): String {
33 | return "DeleteNoteEvent"
34 | }
35 |
36 | override fun shouldDisplayProgressBar() = true
37 | }
38 |
39 | class CreateStateMessageEvent(
40 | val stateMessage: StateMessage
41 | ): NoteDetailStateEvent(){
42 |
43 | override fun errorInfo(): String {
44 | return "Error creating a new state message."
45 | }
46 |
47 | override fun eventName(): String {
48 | return "CreateStateMessageEvent"
49 | }
50 |
51 | override fun shouldDisplayProgressBar() = false
52 | }
53 |
54 | }
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
--------------------------------------------------------------------------------
/app/src/main/java/com/codingwithmitch/cleannotes/framework/presentation/notedetail/state/NoteDetailViewState.kt:
--------------------------------------------------------------------------------
1 | package com.codingwithmitch.cleannotes.framework.presentation.notedetail.state
2 |
3 | import android.os.Parcelable
4 | import com.codingwithmitch.cleannotes.business.domain.model.Note
5 | import com.codingwithmitch.cleannotes.business.domain.state.ViewState
6 | import kotlinx.android.parcel.Parcelize
7 |
8 | @Parcelize
9 | data class NoteDetailViewState(
10 |
11 | var note: Note? = null,
12 |
13 | var isUpdatePending: Boolean? = null
14 |
15 | ) : Parcelable, ViewState
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/app/src/main/java/com/codingwithmitch/cleannotes/framework/presentation/notedetail/state/NoteInteractionManager.kt:
--------------------------------------------------------------------------------
1 | package com.codingwithmitch.cleannotes.framework.presentation.notedetail.state
2 |
3 | import androidx.lifecycle.LiveData
4 | import androidx.lifecycle.MutableLiveData
5 | import com.codingwithmitch.cleannotes.framework.presentation.notedetail.state.CollapsingToolbarState.*
6 | import com.codingwithmitch.cleannotes.framework.presentation.notedetail.state.NoteInteractionState.*
7 |
8 | // Both can not be in 'EditState' at the same time.
9 | class NoteInteractionManager{
10 |
11 | private val _noteTitleState: MutableLiveData
12 | = MutableLiveData(DefaultState())
13 |
14 | private val _noteBodyState: MutableLiveData
15 | = MutableLiveData(DefaultState())
16 |
17 | private val _collapsingToolbarState: MutableLiveData
18 | = MutableLiveData(Expanded())
19 |
20 | val noteTitleState: LiveData
21 | get() = _noteTitleState
22 |
23 | val noteBodyState: LiveData
24 | get() = _noteBodyState
25 |
26 | val collapsingToolbarState: LiveData
27 | get() = _collapsingToolbarState
28 |
29 | fun setCollapsingToolbarState(state: CollapsingToolbarState){
30 | if(!state.toString().equals(_collapsingToolbarState.value.toString())){
31 | _collapsingToolbarState.value = state
32 | }
33 | }
34 |
35 | fun setNewNoteTitleState(state: NoteInteractionState){
36 | if(!noteTitleState.toString().equals(state.toString())){
37 | _noteTitleState.value = state
38 | when(state){
39 |
40 | is EditState -> {
41 | _noteBodyState.value = DefaultState()
42 | }
43 | }
44 | }
45 | }
46 |
47 | fun setNewNoteBodyState(state: NoteInteractionState){
48 | if(!noteBodyState.toString().equals(state.toString())){
49 | _noteBodyState.value = state
50 | when(state){
51 |
52 | is EditState -> {
53 | _noteTitleState.value = DefaultState()
54 | }
55 | }
56 | }
57 | }
58 |
59 | fun isEditingTitle() = noteTitleState.value.toString().equals(EditState().toString())
60 |
61 | fun isEditingBody() = noteBodyState.value.toString().equals(EditState().toString())
62 |
63 | fun exitEditState(){
64 | _noteTitleState.value = DefaultState()
65 | _noteBodyState.value = DefaultState()
66 | }
67 |
68 | // return true if either title or body are in EditState
69 | fun checkEditState() = noteTitleState.value.toString().equals(EditState().toString())
70 | || noteBodyState.value.toString().equals(EditState().toString())
71 |
72 |
73 |
74 | }
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
--------------------------------------------------------------------------------
/app/src/main/java/com/codingwithmitch/cleannotes/framework/presentation/notedetail/state/NoteInteractionState.kt:
--------------------------------------------------------------------------------
1 | package com.codingwithmitch.cleannotes.framework.presentation.notedetail.state
2 |
3 |
4 |
5 | sealed class NoteInteractionState {
6 |
7 | class EditState: NoteInteractionState() {
8 |
9 | override fun toString(): String {
10 | return "EditState"
11 | }
12 | }
13 |
14 | class DefaultState: NoteInteractionState(){
15 |
16 | override fun toString(): String {
17 | return "DefaultState"
18 | }
19 | }
20 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/codingwithmitch/cleannotes/framework/presentation/notelist/NoteItemTouchHelperCallback.kt:
--------------------------------------------------------------------------------
1 | package com.codingwithmitch.cleannotes.framework.presentation.notelist
2 |
3 | import androidx.recyclerview.widget.ItemTouchHelper
4 | import androidx.recyclerview.widget.RecyclerView
5 | import com.codingwithmitch.cleannotes.framework.presentation.notelist.state.NoteListInteractionManager
6 | import kotlinx.coroutines.ExperimentalCoroutinesApi
7 | import kotlinx.coroutines.FlowPreview
8 |
9 | @ExperimentalCoroutinesApi
10 | @FlowPreview
11 | class NoteItemTouchHelperCallback
12 | constructor(
13 | private val itemTouchHelperAdapter: ItemTouchHelperAdapter,
14 | private val noteListInteractionManager: NoteListInteractionManager
15 | ): ItemTouchHelper.Callback() {
16 |
17 | override fun getMovementFlags(
18 | recyclerView: RecyclerView,
19 | viewHolder: RecyclerView.ViewHolder
20 | ): Int {
21 | return makeMovementFlags(
22 | ItemTouchHelper.ACTION_STATE_IDLE,
23 | ItemTouchHelper.START or ItemTouchHelper.END
24 | )
25 | }
26 |
27 | override fun onMove(
28 | recyclerView: RecyclerView,
29 | viewHolder: RecyclerView.ViewHolder,
30 | target: RecyclerView.ViewHolder
31 | ): Boolean {
32 | return true
33 | }
34 |
35 | override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
36 | itemTouchHelperAdapter.onItemSwiped(viewHolder.adapterPosition)
37 | }
38 |
39 | override fun isLongPressDragEnabled(): Boolean {
40 | return false
41 | }
42 |
43 | override fun isItemViewSwipeEnabled(): Boolean {
44 | return !noteListInteractionManager.isMultiSelectionStateActive()
45 | }
46 |
47 | }
48 |
49 |
50 | interface ItemTouchHelperAdapter{
51 |
52 | fun onItemSwiped(position: Int)
53 | }
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
--------------------------------------------------------------------------------
/app/src/main/java/com/codingwithmitch/cleannotes/framework/presentation/notelist/state/NoteListInteractionManager.kt:
--------------------------------------------------------------------------------
1 | package com.codingwithmitch.cleannotes.framework.presentation.notelist.state
2 |
3 | import androidx.lifecycle.LiveData
4 | import androidx.lifecycle.MutableLiveData
5 | import com.codingwithmitch.cleannotes.business.domain.model.Note
6 | import com.codingwithmitch.cleannotes.framework.presentation.notelist.state.NoteListToolbarState.*
7 |
8 | class NoteListInteractionManager {
9 |
10 | private val _selectedNotes: MutableLiveData> = MutableLiveData()
11 |
12 | private val _toolbarState: MutableLiveData
13 | = MutableLiveData(SearchViewState())
14 |
15 | val selectedNotes: LiveData>
16 | get() = _selectedNotes
17 |
18 | val toolbarState: LiveData
19 | get() = _toolbarState
20 |
21 | fun setToolbarState(state: NoteListToolbarState){
22 | _toolbarState.value = state
23 | }
24 |
25 | fun getSelectedNotes():ArrayList = _selectedNotes.value?: ArrayList()
26 |
27 | fun isMultiSelectionStateActive(): Boolean{
28 | return _toolbarState.value.toString() == MultiSelectionState().toString()
29 | }
30 |
31 | fun addOrRemoveNoteFromSelectedList(note: Note){
32 | var list = _selectedNotes.value
33 | if(list == null){
34 | list = ArrayList()
35 | }
36 | if (list.contains(note)){
37 | list.remove(note)
38 | }
39 | else{
40 | list.add(note)
41 | }
42 | _selectedNotes.value = list
43 | }
44 |
45 | fun isNoteSelected(note: Note): Boolean{
46 | return _selectedNotes.value?.contains(note)?: false
47 | }
48 |
49 | fun clearSelectedNotes(){
50 | _selectedNotes.value = null
51 | }
52 |
53 | }
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
--------------------------------------------------------------------------------
/app/src/main/java/com/codingwithmitch/cleannotes/framework/presentation/notelist/state/NoteListToolbarState.kt:
--------------------------------------------------------------------------------
1 | package com.codingwithmitch.cleannotes.framework.presentation.notelist.state
2 |
3 | sealed class NoteListToolbarState {
4 |
5 | class MultiSelectionState: NoteListToolbarState(){
6 |
7 | override fun toString(): String {
8 | return "MultiSelectionState"
9 | }
10 | }
11 |
12 | class SearchViewState: NoteListToolbarState(){
13 |
14 | override fun toString(): String {
15 | return "SearchViewState"
16 | }
17 | }
18 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/codingwithmitch/cleannotes/framework/presentation/notelist/state/NoteListViewState.kt:
--------------------------------------------------------------------------------
1 | package com.codingwithmitch.cleannotes.framework.presentation.notelist.state
2 |
3 | import android.os.Parcelable
4 | import com.codingwithmitch.cleannotes.business.domain.model.Note
5 | import com.codingwithmitch.cleannotes.business.domain.state.ViewState
6 | import kotlinx.android.parcel.Parcelize
7 |
8 | @Parcelize
9 | data class NoteListViewState(
10 |
11 | var noteList: ArrayList? = null,
12 | var newNote: Note? = null, // note that can be created with fab
13 | var notePendingDelete: NotePendingDelete? = null, // set when delete is pending (can be undone)
14 | var searchQuery: String? = null,
15 | var page: Int? = null,
16 | var isQueryExhausted: Boolean? = null,
17 | var filter: String? = null,
18 | var order: String? = null,
19 | var layoutManagerState: Parcelable? = null,
20 | var numNotesInCache: Int? = null
21 |
22 | ) : Parcelable, ViewState {
23 |
24 | @Parcelize
25 | data class NotePendingDelete(
26 | var note: Note? = null,
27 | var listPosition: Int? = null
28 | ) : Parcelable
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 |
--------------------------------------------------------------------------------
/app/src/main/java/com/codingwithmitch/cleannotes/framework/presentation/splash/NoteNetworkSyncManager.kt:
--------------------------------------------------------------------------------
1 | package com.codingwithmitch.cleannotes.framework.presentation.splash
2 |
3 | import androidx.lifecycle.LiveData
4 | import androidx.lifecycle.MutableLiveData
5 | import com.codingwithmitch.cleannotes.business.interactors.splash.SyncDeletedNotes
6 | import com.codingwithmitch.cleannotes.business.interactors.splash.SyncNotes
7 | import com.codingwithmitch.cleannotes.util.printLogD
8 | import kotlinx.coroutines.CoroutineScope
9 | import kotlinx.coroutines.Dispatchers.Main
10 | import kotlinx.coroutines.launch
11 | import javax.inject.Inject
12 | import javax.inject.Singleton
13 |
14 | @Singleton
15 | class NoteNetworkSyncManager
16 | @Inject
17 | constructor(
18 | private val syncNotes: SyncNotes,
19 | private val syncDeletedNotes: SyncDeletedNotes
20 | ){
21 |
22 | private val _hasSyncBeenExecuted: MutableLiveData = MutableLiveData(false)
23 |
24 | val hasSyncBeenExecuted: LiveData
25 | get() = _hasSyncBeenExecuted
26 |
27 | fun executeDataSync(coroutineScope: CoroutineScope){
28 | if(_hasSyncBeenExecuted.value!!){
29 | return
30 | }
31 |
32 | val syncJob = coroutineScope.launch {
33 | val deletesJob = launch {
34 | printLogD("SyncNotes",
35 | "syncing deleted notes.")
36 | syncDeletedNotes.syncDeletedNotes()
37 | }
38 | deletesJob.join()
39 |
40 | launch {
41 | printLogD("SyncNotes",
42 | "syncing notes.")
43 | syncNotes.syncNotes()
44 | }
45 | }
46 | syncJob.invokeOnCompletion {
47 | CoroutineScope(Main).launch{
48 | _hasSyncBeenExecuted.value = true
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 |
--------------------------------------------------------------------------------
/app/src/main/java/com/codingwithmitch/cleannotes/framework/presentation/splash/SplashFragment.kt:
--------------------------------------------------------------------------------
1 | package com.codingwithmitch.cleannotes.framework.presentation.splash
2 |
3 | import android.os.Bundle
4 | import android.view.View
5 | import androidx.fragment.app.viewModels
6 | import androidx.lifecycle.Observer
7 | import androidx.lifecycle.ViewModelProvider
8 | import androidx.navigation.fragment.findNavController
9 | import com.codingwithmitch.cleannotes.R
10 | import com.codingwithmitch.cleannotes.business.domain.state.DialogInputCaptureCallback
11 | import com.codingwithmitch.cleannotes.framework.datasource.network.implementation.NoteFirestoreServiceImpl.Companion.EMAIL
12 | import com.codingwithmitch.cleannotes.framework.presentation.common.BaseNoteFragment
13 | import com.codingwithmitch.cleannotes.util.printLogD
14 | import com.google.firebase.auth.FirebaseAuth
15 | import kotlinx.coroutines.ExperimentalCoroutinesApi
16 | import kotlinx.coroutines.FlowPreview
17 | import javax.inject.Inject
18 | import javax.inject.Singleton
19 |
20 | @FlowPreview
21 | @ExperimentalCoroutinesApi
22 | @Singleton
23 | class SplashFragment
24 | @Inject
25 | constructor(
26 | private val viewModelFactory: ViewModelProvider.Factory
27 | ): BaseNoteFragment(R.layout.fragment_splash) {
28 |
29 | val viewModel: SplashViewModel by viewModels {
30 | viewModelFactory
31 | }
32 |
33 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
34 | super.onViewCreated(view, savedInstanceState)
35 | checkFirebaseAuth()
36 | }
37 |
38 | private fun checkFirebaseAuth(){
39 | if(FirebaseAuth.getInstance().currentUser == null){
40 | displayCapturePassword()
41 | }
42 | else{
43 | subscribeObservers()
44 | }
45 | }
46 |
47 | // add password input b/c someone used my firestore and deleted the data
48 | private fun displayCapturePassword(){
49 | uiController.displayInputCaptureDialog(
50 | getString(R.string.text_enter_password),
51 | object: DialogInputCaptureCallback {
52 | override fun onTextCaptured(text: String) {
53 | FirebaseAuth.getInstance()
54 | .signInWithEmailAndPassword(EMAIL, text)
55 | .addOnCompleteListener {
56 | if(it.isSuccessful){
57 | printLogD("MainActivity",
58 | "Signing in to Firebase: ${it.result}")
59 | subscribeObservers()
60 | }
61 | else{
62 | printLogD("MainActivity", "cannot log in")
63 | }
64 | }
65 | }
66 | }
67 | )
68 | }
69 |
70 | private fun subscribeObservers(){
71 | viewModel.hasSyncBeenExecuted().observe(viewLifecycleOwner, Observer { hasSyncBeenExecuted ->
72 |
73 | if(hasSyncBeenExecuted){
74 | navNoteListFragment()
75 | }
76 | })
77 | }
78 |
79 | private fun navNoteListFragment(){
80 | findNavController().navigate(R.id.action_splashFragment_to_noteListFragment)
81 | }
82 |
83 | override fun inject() {
84 | getAppComponent().inject(this)
85 | }
86 |
87 | }
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
--------------------------------------------------------------------------------
/app/src/main/java/com/codingwithmitch/cleannotes/framework/presentation/splash/SplashViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.codingwithmitch.cleannotes.framework.presentation.splash
2 |
3 | import androidx.lifecycle.LiveData
4 | import androidx.lifecycle.ViewModel
5 | import androidx.lifecycle.asLiveData
6 | import androidx.lifecycle.viewModelScope
7 | import kotlinx.coroutines.flow.Flow
8 | import kotlinx.coroutines.flow.combine
9 | import kotlinx.coroutines.flow.flow
10 | import javax.inject.Inject
11 | import javax.inject.Singleton
12 |
13 | @Singleton
14 | class SplashViewModel
15 | @Inject
16 | constructor(
17 | private val noteNetworkSyncManager: NoteNetworkSyncManager
18 | ): ViewModel(){
19 |
20 | init {
21 | syncCacheWithNetwork()
22 | }
23 |
24 | fun hasSyncBeenExecuted() = noteNetworkSyncManager.hasSyncBeenExecuted
25 |
26 | private fun syncCacheWithNetwork(){
27 | noteNetworkSyncManager.executeDataSync(viewModelScope)
28 | }
29 |
30 | }
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/app/src/main/java/com/codingwithmitch/cleannotes/util/AndroidTestUtils.kt:
--------------------------------------------------------------------------------
1 | package com.codingwithmitch.cleannotes.util
2 |
3 | import javax.inject.Inject
4 | import javax.inject.Singleton
5 |
6 | @Singleton
7 | class AndroidTestUtils
8 | @Inject
9 | constructor(
10 | private val isTest: Boolean
11 | ){
12 | fun isTest() = isTest
13 |
14 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/codingwithmitch/cleannotes/util/Constants.kt:
--------------------------------------------------------------------------------
1 | package com.codingwithmitch.cleannotes.util
2 |
3 | object Constants {
4 |
5 | const val TAG = "AppDebug" // Tag for logs
6 | const val DEBUG = true // enable logging
7 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/codingwithmitch/cleannotes/util/Logger.kt:
--------------------------------------------------------------------------------
1 | package com.codingwithmitch.cleannotes.util
2 |
3 | import android.util.Log
4 | import com.codingwithmitch.cleannotes.util.Constants.DEBUG
5 | import com.codingwithmitch.cleannotes.util.Constants.TAG
6 | import com.google.firebase.crashlytics.FirebaseCrashlytics
7 |
8 | var isUnitTest = false
9 |
10 | fun printLogD(className: String?, message: String ) {
11 | if (DEBUG && !isUnitTest) {
12 | Log.d(TAG, "$className: $message")
13 | }
14 | else if(DEBUG && isUnitTest){
15 | println("$className: $message")
16 | }
17 | }
18 |
19 | /*
20 | Priorities: Log.DEBUG, Log. etc....
21 | */
22 | fun cLog(msg: String?){
23 | msg?.let {
24 | if(!DEBUG){
25 | FirebaseCrashlytics.getInstance().log(it)
26 | }
27 | }
28 |
29 | }
30 |
--------------------------------------------------------------------------------
/app/src/main/java/com/codingwithmitch/cleannotes/util/TodoCallback.kt:
--------------------------------------------------------------------------------
1 | package com.codingwithmitch.cleannotes.util
2 |
3 | // simple callback to execute something after a function is called
4 | interface TodoCallback {
5 |
6 | fun execute()
7 | }
--------------------------------------------------------------------------------
/app/src/main/res/anim/fade_in.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/anim/fade_out.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/anim/slide_in_left.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/anim/slide_in_right.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/anim/slide_out_left.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/anim/slide_out_right.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/color/bottom_nav_selector.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
12 |
13 |
19 |
22 |
25 |
26 |
27 |
28 |
34 |
35 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_add_white_24dp.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_arrow_back_grey_24dp.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_close_grey_24dp.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_date_range_black_24dp.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_delete.xml:
--------------------------------------------------------------------------------
1 |
8 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_done_grey_24dp.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_event_note_white_24dp.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_filter_list_grey_24dp.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_folder_white_24dp.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/list_item_selector.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/logo_250_250.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mitchtabian/Clean-Notes/ea8f6c95c57685aed42b3b1286aecb33cc2bbf77/app/src/main/res/drawable/logo_250_250.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/red_button_drawable.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | -
5 |
6 |
7 |
9 |
12 |
13 |
14 |
15 | -
16 |
17 |
18 |
20 |
21 |
22 |
23 |
24 | -
25 |
26 |
27 |
29 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
22 |
23 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_note_list.xml:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
20 |
21 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
43 |
44 |
49 |
50 |
51 |
52 |
53 |
54 |
68 |
69 |
70 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_splash.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
20 |
21 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/layout_multiselection_toolbar.xml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
13 |
14 |
23 |
24 |
41 |
42 |
43 |
53 |
54 |
55 |
56 |
57 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/layout_note_detail_toolbar.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
10 |
18 |
19 |
36 |
37 |
38 |
47 |
48 |
49 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/layout_note_list_item.xml:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
18 |
19 |
33 |
34 |
35 |
36 |
48 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/layout_searchview_toolbar.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
14 |
15 |
27 |
28 |
40 |
41 |
42 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mitchtabian/Clean-Notes/ea8f6c95c57685aed42b3b1286aecb33cc2bbf77/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mitchtabian/Clean-Notes/ea8f6c95c57685aed42b3b1286aecb33cc2bbf77/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mitchtabian/Clean-Notes/ea8f6c95c57685aed42b3b1286aecb33cc2bbf77/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mitchtabian/Clean-Notes/ea8f6c95c57685aed42b3b1286aecb33cc2bbf77/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mitchtabian/Clean-Notes/ea8f6c95c57685aed42b3b1286aecb33cc2bbf77/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mitchtabian/Clean-Notes/ea8f6c95c57685aed42b3b1286aecb33cc2bbf77/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mitchtabian/Clean-Notes/ea8f6c95c57685aed42b3b1286aecb33cc2bbf77/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mitchtabian/Clean-Notes/ea8f6c95c57685aed42b3b1286aecb33cc2bbf77/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mitchtabian/Clean-Notes/ea8f6c95c57685aed42b3b1286aecb33cc2bbf77/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mitchtabian/Clean-Notes/ea8f6c95c57685aed42b3b1286aecb33cc2bbf77/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/navigation/nav_app_graph.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
10 |
11 |
21 |
22 |
23 |
27 |
34 |
35 |
36 |
40 |
41 |
51 |
52 |
53 |
54 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | #fff
6 | #7d7d7d
7 | #0094DE
8 |
9 |
10 | #0094DE
11 | #9FDAF7
12 | #0000EE
13 | #f2f2f2
14 |
15 |
16 | #e22b2b
17 | #fc8b8d
18 | #f2f2f2
19 | #c4c4c4
20 | #a8a8a8
21 | #7d7d7d
22 | #5c5c5c
23 |
24 | #686868
25 | #EEEEEE
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/app/src/main/res/values/dimen.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | 20sp
6 | 11sp
7 | 21sp
8 |
9 |
10 | 55dp
11 | 120dp
12 |
13 | 65dp
14 |
15 | 16dp
16 | 16dp
17 | -16dp
18 | 24dp
19 | 32dp
20 | 7dp
21 | 5dp
22 |
23 | 80dp
24 |
25 | 80dp
26 |
27 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Clean Notes
3 | DynamicFeature
4 |
5 | Notes module
6 | Reminders module
7 | Settings module
8 |
9 |
10 | notes
11 | settings
12 | reminders
13 |
14 | Error
15 | Success
16 | OK
17 | Cancel
18 | Info
19 | Are you sure?
20 | Yes
21 | Enter a title
22 | Undo
23 |
24 |
25 | Notes go here
26 | Due Date...
27 | Note title can not be empty.
28 | Edit Body
29 | Edit Title
30 | Delete
31 | Filter
32 | Search
33 |
34 | Filter options
35 | ASC
36 | DESC
37 | Title
38 | Date
39 | Filter by title or date
40 | Ascending or descending order
41 | Apply
42 | Enter password
43 |
44 |
45 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/app/src/release/java/com/codingwithmitch/cleannotes/util/EspressoIdlingResource.kt:
--------------------------------------------------------------------------------
1 | package com.codingwithmitch.cleannotes.util
2 |
3 | object EspressoIdlingResource {
4 |
5 | fun increment() {
6 | }
7 |
8 | fun decrement() {
9 | }
10 |
11 | fun clear(){
12 |
13 | }
14 | }
--------------------------------------------------------------------------------
/app/src/test/java/com/codingwithmitch/cleannotes/business/data/NoteDataFactory.kt:
--------------------------------------------------------------------------------
1 | package com.codingwithmitch.cleannotes.business.data
2 |
3 | import com.codingwithmitch.cleannotes.business.domain.model.Note
4 | import com.google.common.reflect.TypeToken
5 | import com.google.gson.Gson
6 |
7 | class NoteDataFactory(
8 | private val testClassLoader: ClassLoader
9 | ) {
10 |
11 | fun produceListOfNotes(): List{
12 | val notes: List = Gson()
13 | .fromJson(
14 | getNotesFromFile("note_list.json"),
15 | object: TypeToken>() {}.type
16 | )
17 | return notes
18 | }
19 |
20 | fun produceHashMapOfNotes(noteList: List): HashMap{
21 | val map = HashMap()
22 | for(note in noteList){
23 | map.put(note.id, note)
24 | }
25 | return map
26 | }
27 |
28 | fun produceEmptyListOfNotes(): List{
29 | return ArrayList()
30 | }
31 |
32 | fun getNotesFromFile(fileName: String): String {
33 | return testClassLoader.getResource(fileName).readText()
34 | }
35 |
36 | }
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
--------------------------------------------------------------------------------
/app/src/test/java/com/codingwithmitch/cleannotes/business/data/network/FakeNoteNetworkDataSourceImpl.kt:
--------------------------------------------------------------------------------
1 | package com.codingwithmitch.cleannotes.business.data.network
2 |
3 | import com.codingwithmitch.cleannotes.business.data.network.abstraction.NoteNetworkDataSource
4 | import com.codingwithmitch.cleannotes.business.domain.model.Note
5 | import com.codingwithmitch.cleannotes.business.domain.util.DateUtil
6 |
7 | class FakeNoteNetworkDataSourceImpl
8 | constructor(
9 | private val notesData: HashMap,
10 | private val deletedNotesData: HashMap,
11 | private val dateUtil: DateUtil
12 | ) : NoteNetworkDataSource{
13 |
14 | override suspend fun insertOrUpdateNote(note: Note) {
15 | val n = Note(
16 | id = note.id,
17 | title = note.title,
18 | body = note.body,
19 | created_at = note.created_at,
20 | updated_at = dateUtil.getCurrentTimestamp()
21 | )
22 | notesData.put(note.id, n)
23 | }
24 |
25 | override suspend fun deleteNote(primaryKey: String) {
26 | notesData.remove(primaryKey)
27 | }
28 |
29 | override suspend fun insertDeletedNote(note: Note) {
30 | deletedNotesData.put(note.id, note)
31 | }
32 |
33 | override suspend fun insertDeletedNotes(notes: List) {
34 | for(note in notes){
35 | deletedNotesData.put(note.id, note)
36 | }
37 | }
38 |
39 | override suspend fun deleteDeletedNote(note: Note) {
40 | deletedNotesData.remove(note.id)
41 | }
42 |
43 | override suspend fun getDeletedNotes(): List {
44 | return ArrayList(deletedNotesData.values)
45 | }
46 |
47 | override suspend fun deleteAllNotes() {
48 | deletedNotesData.clear()
49 | }
50 |
51 | override suspend fun searchNote(note: Note): Note? {
52 | return notesData.get(note.id)
53 | }
54 |
55 | override suspend fun getAllNotes(): List {
56 | return ArrayList(notesData.values)
57 | }
58 |
59 | override suspend fun insertOrUpdateNotes(notes: List) {
60 | for(note in notes){
61 | notesData.put(note.id, note)
62 | }
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/app/src/test/java/com/codingwithmitch/cleannotes/business/interactors/notelist/GetNumNotesTest.kt:
--------------------------------------------------------------------------------
1 | package com.codingwithmitch.cleannotes.business.interactors.notelist
2 |
3 | import com.codingwithmitch.cleannotes.business.data.cache.abstraction.NoteCacheDataSource
4 | import com.codingwithmitch.cleannotes.business.domain.model.NoteFactory
5 | import com.codingwithmitch.cleannotes.business.interactors.notelist.GetNumNotes.Companion.GET_NUM_NOTES_SUCCESS
6 | import com.codingwithmitch.cleannotes.business.domain.state.DataState
7 | import com.codingwithmitch.cleannotes.di.DependencyContainer
8 | import com.codingwithmitch.cleannotes.framework.presentation.notelist.state.NoteListStateEvent.*
9 | import com.codingwithmitch.cleannotes.framework.presentation.notelist.state.NoteListViewState
10 | import kotlinx.coroutines.InternalCoroutinesApi
11 | import kotlinx.coroutines.flow.FlowCollector
12 | import kotlinx.coroutines.runBlocking
13 | import org.junit.jupiter.api.Assertions.*
14 | import org.junit.jupiter.api.Test
15 |
16 | /*
17 |
18 | Test cases:
19 | 1. getNumNotes_success_confirmCorrect()
20 | a) get the number of notes in cache
21 | b) listen for GET_NUM_NOTES_SUCCESS from flow emission
22 | c) compare with the number of notes in the fake data set
23 |
24 | */
25 | @InternalCoroutinesApi
26 | class GetNumNotesTest {
27 |
28 | // system in test
29 | private val getNumNotes: GetNumNotes
30 |
31 | // dependencies
32 | private val dependencyContainer: DependencyContainer
33 | private val noteCacheDataSource: NoteCacheDataSource
34 | private val noteFactory: NoteFactory
35 |
36 | init {
37 | dependencyContainer = DependencyContainer()
38 | dependencyContainer.build()
39 | noteCacheDataSource = dependencyContainer.noteCacheDataSource
40 | noteFactory = dependencyContainer.noteFactory
41 | getNumNotes = GetNumNotes(
42 | noteCacheDataSource = noteCacheDataSource
43 | )
44 | }
45 |
46 |
47 | @Test
48 | fun getNumNotes_success_confirmCorrect() = runBlocking {
49 |
50 | var numNotes = 0
51 | getNumNotes.getNumNotes(
52 | stateEvent = GetNumNotesInCacheEvent()
53 | ).collect(object: FlowCollector?>{
54 | override suspend fun emit(value: DataState?) {
55 | assertEquals(
56 | value?.stateMessage?.response?.message,
57 | GET_NUM_NOTES_SUCCESS
58 | )
59 | numNotes = value?.data?.numNotesInCache?: 0
60 | }
61 | })
62 |
63 | val actualNumNotesInCache = noteCacheDataSource.getNumNotes()
64 | assertTrue { actualNumNotesInCache == numNotes }
65 | }
66 |
67 |
68 | }
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
--------------------------------------------------------------------------------
/app/src/test/java/com/codingwithmitch/cleannotes/business/interactors/splash/SyncDeletedNotesTest.kt:
--------------------------------------------------------------------------------
1 | package com.codingwithmitch.cleannotes.business.interactors.splash
2 |
3 | import com.codingwithmitch.cleannotes.business.data.cache.abstraction.NoteCacheDataSource
4 | import com.codingwithmitch.cleannotes.business.data.network.abstraction.NoteNetworkDataSource
5 | import com.codingwithmitch.cleannotes.business.domain.model.Note
6 | import com.codingwithmitch.cleannotes.business.domain.model.NoteFactory
7 | import com.codingwithmitch.cleannotes.business.domain.util.DateUtil
8 | import com.codingwithmitch.cleannotes.di.DependencyContainer
9 | import kotlinx.coroutines.InternalCoroutinesApi
10 | import kotlinx.coroutines.runBlocking
11 | import org.junit.jupiter.api.Assertions.assertTrue
12 | import org.junit.jupiter.api.Test
13 |
14 | /*
15 |
16 | Test cases:
17 | 1. deleteNetworkNotes_confirmCacheSync()
18 | a) select some notes for deleting from network
19 | b) delete from network
20 | c) perform sync
21 | d) confirm notes from cache were deleted
22 | */
23 |
24 | @InternalCoroutinesApi
25 | class SyncDeletedNotesTest {
26 |
27 | // system in test
28 | private val syncDeletedNotes: SyncDeletedNotes
29 |
30 | // dependencies
31 | private val dependencyContainer: DependencyContainer
32 | private val noteCacheDataSource: NoteCacheDataSource
33 | private val noteNetworkDataSource: NoteNetworkDataSource
34 | private val noteFactory: NoteFactory
35 | private val dateUtil: DateUtil
36 |
37 | init {
38 | dependencyContainer = DependencyContainer()
39 | dependencyContainer.build()
40 | noteCacheDataSource = dependencyContainer.noteCacheDataSource
41 | noteNetworkDataSource = dependencyContainer.noteNetworkDataSource
42 | noteFactory = dependencyContainer.noteFactory
43 | dateUtil = dependencyContainer.dateUtil
44 | syncDeletedNotes = SyncDeletedNotes(
45 | noteCacheDataSource = noteCacheDataSource,
46 | noteNetworkDataSource = noteNetworkDataSource
47 | )
48 | }
49 |
50 | @Test
51 | fun deleteNetworkNotes_confirmCacheSync() = runBlocking {
52 |
53 | // select some notes to be deleted from cache
54 | val networkNotes = noteNetworkDataSource.getAllNotes()
55 | val notesToDelete: ArrayList = ArrayList()
56 | for(note in networkNotes){
57 | notesToDelete.add(note)
58 | noteNetworkDataSource.deleteNote(note.id)
59 | noteNetworkDataSource.insertDeletedNote(note)
60 | if(notesToDelete.size > 3){
61 | break
62 | }
63 | }
64 |
65 | // perform sync
66 | syncDeletedNotes.syncDeletedNotes()
67 |
68 | // confirm notes were deleted from cache
69 | for(note in notesToDelete){
70 | val cachedNote = noteCacheDataSource.searchNoteById(note.id)
71 | assertTrue { cachedNote == null }
72 | }
73 | }
74 | }
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
--------------------------------------------------------------------------------
/app/src/test/java/com/codingwithmitch/cleannotes/di/DependencyContainer.kt:
--------------------------------------------------------------------------------
1 | package com.codingwithmitch.cleannotes.di
2 |
3 | import com.codingwithmitch.cleannotes.business.data.NoteDataFactory
4 | import com.codingwithmitch.cleannotes.business.data.cache.FakeNoteCacheDataSourceImpl
5 | import com.codingwithmitch.cleannotes.business.data.cache.abstraction.NoteCacheDataSource
6 | import com.codingwithmitch.cleannotes.business.data.network.FakeNoteNetworkDataSourceImpl
7 | import com.codingwithmitch.cleannotes.business.data.network.abstraction.NoteNetworkDataSource
8 | import com.codingwithmitch.cleannotes.business.domain.model.Note
9 | import com.codingwithmitch.cleannotes.business.domain.model.NoteFactory
10 | import com.codingwithmitch.cleannotes.business.domain.util.DateUtil
11 | import com.codingwithmitch.cleannotes.util.isUnitTest
12 | import java.text.SimpleDateFormat
13 | import java.util.*
14 | import kotlin.collections.HashMap
15 |
16 | class DependencyContainer {
17 |
18 | private val dateFormat = SimpleDateFormat("yyyy-MM-dd hh:mm:ss a", Locale.ENGLISH)
19 | val dateUtil =
20 | DateUtil(dateFormat)
21 | lateinit var noteNetworkDataSource: NoteNetworkDataSource
22 | lateinit var noteCacheDataSource: NoteCacheDataSource
23 | lateinit var noteFactory: NoteFactory
24 | lateinit var noteDataFactory: NoteDataFactory
25 |
26 | init {
27 | isUnitTest = true // for Logger.kt
28 | }
29 |
30 | fun build() {
31 | this.javaClass.classLoader?.let { classLoader ->
32 | noteDataFactory = NoteDataFactory(classLoader)
33 | }
34 | noteFactory = NoteFactory(dateUtil)
35 | noteNetworkDataSource = FakeNoteNetworkDataSourceImpl(
36 | notesData = noteDataFactory.produceHashMapOfNotes(
37 | noteDataFactory.produceListOfNotes()
38 | ),
39 | deletedNotesData = HashMap(),
40 | dateUtil = dateUtil
41 | )
42 | noteCacheDataSource = FakeNoteCacheDataSourceImpl(
43 | notesData = noteDataFactory.produceHashMapOfNotes(
44 | noteDataFactory.produceListOfNotes()
45 | ),
46 | dateUtil = dateUtil
47 | )
48 | }
49 |
50 | }
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/app/src/test/res/note_list.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "id": "2474abea-7584-486b-9f88-87a21870b0ec",
4 | "title": "Vancouver PNE 2019",
5 | "body": "Here is Jess and I at the Vancouver PNE. We ate a lot of food.",
6 | "updated_at": "2019-04-14 08:41:22 AM",
7 | "created_at": "2019-04-14 07:05:11 AM"
8 | },
9 | {
10 | "id": "2474fbaa-7884-4h6b-9b8z-87a21670b0ec",
11 | "title": "Ready for a Walk",
12 | "body": "Here I am at the park with my dogs Kiba and Maizy. Maizy is the smaller one and Kiba is the larger one.",
13 | "updated_at": "2019-04-17 11:05:24 PM",
14 | "created_at": "2019-04-15 04:44:57 AM"
15 | },
16 | {
17 | "id": "2474mbaa-7884-4htb-9baz-87a216a0b0ec",
18 | "title": "Maizy Sleeping",
19 | "body": "I took this picture while Maizy was sleeping on the couch. She's very cute.",
20 | "updated_at": "2019-02-01 01:55:53 AM",
21 | "created_at": "2019-01-24 12:19:35 PM"
22 | },
23 | {
24 | "id": "2474fpaa-k884-4u6b-9biz-87am1670b0ec",
25 | "title": "My Brother Blake",
26 | "body": "This is a picture of my brother Blake and I. We were taking some pictures for his website.",
27 | "updated_at": "2019-12-14 03:05:16 PM",
28 | "created_at": "2019-12-13 07:05:17 AM"
29 | },
30 | {
31 | "id": "2474abaa-788a-4a6b-948z-87a2167hb0ec",
32 | "title": "Lounging Dogs",
33 | "body": "Kiba and Maizy are laying in the sun relaxing.",
34 | "updated_at": "2019-11-14 06:12:44 AM",
35 | "created_at": "2019-10-14 02:47:13 PM"
36 | },
37 | {
38 | "id": "24742baa-78j4-4z6b-9b8l-87a11670b0ec",
39 | "title": "Mountains in Washington",
40 | "body": "This is an image I found somewhere on the internert. I love pictures like this. I believe it's in Washington, U.S.A.",
41 | "updated_at": "2019-05-19 11:34:16 PM",
42 | "created_at": "2019-04-25 05:16:36 AM"
43 | },
44 | {
45 | "id": "2g74fbaa-78h4-4hab-9b85-87l21670b0ec",
46 | "title": "France Mountain Range",
47 | "body": "Another beautiful picture of nature. You can find more pictures like this one on Reddit.com, in the subreddit: '/r/earthporn'.",
48 | "updated_at": "2019-10-01 12:22:46 AM",
49 | "created_at": "2019-09-19 09:36:57 PM"
50 | },
51 | {
52 | "id": "2477fbaa-7b84-4hjb-9bkl-87a2a670b0ec",
53 | "title": "Aldergrove Park",
54 | "body": "I walk Kiba and Maizy pretty much every day. Usually we go to a park in Aldergrove. It takes about 1 hour, 15 minutes to walk around the entire park.",
55 | "updated_at": "2019-06-12 12:58:55 AM",
56 | "created_at": "2019-03-19 08:49:41 PM"
57 | },
58 | {
59 | "id": "2477fbbb-7b84-g5jb-9bkl-8741a670b0ec",
60 | "title": "My Computer",
61 | "body": "I sit on my computer all day.",
62 | "updated_at": "2019-03-11 12:58:55 AM",
63 | "created_at": "2019-01-15 11:49:41 PM"
64 | },
65 | {
66 | "id": "247aaabb-7564-g5jb-9bkl-8741ah70b0ec",
67 | "title": "Courses",
68 | "body": "I make Android dev courses for a living.",
69 | "updated_at": "2019-05-04 09:44:55 AM",
70 | "created_at": "2019-01-15 10:11:41 PM"
71 | }
72 | ]
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | import dependencies.Versions
2 | import dependencies.Build
3 |
4 |
5 | buildscript {
6 | ext.kotlin_version = Versions.kotlin
7 |
8 | repositories {
9 | google()
10 | jcenter()
11 | }
12 | dependencies {
13 | classpath Build.build_tools
14 | classpath Build.kotlin_gradle_plugin
15 | classpath Build.google_services
16 | classpath Build.crashlytics_gradle
17 | }
18 | }
19 |
20 | allprojects {
21 | repositories {
22 | google()
23 | jcenter()
24 |
25 | }
26 | }
27 |
28 | task clean(type: Delete) {
29 | delete rootProject.buildDir
30 | }
31 |
--------------------------------------------------------------------------------
/buildSrc/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import org.gradle.kotlin.dsl.`kotlin-dsl`
2 |
3 | plugins {
4 | `kotlin-dsl`
5 | }
6 | repositories {
7 | jcenter()
8 | }
--------------------------------------------------------------------------------
/buildSrc/build/classes/kotlin/main/META-INF/buildSrc.kotlin_module:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/buildSrc/build/classes/kotlin/main/dependencies/AnnotationProcessing.class:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mitchtabian/Clean-Notes/ea8f6c95c57685aed42b3b1286aecb33cc2bbf77/buildSrc/build/classes/kotlin/main/dependencies/AnnotationProcessing.class
--------------------------------------------------------------------------------
/buildSrc/build/classes/kotlin/main/dependencies/Application.class:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mitchtabian/Clean-Notes/ea8f6c95c57685aed42b3b1286aecb33cc2bbf77/buildSrc/build/classes/kotlin/main/dependencies/Application.class
--------------------------------------------------------------------------------
/buildSrc/build/classes/kotlin/main/dependencies/Build.class:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mitchtabian/Clean-Notes/ea8f6c95c57685aed42b3b1286aecb33cc2bbf77/buildSrc/build/classes/kotlin/main/dependencies/Build.class
--------------------------------------------------------------------------------
/buildSrc/build/classes/kotlin/main/dependencies/Java.class:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mitchtabian/Clean-Notes/ea8f6c95c57685aed42b3b1286aecb33cc2bbf77/buildSrc/build/classes/kotlin/main/dependencies/Java.class
--------------------------------------------------------------------------------
/buildSrc/build/classes/kotlin/main/dependencies/Repositories.class:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mitchtabian/Clean-Notes/ea8f6c95c57685aed42b3b1286aecb33cc2bbf77/buildSrc/build/classes/kotlin/main/dependencies/Repositories.class
--------------------------------------------------------------------------------
/buildSrc/build/classes/kotlin/main/dependencies/Versions.class:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mitchtabian/Clean-Notes/ea8f6c95c57685aed42b3b1286aecb33cc2bbf77/buildSrc/build/classes/kotlin/main/dependencies/Versions.class
--------------------------------------------------------------------------------
/buildSrc/build/classes/kotlin/main/dependencies/dependencies/AndroidTestDependencies.class:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mitchtabian/Clean-Notes/ea8f6c95c57685aed42b3b1286aecb33cc2bbf77/buildSrc/build/classes/kotlin/main/dependencies/dependencies/AndroidTestDependencies.class
--------------------------------------------------------------------------------
/buildSrc/build/classes/kotlin/main/dependencies/dependencies/Dependencies.class:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mitchtabian/Clean-Notes/ea8f6c95c57685aed42b3b1286aecb33cc2bbf77/buildSrc/build/classes/kotlin/main/dependencies/dependencies/Dependencies.class
--------------------------------------------------------------------------------
/buildSrc/build/classes/kotlin/main/dependencies/dependencies/SupportDependencies.class:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mitchtabian/Clean-Notes/ea8f6c95c57685aed42b3b1286aecb33cc2bbf77/buildSrc/build/classes/kotlin/main/dependencies/dependencies/SupportDependencies.class
--------------------------------------------------------------------------------
/buildSrc/build/classes/kotlin/main/dependencies/dependencies/TestDependencies.class:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mitchtabian/Clean-Notes/ea8f6c95c57685aed42b3b1286aecb33cc2bbf77/buildSrc/build/classes/kotlin/main/dependencies/dependencies/TestDependencies.class
--------------------------------------------------------------------------------
/buildSrc/build/kotlin/buildSrcjar-classes.txt:
--------------------------------------------------------------------------------
1 | D:\Android Studio Projects\CleanNotes\buildSrc\build\classes\kotlin\main\dependencies\AnnotationProcessing.class;D:\Android Studio Projects\CleanNotes\buildSrc\build\classes\kotlin\main\dependencies\Application.class;D:\Android Studio Projects\CleanNotes\buildSrc\build\classes\kotlin\main\dependencies\Build.class;D:\Android Studio Projects\CleanNotes\buildSrc\build\classes\kotlin\main\dependencies\Java.class;D:\Android Studio Projects\CleanNotes\buildSrc\build\classes\kotlin\main\dependencies\Repositories.class;D:\Android Studio Projects\CleanNotes\buildSrc\build\classes\kotlin\main\dependencies\Versions.class;D:\Android Studio Projects\CleanNotes\buildSrc\build\classes\kotlin\main\dependencies\dependencies\AndroidTestDependencies.class;D:\Android Studio Projects\CleanNotes\buildSrc\build\classes\kotlin\main\dependencies\dependencies\Dependencies.class;D:\Android Studio Projects\CleanNotes\buildSrc\build\classes\kotlin\main\dependencies\dependencies\SupportDependencies.class;D:\Android Studio Projects\CleanNotes\buildSrc\build\classes\kotlin\main\dependencies\dependencies\TestDependencies.class
--------------------------------------------------------------------------------
/buildSrc/build/kotlin/compileKotlin/build-history.bin:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mitchtabian/Clean-Notes/ea8f6c95c57685aed42b3b1286aecb33cc2bbf77/buildSrc/build/kotlin/compileKotlin/build-history.bin
--------------------------------------------------------------------------------
/buildSrc/build/kotlin/compileKotlin/caches-jvm/inputs/source-to-output.tab:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mitchtabian/Clean-Notes/ea8f6c95c57685aed42b3b1286aecb33cc2bbf77/buildSrc/build/kotlin/compileKotlin/caches-jvm/inputs/source-to-output.tab
--------------------------------------------------------------------------------
/buildSrc/build/kotlin/compileKotlin/caches-jvm/inputs/source-to-output.tab_i:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mitchtabian/Clean-Notes/ea8f6c95c57685aed42b3b1286aecb33cc2bbf77/buildSrc/build/kotlin/compileKotlin/caches-jvm/inputs/source-to-output.tab_i
--------------------------------------------------------------------------------
/buildSrc/build/kotlin/compileKotlin/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mitchtabian/Clean-Notes/ea8f6c95c57685aed42b3b1286aecb33cc2bbf77/buildSrc/build/kotlin/compileKotlin/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab
--------------------------------------------------------------------------------
/buildSrc/build/kotlin/compileKotlin/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab_i:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mitchtabian/Clean-Notes/ea8f6c95c57685aed42b3b1286aecb33cc2bbf77/buildSrc/build/kotlin/compileKotlin/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab_i
--------------------------------------------------------------------------------
/buildSrc/build/kotlin/compileKotlin/caches-jvm/jvm/kotlin/internal-name-to-source.tab:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mitchtabian/Clean-Notes/ea8f6c95c57685aed42b3b1286aecb33cc2bbf77/buildSrc/build/kotlin/compileKotlin/caches-jvm/jvm/kotlin/internal-name-to-source.tab
--------------------------------------------------------------------------------
/buildSrc/build/kotlin/compileKotlin/caches-jvm/jvm/kotlin/internal-name-to-source.tab_i:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mitchtabian/Clean-Notes/ea8f6c95c57685aed42b3b1286aecb33cc2bbf77/buildSrc/build/kotlin/compileKotlin/caches-jvm/jvm/kotlin/internal-name-to-source.tab_i
--------------------------------------------------------------------------------
/buildSrc/build/kotlin/compileKotlin/caches-jvm/jvm/kotlin/proto.tab:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mitchtabian/Clean-Notes/ea8f6c95c57685aed42b3b1286aecb33cc2bbf77/buildSrc/build/kotlin/compileKotlin/caches-jvm/jvm/kotlin/proto.tab
--------------------------------------------------------------------------------
/buildSrc/build/kotlin/compileKotlin/caches-jvm/jvm/kotlin/proto.tab_i:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mitchtabian/Clean-Notes/ea8f6c95c57685aed42b3b1286aecb33cc2bbf77/buildSrc/build/kotlin/compileKotlin/caches-jvm/jvm/kotlin/proto.tab_i
--------------------------------------------------------------------------------
/buildSrc/build/kotlin/compileKotlin/caches-jvm/jvm/kotlin/source-to-classes.tab:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mitchtabian/Clean-Notes/ea8f6c95c57685aed42b3b1286aecb33cc2bbf77/buildSrc/build/kotlin/compileKotlin/caches-jvm/jvm/kotlin/source-to-classes.tab
--------------------------------------------------------------------------------
/buildSrc/build/kotlin/compileKotlin/caches-jvm/lookups/file-to-id.tab:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mitchtabian/Clean-Notes/ea8f6c95c57685aed42b3b1286aecb33cc2bbf77/buildSrc/build/kotlin/compileKotlin/caches-jvm/lookups/file-to-id.tab
--------------------------------------------------------------------------------
/buildSrc/build/kotlin/compileKotlin/caches-jvm/lookups/file-to-id.tab_i:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mitchtabian/Clean-Notes/ea8f6c95c57685aed42b3b1286aecb33cc2bbf77/buildSrc/build/kotlin/compileKotlin/caches-jvm/lookups/file-to-id.tab_i
--------------------------------------------------------------------------------
/buildSrc/build/kotlin/compileKotlin/caches-jvm/lookups/id-to-file.tab:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mitchtabian/Clean-Notes/ea8f6c95c57685aed42b3b1286aecb33cc2bbf77/buildSrc/build/kotlin/compileKotlin/caches-jvm/lookups/id-to-file.tab
--------------------------------------------------------------------------------
/buildSrc/build/kotlin/compileKotlin/caches-jvm/lookups/lookups.tab:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mitchtabian/Clean-Notes/ea8f6c95c57685aed42b3b1286aecb33cc2bbf77/buildSrc/build/kotlin/compileKotlin/caches-jvm/lookups/lookups.tab
--------------------------------------------------------------------------------
/buildSrc/build/kotlin/compileKotlin/caches-jvm/lookups/lookups.tab_i:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mitchtabian/Clean-Notes/ea8f6c95c57685aed42b3b1286aecb33cc2bbf77/buildSrc/build/kotlin/compileKotlin/caches-jvm/lookups/lookups.tab_i
--------------------------------------------------------------------------------
/buildSrc/build/kotlin/compileKotlin/last-build.bin:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mitchtabian/Clean-Notes/ea8f6c95c57685aed42b3b1286aecb33cc2bbf77/buildSrc/build/kotlin/compileKotlin/last-build.bin
--------------------------------------------------------------------------------
/buildSrc/build/libs/buildSrc.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mitchtabian/Clean-Notes/ea8f6c95c57685aed42b3b1286aecb33cc2bbf77/buildSrc/build/libs/buildSrc.jar
--------------------------------------------------------------------------------
/buildSrc/build/pluginUnderTestMetadata/plugin-under-test-metadata.properties:
--------------------------------------------------------------------------------
1 | implementation-classpath=D\:/Android Studio Projects/CleanNotes/buildSrc/build/classes/java/main;D\:/Android Studio Projects/CleanNotes/buildSrc/build/classes/groovy/main;D\:/Android Studio Projects/CleanNotes/buildSrc/build/classes/kotlin/main;D\:/Android Studio Projects/CleanNotes/buildSrc/build/resources/main
2 |
--------------------------------------------------------------------------------
/buildSrc/build/reports/task-properties/report.txt:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mitchtabian/Clean-Notes/ea8f6c95c57685aed42b3b1286aecb33cc2bbf77/buildSrc/build/reports/task-properties/report.txt
--------------------------------------------------------------------------------
/buildSrc/build/tmp/jar/MANIFEST.MF:
--------------------------------------------------------------------------------
1 | Manifest-Version: 1.0
2 |
3 |
--------------------------------------------------------------------------------
/buildSrc/src/main/java/AnnotationProcessing.kt:
--------------------------------------------------------------------------------
1 | package dependencies
2 |
3 | object AnnotationProcessing {
4 | val room_compiler = "androidx.room:room-compiler:${Versions.room}"
5 | val dagger_compiler = "com.google.dagger:dagger-compiler:${Versions.dagger}"
6 | val lifecycle_compiler = "androidx.lifecycle:lifecycle-compiler:${Versions.lifecycle_version}"
7 | }
--------------------------------------------------------------------------------
/buildSrc/src/main/java/Application.kt:
--------------------------------------------------------------------------------
1 | package dependencies
2 |
3 | object Application {
4 | val id = "com.codingwithmitch.cleannotes"
5 | val version_code = 1
6 | val version_name = "1.0"
7 | }
--------------------------------------------------------------------------------
/buildSrc/src/main/java/Build.kt:
--------------------------------------------------------------------------------
1 | package dependencies
2 |
3 | object Build {
4 | val build_tools = "com.android.tools.build:gradle:${Versions.gradle}"
5 | val kotlin_gradle_plugin = "org.jetbrains.kotlin:kotlin-gradle-plugin:${Versions.kotlin}"
6 | val google_services = "com.google.gms:google-services:${Versions.play_services}"
7 | val junit5 = "de.mannodermaus.gradle.plugins:android-junit5:1.3.2.0"
8 | val crashlytics_gradle = "com.google.firebase:firebase-crashlytics-gradle:${Versions.crashlytics_gradle}"
9 | }
--------------------------------------------------------------------------------
/buildSrc/src/main/java/Java.kt:
--------------------------------------------------------------------------------
1 | package dependencies
2 |
3 | object Java {
4 |
5 | val java_version = "1.8"
6 | }
--------------------------------------------------------------------------------
/buildSrc/src/main/java/Repositories.kt:
--------------------------------------------------------------------------------
1 | package dependencies
2 |
3 | object Repositories{
4 |
5 | val fabric = "https://maven.fabric.io/public"
6 | }
--------------------------------------------------------------------------------
/buildSrc/src/main/java/Versions.kt:
--------------------------------------------------------------------------------
1 | package dependencies
2 |
3 | object Versions {
4 | val gradle = "3.5.3"
5 | val compilesdk = 29
6 | val minsdk = 21
7 | val targetsdk = 29
8 | val kotlin = "1.3.61"
9 | val ktx = "1.2.0"
10 | val dagger = "2.25.4"
11 | val nav_components = "2.3.0-alpha06"
12 | val material_dialogs = "3.2.1"
13 | val room = "2.1.0"
14 | val appcompat = "1.1.0-rc01"
15 | val constraintlayout = "1.1.3"
16 | val material_design = "1.1.0"
17 | val play_core = "1.7.1"
18 | val play_services = "4.3.3"
19 | val leak_canary = "2.0-alpha-3"
20 | val swipe_refresh_layout = "1.1.0-alpha03"
21 | val firebase_firestore = "21.4.2"
22 | val firebase_analytics = "17.4.1"
23 | val firebase_crashlytics = "17.0.0"
24 | val firebase_auth = "19.3.0"
25 | val espresso_core = "3.1.1"
26 | val espresso_idling_resource = "3.2.0"
27 | val mockk_version = "1.9.2"
28 | val test_runner = "1.2.0"
29 | val test_core = "1.2.0"
30 | val coroutines_version = "1.3.0"
31 | val coroutines_play_services = "1.3.2"
32 | val lifecycle_version = "2.2.0-alpha03"
33 | val retrofit2_version = "2.6.0"
34 | val markdown_processor = "0.1.3"
35 | val junit_jupiter_version = "5.6.0"
36 | val junit_4_version = "4.12"
37 | val fragment_version = "1.2.0"
38 | val androidx_test_ext = "1.1.1"
39 | val crashlytics_gradle = "2.1.0"
40 | }
--------------------------------------------------------------------------------
/buildSrc/src/main/java/dependencies/AndroidTestDependencies.kt:
--------------------------------------------------------------------------------
1 | package dependencies.dependencies
2 |
3 | import dependencies.Versions
4 |
5 | object AndroidTestDependencies{
6 |
7 | val kotlin_test = "org.jetbrains.kotlin:kotlin-test-junit:${Versions.kotlin}"
8 | val coroutines_test = "org.jetbrains.kotlinx:kotlinx-coroutines-test:${Versions.coroutines_version}"
9 | val espresso_core = "androidx.test.espresso:espresso-core:${Versions.espresso_core}"
10 | val espresso_contrib = "androidx.test.espresso:espresso-contrib:${Versions.espresso_core}"
11 | val idling_resource = "androidx.test.espresso:espresso-idling-resource:${Versions.espresso_idling_resource}"
12 | val test_runner = "androidx.test:runner:${Versions.test_runner}"
13 | val test_rules = "androidx.test:rules:${Versions.test_runner}"
14 | val text_core_ktx = "androidx.test:core-ktx:${Versions.test_core}"
15 | val mockk_android = "io.mockk:mockk-android:${Versions.mockk_version}"
16 | val fragment_testing = "androidx.fragment:fragment-testing:${Versions.fragment_version}"
17 | val androidx_test_ext = "androidx.test.ext:junit-ktx:${Versions.androidx_test_ext}"
18 | val navigation_testing = "androidx.navigation:navigation-testing:${Versions.nav_components}"
19 |
20 | val instrumentation_runner = "com.codingwithmitch.cleannotes.framework.MockTestRunner"
21 | }
--------------------------------------------------------------------------------
/buildSrc/src/main/java/dependencies/Dependencies.kt:
--------------------------------------------------------------------------------
1 | package dependencies.dependencies
2 |
3 | import dependencies.Versions
4 |
5 | object Dependencies {
6 | val kotlin_standard_library = "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${Versions.kotlin}"
7 | val kotlin_reflect = "org.jetbrains.kotlin:kotlin-reflect:${Versions.kotlin}"
8 | val ktx = "androidx.core:core-ktx:${Versions.ktx}"
9 | val kotlin_coroutines = "org.jetbrains.kotlinx:kotlinx-coroutines-core:${Versions.coroutines_version}"
10 | val kotlin_coroutines_android = "org.jetbrains.kotlinx:kotlinx-coroutines-android:${Versions.coroutines_version}"
11 | val kotlin_coroutines_play_services = "org.jetbrains.kotlinx:kotlinx-coroutines-play-services:${Versions.coroutines_play_services}"
12 | val dagger = "com.google.dagger:dagger:${Versions.dagger}"
13 | val navigation_fragment = "androidx.navigation:navigation-fragment-ktx:${Versions.nav_components}"
14 | val navigation_runtime = "androidx.navigation:navigation-runtime:${Versions.nav_components}"
15 | val navigation_ui = "androidx.navigation:navigation-ui-ktx:${Versions.nav_components}"
16 | val navigation_dynamic = "androidx.navigation:navigation-dynamic-features-fragment:${Versions.nav_components}"
17 | val material_dialogs = "com.afollestad.material-dialogs:core:${Versions.material_dialogs}"
18 | val material_dialogs_input = "com.afollestad.material-dialogs:input:${Versions.material_dialogs}"
19 | val room_runtime = "androidx.room:room-runtime:${Versions.room}"
20 | val room_ktx = "androidx.room:room-ktx:${Versions.room}"
21 | val play_core = "com.google.android.play:core:${Versions.play_core}"
22 | val leak_canary = "com.squareup.leakcanary:leakcanary-android:${Versions.leak_canary}"
23 | val firebase_firestore = "com.google.firebase:firebase-firestore-ktx:${Versions.firebase_firestore}"
24 | val firebase_auth = "com.google.firebase:firebase-auth:${Versions.firebase_auth}"
25 | val firebase_analytics = "com.google.firebase:firebase-analytics-ktx:${Versions.firebase_analytics}"
26 | val firebase_crashlytics = "com.google.firebase:firebase-crashlytics:${Versions.firebase_crashlytics}"
27 | val lifecycle_runtime = "androidx.lifecycle:lifecycle-runtime:${Versions.lifecycle_version}"
28 | val lifecycle_coroutines = "androidx.lifecycle:lifecycle-livedata-ktx:${Versions.lifecycle_version}"
29 | val retrofit = "com.squareup.retrofit2:retrofit:${Versions.retrofit2_version}"
30 | val retrofit_gson = "com.squareup.retrofit2:converter-gson:${Versions.retrofit2_version}"
31 | val markdown_processor = "com.yydcdut:markdown-processor:${Versions.markdown_processor}"
32 | }
33 |
--------------------------------------------------------------------------------
/buildSrc/src/main/java/dependencies/SupportDependencies.kt:
--------------------------------------------------------------------------------
1 | package dependencies.dependencies
2 |
3 | import dependencies.Versions
4 |
5 | object SupportDependencies {
6 |
7 | val appcompat = "androidx.appcompat:appcompat:${Versions.appcompat}"
8 | val constraintlayout = "androidx.constraintlayout:constraintlayout:${Versions.constraintlayout}"
9 | val material_design = "com.google.android.material:material:${Versions.material_design}"
10 | val swipe_refresh_layout = "androidx.swiperefreshlayout:swiperefreshlayout:${Versions.swipe_refresh_layout}"
11 |
12 | }
--------------------------------------------------------------------------------
/buildSrc/src/main/java/dependencies/TestDependencies.kt:
--------------------------------------------------------------------------------
1 | package dependencies.dependencies
2 |
3 | import dependencies.Versions
4 |
5 | object TestDependencies {
6 |
7 | val jupiter_api = "org.junit.jupiter:junit-jupiter-api:${Versions.junit_jupiter_version}"
8 | val jupiter_params = "org.junit.jupiter:junit-jupiter-params:${Versions.junit_jupiter_version}"
9 | val jupiter_engine = "org.junit.jupiter:junit-jupiter-engine:${Versions.junit_jupiter_version}"
10 | val mockk = "io.mockk:mockk:${Versions.mockk_version}"
11 | val junit4 = "junit:junit:${Versions.junit_4_version}"
12 | }
--------------------------------------------------------------------------------
/firebase.json:
--------------------------------------------------------------------------------
1 | {
2 | "firestore": {
3 | "rules": "firestore.rules",
4 | "indexes": "firestore.indexes.json"
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/firestore.indexes.json:
--------------------------------------------------------------------------------
1 | {
2 | "indexes": [],
3 | "fieldOverrides": []
4 | }
5 |
--------------------------------------------------------------------------------
/firestore.rules:
--------------------------------------------------------------------------------
1 | rules_version = '2';
2 | service cloud.firestore {
3 | match /databases/{database}/documents {
4 | match /{document=**} {
5 | allow read, write: if request.auth.uid != null;
6 | }
7 | }
8 | }
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | #org.gradle.jvmargs=-Xmx1536m
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app's APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Automatically convert third-party libraries to use AndroidX
19 | android.enableJetifier=true
20 | # Kotlin code style for this project: "official" or "obsolete":
21 | kotlin.code.style=official
22 | org.gradle.parallel=true
23 | kapt.incremental.apt=true
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mitchtabian/Clean-Notes/ea8f6c95c57685aed42b3b1286aecb33cc2bbf77/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Thu Mar 26 09:39:43 PDT 2020
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip
7 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | set DIRNAME=%~dp0
12 | if "%DIRNAME%" == "" set DIRNAME=.
13 | set APP_BASE_NAME=%~n0
14 | set APP_HOME=%DIRNAME%
15 |
16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17 | set DEFAULT_JVM_OPTS=
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windows variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 |
53 | :win9xME_args
54 | @rem Slurp the command line arguments.
55 | set CMD_LINE_ARGS=
56 | set _SKIP=2
57 |
58 | :win9xME_args_slurp
59 | if "x%~1" == "x" goto execute
60 |
61 | set CMD_LINE_ARGS=%*
62 |
63 | :execute
64 | @rem Setup the command line
65 |
66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
67 |
68 | @rem Execute Gradle
69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
70 |
71 | :end
72 | @rem End local scope for the variables with windows NT shell
73 | if "%ERRORLEVEL%"=="0" goto mainEnd
74 |
75 | :fail
76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
77 | rem the _cmd.exe /c_ return code!
78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
79 | exit /b 1
80 |
81 | :mainEnd
82 | if "%OS%"=="Windows_NT" endlocal
83 |
84 | :omega
85 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app'
2 | rootProject.name='Clean Notes'
3 |
--------------------------------------------------------------------------------
/tests/firestore-debug.log:
--------------------------------------------------------------------------------
1 | API endpoint: http://localhost:8080
2 | If you are using a library that supports the FIRESTORE_EMULATOR_HOST environment variable, run:
3 |
4 | export FIRESTORE_EMULATOR_HOST=localhost:8080
5 |
6 | Dev App Server is now running.
7 |
8 |
--------------------------------------------------------------------------------
/tests/run_tests.sh:
--------------------------------------------------------------------------------
1 | print_blue(){
2 | printf "\e[1;34m$1\e[0m"
3 | }
4 |
5 |
6 | print_blue "\n\n\nStarting Firestore Local Emulator...\n"
7 | firebase emulators:exec --only functions,firestore "ui_and_unit_tests.sh"
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/tests/ui_and_unit_tests.sh:
--------------------------------------------------------------------------------
1 | # Got these colors from a medium article (I can't remember author)
2 | # Functions for customizing colors(Optional)
3 | print_green(){
4 | printf "\e[1;32m$1\e[0m"
5 | }
6 | print_blue(){
7 | printf "\e[1;34m$1\e[0m"
8 | }
9 |
10 | #Start
11 | print_blue "\n\n\nStarting"
12 |
13 | print_blue "\n\n\ncd into working directory...\n"
14 | cd "/d/Android Studio Projects/CleanNotes/"
15 |
16 | print_blue "\n\n\nrun unit tests...\n"
17 | ./gradlew test
18 | print_green "\n\n\n unit tests COMPLETE.\n"
19 |
20 | print_blue "\n\n\n run androidTests...\n"
21 | ./gradlew connectedAndroidTest
22 | print_green "\n\n\n androidTests COMPLETE.\n"
23 |
24 | print_green "\n\n\n Done.\n"
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/ui-debug.log:
--------------------------------------------------------------------------------
1 | Web / API server started at http://localhost:4000
2 |
--------------------------------------------------------------------------------