├── .buildscript └── deploy_website.sh ├── .github └── workflows │ ├── build.yml │ └── deploy-website.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── RELEASING.md ├── blueprint-async-coroutines ├── .gitignore ├── README.md ├── api │ └── blueprint-async-coroutines.api ├── build.gradle.kts ├── gradle.properties └── src │ ├── main │ └── kotlin │ │ └── reactivecircus │ │ └── blueprint │ │ └── async │ │ └── coroutines │ │ └── CoroutineDispatcherProvider.kt │ └── test │ └── kotlin │ └── reactivecircus │ └── blueprint │ └── async │ └── coroutines │ └── CoroutineDispatcherProviderTest.kt ├── blueprint-async-rx2 ├── .gitignore ├── README.md ├── api │ └── blueprint-async-rx2.api ├── build.gradle.kts ├── gradle.properties └── src │ ├── main │ └── kotlin │ │ └── reactivecircus │ │ └── blueprint │ │ └── async │ │ └── rx2 │ │ └── SchedulerProvider.kt │ └── test │ └── kotlin │ └── reactivecircus │ └── blueprint │ └── async │ └── rx2 │ └── SchedulerProviderTest.kt ├── blueprint-async-rx3 ├── .gitignore ├── README.md ├── api │ └── blueprint-async-rx3.api ├── build.gradle.kts ├── gradle.properties └── src │ ├── main │ └── kotlin │ │ └── reactivecircus │ │ └── blueprint │ │ └── async │ │ └── rx3 │ │ └── SchedulerProvider.kt │ └── test │ └── kotlin │ └── reactivecircus │ └── blueprint │ └── async │ └── rx3 │ └── SchedulerProviderTest.kt ├── blueprint-interactor-common ├── .gitignore ├── README.md ├── api │ └── blueprint-interactor-common.api ├── build.gradle.kts ├── gradle.properties └── src │ └── main │ └── kotlin │ └── reactivecircus │ └── blueprint │ └── interactor │ └── InteractorParams.kt ├── blueprint-interactor-coroutines ├── .gitignore ├── README.md ├── api │ └── blueprint-interactor-coroutines.api ├── build.gradle.kts ├── gradle.properties └── src │ ├── main │ └── kotlin │ │ └── reactivecircus │ │ └── blueprint │ │ └── interactor │ │ └── coroutines │ │ ├── FlowInteractor.kt │ │ └── SuspendingInteractor.kt │ └── test │ └── kotlin │ └── reactivecircus │ └── blueprint │ └── interactor │ └── coroutines │ ├── FlowInteractorTest.kt │ ├── SuspendingInteractorTest.kt │ └── TestCoroutineInteractors.kt ├── blueprint-interactor-rx2 ├── .gitignore ├── README.md ├── api │ └── blueprint-interactor-rx2.api ├── build.gradle.kts ├── gradle.properties └── src │ ├── main │ └── kotlin │ │ └── reactivecircus │ │ └── blueprint │ │ └── interactor │ │ └── rx2 │ │ ├── CompletableInteractor.kt │ │ ├── ObservableInteractor.kt │ │ └── SingleInteractor.kt │ └── test │ └── kotlin │ └── reactivecircus │ └── blueprint │ └── interactor │ └── rx2 │ ├── CompletableInteractorTest.kt │ ├── ObservableInteractorTest.kt │ └── SingleInteractorTest.kt ├── blueprint-interactor-rx3 ├── .gitignore ├── README.md ├── api │ └── blueprint-interactor-rx3.api ├── build.gradle.kts ├── gradle.properties └── src │ ├── main │ └── kotlin │ │ └── reactivecircus │ │ └── blueprint │ │ └── interactor │ │ └── rx3 │ │ ├── CompletableInteractor.kt │ │ ├── ObservableInteractor.kt │ │ └── SingleInteractor.kt │ └── test │ └── kotlin │ └── reactivecircus │ └── blueprint │ └── interactor │ └── rx3 │ ├── CompletableInteractorTest.kt │ ├── ObservableInteractorTest.kt │ └── SingleInteractorTest.kt ├── blueprint-testing-robot ├── .gitignore ├── README.md ├── api │ └── blueprint-testing-robot.api ├── build.gradle.kts ├── gradle.properties └── src │ └── main │ ├── AndroidManifest.xml │ └── kotlin │ └── reactivecircus │ └── blueprint │ └── testing │ ├── Instrumentation.kt │ ├── RepeatRule.kt │ ├── ScreenRobot.kt │ ├── ViewActions.kt │ ├── action │ ├── CheckableRobotActions.kt │ ├── DialogRobotActions.kt │ ├── DrawerRobotActions.kt │ ├── GestureRobotActions.kt │ ├── KeyboardRobotActions.kt │ ├── NavigationRobotActions.kt │ ├── RecyclerViewRobotActions.kt │ ├── SnackbarRobotActions.kt │ └── TextRobotActions.kt │ ├── assertion │ ├── CheckableRobotAssertions.kt │ ├── DialogRobotAssertions.kt │ ├── DrawableRobotAssertions.kt │ ├── DrawerRobotAssertions.kt │ ├── KeyboardRobotAssertions.kt │ ├── NavigationRobotAssertions.kt │ ├── RecyclerViewRobotAssertions.kt │ ├── SnackbarRobotAssertions.kt │ ├── TextInputRobotAssertions.kt │ ├── TextRobotAssertions.kt │ ├── ToolbarRobotAssertions.kt │ └── ViewRobotAssertions.kt │ └── matcher │ ├── RecyclerViewMatcher.kt │ └── StringMatchers.kt ├── blueprint-ui ├── .gitignore ├── README.md ├── api │ └── blueprint-ui.api ├── build.gradle.kts ├── gradle.properties └── src │ └── main │ ├── AndroidManifest.xml │ └── kotlin │ └── reactivecircus │ └── blueprint │ └── ui │ └── extension │ ├── Activity.kt │ ├── AppCompat.kt │ ├── Context.kt │ ├── Intent.kt │ └── Window.kt ├── build.gradle.kts ├── buildSrc ├── build.gradle.kts ├── settings.gradle.kts └── src │ └── main │ └── kotlin │ └── reactivecircus │ └── blueprint │ ├── AdditionalCompilerArgs.kt │ ├── AndroidSdk.kt │ ├── ApiCheckConfigs.kt │ ├── BlueprintExtension.kt │ ├── BlueprintPlugin.kt │ ├── DetektConfigs.kt │ ├── DokkaConfigs.kt │ ├── Environment.kt │ ├── ProjectConfigurations.kt │ ├── Publishing.kt │ ├── SlimTests.kt │ └── VariantExt.kt ├── detekt.yml ├── docs └── images │ └── reactive_circus_logo.png ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── mkdocs.yml ├── renovate.json ├── samples ├── README.md ├── demo-common │ ├── .gitignore │ ├── README.md │ ├── build.gradle.kts │ └── src │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── ic_launcher-web.png │ │ ├── kotlin │ │ │ └── reactivecircus │ │ │ │ └── blueprint │ │ │ │ └── demo │ │ │ │ ├── data │ │ │ │ └── cache │ │ │ │ │ ├── InMemoryNoteCache.kt │ │ │ │ │ └── NoteCache.kt │ │ │ │ ├── domain │ │ │ │ └── model │ │ │ │ │ └── Note.kt │ │ │ │ ├── enternote │ │ │ │ └── EnterNoteParams.kt │ │ │ │ ├── noteslist │ │ │ │ └── NotesListAdapter.kt │ │ │ │ └── util │ │ │ │ ├── Date.kt │ │ │ │ └── ViewModel.kt │ │ └── res │ │ │ ├── drawable │ │ │ ├── ic_add_black_24dp.xml │ │ │ ├── ic_check_black_24dp.xml │ │ │ ├── ic_close_primary_24dp.xml │ │ │ └── ic_launcher_foreground.xml │ │ │ ├── layout │ │ │ ├── activity_enter_note.xml │ │ │ ├── activity_notes.xml │ │ │ └── item_note.xml │ │ │ ├── menu │ │ │ └── menu_enter_note.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 │ │ │ ├── values-night │ │ │ ├── colors.xml │ │ │ └── themes.xml │ │ │ └── values │ │ │ ├── colors.xml │ │ │ ├── dimens.xml │ │ │ ├── ic_launcher_background.xml │ │ │ ├── strings.xml │ │ │ ├── styles.xml │ │ │ └── themes.xml │ │ └── test │ │ └── kotlin │ │ └── reactivecircus │ │ └── blueprint │ │ └── demo │ │ ├── data │ │ └── cache │ │ │ └── InMemoryNoteCacheTest.kt │ │ └── util │ │ └── DateTest.kt ├── demo-coroutines │ ├── .gitignore │ ├── README.md │ ├── build.gradle.kts │ ├── shrinker-rules.pro │ └── src │ │ ├── androidTest │ │ └── kotlin │ │ │ └── reactivecircus │ │ │ └── blueprint │ │ │ └── demo │ │ │ ├── CoroutinesBaseScreenTest.kt │ │ │ ├── CoroutinesScreenTestApp.kt │ │ │ ├── CoroutinesScreenTestAppInjector.kt │ │ │ ├── CoroutinesScreenTestRunner.kt │ │ │ ├── enternote │ │ │ └── CoroutinesEnterNoteScreenTest.kt │ │ │ └── noteslist │ │ │ └── CoroutinesNotesListScreenTest.kt │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── ic_launcher-web.png │ │ ├── kotlin │ │ │ └── reactivecircus │ │ │ │ └── blueprint │ │ │ │ └── demo │ │ │ │ ├── BlueprintCoroutinesDemoApp.kt │ │ │ │ ├── CoroutinesAppInjector.kt │ │ │ │ ├── data │ │ │ │ └── repository │ │ │ │ │ └── CoroutinesInMemoryNoteRepository.kt │ │ │ │ ├── domain │ │ │ │ ├── interactor │ │ │ │ │ ├── CoroutinesCreateNote.kt │ │ │ │ │ ├── CoroutinesGetNoteByUuid.kt │ │ │ │ │ ├── CoroutinesStreamAllNotes.kt │ │ │ │ │ └── CoroutinesUpdateNote.kt │ │ │ │ └── repository │ │ │ │ │ └── CoroutinesNoteRepository.kt │ │ │ │ ├── enternote │ │ │ │ ├── CoroutinesEnterNoteActivity.kt │ │ │ │ └── CoroutinesEnterNoteViewModel.kt │ │ │ │ └── noteslist │ │ │ │ ├── CoroutinesNotesListActivity.kt │ │ │ │ └── CoroutinesNotesListViewModel.kt │ │ └── res │ │ │ └── values │ │ │ └── strings.xml │ │ └── test │ │ └── kotlin │ │ └── reactivecircus │ │ └── blueprint │ │ └── demo │ │ ├── data │ │ └── repository │ │ │ └── CoroutinesInMemoryNoteRepositoryTest.kt │ │ ├── domain │ │ └── interactor │ │ │ ├── CoroutinesCreateNoteTest.kt │ │ │ ├── CoroutinesGetNoteByUuidTest.kt │ │ │ ├── CoroutinesStreamAllNotesTest.kt │ │ │ └── CoroutinesUpdateNoteTest.kt │ │ ├── enternote │ │ └── CoroutinesEnterNoteViewModelTest.kt │ │ └── noteslist │ │ └── CoroutinesNotesListViewModelTest.kt ├── demo-rx │ ├── .gitignore │ ├── README.md │ ├── build.gradle.kts │ ├── shrinker-rules.pro │ └── src │ │ ├── androidTest │ │ └── kotlin │ │ │ └── reactivecircus │ │ │ └── blueprint │ │ │ └── demo │ │ │ ├── RxBaseScreenTest.kt │ │ │ ├── RxScreenTestApp.kt │ │ │ ├── RxScreenTestAppInjector.kt │ │ │ ├── RxScreenTestRunner.kt │ │ │ ├── enternote │ │ │ └── RxEnterNoteScreenTest.kt │ │ │ └── noteslist │ │ │ └── RxNotesListScreenTest.kt │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── ic_launcher-web.png │ │ ├── kotlin │ │ │ └── reactivecircus │ │ │ │ └── blueprint │ │ │ │ └── demo │ │ │ │ ├── BlueprintRxDemoApp.kt │ │ │ │ ├── RxAppInjector.kt │ │ │ │ ├── data │ │ │ │ └── repository │ │ │ │ │ └── RxInMemoryNoteRepository.kt │ │ │ │ ├── domain │ │ │ │ ├── interactor │ │ │ │ │ ├── RxCreateNote.kt │ │ │ │ │ ├── RxGetNoteByUuid.kt │ │ │ │ │ ├── RxStreamAllNotes.kt │ │ │ │ │ └── RxUpdateNote.kt │ │ │ │ └── repository │ │ │ │ │ └── RxNoteRepository.kt │ │ │ │ ├── enternote │ │ │ │ ├── RxEnterNoteActivity.kt │ │ │ │ └── RxEnterNoteViewModel.kt │ │ │ │ └── noteslist │ │ │ │ ├── RxNotesListActivity.kt │ │ │ │ └── RxNotesListViewModel.kt │ │ └── res │ │ │ └── values │ │ │ └── strings.xml │ │ └── test │ │ └── kotlin │ │ └── reactivecircus │ │ └── blueprint │ │ └── demo │ │ ├── data │ │ └── repository │ │ │ └── RxInMemoryNoteRepositoryTest.kt │ │ ├── domain │ │ └── interactor │ │ │ ├── RxCreateNoteTest.kt │ │ │ ├── RxGetNoteByUuidTest.kt │ │ │ ├── RxStreamAllNotesTest.kt │ │ │ └── RxUpdateNoteTest.kt │ │ ├── enternote │ │ └── RxEnterNoteViewModelTest.kt │ │ └── noteslist │ │ └── RxNotesListViewModelTest.kt └── demo-testing-common │ ├── .gitignore │ ├── README.md │ ├── build.gradle.kts │ └── src │ └── main │ ├── AndroidManifest.xml │ └── kotlin │ └── reactivecircus │ └── blueprint │ └── demo │ ├── TestData.kt │ ├── enternote │ └── EnterNoteRobot.kt │ └── noteslist │ └── NotesListRobot.kt ├── settings.gradle.kts └── test-utils ├── .gitignore ├── build.gradle.kts └── src └── main └── kotlin └── reactivecircus └── blueprint └── testutils └── CoroutinesTestRule.kt /.buildscript/deploy_website.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | REPO="git@github.com:ReactiveCircus/blueprint.git" 6 | REMOTE_NAME="origin" 7 | DIR=temp-clone 8 | 9 | if [ -n "${CI}" ]; then 10 | REPO="https://github.com/${GITHUB_REPOSITORY}.git" 11 | fi 12 | 13 | # Clone project into a temp directory 14 | rm -rf $DIR 15 | git clone "$REPO" $DIR 16 | cd $DIR 17 | 18 | # Generate API docs 19 | ./gradlew dokkaHtmlMultiModule 20 | 21 | # Copy *.md files into docs directory 22 | cp README.md docs/index.md 23 | mkdir -p docs/samples && cp samples/README.md docs/samples/index.md 24 | mkdir -p docs/samples/demo-coroutines && cp samples/demo-coroutines/README.md docs/samples/demo-coroutines/index.md 25 | mkdir -p docs/samples/demo-rx && cp samples/demo-rx/README.md docs/samples/demo-rx/index.md 26 | mkdir -p docs/samples/demo-common && cp samples/demo-common/README.md docs/samples/demo-common/index.md 27 | mkdir -p docs/samples/demo-testing-common && cp samples/demo-testing-common/README.md docs/samples/demo-testing-common/index.md 28 | mkdir -p docs/blueprint-interactor-coroutines && cp blueprint-interactor-coroutines/README.md docs/blueprint-interactor-coroutines/index.md 29 | mkdir -p docs/blueprint-interactor-rx2 && cp blueprint-interactor-rx2/README.md docs/blueprint-interactor-rx2/index.md 30 | mkdir -p docs/blueprint-interactor-rx3 && cp blueprint-interactor-rx3/README.md docs/blueprint-interactor-rx3/index.md 31 | mkdir -p docs/blueprint-async-coroutines && cp blueprint-async-coroutines/README.md docs/blueprint-async-coroutines/index.md 32 | mkdir -p docs/blueprint-async-rx2 && cp blueprint-async-rx2/README.md docs/blueprint-async-rx2/index.md 33 | mkdir -p docs/blueprint-async-rx3 && cp blueprint-async-rx3/README.md docs/blueprint-async-rx3/index.md 34 | mkdir -p docs/blueprint-ui && cp blueprint-ui/README.md docs/blueprint-ui/index.md 35 | mkdir -p docs/blueprint-testing-robot && cp blueprint-testing-robot/README.md docs/blueprint-testing-robot/index.md 36 | cp CHANGELOG.md docs/changelog.md 37 | 38 | # If on CI, configure git remote with access token 39 | if [ -n "${CI}" ]; then 40 | REMOTE_NAME="https://x-access-token:${DEPLOY_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" 41 | git config --global user.name "${GITHUB_ACTOR}" 42 | git config --global user.email "${GITHUB_ACTOR}@users.noreply.github.com" 43 | git remote add deploy "$REMOTE_NAME" 44 | git fetch deploy && git fetch deploy gh-pages:gh-pages 45 | fi 46 | 47 | # Build the website and deploy to GitHub Pages 48 | mkdocs gh-deploy --remote-name "$REMOTE_NAME" 49 | 50 | # Delete temp directory 51 | cd .. 52 | rm -rf $DIR 53 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | paths-ignore: 8 | - '**/*.md' 9 | push: 10 | branches: 11 | - main 12 | paths-ignore: 13 | - '**/*.md' 14 | 15 | env: 16 | CI: true 17 | GRADLE_OPTS: -Dkotlin.incremental.useClasspathSnapshot=false -Dorg.gradle.jvmargs="-Xmx4g -XX:+HeapDumpOnOutOfMemoryError" 18 | TERM: dumb 19 | 20 | jobs: 21 | assemble: 22 | name: Assemble 23 | runs-on: ubuntu-latest 24 | env: 25 | JAVA_TOOL_OPTIONS: -Xmx4g -XX:+UseParallelGC 26 | 27 | steps: 28 | - uses: actions/checkout@v3 29 | - uses: gradle/wrapper-validation-action@v1 30 | - uses: actions/setup-java@v3 31 | with: 32 | distribution: 'zulu' 33 | java-version: '18' 34 | - uses: gradle/gradle-build-action@v2 35 | - name: Assemble 36 | run: ./gradlew assemble 37 | 38 | checks: 39 | name: Checks (unit tests, static analysis and binary compatibility API check) 40 | runs-on: ubuntu-latest 41 | env: 42 | JAVA_TOOL_OPTIONS: -Xmx4g -XX:+UseParallelGC 43 | 44 | steps: 45 | - uses: actions/checkout@v3 46 | - uses: gradle/wrapper-validation-action@v1 47 | - uses: actions/setup-java@v3 48 | with: 49 | distribution: 'zulu' 50 | java-version: '18' 51 | - uses: gradle/gradle-build-action@v2 52 | - name: Checks 53 | run: ./gradlew test apiCheck detekt lintDebug -PslimTests 54 | 55 | deploy-snapshot: 56 | name: Deploy snapshot 57 | needs: [assemble, checks] 58 | if: github.ref == 'refs/heads/main' 59 | runs-on: ubuntu-latest 60 | env: 61 | JAVA_TOOL_OPTIONS: -Xmx4g -XX:+UseParallelGC 62 | ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.SONATYPE_NEXUS_USERNAME }} 63 | ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.SONATYPE_NEXUS_PASSWORD }} 64 | 65 | steps: 66 | - uses: actions/checkout@v3 67 | - uses: gradle/wrapper-validation-action@v1 68 | - uses: actions/setup-java@v3 69 | with: 70 | distribution: 'zulu' 71 | java-version: '18' 72 | - uses: gradle/gradle-build-action@v2 73 | - name: Deploy snapshot 74 | run: ./gradlew clean kotlinSourcesJar publish 75 | -------------------------------------------------------------------------------- /.github/workflows/deploy-website.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Website 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - '**.md' 9 | - 'mkdocs.yml' 10 | - '.github/workflows/**' 11 | 12 | jobs: 13 | deploy-website: 14 | name: Generate API docs and deploy website 15 | runs-on: macos-latest 16 | steps: 17 | - uses: actions/checkout@v3 18 | - uses: gradle/wrapper-validation-action@v1 19 | - uses: actions/setup-java@v3 20 | with: 21 | distribution: 'zulu' 22 | java-version: '18' 23 | - uses: gradle/gradle-build-action@v2 24 | - uses: actions/setup-python@v4 25 | with: 26 | python-version: '3.10' 27 | - run: | 28 | pip install mkdocs-material mkdocs-minify-plugin 29 | .buildscript/deploy_website.sh 30 | env: 31 | CI: true 32 | JAVA_TOOL_OPTIONS: -Xmx4g -XX:+UseParallelGC 33 | GRADLE_OPTS: -Dkotlin.incremental.useClasspathSnapshot=false -Dorg.gradle.jvmargs="-Xmx4g -XX:+HeapDumpOnOutOfMemoryError" 34 | DEPLOY_TOKEN: ${{ secrets.GH_DEPLOY_TOKEN }} 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Files for the ART/Dalvik VM 2 | *.dex 3 | 4 | # Java class files 5 | *.class 6 | 7 | # Generated files 8 | bin/ 9 | gen/ 10 | out/ 11 | 12 | # Gradle files 13 | .gradle/ 14 | build/ 15 | reports/ 16 | 17 | # Local configuration file (sdk path, etc) 18 | local.properties 19 | 20 | # Log Files 21 | *.log 22 | 23 | # Android Studio captures folder 24 | captures/ 25 | 26 | # Intellij 27 | *.iml 28 | .idea/ 29 | 30 | # External native build folder generated in Android Studio 2.2 and later 31 | .externalNativeBuild 32 | 33 | # Docs 34 | site 35 | docs/api 36 | -------------------------------------------------------------------------------- /RELEASING.md: -------------------------------------------------------------------------------- 1 | # Releasing 2 | 3 | 1. Change the version in top-level `gradle.properties` to a non-SNAPSHOT version. 4 | 2. Update the `CHANGELOG.md` for the impending release. 5 | 3. Update the `README.md` with the new version. 6 | 4. `git commit -am "Prepare for release X.Y.Z."` (where X.Y.Z is the new version). 7 | 5. `./gradlew clean publish` 8 | 6. Visit [Sonatype Nexus](https://s01.oss.sonatype.org/) and promote the artifact. 9 | 7. `git tag -a X.Y.X -m "X.Y.Z"` (where X.Y.Z is the new version) 10 | 8. Update the top-level `gradle.properties` to the next SNAPSHOT version. 11 | 9. `git commit -am "Prepare next development version."` 12 | 10. `git push && git push --tags` 13 | 14 | If step 5 or 6 fails, drop the Sonatype repo, fix the problem, commit, and start again at step 5. 15 | -------------------------------------------------------------------------------- /blueprint-async-coroutines/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /blueprint-async-coroutines/README.md: -------------------------------------------------------------------------------- 1 | # Blueprint Async Coroutines 2 | 3 | This library provides a `CoroutineDispatcherProvider` class to encapsulate the threading behavior with a wrapper API. 4 | 5 | ## Dependency 6 | 7 | ```groovy 8 | implementation "io.github.reactivecircus.blueprint:blueprint-async-coroutines:${blueprint_version}" 9 | ``` 10 | 11 | ## Usage 12 | `CoroutineDispatcherProvider` has 3 properties, representing the common groups of threading use cases in an app: 13 | 14 | * `io: CoroutineDispatcher` - Dispatcher for IO-bound work 15 | * `computation: CoroutineDispatcher` - Dispatcher for computational work 16 | * `ui: CoroutineDispatcher` - Dispatcher for UI work 17 | 18 | An instance of this can be injected to classes which are concerned about executing code on different threads, but they don't and shouldn't need to know about the underlying implementation. A single-threaded version for example can be injected for testing. 19 | 20 | Practically you'll likely only have 1 instance of `CoroutineDispatcherProvider` in the production environment and use DI to inject it into anywhere in the codebase where certain threading behavior is required: 21 | 22 | ```kotlin 23 | CoroutineDispatcherProvider( 24 | io = Dispatchers.IO, 25 | computation = Dispatchers.Default, 26 | ui = Dispatchers.Main.immediate 27 | ) 28 | ``` 29 | 30 | In unit tests you can easily swap out the implementation to make sure code is executed in a single thread: 31 | 32 | ```kotlin 33 | CoroutineDispatcherProvider( 34 | io = testCoroutineDispatcher, 35 | computation = testCoroutineDispatcher, 36 | ui = testCoroutineDispatcher 37 | ) 38 | ``` 39 | 40 | where `testCoroutineDispatcher` is an instance of `TestCoroutineDispatcher` from the `org.jetbrains.kotlinx:kotlinx-coroutines-test` library. 41 | -------------------------------------------------------------------------------- /blueprint-async-coroutines/api/blueprint-async-coroutines.api: -------------------------------------------------------------------------------- 1 | public final class reactivecircus/blueprint/async/coroutines/CoroutineDispatcherProvider { 2 | public fun (Lkotlinx/coroutines/CoroutineDispatcher;Lkotlinx/coroutines/CoroutineDispatcher;Lkotlinx/coroutines/CoroutineDispatcher;)V 3 | public final fun getComputation ()Lkotlinx/coroutines/CoroutineDispatcher; 4 | public final fun getIo ()Lkotlinx/coroutines/CoroutineDispatcher; 5 | public final fun getUi ()Lkotlinx/coroutines/CoroutineDispatcher; 6 | } 7 | 8 | -------------------------------------------------------------------------------- /blueprint-async-coroutines/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | `blueprint-plugin` 3 | kotlin("jvm") 4 | id("com.vanniktech.maven.publish") 5 | id("org.jetbrains.dokka") 6 | } 7 | 8 | blueprint { 9 | enableExplicitApi.set(true) 10 | } 11 | 12 | dependencies { 13 | // Coroutines 14 | implementation(libs.kotlinx.coroutines.core) 15 | 16 | // Unit tests 17 | testImplementation(libs.junit) 18 | testImplementation(libs.truth) 19 | testImplementation(libs.kotlinx.coroutines.test) 20 | } 21 | -------------------------------------------------------------------------------- /blueprint-async-coroutines/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_ARTIFACT_ID=blueprint-async-coroutines 2 | POM_NAME=Blueprint Async Coroutines 3 | POM_DESCRIPTION=Wrapper API for doing async work with Kotlin CoroutineDispatcher 4 | POM_PACKAGING=jar 5 | -------------------------------------------------------------------------------- /blueprint-async-coroutines/src/main/kotlin/reactivecircus/blueprint/async/coroutines/CoroutineDispatcherProvider.kt: -------------------------------------------------------------------------------- 1 | package reactivecircus.blueprint.async.coroutines 2 | 3 | import kotlinx.coroutines.CoroutineDispatcher 4 | 5 | /** 6 | * A wrapper class for common coroutine dispatchers. 7 | * An instance of this can be injected to classes which are concerned about executing code 8 | * on different threads, but they don't need to know about the underlying implementation. 9 | * A single-threaded version for example can be injected for testing. 10 | */ 11 | public class CoroutineDispatcherProvider( 12 | /** 13 | * Dispatcher for IO-bound work 14 | */ 15 | public val io: CoroutineDispatcher, 16 | /** 17 | * Dispatcher for computational work 18 | */ 19 | public val computation: CoroutineDispatcher, 20 | /** 21 | * Dispatcher for UI work 22 | */ 23 | public val ui: CoroutineDispatcher 24 | ) 25 | -------------------------------------------------------------------------------- /blueprint-async-rx2/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /blueprint-async-rx2/README.md: -------------------------------------------------------------------------------- 1 | # Blueprint Async RxJava 2 2 | 3 | This library provides a `SchedulerProvider` class to encapsulate the threading behavior with a wrapper API. 4 | 5 | ## Dependency 6 | 7 | ```groovy 8 | implementation "io.github.reactivecircus.blueprint:blueprint-async-rx2:${blueprint_version}" 9 | ``` 10 | 11 | ## Usage 12 | `SchedulerProvider` has 3 properties, representing the common groups of threading use cases in an app: 13 | 14 | * `io: Scheduler` - Scheduler for IO-bound work 15 | * `computation: Scheduler` - Scheduler for computational work 16 | * `ui: Scheduler` - Scheduler for UI work 17 | 18 | An instance of this can be injected to classes which are concerned about executing code on different threads, but they don't and shouldn't need to know about the underlying implementation. A single-threaded version for example can be injected for testing. 19 | 20 | Practically you'll likely only have 1 instance of `SchedulerProvider` in the production environment and use DI to inject it into anywhere in the codebase where certain threading behavior is required: 21 | 22 | ```kotlin 23 | SchedulerProvider( 24 | io = Schedulers.io(), 25 | computation = Schedulers.computation(), 26 | ui = AndroidSchedulers.mainThread() 27 | ) 28 | ``` 29 | 30 | In unit tests you can easily swap out the implementation to make sure code is executed in a single thread: 31 | 32 | ```kotlin 33 | SchedulerProvider( 34 | io = Schedulers.trampoline(), 35 | computation = Schedulers.trampoline(), 36 | ui = Schedulers.trampoline() 37 | ) 38 | ``` 39 | -------------------------------------------------------------------------------- /blueprint-async-rx2/api/blueprint-async-rx2.api: -------------------------------------------------------------------------------- 1 | public final class reactivecircus/blueprint/async/rx2/SchedulerProvider { 2 | public fun (Lio/reactivex/Scheduler;Lio/reactivex/Scheduler;Lio/reactivex/Scheduler;)V 3 | public final fun getComputation ()Lio/reactivex/Scheduler; 4 | public final fun getIo ()Lio/reactivex/Scheduler; 5 | public final fun getUi ()Lio/reactivex/Scheduler; 6 | } 7 | 8 | -------------------------------------------------------------------------------- /blueprint-async-rx2/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | `blueprint-plugin` 3 | kotlin("jvm") 4 | id("com.vanniktech.maven.publish") 5 | id("org.jetbrains.dokka") 6 | } 7 | 8 | blueprint { 9 | enableExplicitApi.set(true) 10 | } 11 | 12 | dependencies { 13 | // rx 14 | implementation(libs.rxJava2) 15 | 16 | // Unit tests 17 | testImplementation(libs.junit) 18 | } 19 | -------------------------------------------------------------------------------- /blueprint-async-rx2/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_ARTIFACT_ID=blueprint-async-rx2 2 | POM_NAME=Blueprint Async RxJava 2 3 | POM_DESCRIPTION=Wrapper API for doing async work with RxJava 2 Schedulers 4 | POM_PACKAGING=jar 5 | -------------------------------------------------------------------------------- /blueprint-async-rx2/src/main/kotlin/reactivecircus/blueprint/async/rx2/SchedulerProvider.kt: -------------------------------------------------------------------------------- 1 | package reactivecircus.blueprint.async.rx2 2 | 3 | import io.reactivex.Scheduler 4 | 5 | /** 6 | * A wrapper class for common Rx schedulers. 7 | * An instance of this can be injected to classes which are concerned about executing code 8 | * on different threads, but they don't need to know about the underlying implementation. 9 | * A single-threaded version for example can be injected for testing. 10 | */ 11 | public class SchedulerProvider( 12 | /** 13 | * Scheduler for IO-bound work 14 | */ 15 | public val io: Scheduler, 16 | /** 17 | * Scheduler for computational work 18 | */ 19 | public val computation: Scheduler, 20 | /** 21 | * Scheduler for UI work 22 | */ 23 | public val ui: Scheduler 24 | ) 25 | -------------------------------------------------------------------------------- /blueprint-async-rx2/src/test/kotlin/reactivecircus/blueprint/async/rx2/SchedulerProviderTest.kt: -------------------------------------------------------------------------------- 1 | package reactivecircus.blueprint.async.rx2 2 | 3 | import io.reactivex.Single 4 | import io.reactivex.schedulers.Schedulers 5 | import org.junit.Test 6 | 7 | class SchedulerProviderTest { 8 | 9 | @Test 10 | fun `emit and complete immediately with blocking scheduler provider`() { 11 | val schedulerProvider = SchedulerProvider( 12 | io = Schedulers.trampoline(), 13 | computation = Schedulers.trampoline(), 14 | ui = Schedulers.trampoline() 15 | ) 16 | 17 | val testObserver = Single.fromCallable { 3 } 18 | .subscribeOn(schedulerProvider.io) 19 | .flatMap { 20 | Single.fromCallable { 21 | it * it 22 | } 23 | .subscribeOn(schedulerProvider.computation) 24 | } 25 | .observeOn(schedulerProvider.ui).test() 26 | 27 | testObserver.assertValue(9) 28 | .assertComplete() 29 | } 30 | 31 | @Test 32 | fun `does not emit or terminate immediately with non-blocking scheduler provider`() { 33 | val schedulerProvider = SchedulerProvider( 34 | io = Schedulers.single(), 35 | computation = Schedulers.single(), 36 | ui = Schedulers.single() 37 | ) 38 | 39 | val testObserver = Single.fromCallable { 3 } 40 | .subscribeOn(schedulerProvider.io) 41 | .flatMap { 42 | Single.fromCallable { 43 | it * it 44 | } 45 | .subscribeOn(schedulerProvider.computation) 46 | } 47 | .observeOn(schedulerProvider.ui).test() 48 | 49 | testObserver.assertNoValues() 50 | .assertNotComplete() 51 | .assertNoErrors() 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /blueprint-async-rx3/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /blueprint-async-rx3/README.md: -------------------------------------------------------------------------------- 1 | # Blueprint Async RxJava 3 2 | 3 | This library provides a `SchedulerProvider` class to encapsulate the threading behavior with a wrapper API. 4 | 5 | ## Dependency 6 | 7 | ```groovy 8 | implementation "io.github.reactivecircus.blueprint:blueprint-async-rx3:${blueprint_version}" 9 | ``` 10 | 11 | ## Usage 12 | `SchedulerProvider` has 3 properties, representing the common groups of threading use cases in an app: 13 | 14 | * `io: Scheduler` - Scheduler for IO-bound work 15 | * `computation: Scheduler` - Scheduler for computational work 16 | * `ui: Scheduler` - Scheduler for UI work 17 | 18 | An instance of this can be injected to classes which are concerned about executing code on different threads, but they don't and shouldn't need to know about the underlying implementation. A single-threaded version for example can be injected for testing. 19 | 20 | Practically you'll likely only have 1 instance of `SchedulerProvider` in the production environment and use DI to inject it into anywhere in the codebase where certain threading behavior is required: 21 | 22 | ```kotlin 23 | SchedulerProvider( 24 | io = Schedulers.io(), 25 | computation = Schedulers.computation(), 26 | ui = AndroidSchedulers.mainThread() 27 | ) 28 | ``` 29 | 30 | In unit tests you can easily swap out the implementation to make sure code is executed in a single thread: 31 | 32 | ```kotlin 33 | SchedulerProvider( 34 | io = Schedulers.trampoline(), 35 | computation = Schedulers.trampoline(), 36 | ui = Schedulers.trampoline() 37 | ) 38 | ``` 39 | -------------------------------------------------------------------------------- /blueprint-async-rx3/api/blueprint-async-rx3.api: -------------------------------------------------------------------------------- 1 | public final class reactivecircus/blueprint/async/rx3/SchedulerProvider { 2 | public fun (Lio/reactivex/rxjava3/core/Scheduler;Lio/reactivex/rxjava3/core/Scheduler;Lio/reactivex/rxjava3/core/Scheduler;)V 3 | public final fun getComputation ()Lio/reactivex/rxjava3/core/Scheduler; 4 | public final fun getIo ()Lio/reactivex/rxjava3/core/Scheduler; 5 | public final fun getUi ()Lio/reactivex/rxjava3/core/Scheduler; 6 | } 7 | 8 | -------------------------------------------------------------------------------- /blueprint-async-rx3/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | `blueprint-plugin` 3 | kotlin("jvm") 4 | id("com.vanniktech.maven.publish") 5 | id("org.jetbrains.dokka") 6 | } 7 | 8 | blueprint { 9 | enableExplicitApi.set(true) 10 | } 11 | 12 | dependencies { 13 | // rx 14 | implementation(libs.rxJava3) 15 | 16 | // Unit tests 17 | testImplementation(libs.junit) 18 | } 19 | -------------------------------------------------------------------------------- /blueprint-async-rx3/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_ARTIFACT_ID=blueprint-async-rx3 2 | POM_NAME=Blueprint Async RxJava 3 3 | POM_DESCRIPTION=Wrapper API for doing async work with RxJava 3 Schedulers 4 | POM_PACKAGING=jar 5 | -------------------------------------------------------------------------------- /blueprint-async-rx3/src/main/kotlin/reactivecircus/blueprint/async/rx3/SchedulerProvider.kt: -------------------------------------------------------------------------------- 1 | package reactivecircus.blueprint.async.rx3 2 | 3 | import io.reactivex.rxjava3.core.Scheduler 4 | 5 | /** 6 | * A wrapper class for common Rx schedulers. 7 | * An instance of this can be injected to classes which are concerned about executing code 8 | * on different threads, but they don't need to know about the underlying implementation. 9 | * A single-threaded version for example can be injected for testing. 10 | */ 11 | public class SchedulerProvider( 12 | /** 13 | * Scheduler for IO-bound work 14 | */ 15 | public val io: Scheduler, 16 | /** 17 | * Scheduler for computational work 18 | */ 19 | public val computation: Scheduler, 20 | /** 21 | * Scheduler for UI work 22 | */ 23 | public val ui: Scheduler 24 | ) 25 | -------------------------------------------------------------------------------- /blueprint-async-rx3/src/test/kotlin/reactivecircus/blueprint/async/rx3/SchedulerProviderTest.kt: -------------------------------------------------------------------------------- 1 | package reactivecircus.blueprint.async.rx3 2 | 3 | import io.reactivex.rxjava3.core.Single 4 | import io.reactivex.rxjava3.schedulers.Schedulers 5 | import org.junit.Test 6 | 7 | class SchedulerProviderTest { 8 | 9 | @Test 10 | fun `emit and complete immediately with blocking scheduler provider`() { 11 | val schedulerProvider = SchedulerProvider( 12 | io = Schedulers.trampoline(), 13 | computation = Schedulers.trampoline(), 14 | ui = Schedulers.trampoline() 15 | ) 16 | 17 | val testObserver = Single.fromCallable { 3 } 18 | .subscribeOn(schedulerProvider.io) 19 | .flatMap { 20 | Single.fromCallable { 21 | it * it 22 | } 23 | .subscribeOn(schedulerProvider.computation) 24 | } 25 | .observeOn(schedulerProvider.ui).test() 26 | 27 | testObserver.assertValue(9) 28 | .assertComplete() 29 | } 30 | 31 | @Test 32 | fun `does not emit or terminate immediately with non-blocking scheduler provider`() { 33 | val schedulerProvider = SchedulerProvider( 34 | io = Schedulers.single(), 35 | computation = Schedulers.single(), 36 | ui = Schedulers.single() 37 | ) 38 | 39 | val testObserver = Single.fromCallable { 3 } 40 | .subscribeOn(schedulerProvider.io) 41 | .flatMap { 42 | Single.fromCallable { 43 | it * it 44 | } 45 | .subscribeOn(schedulerProvider.computation) 46 | } 47 | .observeOn(schedulerProvider.ui).test() 48 | 49 | testObserver.assertNoValues() 50 | .assertNotComplete() 51 | .assertNoErrors() 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /blueprint-interactor-common/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /blueprint-interactor-common/README.md: -------------------------------------------------------------------------------- 1 | # Blueprint Interactor Common 2 | -------------------------------------------------------------------------------- /blueprint-interactor-common/api/blueprint-interactor-common.api: -------------------------------------------------------------------------------- 1 | public final class reactivecircus/blueprint/interactor/EmptyParams : reactivecircus/blueprint/interactor/InteractorParams { 2 | public static final field INSTANCE Lreactivecircus/blueprint/interactor/EmptyParams; 3 | } 4 | 5 | public abstract interface class reactivecircus/blueprint/interactor/InteractorParams { 6 | } 7 | 8 | -------------------------------------------------------------------------------- /blueprint-interactor-common/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | `blueprint-plugin` 3 | kotlin("jvm") 4 | id("com.vanniktech.maven.publish") 5 | id("org.jetbrains.dokka") 6 | } 7 | 8 | blueprint { 9 | enableExplicitApi.set(true) 10 | } 11 | -------------------------------------------------------------------------------- /blueprint-interactor-common/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_ARTIFACT_ID=blueprint-interactor-common 2 | POM_NAME=Blueprint Interactor Common 3 | POM_DESCRIPTION=Common APIs for all Blueprint Interactor implementations 4 | POM_PACKAGING=jar 5 | -------------------------------------------------------------------------------- /blueprint-interactor-common/src/main/kotlin/reactivecircus/blueprint/interactor/InteractorParams.kt: -------------------------------------------------------------------------------- 1 | package reactivecircus.blueprint.interactor 2 | 3 | /** 4 | * Interface representing params to be passed in for each interactor. 5 | * Implement this for each interactor that requires specific params. 6 | */ 7 | public interface InteractorParams 8 | 9 | /** 10 | * A special [InteractorParams] representing empty params. 11 | * Use this when the interactor requires no params. 12 | */ 13 | public object EmptyParams : InteractorParams 14 | -------------------------------------------------------------------------------- /blueprint-interactor-coroutines/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /blueprint-interactor-coroutines/api/blueprint-interactor-coroutines.api: -------------------------------------------------------------------------------- 1 | public abstract class reactivecircus/blueprint/interactor/coroutines/FlowInteractor { 2 | public fun ()V 3 | public final fun buildFlow (Lreactivecircus/blueprint/interactor/InteractorParams;)Lkotlinx/coroutines/flow/Flow; 4 | protected abstract fun createFlow (Lreactivecircus/blueprint/interactor/InteractorParams;)Lkotlinx/coroutines/flow/Flow; 5 | public abstract fun getDispatcher ()Lkotlinx/coroutines/CoroutineDispatcher; 6 | } 7 | 8 | public abstract class reactivecircus/blueprint/interactor/coroutines/SuspendingInteractor { 9 | public fun ()V 10 | protected abstract fun doWork (Lreactivecircus/blueprint/interactor/InteractorParams;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; 11 | public final fun execute (Lreactivecircus/blueprint/interactor/InteractorParams;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; 12 | public abstract fun getDispatcher ()Lkotlinx/coroutines/CoroutineDispatcher; 13 | } 14 | 15 | -------------------------------------------------------------------------------- /blueprint-interactor-coroutines/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | `blueprint-plugin` 3 | kotlin("jvm") 4 | id("com.vanniktech.maven.publish") 5 | id("org.jetbrains.dokka") 6 | } 7 | 8 | blueprint { 9 | enableExplicitApi.set(true) 10 | } 11 | 12 | dependencies { 13 | api(project(":blueprint-interactor-common")) 14 | 15 | // Coroutines 16 | implementation(libs.kotlinx.coroutines.core) 17 | 18 | // Unit tests 19 | testImplementation(libs.junit) 20 | testImplementation(libs.truth) 21 | testImplementation(libs.kotlinx.coroutines.test) 22 | } 23 | -------------------------------------------------------------------------------- /blueprint-interactor-coroutines/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_ARTIFACT_ID=blueprint-interactor-coroutines 2 | POM_NAME=Blueprint Interactor Coroutines 3 | POM_DESCRIPTION=Interactors (use case in Clean Architecture) based on Kotlin Coroutines 4 | POM_PACKAGING=jar 5 | -------------------------------------------------------------------------------- /blueprint-interactor-coroutines/src/main/kotlin/reactivecircus/blueprint/interactor/coroutines/FlowInteractor.kt: -------------------------------------------------------------------------------- 1 | package reactivecircus.blueprint.interactor.coroutines 2 | 3 | import kotlinx.coroutines.CoroutineDispatcher 4 | import kotlinx.coroutines.flow.Flow 5 | import kotlinx.coroutines.flow.flowOn 6 | import reactivecircus.blueprint.interactor.InteractorParams 7 | 8 | /** 9 | * An interactor (use case in Clean Architecture) represents an execution unit of asynchronous work. 10 | * A [FlowInteractor] exposes a cold stream of values implemented with Kotlin [Flow]. 11 | * 12 | * Work will be executed on thread as specified by the [dispatcher] of the interactor. 13 | */ 14 | public abstract class FlowInteractor { 15 | 16 | /** 17 | * The coroutine context this interactor should execute on. 18 | */ 19 | public abstract val dispatcher: CoroutineDispatcher 20 | 21 | /** 22 | * Create a [Flow] for this interactor. 23 | */ 24 | protected abstract fun createFlow(params: P): Flow 25 | 26 | /** 27 | * Build a new [Flow] from this interactor. 28 | */ 29 | public fun buildFlow(params: P): Flow = createFlow(params).flowOn(dispatcher) 30 | } 31 | -------------------------------------------------------------------------------- /blueprint-interactor-coroutines/src/main/kotlin/reactivecircus/blueprint/interactor/coroutines/SuspendingInteractor.kt: -------------------------------------------------------------------------------- 1 | package reactivecircus.blueprint.interactor.coroutines 2 | 3 | import kotlinx.coroutines.CoroutineDispatcher 4 | import kotlinx.coroutines.withContext 5 | import reactivecircus.blueprint.interactor.InteractorParams 6 | 7 | /** 8 | * An interactor (use case in Clean Architecture) represents an execution unit of asynchronous work. 9 | * A [SuspendingInteractor] returns a single response through a suspend function. 10 | * 11 | * Work will be executed on thread as specified by the [dispatcher] of the interactor. 12 | */ 13 | public abstract class SuspendingInteractor { 14 | 15 | /** 16 | * The coroutine context this interactor should execute on. 17 | */ 18 | public abstract val dispatcher: CoroutineDispatcher 19 | 20 | /** 21 | * Define the work to be performed by this interactor. 22 | */ 23 | protected abstract suspend fun doWork(params: P): R 24 | 25 | /** 26 | * Execute the the interactor. 27 | */ 28 | public suspend fun execute(params: P): R = withContext(context = dispatcher) { 29 | doWork(params) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /blueprint-interactor-coroutines/src/test/kotlin/reactivecircus/blueprint/interactor/coroutines/FlowInteractorTest.kt: -------------------------------------------------------------------------------- 1 | package reactivecircus.blueprint.interactor.coroutines 2 | 3 | import com.google.common.truth.Truth.assertThat 4 | import java.io.IOException 5 | import kotlinx.coroutines.ExperimentalCoroutinesApi 6 | import kotlinx.coroutines.flow.collect 7 | import kotlinx.coroutines.flow.toList 8 | import kotlinx.coroutines.test.StandardTestDispatcher 9 | import kotlinx.coroutines.test.runTest 10 | import org.junit.Test 11 | import reactivecircus.blueprint.interactor.EmptyParams 12 | 13 | @ExperimentalCoroutinesApi 14 | class FlowInteractorTest { 15 | 16 | private val testDispatcher = StandardTestDispatcher() 17 | 18 | @Test 19 | fun `emits each value when flow interactor produces multiple values`() = 20 | runTest(testDispatcher) { 21 | val results = FlowInteractorWithThreeEmissions(testDispatcher) 22 | .buildFlow(EmptyParams) 23 | .toList() 24 | 25 | assertThat(results) 26 | .isEqualTo(listOf(0, 1, 2)) 27 | } 28 | 29 | @Test(expected = IOException::class) 30 | fun `catches exception thrown by flow interactor`() = runTest(testDispatcher) { 31 | FlowInteractorWithException(testDispatcher) 32 | .buildFlow(EmptyParams) 33 | .collect() 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /blueprint-interactor-coroutines/src/test/kotlin/reactivecircus/blueprint/interactor/coroutines/SuspendingInteractorTest.kt: -------------------------------------------------------------------------------- 1 | package reactivecircus.blueprint.interactor.coroutines 2 | 3 | import com.google.common.truth.Truth.assertThat 4 | import java.io.IOException 5 | import kotlinx.coroutines.ExperimentalCoroutinesApi 6 | import kotlinx.coroutines.test.StandardTestDispatcher 7 | import kotlinx.coroutines.test.runTest 8 | import org.junit.Test 9 | import reactivecircus.blueprint.interactor.EmptyParams 10 | 11 | @ExperimentalCoroutinesApi 12 | class SuspendingInteractorTest { 13 | 14 | private val testDispatcher = StandardTestDispatcher() 15 | 16 | @Test 17 | fun `returns result when interactor executed successfully`() = runTest(testDispatcher) { 18 | assertThat(CalculateSquare(testDispatcher).execute(CalculateSquare.Params(3))) 19 | .isEqualTo(9) 20 | } 21 | 22 | @Test(expected = IOException::class) 23 | fun `throws exception when interactor execution failed`() = runTest(testDispatcher) { 24 | FailingSuspendingInteractor(testDispatcher).execute(EmptyParams) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /blueprint-interactor-coroutines/src/test/kotlin/reactivecircus/blueprint/interactor/coroutines/TestCoroutineInteractors.kt: -------------------------------------------------------------------------------- 1 | package reactivecircus.blueprint.interactor.coroutines 2 | 3 | import kotlinx.coroutines.CoroutineDispatcher 4 | import kotlinx.coroutines.delay 5 | import kotlinx.coroutines.flow.Flow 6 | import kotlinx.coroutines.flow.flow 7 | import reactivecircus.blueprint.interactor.EmptyParams 8 | import reactivecircus.blueprint.interactor.InteractorParams 9 | import java.io.IOException 10 | 11 | class CalculateSquare( 12 | override val dispatcher: CoroutineDispatcher 13 | ) : SuspendingInteractor() { 14 | override suspend fun doWork(params: Params): Int { 15 | delay(1000L) 16 | return params.value * params.value 17 | } 18 | 19 | class Params(val value: Int) : InteractorParams 20 | } 21 | 22 | class FailingSuspendingInteractor( 23 | override val dispatcher: CoroutineDispatcher 24 | ) : SuspendingInteractor() { 25 | override suspend fun doWork(params: EmptyParams) { 26 | delay(1000L) 27 | throw IOException() 28 | } 29 | } 30 | 31 | class FlowInteractorWithThreeEmissions( 32 | override val dispatcher: CoroutineDispatcher 33 | ) : FlowInteractor() { 34 | override fun createFlow(params: EmptyParams): Flow { 35 | return flow { 36 | delay(1000L) 37 | repeat(3) { 38 | emit(it) 39 | } 40 | } 41 | } 42 | } 43 | 44 | class FlowInteractorWithException( 45 | override val dispatcher: CoroutineDispatcher 46 | ) : FlowInteractor() { 47 | override fun createFlow(params: EmptyParams): Flow { 48 | return flow { 49 | delay(1000L) 50 | throw IOException() 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /blueprint-interactor-rx2/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /blueprint-interactor-rx2/api/blueprint-interactor-rx2.api: -------------------------------------------------------------------------------- 1 | public abstract class reactivecircus/blueprint/interactor/rx2/CompletableInteractor { 2 | public fun (Lio/reactivex/Scheduler;Lio/reactivex/Scheduler;)V 3 | public final fun buildCompletable (Lreactivecircus/blueprint/interactor/InteractorParams;)Lio/reactivex/Completable; 4 | protected abstract fun createInteractor (Lreactivecircus/blueprint/interactor/InteractorParams;)Lio/reactivex/Completable; 5 | } 6 | 7 | public abstract class reactivecircus/blueprint/interactor/rx2/ObservableInteractor { 8 | public fun (Lio/reactivex/Scheduler;Lio/reactivex/Scheduler;)V 9 | public final fun buildObservable (Lreactivecircus/blueprint/interactor/InteractorParams;)Lio/reactivex/Observable; 10 | protected abstract fun createInteractor (Lreactivecircus/blueprint/interactor/InteractorParams;)Lio/reactivex/Observable; 11 | } 12 | 13 | public abstract class reactivecircus/blueprint/interactor/rx2/SingleInteractor { 14 | public fun (Lio/reactivex/Scheduler;Lio/reactivex/Scheduler;)V 15 | public final fun buildSingle (Lreactivecircus/blueprint/interactor/InteractorParams;Z)Lio/reactivex/Single; 16 | public static synthetic fun buildSingle$default (Lreactivecircus/blueprint/interactor/rx2/SingleInteractor;Lreactivecircus/blueprint/interactor/InteractorParams;ZILjava/lang/Object;)Lio/reactivex/Single; 17 | protected abstract fun createInteractor (Lreactivecircus/blueprint/interactor/InteractorParams;)Lio/reactivex/Single; 18 | } 19 | 20 | -------------------------------------------------------------------------------- /blueprint-interactor-rx2/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | `blueprint-plugin` 3 | kotlin("jvm") 4 | id("com.vanniktech.maven.publish") 5 | id("org.jetbrains.dokka") 6 | } 7 | 8 | blueprint { 9 | enableExplicitApi.set(true) 10 | } 11 | 12 | dependencies { 13 | api(project(":blueprint-interactor-common")) 14 | 15 | // rx 16 | implementation(libs.rxJava2) 17 | 18 | // Unit tests 19 | testImplementation(libs.junit) 20 | testImplementation(libs.truth) 21 | } 22 | -------------------------------------------------------------------------------- /blueprint-interactor-rx2/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_ARTIFACT_ID=blueprint-interactor-rx2 2 | POM_NAME=Blueprint Interactor RxJava 2 3 | POM_DESCRIPTION=Interactors (use case in Clean Architecture) based on RxJava 2 4 | POM_PACKAGING=jar 5 | -------------------------------------------------------------------------------- /blueprint-interactor-rx2/src/main/kotlin/reactivecircus/blueprint/interactor/rx2/CompletableInteractor.kt: -------------------------------------------------------------------------------- 1 | package reactivecircus.blueprint.interactor.rx2 2 | 3 | import io.reactivex.Completable 4 | import io.reactivex.Scheduler 5 | import reactivecircus.blueprint.interactor.InteractorParams 6 | 7 | /** 8 | * Abstract class for a use case, representing an execution unit of asynchronous work. 9 | * This use case type uses [Completable] as the return type. 10 | * Upon subscription a use case will execute its job in the thread specified by the [ioScheduler]. 11 | * and will post the result to the thread specified by [uiScheduler]. 12 | */ 13 | public abstract class CompletableInteractor

( 14 | private val ioScheduler: Scheduler, 15 | private val uiScheduler: Scheduler 16 | ) { 17 | 18 | /** 19 | * Create a [Completable] for this interactor. 20 | */ 21 | protected abstract fun createInteractor(params: P): Completable 22 | 23 | /** 24 | * Build a use case with the provided execution thread and post execution thread 25 | */ 26 | public fun buildCompletable(params: P): Completable { 27 | return createInteractor(params) 28 | .subscribeOn(ioScheduler) 29 | .observeOn(uiScheduler) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /blueprint-interactor-rx2/src/main/kotlin/reactivecircus/blueprint/interactor/rx2/ObservableInteractor.kt: -------------------------------------------------------------------------------- 1 | package reactivecircus.blueprint.interactor.rx2 2 | 3 | import io.reactivex.Observable 4 | import io.reactivex.Scheduler 5 | import reactivecircus.blueprint.interactor.InteractorParams 6 | 7 | /** 8 | * Abstract class for a use case, representing an execution unit of asynchronous work. 9 | * This use case type uses [Observable] as the return type. 10 | * Upon subscription a use case will execute its job in the thread specified by the [ioScheduler]. 11 | * and will post the result to the thread specified by [uiScheduler]. 12 | */ 13 | public abstract class ObservableInteractor

( 14 | private val ioScheduler: Scheduler, 15 | private val uiScheduler: Scheduler 16 | ) { 17 | 18 | /** 19 | * Create a [Observable] for this interactor. 20 | */ 21 | protected abstract fun createInteractor(params: P): Observable 22 | 23 | /** 24 | * Build a use case with the provided execution thread and post execution thread 25 | */ 26 | public fun buildObservable(params: P): Observable { 27 | return createInteractor(params) 28 | .subscribeOn(ioScheduler) 29 | .observeOn(uiScheduler) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /blueprint-interactor-rx2/src/main/kotlin/reactivecircus/blueprint/interactor/rx2/SingleInteractor.kt: -------------------------------------------------------------------------------- 1 | package reactivecircus.blueprint.interactor.rx2 2 | 3 | import io.reactivex.Scheduler 4 | import io.reactivex.Single 5 | import reactivecircus.blueprint.interactor.InteractorParams 6 | 7 | /** 8 | * Abstract class for a use case, representing an execution unit of asynchronous work. 9 | * This use case type uses [Single] as the return type. 10 | * Upon subscription a use case will execute its job in the thread specified by the [ioScheduler]. 11 | * and will post the result to the thread specified by [uiScheduler]. 12 | */ 13 | public abstract class SingleInteractor

( 14 | private val ioScheduler: Scheduler, 15 | private val uiScheduler: Scheduler 16 | ) { 17 | 18 | /** 19 | * Create a [Single] for this interactor. 20 | */ 21 | protected abstract fun createInteractor(params: P): Single 22 | 23 | /** 24 | * Build a use case with the provided execution thread and post execution thread 25 | * @param params - parameters required for this interactor 26 | * @param blocking - when set to true the single will be subscribed and observed on the current thread 27 | */ 28 | public fun buildSingle(params: P, blocking: Boolean = false): Single { 29 | return if (blocking) { 30 | createInteractor(params) 31 | } else { 32 | createInteractor(params) 33 | .subscribeOn(ioScheduler) 34 | .observeOn(uiScheduler) 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /blueprint-interactor-rx2/src/test/kotlin/reactivecircus/blueprint/interactor/rx2/CompletableInteractorTest.kt: -------------------------------------------------------------------------------- 1 | package reactivecircus.blueprint.interactor.rx2 2 | 3 | import io.reactivex.Completable 4 | import io.reactivex.Scheduler 5 | import io.reactivex.observers.TestObserver 6 | import io.reactivex.schedulers.TestScheduler 7 | import org.junit.Test 8 | import reactivecircus.blueprint.interactor.EmptyParams 9 | import reactivecircus.blueprint.interactor.InteractorParams 10 | 11 | class CompletableInteractorTest { 12 | 13 | @Suppress("ForbiddenVoid") 14 | private lateinit var testObserver: TestObserver 15 | 16 | private val ioScheduler = TestScheduler() 17 | private val uiScheduler = TestScheduler() 18 | 19 | private val interactor = CompletableInteractorImpl( 20 | ioScheduler = ioScheduler, 21 | uiScheduler = uiScheduler 22 | ) 23 | private val emptyParamsInteractor = EmptyParamsCompletableInteractorImpl( 24 | ioScheduler = ioScheduler, 25 | uiScheduler = uiScheduler 26 | ) 27 | 28 | @Test 29 | fun `complete or fail based on the underlying Completable implementation`() { 30 | testObserver = interactor.buildCompletable(CompletableParams(shouldFail = false)).test() 31 | 32 | ioScheduler.triggerActions() 33 | uiScheduler.triggerActions() 34 | 35 | testObserver.await() 36 | .assertComplete() 37 | .assertNoErrors() 38 | 39 | testObserver = interactor.buildCompletable(CompletableParams(shouldFail = true)).test() 40 | 41 | ioScheduler.triggerActions() 42 | uiScheduler.triggerActions() 43 | 44 | testObserver.await() 45 | .assertError(IllegalArgumentException::class.java) 46 | } 47 | 48 | @Test 49 | fun `complete asynchronously`() { 50 | testObserver = emptyParamsInteractor.buildCompletable(EmptyParams).test() 51 | 52 | ioScheduler.triggerActions() 53 | uiScheduler.triggerActions() 54 | 55 | testObserver.await() 56 | .assertComplete() 57 | .assertNoErrors() 58 | } 59 | 60 | private inner class CompletableInteractorImpl( 61 | ioScheduler: Scheduler, 62 | uiScheduler: Scheduler 63 | ) : 64 | CompletableInteractor( 65 | ioScheduler, 66 | uiScheduler 67 | ) { 68 | override fun createInteractor(params: CompletableParams): Completable { 69 | return if (params.shouldFail) { 70 | Completable.error(IllegalArgumentException()) 71 | } else { 72 | Completable.complete() 73 | } 74 | } 75 | } 76 | 77 | private inner class CompletableParams(val shouldFail: Boolean) : InteractorParams 78 | 79 | private inner class EmptyParamsCompletableInteractorImpl( 80 | ioScheduler: Scheduler, 81 | uiScheduler: Scheduler 82 | ) : CompletableInteractor( 83 | ioScheduler, 84 | uiScheduler 85 | ) { 86 | override fun createInteractor(params: EmptyParams): Completable { 87 | return Completable.complete() 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /blueprint-interactor-rx2/src/test/kotlin/reactivecircus/blueprint/interactor/rx2/ObservableInteractorTest.kt: -------------------------------------------------------------------------------- 1 | package reactivecircus.blueprint.interactor.rx2 2 | 3 | import io.reactivex.Observable 4 | import io.reactivex.Scheduler 5 | import io.reactivex.observers.TestObserver 6 | import io.reactivex.schedulers.TestScheduler 7 | import org.junit.Test 8 | import reactivecircus.blueprint.interactor.EmptyParams 9 | import reactivecircus.blueprint.interactor.InteractorParams 10 | 11 | class ObservableInteractorTest { 12 | 13 | private val dummyResult = "result" 14 | 15 | private lateinit var testObserver: TestObserver 16 | 17 | private val ioScheduler = TestScheduler() 18 | private val uiScheduler = TestScheduler() 19 | 20 | private val interactor = ObservableInteractorImpl( 21 | ioScheduler = ioScheduler, 22 | uiScheduler = uiScheduler 23 | ) 24 | private val emptyParamsInteractor = EmptyParamsObservableInteractorImpl( 25 | ioScheduler = ioScheduler, 26 | uiScheduler = uiScheduler 27 | ) 28 | 29 | @Test 30 | fun `emit value or fail based on the underlying Observable implementation`() { 31 | testObserver = interactor.buildObservable(ObservableParams(shouldFail = false)).test() 32 | 33 | ioScheduler.triggerActions() 34 | uiScheduler.triggerActions() 35 | 36 | testObserver.await() 37 | .assertValue(dummyResult) 38 | .assertNoErrors() 39 | 40 | testObserver = interactor.buildObservable(ObservableParams(shouldFail = true)).test() 41 | 42 | ioScheduler.triggerActions() 43 | uiScheduler.triggerActions() 44 | 45 | testObserver.await() 46 | .assertNoValues() 47 | .assertError(IllegalArgumentException::class.java) 48 | } 49 | 50 | @Test 51 | fun `emit value asynchronously`() { 52 | testObserver = emptyParamsInteractor.buildObservable(EmptyParams).test() 53 | 54 | ioScheduler.triggerActions() 55 | uiScheduler.triggerActions() 56 | 57 | testObserver.await() 58 | .assertValue(dummyResult) 59 | .assertNoErrors() 60 | } 61 | 62 | private inner class ObservableInteractorImpl( 63 | ioScheduler: Scheduler, 64 | uiScheduler: Scheduler 65 | ) : 66 | ObservableInteractor( 67 | ioScheduler, 68 | uiScheduler 69 | ) { 70 | override fun createInteractor(params: ObservableParams): Observable { 71 | return if (params.shouldFail) { 72 | Observable.error(IllegalArgumentException()) 73 | } else { 74 | Observable.just(dummyResult) 75 | } 76 | } 77 | } 78 | 79 | private inner class ObservableParams(val shouldFail: Boolean) : InteractorParams 80 | 81 | private inner class EmptyParamsObservableInteractorImpl( 82 | ioScheduler: Scheduler, 83 | uiScheduler: Scheduler 84 | ) : ObservableInteractor( 85 | ioScheduler, 86 | uiScheduler 87 | ) { 88 | override fun createInteractor(params: EmptyParams): Observable { 89 | return Observable.just(dummyResult) 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /blueprint-interactor-rx3/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /blueprint-interactor-rx3/api/blueprint-interactor-rx3.api: -------------------------------------------------------------------------------- 1 | public abstract class reactivecircus/blueprint/interactor/rx3/CompletableInteractor { 2 | public fun (Lio/reactivex/rxjava3/core/Scheduler;Lio/reactivex/rxjava3/core/Scheduler;)V 3 | public final fun buildCompletable (Lreactivecircus/blueprint/interactor/InteractorParams;)Lio/reactivex/rxjava3/core/Completable; 4 | protected abstract fun createInteractor (Lreactivecircus/blueprint/interactor/InteractorParams;)Lio/reactivex/rxjava3/core/Completable; 5 | } 6 | 7 | public abstract class reactivecircus/blueprint/interactor/rx3/ObservableInteractor { 8 | public fun (Lio/reactivex/rxjava3/core/Scheduler;Lio/reactivex/rxjava3/core/Scheduler;)V 9 | public final fun buildObservable (Lreactivecircus/blueprint/interactor/InteractorParams;)Lio/reactivex/rxjava3/core/Observable; 10 | protected abstract fun createInteractor (Lreactivecircus/blueprint/interactor/InteractorParams;)Lio/reactivex/rxjava3/core/Observable; 11 | } 12 | 13 | public abstract class reactivecircus/blueprint/interactor/rx3/SingleInteractor { 14 | public fun (Lio/reactivex/rxjava3/core/Scheduler;Lio/reactivex/rxjava3/core/Scheduler;)V 15 | public final fun buildSingle (Lreactivecircus/blueprint/interactor/InteractorParams;Z)Lio/reactivex/rxjava3/core/Single; 16 | public static synthetic fun buildSingle$default (Lreactivecircus/blueprint/interactor/rx3/SingleInteractor;Lreactivecircus/blueprint/interactor/InteractorParams;ZILjava/lang/Object;)Lio/reactivex/rxjava3/core/Single; 17 | protected abstract fun createInteractor (Lreactivecircus/blueprint/interactor/InteractorParams;)Lio/reactivex/rxjava3/core/Single; 18 | } 19 | 20 | -------------------------------------------------------------------------------- /blueprint-interactor-rx3/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | `blueprint-plugin` 3 | kotlin("jvm") 4 | id("com.vanniktech.maven.publish") 5 | id("org.jetbrains.dokka") 6 | } 7 | 8 | blueprint { 9 | enableExplicitApi.set(true) 10 | } 11 | 12 | dependencies { 13 | api(project(":blueprint-interactor-common")) 14 | 15 | // rx 16 | implementation(libs.rxJava3) 17 | 18 | // Unit tests 19 | testImplementation(libs.junit) 20 | testImplementation(libs.truth) 21 | } 22 | -------------------------------------------------------------------------------- /blueprint-interactor-rx3/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_ARTIFACT_ID=blueprint-interactor-rx3 2 | POM_NAME=Blueprint Interactor RxJava 3 3 | POM_DESCRIPTION=Interactors (use case in Clean Architecture) based on RxJava 3 4 | POM_PACKAGING=jar 5 | -------------------------------------------------------------------------------- /blueprint-interactor-rx3/src/main/kotlin/reactivecircus/blueprint/interactor/rx3/CompletableInteractor.kt: -------------------------------------------------------------------------------- 1 | package reactivecircus.blueprint.interactor.rx3 2 | 3 | import io.reactivex.rxjava3.core.Completable 4 | import io.reactivex.rxjava3.core.Scheduler 5 | import reactivecircus.blueprint.interactor.InteractorParams 6 | 7 | /** 8 | * Abstract class for a use case, representing an execution unit of asynchronous work. 9 | * This use case type uses [Completable] as the return type. 10 | * Upon subscription a use case will execute its job in the thread specified by the [ioScheduler]. 11 | * and will post the result to the thread specified by [uiScheduler]. 12 | */ 13 | public abstract class CompletableInteractor

( 14 | private val ioScheduler: Scheduler, 15 | private val uiScheduler: Scheduler 16 | ) { 17 | 18 | /** 19 | * Create a [Completable] for this interactor. 20 | */ 21 | protected abstract fun createInteractor(params: P): Completable 22 | 23 | /** 24 | * Build a use case with the provided execution thread and post execution thread 25 | */ 26 | public fun buildCompletable(params: P): Completable { 27 | return createInteractor(params) 28 | .subscribeOn(ioScheduler) 29 | .observeOn(uiScheduler) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /blueprint-interactor-rx3/src/main/kotlin/reactivecircus/blueprint/interactor/rx3/ObservableInteractor.kt: -------------------------------------------------------------------------------- 1 | package reactivecircus.blueprint.interactor.rx3 2 | 3 | import io.reactivex.rxjava3.core.Observable 4 | import io.reactivex.rxjava3.core.Scheduler 5 | import reactivecircus.blueprint.interactor.InteractorParams 6 | 7 | /** 8 | * Abstract class for a use case, representing an execution unit of asynchronous work. 9 | * This use case type uses [Observable] as the return type. 10 | * Upon subscription a use case will execute its job in the thread specified by the [ioScheduler]. 11 | * and will post the result to the thread specified by [uiScheduler]. 12 | */ 13 | public abstract class ObservableInteractor

( 14 | private val ioScheduler: Scheduler, 15 | private val uiScheduler: Scheduler 16 | ) { 17 | 18 | /** 19 | * Create a [Observable] for this interactor. 20 | */ 21 | protected abstract fun createInteractor(params: P): Observable 22 | 23 | /** 24 | * Build a use case with the provided execution thread and post execution thread 25 | */ 26 | public fun buildObservable(params: P): Observable { 27 | return createInteractor(params) 28 | .subscribeOn(ioScheduler) 29 | .observeOn(uiScheduler) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /blueprint-interactor-rx3/src/main/kotlin/reactivecircus/blueprint/interactor/rx3/SingleInteractor.kt: -------------------------------------------------------------------------------- 1 | package reactivecircus.blueprint.interactor.rx3 2 | 3 | import io.reactivex.rxjava3.core.Scheduler 4 | import io.reactivex.rxjava3.core.Single 5 | import reactivecircus.blueprint.interactor.InteractorParams 6 | 7 | /** 8 | * Abstract class for a use case, representing an execution unit of asynchronous work. 9 | * This use case type uses [Single] as the return type. 10 | * Upon subscription a use case will execute its job in the thread specified by the [ioScheduler]. 11 | * and will post the result to the thread specified by [uiScheduler]. 12 | */ 13 | public abstract class SingleInteractor

( 14 | private val ioScheduler: Scheduler, 15 | private val uiScheduler: Scheduler 16 | ) { 17 | 18 | /** 19 | * Create a [Single] for this interactor. 20 | */ 21 | protected abstract fun createInteractor(params: P): Single 22 | 23 | /** 24 | * Build a use case with the provided execution thread and post execution thread 25 | * @param params - parameters required for this interactor 26 | * @param blocking - when set to true the single will be subscribed and observed on the current thread 27 | */ 28 | public fun buildSingle(params: P, blocking: Boolean = false): Single { 29 | return if (blocking) { 30 | createInteractor(params) 31 | } else { 32 | createInteractor(params) 33 | .subscribeOn(ioScheduler) 34 | .observeOn(uiScheduler) 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /blueprint-interactor-rx3/src/test/kotlin/reactivecircus/blueprint/interactor/rx3/CompletableInteractorTest.kt: -------------------------------------------------------------------------------- 1 | package reactivecircus.blueprint.interactor.rx3 2 | 3 | import io.reactivex.rxjava3.core.Completable 4 | import io.reactivex.rxjava3.core.Scheduler 5 | import io.reactivex.rxjava3.observers.TestObserver 6 | import io.reactivex.rxjava3.schedulers.TestScheduler 7 | import org.junit.Test 8 | import reactivecircus.blueprint.interactor.EmptyParams 9 | import reactivecircus.blueprint.interactor.InteractorParams 10 | 11 | class CompletableInteractorTest { 12 | 13 | @Suppress("ForbiddenVoid") 14 | private lateinit var testObserver: TestObserver 15 | 16 | private val ioScheduler = TestScheduler() 17 | private val uiScheduler = TestScheduler() 18 | 19 | private val interactor = CompletableInteractorImpl( 20 | ioScheduler = ioScheduler, 21 | uiScheduler = uiScheduler 22 | ) 23 | private val emptyParamsInteractor = EmptyParamsCompletableInteractorImpl( 24 | ioScheduler = ioScheduler, 25 | uiScheduler = uiScheduler 26 | ) 27 | 28 | @Test 29 | fun `complete or fail based on the underlying Completable implementation`() { 30 | testObserver = interactor.buildCompletable(CompletableParams(shouldFail = false)).test() 31 | 32 | ioScheduler.triggerActions() 33 | uiScheduler.triggerActions() 34 | 35 | testObserver.await() 36 | .assertComplete() 37 | .assertNoErrors() 38 | 39 | testObserver = interactor.buildCompletable(CompletableParams(shouldFail = true)).test() 40 | 41 | ioScheduler.triggerActions() 42 | uiScheduler.triggerActions() 43 | 44 | testObserver.await() 45 | .assertError(IllegalArgumentException::class.java) 46 | } 47 | 48 | @Test 49 | fun `complete asynchronously`() { 50 | testObserver = emptyParamsInteractor.buildCompletable(EmptyParams).test() 51 | 52 | ioScheduler.triggerActions() 53 | uiScheduler.triggerActions() 54 | 55 | testObserver.await() 56 | .assertComplete() 57 | .assertNoErrors() 58 | } 59 | 60 | private inner class CompletableInteractorImpl( 61 | ioScheduler: Scheduler, 62 | uiScheduler: Scheduler 63 | ) : 64 | CompletableInteractor( 65 | ioScheduler, 66 | uiScheduler 67 | ) { 68 | override fun createInteractor(params: CompletableParams): Completable { 69 | return if (params.shouldFail) { 70 | Completable.error(IllegalArgumentException()) 71 | } else { 72 | Completable.complete() 73 | } 74 | } 75 | } 76 | 77 | private inner class CompletableParams(val shouldFail: Boolean) : InteractorParams 78 | 79 | private inner class EmptyParamsCompletableInteractorImpl( 80 | ioScheduler: Scheduler, 81 | uiScheduler: Scheduler 82 | ) : CompletableInteractor( 83 | ioScheduler, 84 | uiScheduler 85 | ) { 86 | override fun createInteractor(params: EmptyParams): Completable { 87 | return Completable.complete() 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /blueprint-testing-robot/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /blueprint-testing-robot/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | `blueprint-plugin` 3 | id("com.android.library") 4 | kotlin("android") 5 | id("com.vanniktech.maven.publish") 6 | id("org.jetbrains.dokka") 7 | } 8 | 9 | blueprint { 10 | enableExplicitApi.set(true) 11 | } 12 | 13 | android { 14 | namespace = "reactivecircus.blueprint.testing" 15 | defaultConfig { 16 | testApplicationId = "reactivecircus.blueprint.testing.testapp" 17 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 18 | } 19 | } 20 | 21 | dependencies { 22 | // Coroutines 23 | implementation(libs.kotlinx.coroutines.core) 24 | 25 | // AndroidX 26 | implementation(libs.androidx.fragment.ktx) 27 | implementation(libs.androidx.recyclerView) 28 | 29 | // Material components 30 | implementation(libs.material) 31 | 32 | implementation(libs.truth) 33 | implementation(libs.androidx.test.core) 34 | implementation(libs.androidx.espresso.core) 35 | implementation(libs.androidx.espresso.contrib) 36 | implementation(libs.androidx.espresso.intents) 37 | 38 | // Android tests 39 | debugImplementation(libs.androidx.fragment.testing) { 40 | exclude(group = "androidx.test") 41 | } 42 | androidTestImplementation(libs.androidx.test.monitor) 43 | androidTestImplementation(libs.androidx.test.runner) 44 | androidTestImplementation(libs.androidx.test.rules) 45 | androidTestImplementation(libs.androidx.test.ext.junit) 46 | } 47 | -------------------------------------------------------------------------------- /blueprint-testing-robot/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_ARTIFACT_ID=blueprint-testing-robot 2 | POM_NAME=Blueprint Testing Robot 3 | POM_DESCRIPTION=Android UI testing framework with Testing Robot DSL 4 | POM_PACKAGING=aar 5 | -------------------------------------------------------------------------------- /blueprint-testing-robot/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /blueprint-testing-robot/src/main/kotlin/reactivecircus/blueprint/testing/Instrumentation.kt: -------------------------------------------------------------------------------- 1 | package reactivecircus.blueprint.testing 2 | 3 | import android.app.Activity 4 | import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation 5 | import androidx.test.runner.lifecycle.ActivityLifecycleMonitorRegistry 6 | import androidx.test.runner.lifecycle.Stage.RESUMED 7 | import java.util.concurrent.atomic.AtomicReference 8 | 9 | /** 10 | * Finds the activity in the foreground (if any). 11 | */ 12 | public fun currentActivity(): Activity? { 13 | val currentActivityReference = AtomicReference() 14 | getInstrumentation().runOnMainSync { 15 | val resumedActivities = ActivityLifecycleMonitorRegistry.getInstance() 16 | .getActivitiesInStage(RESUMED) 17 | if (resumedActivities.iterator().hasNext()) { 18 | currentActivityReference.set(resumedActivities.iterator().next() as Activity) 19 | } 20 | } 21 | 22 | return currentActivityReference.get() 23 | } 24 | -------------------------------------------------------------------------------- /blueprint-testing-robot/src/main/kotlin/reactivecircus/blueprint/testing/RepeatRule.kt: -------------------------------------------------------------------------------- 1 | package reactivecircus.blueprint.testing 2 | 3 | import org.junit.rules.TestRule 4 | import org.junit.runner.Description 5 | import org.junit.runners.model.Statement 6 | 7 | /** 8 | * TestRule to execute tests multiple times. 9 | * This can be used to debug flaky tests. 10 | */ 11 | public class RepeatRule(private val iterations: Int) : TestRule { 12 | 13 | init { 14 | require(iterations > 0) { "iterations < 1: $iterations" } 15 | } 16 | 17 | override fun apply(base: Statement, description: Description): Statement { 18 | return object : Statement() { 19 | @Throws(Throwable::class) 20 | override fun evaluate() { 21 | repeat(iterations) { 22 | base.evaluate() 23 | } 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /blueprint-testing-robot/src/main/kotlin/reactivecircus/blueprint/testing/ScreenRobot.kt: -------------------------------------------------------------------------------- 1 | package reactivecircus.blueprint.testing 2 | 3 | /** 4 | * Base class for implementing a robot DSL. 5 | */ 6 | public abstract class ScreenRobot( 7 | private val robotActions: A, 8 | private val robotAssertions: S 9 | ) { 10 | public fun given(block: () -> Unit): Unit = block() 11 | public fun perform(block: A.() -> Unit): A = robotActions.apply { block() } 12 | public fun check(block: S.() -> Unit): S = robotAssertions.apply { block() } 13 | } 14 | 15 | /** 16 | * Robot actions for performing common view actions. 17 | */ 18 | public interface RobotActions 19 | 20 | /** 21 | * Robot assertions for performing common view assertions. 22 | */ 23 | public interface RobotAssertions 24 | -------------------------------------------------------------------------------- /blueprint-testing-robot/src/main/kotlin/reactivecircus/blueprint/testing/action/CheckableRobotActions.kt: -------------------------------------------------------------------------------- 1 | package reactivecircus.blueprint.testing.action 2 | 3 | import androidx.annotation.IdRes 4 | import androidx.test.espresso.Espresso 5 | import androidx.test.espresso.action.ViewActions 6 | import androidx.test.espresso.matcher.ViewMatchers 7 | import org.hamcrest.core.AllOf 8 | import reactivecircus.blueprint.testing.RobotActions 9 | import reactivecircus.blueprint.testing.scrollTo 10 | 11 | /** 12 | * Click on the radio button with [buttonText] 13 | * within the radio group associated with [radioGroupId]. 14 | */ 15 | public fun RobotActions.clickRadioButton(@IdRes radioGroupId: Int, buttonText: String) { 16 | scrollTo(buttonText) 17 | Espresso.onView( 18 | AllOf.allOf( 19 | ViewMatchers.withParent( 20 | AllOf.allOf(ViewMatchers.isDisplayed(), ViewMatchers.withId(radioGroupId)) 21 | ), 22 | AllOf.allOf(ViewMatchers.isDisplayed(), ViewMatchers.withText(buttonText)) 23 | ) 24 | ).perform(ViewActions.click()) 25 | } 26 | 27 | /** 28 | * Select the checkbox with [text] within the view group associated with [layoutId]. 29 | */ 30 | public fun RobotActions.selectCheckBox(@IdRes layoutId: Int, text: String) { 31 | scrollTo(text) 32 | Espresso.onView( 33 | AllOf.allOf( 34 | ViewMatchers.withParent( 35 | AllOf.allOf(ViewMatchers.isDisplayed(), ViewMatchers.withId(layoutId)) 36 | ), 37 | AllOf.allOf(ViewMatchers.isDisplayed(), ViewMatchers.withText(text)) 38 | ) 39 | ).perform(ViewActions.click()) 40 | } 41 | -------------------------------------------------------------------------------- /blueprint-testing-robot/src/main/kotlin/reactivecircus/blueprint/testing/action/DialogRobotActions.kt: -------------------------------------------------------------------------------- 1 | package reactivecircus.blueprint.testing.action 2 | 3 | import androidx.test.espresso.Espresso 4 | import androidx.test.espresso.action.ViewActions 5 | import androidx.test.espresso.matcher.ViewMatchers 6 | 7 | /** 8 | * Click on button 1 of the currently displayed dialog. 9 | */ 10 | public fun clickDialogButton1() { 11 | Espresso.onView(ViewMatchers.withId(android.R.id.button1)).perform(ViewActions.click()) 12 | } 13 | 14 | /** 15 | * Click on button 2 of the currently displayed dialog. 16 | */ 17 | public fun clickDialogButton2() { 18 | Espresso.onView(ViewMatchers.withId(android.R.id.button2)).perform(ViewActions.click()) 19 | } 20 | 21 | /** 22 | * Click on button 3 of the currently displayed dialog. 23 | */ 24 | public fun clickDialogButton3() { 25 | Espresso.onView(ViewMatchers.withId(android.R.id.button3)).perform(ViewActions.click()) 26 | } 27 | -------------------------------------------------------------------------------- /blueprint-testing-robot/src/main/kotlin/reactivecircus/blueprint/testing/action/DrawerRobotActions.kt: -------------------------------------------------------------------------------- 1 | package reactivecircus.blueprint.testing.action 2 | 3 | import androidx.annotation.IdRes 4 | import androidx.test.espresso.Espresso 5 | import androidx.test.espresso.contrib.DrawerActions 6 | import androidx.test.espresso.matcher.ViewMatchers 7 | import reactivecircus.blueprint.testing.RobotActions 8 | 9 | /** 10 | * Open the drawer associated with [drawerId]. 11 | */ 12 | public fun RobotActions.openDrawer(@IdRes drawerId: Int) { 13 | Espresso.onView(ViewMatchers.withId(drawerId)).perform(DrawerActions.open()) 14 | } 15 | 16 | /** 17 | * Close the drawer associated with [drawerId]. 18 | */ 19 | public fun RobotActions.closeDrawer(@IdRes drawerId: Int) { 20 | Espresso.onView(ViewMatchers.withId(drawerId)).perform(DrawerActions.close()) 21 | } 22 | -------------------------------------------------------------------------------- /blueprint-testing-robot/src/main/kotlin/reactivecircus/blueprint/testing/action/GestureRobotActions.kt: -------------------------------------------------------------------------------- 1 | package reactivecircus.blueprint.testing.action 2 | 3 | import androidx.annotation.IdRes 4 | import androidx.test.espresso.Espresso 5 | import androidx.test.espresso.action.ViewActions 6 | import androidx.test.espresso.matcher.ViewMatchers 7 | import reactivecircus.blueprint.testing.RobotActions 8 | 9 | /** 10 | * Click on the view associated with [viewId]. 11 | */ 12 | public fun RobotActions.clickView(@IdRes viewId: Int) { 13 | Espresso.onView(ViewMatchers.withId(viewId)).perform(ViewActions.click()) 14 | } 15 | 16 | /** 17 | * Long-click on the view associated with [viewId]. 18 | */ 19 | public fun RobotActions.longClickView(@IdRes viewId: Int) { 20 | Espresso.onView(ViewMatchers.withId(viewId)).perform(ViewActions.longClick()) 21 | } 22 | 23 | /** 24 | * Swipe left on the view associated with [viewId]. 25 | */ 26 | public fun swipeLeftOnView(@IdRes viewId: Int) { 27 | Espresso.onView(ViewMatchers.withId(viewId)).perform(ViewActions.swipeLeft()) 28 | } 29 | 30 | /** 31 | * Swipe right on the view associated with [viewId]. 32 | */ 33 | public fun swipeRightOnView(@IdRes viewId: Int) { 34 | Espresso.onView(ViewMatchers.withId(viewId)).perform(ViewActions.swipeRight()) 35 | } 36 | 37 | /** 38 | * Swipe up on the view associated with [viewId]. 39 | */ 40 | public fun swipeUpOnView(@IdRes viewId: Int) { 41 | Espresso.onView(ViewMatchers.withId(viewId)).perform(ViewActions.swipeUp()) 42 | } 43 | 44 | /** 45 | * Swipe up down the view associated with [viewId]. 46 | */ 47 | public fun swipeDownOnView(@IdRes viewId: Int) { 48 | Espresso.onView(ViewMatchers.withId(viewId)).perform(ViewActions.swipeDown()) 49 | } 50 | -------------------------------------------------------------------------------- /blueprint-testing-robot/src/main/kotlin/reactivecircus/blueprint/testing/action/KeyboardRobotActions.kt: -------------------------------------------------------------------------------- 1 | package reactivecircus.blueprint.testing.action 2 | 3 | import androidx.annotation.IdRes 4 | import androidx.test.espresso.Espresso 5 | import androidx.test.espresso.action.ViewActions 6 | import androidx.test.espresso.matcher.ViewMatchers 7 | import reactivecircus.blueprint.testing.RobotActions 8 | 9 | /** 10 | * Close the soft keyboard. 11 | */ 12 | public fun RobotActions.closeKeyboard(@IdRes viewId: Int) { 13 | Espresso.onView(ViewMatchers.withId(viewId)).perform(ViewActions.closeSoftKeyboard()) 14 | } 15 | 16 | /** 17 | * Press the action button on the keyboard. 18 | */ 19 | public fun RobotActions.pressKeyboardActionButton(@IdRes viewId: Int) { 20 | Espresso.onView(ViewMatchers.withId(viewId)).perform(ViewActions.pressImeActionButton()) 21 | } 22 | -------------------------------------------------------------------------------- /blueprint-testing-robot/src/main/kotlin/reactivecircus/blueprint/testing/action/RecyclerViewRobotActions.kt: -------------------------------------------------------------------------------- 1 | package reactivecircus.blueprint.testing.action 2 | 3 | import androidx.annotation.IdRes 4 | import androidx.recyclerview.widget.RecyclerView 5 | import androidx.test.espresso.Espresso 6 | import androidx.test.espresso.action.ViewActions 7 | import androidx.test.espresso.contrib.RecyclerViewActions 8 | import androidx.test.espresso.matcher.ViewMatchers 9 | import reactivecircus.blueprint.testing.RobotActions 10 | import reactivecircus.blueprint.testing.scrollToItemInRecyclerView 11 | 12 | /** 13 | * Click on the item at [position] within the recycler view associated with [recyclerViewId]. 14 | */ 15 | public fun RobotActions.clickRecyclerViewItem(@IdRes recyclerViewId: Int, position: Int) { 16 | // scroll to the item to make sure it's visible 17 | scrollToItemInRecyclerView(recyclerViewId, position) 18 | 19 | Espresso.onView(ViewMatchers.withId(recyclerViewId)) 20 | .perform( 21 | RecyclerViewActions.actionOnItemAtPosition( 22 | position, 23 | ViewActions.click() 24 | ) 25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /blueprint-testing-robot/src/main/kotlin/reactivecircus/blueprint/testing/action/SnackbarRobotActions.kt: -------------------------------------------------------------------------------- 1 | package reactivecircus.blueprint.testing.action 2 | 3 | import androidx.test.espresso.Espresso 4 | import androidx.test.espresso.action.ViewActions 5 | import androidx.test.espresso.matcher.ViewMatchers 6 | import reactivecircus.blueprint.testing.RobotActions 7 | import com.google.android.material.R as MaterialR 8 | 9 | /** 10 | * Click the action button on the currently displayed Snackbar. 11 | */ 12 | public fun RobotActions.clickSnackbarActionButton() { 13 | Espresso.onView(ViewMatchers.withId(MaterialR.id.snackbar_action)).perform(ViewActions.click()) 14 | } 15 | -------------------------------------------------------------------------------- /blueprint-testing-robot/src/main/kotlin/reactivecircus/blueprint/testing/assertion/DialogRobotAssertions.kt: -------------------------------------------------------------------------------- 1 | package reactivecircus.blueprint.testing.assertion 2 | 3 | import androidx.annotation.StringRes 4 | import androidx.test.espresso.Espresso 5 | import androidx.test.espresso.assertion.ViewAssertions 6 | import androidx.test.espresso.matcher.RootMatchers 7 | import androidx.test.espresso.matcher.ViewMatchers 8 | import reactivecircus.blueprint.testing.RobotAssertions 9 | 10 | /** 11 | * Check if a dialog with the title of a string associated with [titleResId] is displayed. 12 | */ 13 | public fun RobotAssertions.dialogWithTextDisplayed(@StringRes titleResId: Int) { 14 | Espresso.onView(ViewMatchers.withText(titleResId)) 15 | .inRoot(RootMatchers.isDialog()) 16 | .check(ViewAssertions.matches(ViewMatchers.isDisplayed())) 17 | } 18 | 19 | /** 20 | * Check if a dialog with the title [expected] is displayed. 21 | */ 22 | public fun RobotAssertions.dialogWithTextDisplayed(expected: String) { 23 | Espresso.onView(ViewMatchers.withText(expected)) 24 | .inRoot(RootMatchers.isDialog()) 25 | .check(ViewAssertions.matches(ViewMatchers.isDisplayed())) 26 | } 27 | 28 | /** 29 | * Check if a dialog with [buttonTextResId] as the button 1 label is displayed. 30 | */ 31 | public fun RobotAssertions.dialogWithButton1Displayed(@StringRes buttonTextResId: Int) { 32 | Espresso.onView(ViewMatchers.withId(android.R.id.button1)) 33 | .inRoot(RootMatchers.isDialog()) 34 | .check(ViewAssertions.matches(ViewMatchers.withText(buttonTextResId))) 35 | .check(ViewAssertions.matches(ViewMatchers.isDisplayed())) 36 | } 37 | 38 | /** 39 | * Check if a dialog with [buttonTextResId] as the button 2 label is displayed. 40 | */ 41 | public fun RobotAssertions.dialogWithButton2Displayed(@StringRes buttonTextResId: Int) { 42 | Espresso.onView(ViewMatchers.withId(android.R.id.button2)) 43 | .inRoot(RootMatchers.isDialog()) 44 | .check(ViewAssertions.matches(ViewMatchers.withText(buttonTextResId))) 45 | .check(ViewAssertions.matches(ViewMatchers.isDisplayed())) 46 | } 47 | 48 | /** 49 | * Check if a dialog with [buttonTextResId] as the button 3 label is displayed. 50 | */ 51 | public fun RobotAssertions.dialogWithButton3Displayed(@StringRes buttonTextResId: Int) { 52 | Espresso.onView(ViewMatchers.withId(android.R.id.button3)) 53 | .inRoot(RootMatchers.isDialog()) 54 | .check(ViewAssertions.matches(ViewMatchers.withText(buttonTextResId))) 55 | .check(ViewAssertions.matches(ViewMatchers.isDisplayed())) 56 | } 57 | -------------------------------------------------------------------------------- /blueprint-testing-robot/src/main/kotlin/reactivecircus/blueprint/testing/assertion/DrawerRobotAssertions.kt: -------------------------------------------------------------------------------- 1 | package reactivecircus.blueprint.testing.assertion 2 | 3 | import androidx.annotation.IdRes 4 | import androidx.test.espresso.Espresso 5 | import androidx.test.espresso.assertion.ViewAssertions 6 | import androidx.test.espresso.contrib.DrawerMatchers 7 | import androidx.test.espresso.matcher.ViewMatchers 8 | import reactivecircus.blueprint.testing.RobotAssertions 9 | 10 | /** 11 | * Check if the drawer associated with [drawerId] is opened. 12 | */ 13 | public fun RobotAssertions.drawerOpened(@IdRes drawerId: Int) { 14 | Espresso.onView(ViewMatchers.withId(drawerId)) 15 | .check(ViewAssertions.matches(DrawerMatchers.isOpen())) 16 | } 17 | 18 | /** 19 | * Check if the drawer associated with [drawerId] is closed. 20 | */ 21 | public fun RobotAssertions.drawerClosed(@IdRes drawerId: Int) { 22 | Espresso.onView(ViewMatchers.withId(drawerId)) 23 | .check(ViewAssertions.matches(DrawerMatchers.isClosed())) 24 | } 25 | -------------------------------------------------------------------------------- /blueprint-testing-robot/src/main/kotlin/reactivecircus/blueprint/testing/assertion/KeyboardRobotAssertions.kt: -------------------------------------------------------------------------------- 1 | package reactivecircus.blueprint.testing.assertion 2 | 3 | import android.text.InputType 4 | import android.view.View 5 | import android.widget.EditText 6 | import androidx.annotation.IdRes 7 | import androidx.test.espresso.Espresso 8 | import androidx.test.espresso.assertion.ViewAssertions 9 | import androidx.test.espresso.matcher.BoundedMatcher 10 | import androidx.test.espresso.matcher.ViewMatchers 11 | import org.hamcrest.Description 12 | import org.hamcrest.Matcher 13 | import reactivecircus.blueprint.testing.RobotAssertions 14 | 15 | /** 16 | * Check if the edit text associated with [viewId] has an email input type. 17 | */ 18 | public fun RobotAssertions.keyboardInputTypeIsEmail(@IdRes viewId: Int) { 19 | Espresso.onView(ViewMatchers.withId(viewId)) 20 | .check(ViewAssertions.matches(withEmailInputType())) 21 | } 22 | 23 | /** 24 | * Returns a matcher that matches the [EditText] with the Email input type. 25 | */ 26 | private fun withEmailInputType(): Matcher { 27 | return object : BoundedMatcher(EditText::class.java) { 28 | 29 | override fun matchesSafely(item: EditText): Boolean { 30 | return item.inputType and InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS > 0 31 | } 32 | 33 | override fun describeTo(description: Description) { 34 | description.appendText("with error: The input type of the EditText is not Email Address input type.") 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /blueprint-testing-robot/src/main/kotlin/reactivecircus/blueprint/testing/assertion/RecyclerViewRobotAssertions.kt: -------------------------------------------------------------------------------- 1 | package reactivecircus.blueprint.testing.assertion 2 | 3 | import android.view.View 4 | import androidx.annotation.IdRes 5 | import androidx.recyclerview.widget.RecyclerView 6 | import androidx.test.espresso.Espresso 7 | import androidx.test.espresso.NoMatchingViewException 8 | import androidx.test.espresso.ViewAssertion 9 | import androidx.test.espresso.matcher.ViewMatchers 10 | import androidx.test.espresso.matcher.ViewMatchers.assertThat 11 | import org.hamcrest.CoreMatchers 12 | import org.hamcrest.core.AllOf.allOf 13 | import reactivecircus.blueprint.testing.RobotAssertions 14 | 15 | /** 16 | * Check if the recycler view associated with [recyclerViewId] has the size of [size]. 17 | */ 18 | public fun RobotAssertions.recyclerViewHasSize(@IdRes recyclerViewId: Int, size: Int) { 19 | Espresso.onView( 20 | allOf( 21 | ViewMatchers.withId(recyclerViewId), 22 | ViewMatchers.isDisplayed() 23 | ) 24 | ) 25 | .check(RecyclerViewItemCountAssertion(size)) 26 | } 27 | 28 | /** 29 | * A view assertion that checks if a [RecyclerView] has the expected number of items. 30 | */ 31 | private class RecyclerViewItemCountAssertion(private val expectedCount: Int) : ViewAssertion { 32 | 33 | override fun check(view: View?, noViewFoundException: NoMatchingViewException?) { 34 | if (noViewFoundException != null) { 35 | throw noViewFoundException 36 | } 37 | if (view !is RecyclerView) { 38 | throw IllegalStateException("The asserted view is not RecyclerView") 39 | } 40 | 41 | if (view.adapter == null) { 42 | throw IllegalStateException("No adapter is assigned to RecyclerView") 43 | } 44 | 45 | assertThat(view.adapter?.itemCount, CoreMatchers.equalTo(expectedCount)) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /blueprint-testing-robot/src/main/kotlin/reactivecircus/blueprint/testing/assertion/SnackbarRobotAssertions.kt: -------------------------------------------------------------------------------- 1 | package reactivecircus.blueprint.testing.assertion 2 | 3 | import androidx.test.espresso.Espresso 4 | import androidx.test.espresso.assertion.ViewAssertions 5 | import androidx.test.espresso.matcher.ViewMatchers 6 | import com.google.android.material.R 7 | import org.hamcrest.CoreMatchers 8 | import reactivecircus.blueprint.testing.RobotAssertions 9 | 10 | /** 11 | * Check if a snackbar with [text] as message is displayed. 12 | */ 13 | public fun RobotAssertions.snackBarDisplayed(text: String) { 14 | Espresso.onView( 15 | CoreMatchers.allOf( 16 | ViewMatchers.withId(R.id.snackbar_text), 17 | ViewMatchers.withText(text) 18 | ) 19 | ) 20 | .check(ViewAssertions.matches(ViewMatchers.isDisplayed())) 21 | } 22 | -------------------------------------------------------------------------------- /blueprint-testing-robot/src/main/kotlin/reactivecircus/blueprint/testing/assertion/ViewRobotAssertions.kt: -------------------------------------------------------------------------------- 1 | package reactivecircus.blueprint.testing.assertion 2 | 3 | import androidx.annotation.IdRes 4 | import androidx.test.espresso.Espresso.onView 5 | import androidx.test.espresso.assertion.ViewAssertions 6 | import androidx.test.espresso.assertion.ViewAssertions.doesNotExist 7 | import androidx.test.espresso.matcher.ViewMatchers 8 | import androidx.test.espresso.matcher.ViewMatchers.isClickable 9 | import org.hamcrest.CoreMatchers.not 10 | import reactivecircus.blueprint.testing.RobotAssertions 11 | 12 | /** 13 | * Check if all views associated with [viewIds] are displayed. 14 | */ 15 | public fun RobotAssertions.viewDisplayed(@IdRes vararg viewIds: Int) { 16 | viewIds.forEach { viewId -> 17 | onView(ViewMatchers.withId(viewId)) 18 | .check(ViewAssertions.matches(ViewMatchers.isDisplayed())) 19 | } 20 | } 21 | 22 | /** 23 | * Check if all views associated with [viewIds] are NOT displayed. 24 | */ 25 | public fun RobotAssertions.viewNotDisplayed(@IdRes vararg viewIds: Int) { 26 | viewIds.forEach { viewId -> 27 | onView(ViewMatchers.withId(viewId)) 28 | .check(ViewAssertions.matches(not(ViewMatchers.isDisplayed()))) 29 | } 30 | } 31 | 32 | /** 33 | * Check if all views associated with [viewIds] are not present in the view hierarchy. 34 | */ 35 | public fun RobotAssertions.viewNotExists(@IdRes vararg viewIds: Int) { 36 | viewIds.forEach { viewId -> 37 | onView(ViewMatchers.withId(viewId)).check(doesNotExist()) 38 | } 39 | } 40 | 41 | /** 42 | * Check if the view associated with [viewId] is enabled. 43 | */ 44 | public fun RobotAssertions.viewEnabled(@IdRes viewId: Int) { 45 | onView(ViewMatchers.withId(viewId)) 46 | .check(ViewAssertions.matches(ViewMatchers.isEnabled())) 47 | } 48 | 49 | /** 50 | * Check if the view associated with [viewId] is disabled. 51 | */ 52 | public fun RobotAssertions.viewDisabled(@IdRes viewId: Int) { 53 | onView(ViewMatchers.withId(viewId)) 54 | .check(ViewAssertions.matches(not(ViewMatchers.isEnabled()))) 55 | } 56 | 57 | /** 58 | * Check if the view associated with [viewId] is clickable. 59 | */ 60 | public fun RobotAssertions.viewClickable(@IdRes viewId: Int) { 61 | onView(ViewMatchers.withId(viewId)) 62 | .check(ViewAssertions.matches(isClickable())) 63 | } 64 | 65 | /** 66 | * Check if the view associated with [viewId] is NOT clickable. 67 | */ 68 | public fun RobotAssertions.viewNotClickable(@IdRes viewId: Int) { 69 | onView(ViewMatchers.withId(viewId)) 70 | .check(ViewAssertions.matches(not(isClickable()))) 71 | } 72 | -------------------------------------------------------------------------------- /blueprint-testing-robot/src/main/kotlin/reactivecircus/blueprint/testing/matcher/StringMatchers.kt: -------------------------------------------------------------------------------- 1 | package reactivecircus.blueprint.testing.matcher 2 | 3 | import org.hamcrest.Description 4 | import org.hamcrest.Matcher 5 | import org.hamcrest.TypeSafeMatcher 6 | 7 | /** 8 | * Returns a matcher that matches string containing a subString (case insensitive). 9 | */ 10 | public fun containsIgnoringCase(subString: String): Matcher { 11 | return object : TypeSafeMatcher() { 12 | override fun matchesSafely(actualString: String): Boolean { 13 | return actualString.lowercase() 14 | .contains(subString.lowercase()) 15 | } 16 | 17 | override fun describeTo(description: Description) { 18 | description.appendText("containing substring \"$subString\"") 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /blueprint-ui/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /blueprint-ui/README.md: -------------------------------------------------------------------------------- 1 | # Blueprint UI 2 | 3 | The **Blueprint UI** provides a number of convenient Kotlin extensions and widgets for working with the Android UI toolkit. 4 | 5 | ## Dependency 6 | 7 | ```groovy 8 | implementation "io.github.reactivecircus.blueprint:blueprint-ui:${blueprint_version}" 9 | ``` 10 | 11 | Note that the library uses `androidx.appcompat:appcompat` transitively. 12 | 13 | ## Extensions 14 | 15 | Kotlin extensions on `Activity`: 16 | 17 | ```Kotlin 18 | /** 19 | * Shows status bar on the activity. 20 | */ 21 | fun Activity.showStatusBar() 22 | 23 | /** 24 | * Hides status bar from the activity. 25 | */ 26 | fun Activity.hideStatusBar() 27 | 28 | /** 29 | * Sets status bar color on the activity and optionally draws the status bar system ui in light or dark mode. 30 | */ 31 | fun Activity.setStatusBarColor(@ColorRes colorRes: Int, lightBackground: Boolean = false) 32 | 33 | /** 34 | * Returns screen size of the activity. 35 | */ 36 | val Activity.screenSize: DisplayMetrics 37 | ``` 38 | 39 | Kotlin extensions on `Context`: 40 | 41 | ```Kotlin 42 | /** 43 | * Apply tinting to a vector drawable. 44 | */ 45 | fun Context.tintVectorDrawable( 46 | theme: Resources.Theme, 47 | @DrawableRes resId: Int, 48 | @ColorInt tint: Int 49 | ): Drawable 50 | 51 | /** 52 | * Whether animation is turned on on the device. 53 | */ 54 | val Context.isAnimationOn: Boolean 55 | ``` 56 | 57 | Kotlin extensions on `AppCompat`: 58 | 59 | ```Kotlin 60 | /** 61 | * Sets the precomputed text future on the [AppCompatTextView]. 62 | * 63 | * @param charSequence the text to be displayed 64 | * @param executor the executor to be used for processing the text layout. 65 | * Default single threaded pool will be used if null is passed in. 66 | */ 67 | fun AppCompatTextView.setPrecomputedTextFuture(charSequence: CharSequence, executor: Executor? = null) 68 | ``` 69 | 70 | Kotlin extensions on `Window`: 71 | 72 | ```kotlin 73 | /** 74 | * Programmatically shows the soft keyboard. 75 | */ 76 | fun Window.showSoftKeyboard() 77 | 78 | /** 79 | * Programmatically hides the soft keyboard. 80 | */ 81 | fun Window.hideSoftKeyboard() 82 | ``` 83 | 84 | [Intent.kt][intent-extensions] has extensions on `Activity` and `Context` for launching new activity. 85 | 86 | For example to launch a new Activity from an Activity: 87 | 88 | ```kotlin 89 | launchActivity { 90 | putExtra(EXTRA_ENTER_NOTE_ID, 1) 91 | } 92 | ``` 93 | 94 | To launch a new Activity, passing in a **request code**: 95 | ```kotlin 96 | launchActivity(requestCode = SCAN_QR_CODE_REQUEST) 97 | ``` 98 | 99 | This internally launches the activity with `Activity.startActivityForResult(...)`. 100 | 101 | [intent-extensions]: https://github.com/ReactiveCircus/blueprint/tree/main/blueprint-ui/src/main/kotlin/reactivecircus/blueprint/ui/extension/Intent.kt 102 | -------------------------------------------------------------------------------- /blueprint-ui/api/blueprint-ui.api: -------------------------------------------------------------------------------- 1 | public final class reactivecircus/blueprint/ui/extension/ActivityKt { 2 | public static final fun getScreenSize (Landroid/app/Activity;)Landroid/util/DisplayMetrics; 3 | public static final fun hideStatusBar (Landroid/app/Activity;)V 4 | public static final fun setStatusBarColor (Landroid/app/Activity;IZ)V 5 | public static synthetic fun setStatusBarColor$default (Landroid/app/Activity;IZILjava/lang/Object;)V 6 | public static final fun showStatusBar (Landroid/app/Activity;)V 7 | } 8 | 9 | public final class reactivecircus/blueprint/ui/extension/AppCompatKt { 10 | public static final fun setPrecomputedTextFuture (Landroidx/appcompat/widget/AppCompatTextView;Ljava/lang/CharSequence;Ljava/util/concurrent/Executor;)V 11 | public static synthetic fun setPrecomputedTextFuture$default (Landroidx/appcompat/widget/AppCompatTextView;Ljava/lang/CharSequence;Ljava/util/concurrent/Executor;ILjava/lang/Object;)V 12 | } 13 | 14 | public final class reactivecircus/blueprint/ui/extension/ContextKt { 15 | public static final fun isAnimationOn (Landroid/content/Context;)Z 16 | public static final fun tintVectorDrawable (Landroid/content/Context;Landroid/content/res/Resources$Theme;II)Landroid/graphics/drawable/Drawable; 17 | } 18 | 19 | public final class reactivecircus/blueprint/ui/extension/WindowKt { 20 | public static final fun hideSoftKeyboard (Landroid/view/Window;)V 21 | public static final fun showSoftKeyboard (Landroid/view/Window;)V 22 | } 23 | 24 | -------------------------------------------------------------------------------- /blueprint-ui/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | `blueprint-plugin` 3 | id("com.android.library") 4 | kotlin("android") 5 | id("com.vanniktech.maven.publish") 6 | id("org.jetbrains.dokka") 7 | } 8 | 9 | blueprint { 10 | enableExplicitApi.set(true) 11 | } 12 | 13 | android { 14 | namespace = "reactivecircus.blueprint.ui" 15 | } 16 | 17 | dependencies { 18 | // AndroidX 19 | implementation(libs.androidx.appCompat) 20 | implementation(libs.androidx.core) 21 | } 22 | -------------------------------------------------------------------------------- /blueprint-ui/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_ARTIFACT_ID=blueprint-ui 2 | POM_NAME=Blueprint UI 3 | POM_DESCRIPTION=Android UI extensions, utilities and widgets 4 | POM_PACKAGING=aar 5 | -------------------------------------------------------------------------------- /blueprint-ui/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /blueprint-ui/src/main/kotlin/reactivecircus/blueprint/ui/extension/Activity.kt: -------------------------------------------------------------------------------- 1 | package reactivecircus.blueprint.ui.extension 2 | 3 | import android.app.Activity 4 | import android.os.Build 5 | import android.util.DisplayMetrics 6 | import androidx.annotation.ColorRes 7 | import androidx.core.content.ContextCompat 8 | import androidx.core.view.WindowInsetsCompat 9 | import androidx.core.view.WindowInsetsControllerCompat 10 | 11 | /** 12 | * Shows status bar on the activity. 13 | */ 14 | public fun Activity.showStatusBar() { 15 | WindowInsetsControllerCompat(window, window.decorView.findViewById(android.R.id.content)) 16 | .show(WindowInsetsCompat.Type.statusBars()) 17 | } 18 | 19 | /** 20 | * Hides status bar from the activity. 21 | */ 22 | public fun Activity.hideStatusBar() { 23 | WindowInsetsControllerCompat(window, window.decorView.findViewById(android.R.id.content)) 24 | .hide(WindowInsetsCompat.Type.statusBars()) 25 | } 26 | 27 | /** 28 | * Sets status bar color on the activity and optionally draws the status bar system ui in light or dark mode. 29 | * @param colorRes resource ID of the color to be set to the status bar. 30 | * @param lightBackground whether to draw the status bar such that 31 | * it is compatible with a light status bar background. 32 | */ 33 | public fun Activity.setStatusBarColor( 34 | @ColorRes colorRes: Int, 35 | lightBackground: Boolean = false, 36 | ) { 37 | window.statusBarColor = ContextCompat.getColor(this, colorRes) 38 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 39 | WindowInsetsControllerCompat(window, window.decorView.findViewById(android.R.id.content)) 40 | .isAppearanceLightStatusBars = lightBackground 41 | } 42 | } 43 | 44 | /** 45 | * Returns screen size of the activity. 46 | */ 47 | public val Activity.screenSize: DisplayMetrics 48 | get() { 49 | val display = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { 50 | display 51 | } else { 52 | @Suppress("DEPRECATION") 53 | windowManager.defaultDisplay 54 | } 55 | val metrics = DisplayMetrics() 56 | display?.getRealMetrics(metrics) 57 | return metrics 58 | } 59 | -------------------------------------------------------------------------------- /blueprint-ui/src/main/kotlin/reactivecircus/blueprint/ui/extension/AppCompat.kt: -------------------------------------------------------------------------------- 1 | package reactivecircus.blueprint.ui.extension 2 | 3 | import androidx.appcompat.widget.AppCompatTextView 4 | import androidx.core.text.PrecomputedTextCompat 5 | import androidx.core.widget.TextViewCompat 6 | import java.util.concurrent.Executor 7 | 8 | /** 9 | * Sets the precomputed text future on the [AppCompatTextView]. 10 | * 11 | * @param charSequence the text to be displayed. 12 | * Precomputed text future is not set if null is passed in. 13 | * @param executor the executor to be used for processing the text layout. 14 | * Default single threaded pool will be used if null is passed in. 15 | */ 16 | public fun AppCompatTextView.setPrecomputedTextFuture( 17 | charSequence: CharSequence?, 18 | executor: Executor? = null, 19 | ) { 20 | if (charSequence == null) return 21 | setTextFuture( 22 | PrecomputedTextCompat.getTextFuture( 23 | charSequence, 24 | TextViewCompat.getTextMetricsParams(this), 25 | executor 26 | ) 27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /blueprint-ui/src/main/kotlin/reactivecircus/blueprint/ui/extension/Context.kt: -------------------------------------------------------------------------------- 1 | package reactivecircus.blueprint.ui.extension 2 | 3 | import android.content.Context 4 | import android.content.res.Resources 5 | import android.graphics.drawable.Drawable 6 | import android.provider.Settings 7 | import androidx.annotation.ColorInt 8 | import androidx.annotation.DrawableRes 9 | import androidx.core.graphics.drawable.DrawableCompat 10 | import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat 11 | 12 | /** 13 | * Apply tinting to a vector drawable. 14 | */ 15 | public fun Context.tintVectorDrawable( 16 | theme: Resources.Theme, 17 | @DrawableRes resId: Int, 18 | @ColorInt tint: Int, 19 | ): Drawable { 20 | val drawable: Drawable = VectorDrawableCompat.create(resources, resId, theme) as Drawable 21 | val wrapped = DrawableCompat.wrap(drawable) 22 | drawable.mutate() 23 | DrawableCompat.setTint(wrapped, tint) 24 | return drawable 25 | } 26 | 27 | /** 28 | * Whether animation is turned on on the device. 29 | */ 30 | public val Context.isAnimationOn: Boolean 31 | get() = Settings.Global.getFloat( 32 | contentResolver, 33 | Settings.Global.ANIMATOR_DURATION_SCALE, 34 | 1f 35 | ) > 0 36 | -------------------------------------------------------------------------------- /blueprint-ui/src/main/kotlin/reactivecircus/blueprint/ui/extension/Intent.kt: -------------------------------------------------------------------------------- 1 | package reactivecircus.blueprint.ui.extension 2 | 3 | import android.app.Activity 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.os.Bundle 7 | 8 | /** 9 | * Launches an activity from an [Activity] via [Activity.startActivityForResult]. 10 | * @param requestCode - the requestCode to be passed into [Activity.startActivityForResult] 11 | * @param options - the options bundle to be passed into [Activity.startActivityForResult] 12 | */ 13 | public inline fun Activity.launchActivity( 14 | requestCode: Int, 15 | options: Bundle? = null, 16 | noinline init: Intent.() -> Unit = {} 17 | ) { 18 | val intent = newIntent(this) 19 | intent.init() 20 | startActivityForResult(intent, requestCode, options) 21 | } 22 | 23 | /** 24 | * Launches an activity from a [Context] via [Activity.startActivity]. 25 | * @param options - the options bundle to be passed into [Activity.startActivity] 26 | */ 27 | public inline fun Context.launchActivity( 28 | options: Bundle? = null, 29 | noinline init: Intent.() -> Unit = {} 30 | ) { 31 | val intent = newIntent(this) 32 | intent.init() 33 | startActivity(intent, options) 34 | } 35 | 36 | /** 37 | * Creates a new intent of type [T]. 38 | */ 39 | public inline fun newIntent(context: Context): Intent = 40 | Intent(context, T::class.java) 41 | -------------------------------------------------------------------------------- /blueprint-ui/src/main/kotlin/reactivecircus/blueprint/ui/extension/Window.kt: -------------------------------------------------------------------------------- 1 | package reactivecircus.blueprint.ui.extension 2 | 3 | import android.view.Window 4 | import androidx.core.view.WindowInsetsCompat 5 | import androidx.core.view.WindowInsetsControllerCompat 6 | 7 | /** 8 | * Programmatically shows the soft keyboard. 9 | */ 10 | public fun Window.showSoftKeyboard() { 11 | WindowInsetsControllerCompat(this, decorView.findViewById(android.R.id.content)) 12 | .show(WindowInsetsCompat.Type.ime()) 13 | } 14 | 15 | /** 16 | * Programmatically hides the soft keyboard. 17 | */ 18 | public fun Window.hideSoftKeyboard() { 19 | WindowInsetsControllerCompat(this, decorView.findViewById(android.R.id.content)) 20 | .hide(WindowInsetsCompat.Type.ime()) 21 | } 22 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | `blueprint-plugin` 3 | } 4 | -------------------------------------------------------------------------------- /buildSrc/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | `kotlin-dsl` 3 | } 4 | 5 | @Suppress("UnstableApiUsage") 6 | dependencies { 7 | // TODO: remove once https://github.com/gradle/gradle/issues/15383#issuecomment-779893192 is fixed 8 | implementation(files(libs.javaClass.superclass.protectionDomain.codeSource.location)) 9 | implementation(libs.plugin.kotlin) 10 | implementation(libs.plugin.dokka) 11 | implementation(libs.plugin.agp) 12 | implementation(libs.plugin.detekt) 13 | implementation(libs.plugin.binaryCompatibilityValidator) 14 | implementation(libs.plugin.mavenPublish) 15 | } 16 | 17 | gradlePlugin { 18 | plugins { 19 | register("blueprint") { 20 | id = "blueprint-plugin" 21 | implementationClass = "reactivecircus.blueprint.BlueprintPlugin" 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /buildSrc/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | @Suppress("UnstableApiUsage") 2 | dependencyResolutionManagement { 3 | repositories { 4 | mavenCentral() 5 | google() 6 | gradlePluginPortal() 7 | } 8 | 9 | versionCatalogs { 10 | create("libs") { 11 | from(files("../gradle/libs.versions.toml")) 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /buildSrc/src/main/kotlin/reactivecircus/blueprint/AdditionalCompilerArgs.kt: -------------------------------------------------------------------------------- 1 | package reactivecircus.blueprint 2 | 3 | val additionalCompilerArgs = listOf( 4 | "-progressive", 5 | "-Xjvm-default=all", 6 | "-opt-in=kotlin.Experimental" 7 | ) 8 | -------------------------------------------------------------------------------- /buildSrc/src/main/kotlin/reactivecircus/blueprint/AndroidSdk.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("ClassName") 2 | 3 | package reactivecircus.blueprint 4 | 5 | object androidSdk { 6 | const val minSdk = 21 7 | const val targetSdk = 33 8 | const val compileSdk = 33 9 | const val buildTools = "33.0.0" 10 | } 11 | -------------------------------------------------------------------------------- /buildSrc/src/main/kotlin/reactivecircus/blueprint/ApiCheckConfigs.kt: -------------------------------------------------------------------------------- 1 | package reactivecircus.blueprint 2 | 3 | import org.gradle.api.Project 4 | import org.gradle.kotlin.dsl.configure 5 | import org.gradle.kotlin.dsl.withType 6 | import kotlinx.validation.ApiValidationExtension 7 | import kotlinx.validation.BinaryCompatibilityValidatorPlugin 8 | 9 | /** 10 | * Apply and configure the [BinaryCompatibilityValidatorPlugin] for the [Project]. 11 | */ 12 | internal fun Project.configureBinaryCompatibilityValidation() { 13 | pluginManager.apply(BinaryCompatibilityValidatorPlugin::class.java) 14 | plugins.withType { 15 | extensions.configure { 16 | ignoredProjects.addAll(IGNORED_PROJECTS) 17 | } 18 | } 19 | } 20 | 21 | private val IGNORED_PROJECTS = listOf( 22 | "demo-common", 23 | "demo-coroutines", 24 | "demo-rx", 25 | "demo-testing-common", 26 | "test-utils", 27 | ) 28 | -------------------------------------------------------------------------------- /buildSrc/src/main/kotlin/reactivecircus/blueprint/BlueprintExtension.kt: -------------------------------------------------------------------------------- 1 | package reactivecircus.blueprint 2 | 3 | import org.gradle.api.model.ObjectFactory 4 | import org.gradle.api.provider.Property 5 | import org.gradle.kotlin.dsl.property 6 | 7 | /** 8 | * Extension for [BlueprintPlugin]. 9 | */ 10 | @Suppress("UnstableApiUsage", "unused") 11 | open class BlueprintExtension internal constructor(objects: ObjectFactory) { 12 | 13 | /** 14 | * Whether to enable strict explicit API mode for the project. 15 | * 16 | * Default is `false`. 17 | */ 18 | val enableExplicitApi: Property = objects.property().convention(false) 19 | } 20 | -------------------------------------------------------------------------------- /buildSrc/src/main/kotlin/reactivecircus/blueprint/BlueprintPlugin.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("unused") 2 | 3 | package reactivecircus.blueprint 4 | 5 | import com.android.build.gradle.AppPlugin 6 | import com.android.build.gradle.LibraryPlugin 7 | import org.gradle.api.JavaVersion 8 | import org.gradle.api.Plugin 9 | import org.gradle.api.Project 10 | import org.gradle.api.plugins.JavaLibraryPlugin 11 | import org.gradle.api.plugins.JavaPlugin 12 | import org.gradle.api.plugins.JavaPluginExtension 13 | import org.gradle.kotlin.dsl.getByType 14 | 15 | /** 16 | * A plugin that provides baseline gradle configurations for all projects, including: 17 | * - root project 18 | * - Android Application projects 19 | * - Android Library projects 20 | * - Kotlin JVM projects 21 | * - Java JVM projects 22 | * 23 | * Apply this plugin to the build.gradle.kts file in all projects: 24 | * ``` 25 | * plugins { 26 | * id 'blueprint-plugin' 27 | * } 28 | * ``` 29 | */ 30 | @ExperimentalStdlibApi 31 | class BlueprintPlugin : Plugin { 32 | override fun apply(project: Project) { 33 | val flowBindingExtension = project.extensions.create("blueprint", BlueprintExtension::class.java) 34 | 35 | project.configureForAllProjects(flowBindingExtension.enableExplicitApi) 36 | 37 | // apply configurations specific to root project 38 | if (project.isRoot) { 39 | project.configureRootProject() 40 | } 41 | 42 | // apply baseline configurations based on plugins applied 43 | project.plugins.all { 44 | when (this) { 45 | is JavaPlugin, 46 | is JavaLibraryPlugin -> { 47 | project.extensions.getByType().apply { 48 | sourceCompatibility = JavaVersion.VERSION_11 49 | targetCompatibility = JavaVersion.VERSION_11 50 | } 51 | } 52 | is LibraryPlugin -> { 53 | project.configureAndroidLibrary() 54 | } 55 | is AppPlugin -> { 56 | project.configureAndroidApplication() 57 | } 58 | } 59 | } 60 | } 61 | } 62 | 63 | val Project.isRoot get() = this == this.rootProject 64 | -------------------------------------------------------------------------------- /buildSrc/src/main/kotlin/reactivecircus/blueprint/DetektConfigs.kt: -------------------------------------------------------------------------------- 1 | package reactivecircus.blueprint 2 | 3 | import io.gitlab.arturbosch.detekt.Detekt 4 | import io.gitlab.arturbosch.detekt.DetektPlugin 5 | import io.gitlab.arturbosch.detekt.extensions.DetektExtension 6 | import org.gradle.accessors.dm.LibrariesForLibs 7 | import org.gradle.api.Project 8 | import org.gradle.kotlin.dsl.configure 9 | import org.gradle.kotlin.dsl.the 10 | import org.gradle.kotlin.dsl.withType 11 | 12 | /** 13 | * Apply detekt configs to the [Project]. 14 | */ 15 | internal fun Project.configureDetektPlugin() { 16 | // apply detekt plugin 17 | pluginManager.apply(DetektPlugin::class.java) 18 | 19 | // enable Ktlint formatting 20 | val detektVersion = the().versions.detekt.get() 21 | dependencies.add("detektPlugins", "io.gitlab.arturbosch.detekt:detekt-formatting:$detektVersion") 22 | 23 | plugins.withType { 24 | extensions.configure { 25 | source = files("src/") 26 | config = files("${project.rootDir}/detekt.yml") 27 | buildUponDefaultConfig = true 28 | allRules = true 29 | } 30 | tasks.withType().configureEach { 31 | reports { 32 | html.outputLocation.set(file("build/reports/detekt/${project.name}.html")) 33 | } 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /buildSrc/src/main/kotlin/reactivecircus/blueprint/DokkaConfigs.kt: -------------------------------------------------------------------------------- 1 | package reactivecircus.blueprint 2 | 3 | import org.gradle.api.Project 4 | import org.gradle.kotlin.dsl.withType 5 | import org.jetbrains.dokka.gradle.DokkaMultiModuleTask 6 | import org.jetbrains.dokka.gradle.DokkaPlugin 7 | 8 | /** 9 | * Apply and configure the [DokkaPlugin] for the [Project]. 10 | */ 11 | internal fun Project.configureDokka() { 12 | pluginManager.apply(DokkaPlugin::class.java) 13 | tasks.withType().configureEach { 14 | val apiDir = rootDir.resolve("docs/api") 15 | outputDirectory.set(apiDir) 16 | doLast { 17 | apiDir.resolve("-modules.html").renameTo(apiDir.resolve("index.html")) 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /buildSrc/src/main/kotlin/reactivecircus/blueprint/Environment.kt: -------------------------------------------------------------------------------- 1 | package reactivecircus.blueprint 2 | 3 | import org.gradle.api.Project 4 | 5 | @Suppress("UnstableApiUsage") 6 | val Project.isCiBuild: Boolean 7 | get() = providers.environmentVariable("CI").orNull == "true" 8 | -------------------------------------------------------------------------------- /buildSrc/src/main/kotlin/reactivecircus/blueprint/Publishing.kt: -------------------------------------------------------------------------------- 1 | package reactivecircus.blueprint 2 | 3 | import com.vanniktech.maven.publish.MavenPublishBaseExtension 4 | import com.vanniktech.maven.publish.MavenPublishBasePlugin 5 | import com.vanniktech.maven.publish.SonatypeHost 6 | import org.gradle.api.Project 7 | import org.gradle.api.publish.maven.plugins.MavenPublishPlugin 8 | import org.gradle.kotlin.dsl.configure 9 | import org.gradle.kotlin.dsl.withType 10 | 11 | /** 12 | * Configure the [MavenPublishPlugin] if applied. 13 | */ 14 | internal fun Project.configureMavenPublishing() { 15 | plugins.withType { 16 | extensions.configure { 17 | publishToMavenCentral(SonatypeHost.S01, automaticRelease = true) 18 | signAllPublications() 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /buildSrc/src/main/kotlin/reactivecircus/blueprint/SlimTests.kt: -------------------------------------------------------------------------------- 1 | package reactivecircus.blueprint 2 | 3 | import com.android.build.api.variant.ApplicationAndroidComponentsExtension 4 | import com.android.build.api.variant.LibraryAndroidComponentsExtension 5 | import org.gradle.api.Project 6 | import org.gradle.kotlin.dsl.findByType 7 | import org.gradle.language.nativeplatform.internal.BuildType 8 | 9 | /** 10 | * When the "slimTests" project property is provided, disable the unit test tasks 11 | * on `release` build type to avoid running the same tests repeatedly 12 | * in different build variants. 13 | * 14 | * Examples: 15 | * `./gradlew test -PslimTests` will run unit tests for `debug` build variants 16 | * in Android App and Library projects, and all tests in JVM projects. 17 | */ 18 | @Suppress("UnstableApiUsage") 19 | internal fun Project.configureSlimTests() { 20 | if (providers.gradleProperty(SLIM_TESTS_PROPERTY).isPresent) { 21 | // disable unit test tasks on the release build type for Android Library projects 22 | extensions.findByType()?.run { 23 | beforeVariants(selector().withBuildType(BuildType.RELEASE.name)) { 24 | it.enableUnitTest = false 25 | } 26 | } 27 | 28 | // disable unit test tasks on the release build type for Android Application projects. 29 | extensions.findByType()?.run { 30 | beforeVariants(selector().withBuildType(BuildType.RELEASE.name)) { 31 | it.enableUnitTest = false 32 | } 33 | } 34 | } 35 | } 36 | 37 | private const val SLIM_TESTS_PROPERTY = "slimTests" 38 | -------------------------------------------------------------------------------- /buildSrc/src/main/kotlin/reactivecircus/blueprint/VariantExt.kt: -------------------------------------------------------------------------------- 1 | package reactivecircus.blueprint 2 | 3 | import com.android.build.api.variant.BuildConfigField 4 | import com.android.build.api.variant.ResValue 5 | import com.android.build.api.variant.Variant 6 | import java.io.Serializable 7 | 8 | @Suppress("UnstableApiUsage") 9 | fun Variant.addResValue(key: String, type: String, value: String) { 10 | resValues.put(makeResValueKey(type, key), ResValue(value)) 11 | } 12 | 13 | @Suppress("UnstableApiUsage") 14 | fun Variant.addBuildConfigField(key: String, value: T) { 15 | val buildConfigField = BuildConfigField(type = value::class.java.simpleName, value = value, comment = null) 16 | buildConfigFields.put(key, buildConfigField) 17 | } 18 | -------------------------------------------------------------------------------- /detekt.yml: -------------------------------------------------------------------------------- 1 | formatting: 2 | android: true 3 | ImportOrdering: 4 | active: false 5 | 6 | style: 7 | UnnecessaryAbstractClass: 8 | active: false 9 | UseCheckOrError: 10 | active: false 11 | -------------------------------------------------------------------------------- /docs/images/reactive_circus_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReactiveCircus/blueprint/5098538a9e5ccc9e2c503444d3e5c35a47ecbe68/docs/images/reactive_circus_logo.png -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | GROUP=io.github.reactivecircus.blueprint 2 | VERSION_NAME=1.19.0-SNAPSHOT 3 | 4 | POM_URL=https://github.com/reactivecircus/blueprint 5 | POM_SCM_URL=https://github.com/reactivecircus/blueprint 6 | POM_SCM_CONNECTION=scm:git:https://github.com/reactivecircus/blueprint.git 7 | POM_SCM_DEV_CONNECTION=scm:git:ssh://git@github.com/reactivecircus/blueprint.git 8 | 9 | POM_LICENCE_NAME=The Apache Software License, Version 2.0 10 | POM_LICENCE_URL=http://www.apache.org/licenses/LICENSE-2.0.txt 11 | POM_LICENCE_DIST=repo 12 | 13 | POM_DEVELOPER_ID=reactivecircus 14 | POM_DEVELOPER_NAME=Reactive Circus 15 | 16 | org.gradle.parallel=true 17 | org.gradle.configureondemand=true 18 | org.gradle.caching=true 19 | 20 | # Enable Kotlin incremental compilation 21 | kotlin.incremental.useClasspathSnapshot=true 22 | 23 | # Kotlin code style 24 | kotlin.code.style=official 25 | 26 | # Enable incremental annotation processor for KAPT 27 | kapt.incremental.apt=true 28 | 29 | # Turn off AP discovery in compile path to enable compile avoidance 30 | kapt.include.compile.classpath=false 31 | 32 | # Use R8 instead of ProGuard for code shrinking. 33 | android.enableR8.fullMode=true 34 | 35 | # Enable AndroidX 36 | android.useAndroidX=true 37 | 38 | # Enable non-transitive R class namespacing where each library only contains 39 | # references to the resources it declares instead of declarations plus all 40 | # transitive dependency references. 41 | android.nonTransitiveRClass=true 42 | 43 | # Generate compile-time only R class for app modules. 44 | # TODO re-enable once fixed in AGP (https://issuetracker.google.com/issues/182198793) 45 | #android.enableAppCompileTimeRClass=true 46 | 47 | # Only keep the single relevant constructor for types mentioned in XML files 48 | # instead of using a parameter wildcard which keeps them all. 49 | android.useMinimalKeepRules=true 50 | 51 | # Enable resource optimizations for release build 52 | android.enableResourceOptimizations=true 53 | 54 | # Default Android build features 55 | android.defaults.buildfeatures.buildconfig=false 56 | android.defaults.buildfeatures.aidl=false 57 | android.defaults.buildfeatures.renderscript=false 58 | android.defaults.buildfeatures.resvalues=false 59 | android.defaults.buildfeatures.shaders=false 60 | android.library.defaults.buildfeatures.androidresources=false 61 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReactiveCircus/blueprint/5098538a9e5ccc9e2c503444d3e5c35a47ecbe68/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip 4 | networkTimeout=10000 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 48 | echo. 49 | echo Please set the JAVA_HOME variable in your environment to match the 50 | echo location of your Java installation. 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 62 | echo. 63 | echo Please set the JAVA_HOME variable in your environment to match the 64 | echo location of your Java installation. 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: 'Blueprint' 2 | site_description: 'Architectural frameworks and toolkits for bootstrapping modern Android codebases' 3 | site_author: 'Yang Chen' 4 | site_url: 'https://reactivecircus.github.io/blueprint' 5 | remote_branch: gh-pages 6 | 7 | repo_name: 'blueprint' 8 | repo_url: 'https://github.com/ReactiveCircus/blueprint' 9 | 10 | copyright: 'Copyright © 2019 Yang Chen' 11 | 12 | theme: 13 | name: 'material' 14 | language: 'en' 15 | favicon: 'images/reactive_circus_logo.png' 16 | logo: 'images/reactive_circus_logo.png' 17 | palette: 18 | primary: 'white' 19 | accent: 'white' 20 | font: 21 | text: 'Fira Sans' 22 | code: 'Fira Code' 23 | icon: 24 | repo: fontawesome/brands/github 25 | 26 | extra: 27 | social: 28 | - icon: 'fontawesome/brands/github' 29 | link: 'https://github.com/ReactiveCircus/blueprint' 30 | 31 | nav: 32 | - 'Overview': index.md 33 | - 'Samples': 34 | - 'Demos Overview': samples/index.md 35 | - 'Coroutines Demo': samples/demo-coroutines/index.md 36 | - 'RxJava Demo': samples/demo-rx/index.md 37 | - 'Demo Shared Library': samples/demo-common/index.md 38 | - 'Demo Testing Infrastructure': samples/demo-testing-common/index.md 39 | - 'Interactor Coroutines': blueprint-interactor-coroutines/index.md 40 | - 'Interactor RxJava 2': blueprint-interactor-rx2/index.md 41 | - 'Interactor RxJava 3': blueprint-interactor-rx3/index.md 42 | - 'Async Coroutines': blueprint-async-coroutines/index.md 43 | - 'Async RxJava 2': blueprint-async-rx2/index.md 44 | - 'Async RxJava 3': blueprint-async-rx3/index.md 45 | - 'UI': blueprint-ui/index.md 46 | - 'Testing Robot': blueprint-testing-robot/index.md 47 | - 'Change Log': changelog.md 48 | - 'API Docs': api/index.html" target="_blank 49 | 50 | markdown_extensions: 51 | - admonition 52 | - smarty 53 | - codehilite: 54 | guess_lang: false 55 | linenums: True 56 | - footnotes 57 | - meta 58 | - toc: 59 | permalink: true 60 | - pymdownx.betterem: 61 | smart_enable: all 62 | - pymdownx.caret 63 | - pymdownx.details 64 | - pymdownx.inlinehilite 65 | - pymdownx.magiclink 66 | - pymdownx.smartsymbols 67 | - pymdownx.superfences 68 | - tables 69 | 70 | plugins: 71 | - search 72 | - minify: 73 | minify_html: true 74 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:base" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /samples/demo-common/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /samples/demo-common/README.md: -------------------------------------------------------------------------------- 1 | # Blueprint Demo Common 2 | 3 | This is a library module shared by both [demo-coroutines][demo-coroutines] and [demo-rx][demo-rx]. This has nothing to do with **Blueprint** and just provides common code used by both apps: 4 | 5 | * Resources 6 | * An in-memory cache 7 | * Domain model 8 | * A couple of utils and extensions 9 | 10 | [demo-coroutines]: ../demo-coroutines/ 11 | [demo-rx]: ../demo-rx/ 12 | -------------------------------------------------------------------------------- /samples/demo-common/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | `blueprint-plugin` 3 | id("com.android.library") 4 | kotlin("android") 5 | id("kotlin-parcelize") 6 | } 7 | 8 | android { 9 | namespace = "reactivecircus.blueprint.common" 10 | buildFeatures { 11 | androidResources = true 12 | } 13 | lint { 14 | disable.add("IconDuplicates") 15 | disable.add("MissingClass") 16 | } 17 | } 18 | 19 | dependencies { 20 | implementation(project(":blueprint-ui")) 21 | 22 | // AndroidX 23 | implementation(libs.androidx.coordinatorLayout) 24 | implementation(libs.androidx.constraintLayout) 25 | implementation(libs.androidx.activity) 26 | implementation(libs.androidx.fragment.ktx) 27 | implementation(libs.androidx.recyclerView) 28 | implementation(libs.androidx.lifecycle.viewModel) 29 | 30 | // Material Components 31 | implementation(libs.material) 32 | 33 | // timber 34 | implementation(libs.timber) 35 | 36 | // Unit tests 37 | testImplementation(project(":test-utils")) 38 | testImplementation(libs.junit) 39 | testImplementation(libs.mockk) 40 | testImplementation(libs.truth) 41 | testImplementation(libs.kotlinx.coroutines.test) 42 | } 43 | -------------------------------------------------------------------------------- /samples/demo-common/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /samples/demo-common/src/main/ic_launcher-web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReactiveCircus/blueprint/5098538a9e5ccc9e2c503444d3e5c35a47ecbe68/samples/demo-common/src/main/ic_launcher-web.png -------------------------------------------------------------------------------- /samples/demo-common/src/main/kotlin/reactivecircus/blueprint/demo/data/cache/InMemoryNoteCache.kt: -------------------------------------------------------------------------------- 1 | package reactivecircus.blueprint.demo.data.cache 2 | 3 | import reactivecircus.blueprint.demo.domain.model.Note 4 | 5 | /** 6 | * In-memory implementation of [NoteCache]. 7 | */ 8 | class InMemoryNoteCache : NoteCache { 9 | 10 | private val notes = mutableListOf() 11 | 12 | override fun allNotes(): List { 13 | return notes.toList() 14 | } 15 | 16 | override fun findNote(criteria: (Note) -> Boolean): Note? { 17 | return notes.find(criteria) 18 | } 19 | 20 | override fun addNotes(newNotes: List) { 21 | require( 22 | notes.none { existingNote -> 23 | newNotes.any { newNote -> 24 | existingNote.uuid == newNote.uuid 25 | } 26 | } 27 | ) { 28 | "Note already exists." 29 | } 30 | 31 | notes.addAll(newNotes) 32 | } 33 | 34 | override fun updateNote(noteToUpdate: Note) { 35 | val noteIndex = notes.indexOfFirst { 36 | it.uuid == noteToUpdate.uuid 37 | } 38 | 39 | require(noteIndex >= 0) { 40 | "Note does not exist." 41 | } 42 | 43 | notes[noteIndex] = noteToUpdate 44 | } 45 | 46 | override fun deleteAllNotes() { 47 | notes.clear() 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /samples/demo-common/src/main/kotlin/reactivecircus/blueprint/demo/data/cache/NoteCache.kt: -------------------------------------------------------------------------------- 1 | package reactivecircus.blueprint.demo.data.cache 2 | 3 | import reactivecircus.blueprint.demo.domain.model.Note 4 | 5 | interface NoteCache { 6 | 7 | /** 8 | * Return all existing notes. 9 | */ 10 | fun allNotes(): List 11 | 12 | /** 13 | * Find a [Note] that matches the provided [criteria]. 14 | * Return null if note matching the [criteria] could not be found. 15 | */ 16 | fun findNote(criteria: (Note) -> Boolean): Note? 17 | 18 | /** 19 | * Add a list of [Note]s to the list of existing notes. 20 | * @throws IllegalArgumentException if any new note with same uuid already exists. 21 | */ 22 | fun addNotes(newNotes: List) 23 | 24 | /** 25 | * Update a [Note] by finding the existing note by uuid and replacing it. 26 | * @throws IllegalArgumentException if note with same uuid does not exist. 27 | */ 28 | fun updateNote(noteToUpdate: Note) 29 | 30 | /** 31 | * Delete all existing notes. 32 | */ 33 | fun deleteAllNotes() 34 | } 35 | -------------------------------------------------------------------------------- /samples/demo-common/src/main/kotlin/reactivecircus/blueprint/demo/domain/model/Note.kt: -------------------------------------------------------------------------------- 1 | package reactivecircus.blueprint.demo.domain.model 2 | 3 | import java.util.UUID 4 | 5 | data class Note( 6 | val uuid: String = UUID.randomUUID().toString(), 7 | val content: String, 8 | val timeCreated: Long, 9 | val timeLastUpdated: Long 10 | ) 11 | -------------------------------------------------------------------------------- /samples/demo-common/src/main/kotlin/reactivecircus/blueprint/demo/enternote/EnterNoteParams.kt: -------------------------------------------------------------------------------- 1 | package reactivecircus.blueprint.demo.enternote 2 | 3 | import android.os.Parcelable 4 | import kotlinx.parcelize.Parcelize 5 | 6 | sealed class EnterNoteParams : Parcelable { 7 | 8 | @Parcelize 9 | object CreateNew : EnterNoteParams() 10 | 11 | @Parcelize 12 | class Update(val uuid: String) : EnterNoteParams() 13 | } 14 | -------------------------------------------------------------------------------- /samples/demo-common/src/main/kotlin/reactivecircus/blueprint/demo/util/Date.kt: -------------------------------------------------------------------------------- 1 | package reactivecircus.blueprint.demo.util 2 | 3 | import java.text.SimpleDateFormat 4 | import java.util.Date 5 | import java.util.Locale 6 | import java.util.TimeZone 7 | 8 | /** 9 | * Converts the timestamp to a formatted String 10 | */ 11 | fun Long.toFormattedDateString(pattern: String, locale: Locale = Locale.getDefault()): String { 12 | return SimpleDateFormat(pattern, locale).apply { 13 | timeZone = TimeZone.getDefault() 14 | }.format(Date(this)) 15 | } 16 | -------------------------------------------------------------------------------- /samples/demo-common/src/main/kotlin/reactivecircus/blueprint/demo/util/ViewModel.kt: -------------------------------------------------------------------------------- 1 | package reactivecircus.blueprint.demo.util 2 | 3 | import androidx.activity.ComponentActivity 4 | import androidx.activity.viewModels 5 | import androidx.lifecycle.ViewModel 6 | import androidx.lifecycle.ViewModelProvider 7 | 8 | /** 9 | * Lazily retrieve a viewModel instance associated with the current [ComponentActivity]. 10 | */ 11 | inline fun ComponentActivity.viewModel(crossinline provider: () -> T) = 12 | viewModels { 13 | object : ViewModelProvider.Factory { 14 | @Suppress("UNCHECKED_CAST") 15 | override fun create(modelClass: Class) = provider() as T 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /samples/demo-common/src/main/res/drawable/ic_add_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /samples/demo-common/src/main/res/drawable/ic_check_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /samples/demo-common/src/main/res/drawable/ic_close_primary_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /samples/demo-common/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 9 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /samples/demo-common/src/main/res/layout/activity_enter_note.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 11 | 12 | 17 | 18 | 19 | 20 | 24 | 25 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /samples/demo-common/src/main/res/layout/item_note.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 13 | 14 | 22 | 23 | 29 | 30 | 31 | 32 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /samples/demo-common/src/main/res/menu/menu_enter_note.xml: -------------------------------------------------------------------------------- 1 | 2 |

4 | 5 | 10 | 11 | -------------------------------------------------------------------------------- /samples/demo-common/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /samples/demo-common/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /samples/demo-common/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReactiveCircus/blueprint/5098538a9e5ccc9e2c503444d3e5c35a47ecbe68/samples/demo-common/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /samples/demo-common/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReactiveCircus/blueprint/5098538a9e5ccc9e2c503444d3e5c35a47ecbe68/samples/demo-common/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /samples/demo-common/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReactiveCircus/blueprint/5098538a9e5ccc9e2c503444d3e5c35a47ecbe68/samples/demo-common/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /samples/demo-common/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReactiveCircus/blueprint/5098538a9e5ccc9e2c503444d3e5c35a47ecbe68/samples/demo-common/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /samples/demo-common/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReactiveCircus/blueprint/5098538a9e5ccc9e2c503444d3e5c35a47ecbe68/samples/demo-common/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /samples/demo-common/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReactiveCircus/blueprint/5098538a9e5ccc9e2c503444d3e5c35a47ecbe68/samples/demo-common/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /samples/demo-common/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReactiveCircus/blueprint/5098538a9e5ccc9e2c503444d3e5c35a47ecbe68/samples/demo-common/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /samples/demo-common/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReactiveCircus/blueprint/5098538a9e5ccc9e2c503444d3e5c35a47ecbe68/samples/demo-common/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /samples/demo-common/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReactiveCircus/blueprint/5098538a9e5ccc9e2c503444d3e5c35a47ecbe68/samples/demo-common/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /samples/demo-common/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReactiveCircus/blueprint/5098538a9e5ccc9e2c503444d3e5c35a47ecbe68/samples/demo-common/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /samples/demo-common/src/main/res/values-night/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | #1e88e5 10 | #005cb2 11 | #1e88e5 12 | #005cb2 13 | 14 | #000000 15 | #000000 16 | #cf6679 17 | 18 | #000000 19 | #000000 20 | #ffffff 21 | #ffffff 22 | #000000 23 | 24 | 25 | -------------------------------------------------------------------------------- /samples/demo-common/src/main/res/values-night/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 12 | 13 | -------------------------------------------------------------------------------- /samples/demo-common/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | #1e88e5 10 | #005cb2 11 | #1e88e5 12 | #005cb2 13 | 14 | 15 | 16 | 17 | 18 | #ffffff 19 | #ffffff 20 | #b00020 21 | 22 | 23 | 24 | #ffffff 25 | #ffffff 26 | #000000 27 | #000000 28 | #ffffff 29 | 30 | 31 | -------------------------------------------------------------------------------- /samples/demo-common/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4dp 4 | 8dp 5 | 16dp 6 | 24dp 7 | 32dp 8 | 40dp 9 | 48dp 10 | 64dp 11 | 52dp 12 | 56dp 13 | 72dp 14 | 80dp 15 | 96dp 16 | 104dp 17 | 18 | 1dp 19 | 20 | -------------------------------------------------------------------------------- /samples/demo-common/src/main/res/values/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #212F5C 4 | -------------------------------------------------------------------------------- /samples/demo-common/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | No notes. 3 | 4 | Add Note 5 | 6 | Create Note 7 | 8 | Update Note 9 | 10 | Enter note 11 | 12 | Save 13 | 14 | 15 | -------------------------------------------------------------------------------- /samples/demo-common/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /samples/demo-common/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 12 | 13 | 14 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /samples/demo-common/src/test/kotlin/reactivecircus/blueprint/demo/util/DateTest.kt: -------------------------------------------------------------------------------- 1 | package reactivecircus.blueprint.demo.util 2 | 3 | import com.google.common.truth.Truth.assertThat 4 | import org.junit.Test 5 | import java.util.Calendar 6 | import java.util.Locale 7 | import java.util.TimeZone 8 | 9 | class DateTest { 10 | 11 | @Test 12 | fun `timestamp can be converted to a formatted date String given a date pattern`() { 13 | val pattern = "EEE d MMM 'at' h:mm a" 14 | 15 | // Monday, 25 June 2018 08:30:00 16 | val timestamp = Calendar.getInstance().apply { 17 | set(2018, 5, 25, 8, 30) 18 | timeZone = TimeZone.getDefault() 19 | }.time.toInstant().toEpochMilli() 20 | 21 | assertThat(timestamp.toFormattedDateString(pattern, Locale.ENGLISH)) 22 | .isEqualTo("Mon 25 Jun at 8:30 AM") 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /samples/demo-coroutines/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /samples/demo-coroutines/shrinker-rules.pro: -------------------------------------------------------------------------------- 1 | -verbose 2 | 3 | # Keep annotations with RUNTIME retention and their defaults. 4 | -keepattributes RuntimeVisible*Annotations, AnnotationDefault 5 | 6 | # For crash reporting 7 | -keepattributes LineNumberTable, SourceFile 8 | 9 | # For native methods, see http://proguard.sourceforge.net/manual/examples.html#native 10 | -keepclasseswithmembernames class * { 11 | native ; 12 | } 13 | 14 | # Enum.valueOf(Class, String) and others invoke this method reflectively. 15 | -keepclassmembers,allowoptimization enum * { 16 | public static **[] values(); 17 | } 18 | 19 | # Parcelable CREATOR fields are looked up reflectively when de-parceling. 20 | -keepclassmembers class * implements android.os.Parcelable { 21 | public static final android.os.Parcelable$Creator CREATOR; 22 | } 23 | -------------------------------------------------------------------------------- /samples/demo-coroutines/src/androidTest/kotlin/reactivecircus/blueprint/demo/CoroutinesBaseScreenTest.kt: -------------------------------------------------------------------------------- 1 | package reactivecircus.blueprint.demo 2 | 3 | import android.app.Activity 4 | import androidx.test.core.app.ActivityScenario 5 | import androidx.test.core.app.ApplicationProvider 6 | import androidx.test.core.app.launchActivity 7 | import androidx.test.espresso.Espresso 8 | import androidx.test.espresso.intent.Intents 9 | import org.junit.After 10 | import org.junit.Before 11 | import reactivecircus.blueprint.demo.data.cache.NoteCache 12 | 13 | abstract class CoroutinesBaseScreenTest { 14 | 15 | val noteCache: NoteCache by lazy { 16 | (ApplicationProvider.getApplicationContext() as CoroutinesScreenTestApp).injector.provideNoteCache() 17 | } 18 | 19 | @Before 20 | open fun setUp() { 21 | Intents.init() 22 | } 23 | 24 | @After 25 | open fun tearDown() { 26 | Intents.release() 27 | } 28 | 29 | inline fun launchActivityScenario( 30 | intent: android.content.Intent? = null, 31 | ): ActivityScenario { 32 | return launchActivity(intent).also { 33 | Espresso.onIdle() 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /samples/demo-coroutines/src/androidTest/kotlin/reactivecircus/blueprint/demo/CoroutinesScreenTestApp.kt: -------------------------------------------------------------------------------- 1 | package reactivecircus.blueprint.demo 2 | 3 | class CoroutinesScreenTestApp : BlueprintCoroutinesDemoApp() { 4 | override val injector: CoroutinesAppInjector = CoroutinesScreenTestAppInjector() 5 | } 6 | -------------------------------------------------------------------------------- /samples/demo-coroutines/src/androidTest/kotlin/reactivecircus/blueprint/demo/CoroutinesScreenTestAppInjector.kt: -------------------------------------------------------------------------------- 1 | package reactivecircus.blueprint.demo 2 | 3 | import android.os.AsyncTask 4 | import kotlinx.coroutines.Dispatchers 5 | import kotlinx.coroutines.asCoroutineDispatcher 6 | import reactivecircus.blueprint.async.coroutines.CoroutineDispatcherProvider 7 | 8 | class CoroutinesScreenTestAppInjector : CoroutinesAppInjector() { 9 | 10 | private val testCoroutineDispatcherProvider: CoroutineDispatcherProvider by lazy { 11 | CoroutineDispatcherProvider( 12 | io = AsyncTask.THREAD_POOL_EXECUTOR.asCoroutineDispatcher(), 13 | computation = Dispatchers.Default, 14 | ui = Dispatchers.Main.immediate 15 | ) 16 | } 17 | 18 | override fun provideCoroutineDispatcherProvider(): CoroutineDispatcherProvider { 19 | return testCoroutineDispatcherProvider 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /samples/demo-coroutines/src/androidTest/kotlin/reactivecircus/blueprint/demo/CoroutinesScreenTestRunner.kt: -------------------------------------------------------------------------------- 1 | package reactivecircus.blueprint.demo 2 | 3 | import android.app.Application 4 | import android.content.Context 5 | import androidx.test.runner.AndroidJUnitRunner 6 | 7 | class CoroutinesScreenTestRunner : AndroidJUnitRunner() { 8 | 9 | @Throws(InstantiationException::class, IllegalAccessException::class, ClassNotFoundException::class) 10 | override fun newApplication(cl: ClassLoader, className: String, context: Context): Application { 11 | return super.newApplication(cl, CoroutinesScreenTestApp::class.java.name, context) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /samples/demo-coroutines/src/androidTest/kotlin/reactivecircus/blueprint/demo/noteslist/CoroutinesNotesListScreenTest.kt: -------------------------------------------------------------------------------- 1 | package reactivecircus.blueprint.demo.noteslist 2 | 3 | import androidx.test.filters.LargeTest 4 | import kotlinx.coroutines.ExperimentalCoroutinesApi 5 | import org.junit.Test 6 | import reactivecircus.blueprint.demo.CoroutinesBaseScreenTest 7 | import reactivecircus.blueprint.demo.enternote.CoroutinesEnterNoteActivity 8 | import reactivecircus.blueprint.demo.testNotes 9 | import reactivecircus.blueprint.testing.assertion.activityLaunched 10 | 11 | @ExperimentalCoroutinesApi 12 | @LargeTest 13 | class CoroutinesNotesListScreenTest : CoroutinesBaseScreenTest() { 14 | 15 | override fun tearDown() { 16 | super.tearDown() 17 | noteCache.deleteAllNotes() 18 | } 19 | 20 | @Test 21 | fun openNotesListScreenWithExistingNotes_notesDisplayed() { 22 | notesListScreen { 23 | given { 24 | noteCache.addNotes(testNotes) 25 | } 26 | perform { 27 | launchActivityScenario() 28 | } 29 | check { 30 | createNoteButtonDisplayed() 31 | notesDisplayed(testNotes) 32 | } 33 | } 34 | } 35 | 36 | @Test 37 | fun openNotesListScreenWithoutExistingNotes_emptyStateDisplayed() { 38 | notesListScreen { 39 | given { 40 | noteCache.deleteAllNotes() 41 | } 42 | perform { 43 | launchActivityScenario() 44 | } 45 | check { 46 | createNoteButtonDisplayed() 47 | emptyStateDisplayed() 48 | } 49 | } 50 | } 51 | 52 | @Test 53 | fun clickNote_enterNoteScreenLaunched() { 54 | notesListScreen { 55 | given { 56 | noteCache.addNotes(testNotes) 57 | } 58 | perform { 59 | launchActivityScenario() 60 | // select the first note 61 | clickNoteAt(0) 62 | } 63 | check { 64 | activityLaunched() 65 | } 66 | } 67 | } 68 | 69 | @Test 70 | fun clickCreateNoteButton_enterNoteScreenLaunched() { 71 | notesListScreen { 72 | perform { 73 | launchActivityScenario() 74 | clickCreateNoteButton() 75 | } 76 | check { 77 | activityLaunched() 78 | } 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /samples/demo-coroutines/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 14 | 15 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /samples/demo-coroutines/src/main/ic_launcher-web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReactiveCircus/blueprint/5098538a9e5ccc9e2c503444d3e5c35a47ecbe68/samples/demo-coroutines/src/main/ic_launcher-web.png -------------------------------------------------------------------------------- /samples/demo-coroutines/src/main/kotlin/reactivecircus/blueprint/demo/BlueprintCoroutinesDemoApp.kt: -------------------------------------------------------------------------------- 1 | package reactivecircus.blueprint.demo 2 | 3 | import android.app.Application 4 | import timber.log.Timber 5 | 6 | open class BlueprintCoroutinesDemoApp : Application() { 7 | 8 | override fun onCreate() { 9 | super.onCreate() 10 | Timber.plant(Timber.DebugTree()) 11 | } 12 | 13 | open val injector: CoroutinesAppInjector = CoroutinesAppInjector() 14 | } 15 | -------------------------------------------------------------------------------- /samples/demo-coroutines/src/main/kotlin/reactivecircus/blueprint/demo/data/repository/CoroutinesInMemoryNoteRepository.kt: -------------------------------------------------------------------------------- 1 | package reactivecircus.blueprint.demo.data.repository 2 | 3 | import kotlinx.coroutines.flow.Flow 4 | import kotlinx.coroutines.flow.MutableSharedFlow 5 | import kotlinx.coroutines.flow.map 6 | import kotlinx.coroutines.flow.onStart 7 | import reactivecircus.blueprint.demo.data.cache.NoteCache 8 | import reactivecircus.blueprint.demo.domain.model.Note 9 | import reactivecircus.blueprint.demo.domain.repository.CoroutinesNoteRepository 10 | 11 | class CoroutinesInMemoryNoteRepository( 12 | private val noteCache: NoteCache, 13 | ) : CoroutinesNoteRepository { 14 | 15 | private val notesEmitter = MutableSharedFlow() 16 | 17 | override fun streamAllNotes(): Flow> { 18 | return notesEmitter 19 | .map { noteCache.allNotes() } 20 | .onStart { emit(noteCache.allNotes()) } 21 | } 22 | 23 | override suspend fun getNoteByUuid(uuid: String): Note? { 24 | return noteCache.findNote { it.uuid == uuid } 25 | } 26 | 27 | override suspend fun addNote(note: Note) { 28 | noteCache.addNotes(listOf(note)) 29 | 30 | // refresh all notes stream 31 | notesEmitter.emit(Unit) 32 | } 33 | 34 | override suspend fun updateNote(note: Note) { 35 | noteCache.updateNote(note) 36 | 37 | // refresh all notes stream 38 | notesEmitter.emit(Unit) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /samples/demo-coroutines/src/main/kotlin/reactivecircus/blueprint/demo/domain/interactor/CoroutinesCreateNote.kt: -------------------------------------------------------------------------------- 1 | package reactivecircus.blueprint.demo.domain.interactor 2 | 3 | import kotlinx.coroutines.CoroutineDispatcher 4 | import reactivecircus.blueprint.demo.domain.model.Note 5 | import reactivecircus.blueprint.demo.domain.repository.CoroutinesNoteRepository 6 | import reactivecircus.blueprint.interactor.InteractorParams 7 | import reactivecircus.blueprint.interactor.coroutines.SuspendingInteractor 8 | import reactivecircus.blueprint.async.coroutines.CoroutineDispatcherProvider 9 | 10 | class CoroutinesCreateNote( 11 | private val noteRepository: CoroutinesNoteRepository, 12 | coroutineDispatcherProvider: CoroutineDispatcherProvider 13 | ) : SuspendingInteractor() { 14 | override val dispatcher: CoroutineDispatcher = coroutineDispatcherProvider.io 15 | 16 | override suspend fun doWork(params: Params) { 17 | noteRepository.addNote(params.note) 18 | } 19 | 20 | class Params(internal val note: Note) : InteractorParams 21 | } 22 | -------------------------------------------------------------------------------- /samples/demo-coroutines/src/main/kotlin/reactivecircus/blueprint/demo/domain/interactor/CoroutinesGetNoteByUuid.kt: -------------------------------------------------------------------------------- 1 | package reactivecircus.blueprint.demo.domain.interactor 2 | 3 | import kotlinx.coroutines.CoroutineDispatcher 4 | import reactivecircus.blueprint.demo.domain.model.Note 5 | import reactivecircus.blueprint.demo.domain.repository.CoroutinesNoteRepository 6 | import reactivecircus.blueprint.interactor.InteractorParams 7 | import reactivecircus.blueprint.interactor.coroutines.SuspendingInteractor 8 | import reactivecircus.blueprint.async.coroutines.CoroutineDispatcherProvider 9 | 10 | class CoroutinesGetNoteByUuid( 11 | private val noteRepository: CoroutinesNoteRepository, 12 | coroutineDispatcherProvider: CoroutineDispatcherProvider 13 | ) : SuspendingInteractor() { 14 | override val dispatcher: CoroutineDispatcher = coroutineDispatcherProvider.io 15 | 16 | override suspend fun doWork(params: Params): Note { 17 | return checkNotNull(noteRepository.getNoteByUuid(params.uuid)) { 18 | "Could not find note by uuid." 19 | } 20 | } 21 | 22 | class Params(internal val uuid: String) : InteractorParams 23 | } 24 | -------------------------------------------------------------------------------- /samples/demo-coroutines/src/main/kotlin/reactivecircus/blueprint/demo/domain/interactor/CoroutinesStreamAllNotes.kt: -------------------------------------------------------------------------------- 1 | package reactivecircus.blueprint.demo.domain.interactor 2 | 3 | import kotlinx.coroutines.CoroutineDispatcher 4 | import kotlinx.coroutines.flow.Flow 5 | import kotlinx.coroutines.flow.map 6 | import reactivecircus.blueprint.demo.domain.model.Note 7 | import reactivecircus.blueprint.demo.domain.repository.CoroutinesNoteRepository 8 | import reactivecircus.blueprint.interactor.InteractorParams 9 | import reactivecircus.blueprint.interactor.coroutines.FlowInteractor 10 | import reactivecircus.blueprint.async.coroutines.CoroutineDispatcherProvider 11 | 12 | class CoroutinesStreamAllNotes( 13 | private val noteRepository: CoroutinesNoteRepository, 14 | coroutineDispatcherProvider: CoroutineDispatcherProvider 15 | ) : FlowInteractor>() { 16 | override val dispatcher: CoroutineDispatcher = coroutineDispatcherProvider.io 17 | 18 | override fun createFlow(params: Params): Flow> { 19 | return noteRepository.streamAllNotes() 20 | .map { notes -> 21 | if (params.sortedBy === SortedBy.TIME_CREATED) { 22 | notes.sortedByDescending { it.timeCreated } 23 | } else { 24 | notes.sortedByDescending { it.timeLastUpdated } 25 | } 26 | } 27 | } 28 | 29 | class Params(internal val sortedBy: SortedBy) : InteractorParams 30 | 31 | enum class SortedBy { 32 | TIME_CREATED, 33 | TIME_LAST_UPDATED 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /samples/demo-coroutines/src/main/kotlin/reactivecircus/blueprint/demo/domain/interactor/CoroutinesUpdateNote.kt: -------------------------------------------------------------------------------- 1 | package reactivecircus.blueprint.demo.domain.interactor 2 | 3 | import kotlinx.coroutines.CoroutineDispatcher 4 | import reactivecircus.blueprint.demo.domain.model.Note 5 | import reactivecircus.blueprint.demo.domain.repository.CoroutinesNoteRepository 6 | import reactivecircus.blueprint.interactor.InteractorParams 7 | import reactivecircus.blueprint.interactor.coroutines.SuspendingInteractor 8 | import reactivecircus.blueprint.async.coroutines.CoroutineDispatcherProvider 9 | 10 | class CoroutinesUpdateNote( 11 | private val noteRepository: CoroutinesNoteRepository, 12 | coroutineDispatcherProvider: CoroutineDispatcherProvider 13 | ) : SuspendingInteractor() { 14 | override val dispatcher: CoroutineDispatcher = coroutineDispatcherProvider.io 15 | 16 | override suspend fun doWork(params: Params) { 17 | noteRepository.updateNote(params.note) 18 | } 19 | 20 | class Params(internal val note: Note) : InteractorParams 21 | } 22 | -------------------------------------------------------------------------------- /samples/demo-coroutines/src/main/kotlin/reactivecircus/blueprint/demo/domain/repository/CoroutinesNoteRepository.kt: -------------------------------------------------------------------------------- 1 | package reactivecircus.blueprint.demo.domain.repository 2 | 3 | import kotlinx.coroutines.flow.Flow 4 | import reactivecircus.blueprint.demo.domain.model.Note 5 | 6 | interface CoroutinesNoteRepository { 7 | 8 | fun streamAllNotes(): Flow> 9 | 10 | suspend fun getNoteByUuid(uuid: String): Note? 11 | 12 | suspend fun addNote(note: Note) 13 | 14 | suspend fun updateNote(note: Note) 15 | } 16 | -------------------------------------------------------------------------------- /samples/demo-coroutines/src/main/kotlin/reactivecircus/blueprint/demo/enternote/CoroutinesEnterNoteViewModel.kt: -------------------------------------------------------------------------------- 1 | package reactivecircus.blueprint.demo.enternote 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.viewModelScope 5 | import kotlinx.coroutines.flow.MutableStateFlow 6 | import kotlinx.coroutines.flow.StateFlow 7 | import kotlinx.coroutines.launch 8 | import reactivecircus.blueprint.demo.domain.interactor.CoroutinesCreateNote 9 | import reactivecircus.blueprint.demo.domain.interactor.CoroutinesGetNoteByUuid 10 | import reactivecircus.blueprint.demo.domain.interactor.CoroutinesUpdateNote 11 | import reactivecircus.blueprint.demo.domain.model.Note 12 | 13 | sealed class State { 14 | object Loading : State() 15 | data class Idle(val note: Note?) : State() 16 | } 17 | 18 | class CoroutinesEnterNoteViewModel( 19 | noteUuid: String?, 20 | getNoteByUuid: CoroutinesGetNoteByUuid, 21 | private val createNote: CoroutinesCreateNote, 22 | private val updateNote: CoroutinesUpdateNote 23 | ) : ViewModel() { 24 | 25 | private val noteDataFlow = MutableStateFlow(State.Loading) 26 | 27 | val noteStateFlow: StateFlow get() = noteDataFlow 28 | 29 | init { 30 | viewModelScope.launch { 31 | if (noteUuid != null) { 32 | val note = getNoteByUuid.execute(CoroutinesGetNoteByUuid.Params(noteUuid)) 33 | noteDataFlow.value = State.Idle(note) 34 | } else { 35 | noteDataFlow.value = State.Idle(null) 36 | } 37 | } 38 | } 39 | 40 | fun createNote(content: String) { 41 | viewModelScope.launch { 42 | val time = System.currentTimeMillis() 43 | val newNote = Note( 44 | content = content, 45 | timeCreated = time, 46 | timeLastUpdated = time 47 | ) 48 | createNote.execute(CoroutinesCreateNote.Params(newNote)) 49 | } 50 | } 51 | 52 | fun updateNote(updatedNote: Note) { 53 | viewModelScope.launch { 54 | updateNote.execute(CoroutinesUpdateNote.Params(updatedNote)) 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /samples/demo-coroutines/src/main/kotlin/reactivecircus/blueprint/demo/noteslist/CoroutinesNotesListViewModel.kt: -------------------------------------------------------------------------------- 1 | package reactivecircus.blueprint.demo.noteslist 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.viewModelScope 5 | import kotlinx.coroutines.flow.Flow 6 | import kotlinx.coroutines.flow.MutableStateFlow 7 | import kotlinx.coroutines.flow.catch 8 | import kotlinx.coroutines.flow.launchIn 9 | import kotlinx.coroutines.flow.map 10 | import kotlinx.coroutines.flow.onEach 11 | import reactivecircus.blueprint.demo.domain.interactor.CoroutinesStreamAllNotes 12 | import reactivecircus.blueprint.demo.domain.model.Note 13 | import timber.log.Timber 14 | 15 | sealed class State { 16 | object LoadingNotes : State() 17 | data class Idle(val notes: List) : State() 18 | } 19 | 20 | class CoroutinesNotesListViewModel( 21 | streamAllNotes: CoroutinesStreamAllNotes, 22 | ) : ViewModel() { 23 | 24 | private val notesStateFlow = MutableStateFlow(State.LoadingNotes) 25 | 26 | val notesFlow: Flow get() = notesStateFlow 27 | 28 | init { 29 | streamAllNotes.buildFlow(CoroutinesStreamAllNotes.Params(CoroutinesStreamAllNotes.SortedBy.TIME_LAST_UPDATED)) 30 | .map { State.Idle(it) } 31 | .onEach { 32 | notesStateFlow.value = it 33 | } 34 | .catch { 35 | Timber.e(it) 36 | } 37 | .launchIn(viewModelScope) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /samples/demo-coroutines/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Blueprint Coroutines Demo 3 | 4 | -------------------------------------------------------------------------------- /samples/demo-coroutines/src/test/kotlin/reactivecircus/blueprint/demo/domain/interactor/CoroutinesCreateNoteTest.kt: -------------------------------------------------------------------------------- 1 | package reactivecircus.blueprint.demo.domain.interactor 2 | 3 | import com.google.common.truth.Truth.assertThat 4 | import io.mockk.coEvery 5 | import io.mockk.coVerify 6 | import io.mockk.every 7 | import io.mockk.mockk 8 | import kotlinx.coroutines.ExperimentalCoroutinesApi 9 | import kotlinx.coroutines.test.StandardTestDispatcher 10 | import kotlinx.coroutines.test.runTest 11 | import org.junit.Test 12 | import reactivecircus.blueprint.async.coroutines.CoroutineDispatcherProvider 13 | import reactivecircus.blueprint.demo.domain.model.Note 14 | import reactivecircus.blueprint.demo.domain.repository.CoroutinesNoteRepository 15 | 16 | @ExperimentalCoroutinesApi 17 | class CoroutinesCreateNoteTest { 18 | 19 | private val testDispatcher = StandardTestDispatcher() 20 | 21 | private val noteRepository = mockk { 22 | coEvery { addNote(any()) } returns Unit 23 | } 24 | 25 | private val coroutineDispatcherProvider = mockk { 26 | every { io } returns testDispatcher 27 | } 28 | 29 | private val createNote = CoroutinesCreateNote( 30 | noteRepository = noteRepository, 31 | coroutineDispatcherProvider = coroutineDispatcherProvider 32 | ) 33 | 34 | @Test 35 | fun `add note through repository`() = runTest(testDispatcher) { 36 | val dummyNote = Note( 37 | content = "note", 38 | timeCreated = System.currentTimeMillis(), 39 | timeLastUpdated = System.currentTimeMillis() 40 | ) 41 | 42 | assertThat(createNote.execute(CoroutinesCreateNote.Params(dummyNote))) 43 | .isEqualTo(Unit) 44 | 45 | coVerify { noteRepository.addNote(any()) } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /samples/demo-coroutines/src/test/kotlin/reactivecircus/blueprint/demo/domain/interactor/CoroutinesGetNoteByUuidTest.kt: -------------------------------------------------------------------------------- 1 | package reactivecircus.blueprint.demo.domain.interactor 2 | 3 | import com.google.common.truth.Truth.assertThat 4 | import io.mockk.coEvery 5 | import io.mockk.coVerify 6 | import io.mockk.every 7 | import io.mockk.mockk 8 | import kotlinx.coroutines.ExperimentalCoroutinesApi 9 | import kotlinx.coroutines.test.StandardTestDispatcher 10 | import kotlinx.coroutines.test.runTest 11 | import org.junit.Assert.assertThrows 12 | import org.junit.Test 13 | import reactivecircus.blueprint.async.coroutines.CoroutineDispatcherProvider 14 | import reactivecircus.blueprint.demo.domain.model.Note 15 | import reactivecircus.blueprint.demo.domain.repository.CoroutinesNoteRepository 16 | 17 | @ExperimentalCoroutinesApi 18 | class CoroutinesGetNoteByUuidTest { 19 | 20 | private val noteRepository = mockk() 21 | 22 | private val testDispatcher = StandardTestDispatcher() 23 | 24 | private val coroutineDispatcherProvider = mockk { 25 | every { io } returns testDispatcher 26 | } 27 | 28 | private val getNoteByUuid = CoroutinesGetNoteByUuid( 29 | noteRepository = noteRepository, 30 | coroutineDispatcherProvider = coroutineDispatcherProvider 31 | ) 32 | 33 | @Test 34 | fun `get note by uuid from repository`() = runTest(testDispatcher) { 35 | val dummyNote = Note( 36 | content = "note", 37 | timeCreated = System.currentTimeMillis(), 38 | timeLastUpdated = System.currentTimeMillis() 39 | ) 40 | 41 | coEvery { noteRepository.getNoteByUuid(any()) } returns dummyNote 42 | 43 | assertThat(getNoteByUuid.execute(CoroutinesGetNoteByUuid.Params("uuid"))) 44 | .isEqualTo(dummyNote) 45 | 46 | coVerify { noteRepository.getNoteByUuid(any()) } 47 | } 48 | 49 | @Test 50 | fun `throw exception when note cannot be found from repository`() = runTest(testDispatcher) { 51 | coEvery { noteRepository.getNoteByUuid(any()) } returns null 52 | 53 | assertThrows(IllegalStateException::class.java) { 54 | runTest(testDispatcher) { 55 | getNoteByUuid.execute(CoroutinesGetNoteByUuid.Params("uuid")) 56 | } 57 | } 58 | 59 | coVerify { noteRepository.getNoteByUuid(any()) } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /samples/demo-coroutines/src/test/kotlin/reactivecircus/blueprint/demo/domain/interactor/CoroutinesUpdateNoteTest.kt: -------------------------------------------------------------------------------- 1 | package reactivecircus.blueprint.demo.domain.interactor 2 | 3 | import com.google.common.truth.Truth.assertThat 4 | import io.mockk.coEvery 5 | import io.mockk.coVerify 6 | import io.mockk.every 7 | import io.mockk.mockk 8 | import kotlinx.coroutines.ExperimentalCoroutinesApi 9 | import kotlinx.coroutines.test.StandardTestDispatcher 10 | import kotlinx.coroutines.test.runTest 11 | import org.junit.Test 12 | import reactivecircus.blueprint.async.coroutines.CoroutineDispatcherProvider 13 | import reactivecircus.blueprint.demo.domain.model.Note 14 | import reactivecircus.blueprint.demo.domain.repository.CoroutinesNoteRepository 15 | 16 | @ExperimentalCoroutinesApi 17 | class CoroutinesUpdateNoteTest { 18 | 19 | private val testDispatcher = StandardTestDispatcher() 20 | 21 | private val noteRepository = mockk { 22 | coEvery { updateNote(any()) } returns Unit 23 | } 24 | 25 | private val coroutineDispatcherProvider = mockk { 26 | every { io } returns testDispatcher 27 | } 28 | 29 | private val updateNote = CoroutinesUpdateNote( 30 | noteRepository = noteRepository, 31 | coroutineDispatcherProvider = coroutineDispatcherProvider 32 | ) 33 | 34 | @Test 35 | fun `update note in repository`() = runTest(testDispatcher) { 36 | val dummyNote = Note( 37 | content = "note", 38 | timeCreated = System.currentTimeMillis(), 39 | timeLastUpdated = System.currentTimeMillis() 40 | ) 41 | 42 | assertThat(updateNote.execute(CoroutinesUpdateNote.Params(dummyNote))) 43 | .isEqualTo(Unit) 44 | 45 | coVerify { noteRepository.updateNote(any()) } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /samples/demo-coroutines/src/test/kotlin/reactivecircus/blueprint/demo/noteslist/CoroutinesNotesListViewModelTest.kt: -------------------------------------------------------------------------------- 1 | package reactivecircus.blueprint.demo.noteslist 2 | 3 | import com.google.common.truth.Truth.assertThat 4 | import io.mockk.every 5 | import io.mockk.mockk 6 | import io.mockk.verify 7 | import kotlinx.coroutines.ExperimentalCoroutinesApi 8 | import kotlinx.coroutines.flow.MutableSharedFlow 9 | import kotlinx.coroutines.flow.emptyFlow 10 | import kotlinx.coroutines.flow.first 11 | import kotlinx.coroutines.flow.single 12 | import kotlinx.coroutines.flow.take 13 | import kotlinx.coroutines.test.UnconfinedTestDispatcher 14 | import kotlinx.coroutines.test.runTest 15 | import org.junit.Rule 16 | import org.junit.Test 17 | import reactivecircus.blueprint.demo.domain.interactor.CoroutinesStreamAllNotes 18 | import reactivecircus.blueprint.demo.domain.model.Note 19 | import reactivecircus.blueprint.testutils.CoroutinesTestRule 20 | 21 | @ExperimentalCoroutinesApi 22 | class CoroutinesNotesListViewModelTest { 23 | 24 | @get:Rule 25 | val coroutinesTestRule = CoroutinesTestRule(UnconfinedTestDispatcher()) 26 | 27 | private val dummyNotes = listOf( 28 | Note( 29 | content = "Note 1", 30 | timeCreated = System.currentTimeMillis(), 31 | timeLastUpdated = System.currentTimeMillis() 32 | ), 33 | Note( 34 | content = "Note 2", 35 | timeCreated = System.currentTimeMillis(), 36 | timeLastUpdated = System.currentTimeMillis() 37 | ) 38 | ) 39 | 40 | private val streamAllNotes = mockk() 41 | 42 | private val viewModel: CoroutinesNotesListViewModel by lazy { 43 | CoroutinesNotesListViewModel(streamAllNotes) 44 | } 45 | 46 | @Test 47 | fun `emit State#LoadingNotes when initialized`() = runTest { 48 | every { streamAllNotes.buildFlow(any()) } returns emptyFlow() 49 | 50 | assertThat(viewModel.notesFlow.first()) 51 | .isEqualTo(State.LoadingNotes) 52 | } 53 | 54 | @Test 55 | fun `emit State#Idle with notes when streamAllNotes emits`() = runTest { 56 | val emitter = MutableSharedFlow>() 57 | every { streamAllNotes.buildFlow(any()) } returns emitter 58 | 59 | assertThat(viewModel.notesFlow.take(1).single()) 60 | .isEqualTo(State.LoadingNotes) 61 | 62 | verify(exactly = 1) { 63 | streamAllNotes.buildFlow(any()) 64 | } 65 | 66 | emitter.emit(dummyNotes) 67 | 68 | assertThat(viewModel.notesFlow.take(1).single()) 69 | .isEqualTo(State.Idle(dummyNotes)) 70 | 71 | val updatedDummyNotes = dummyNotes + listOf( 72 | Note( 73 | content = "Note 3", 74 | timeCreated = System.currentTimeMillis(), 75 | timeLastUpdated = System.currentTimeMillis() 76 | ) 77 | ) 78 | 79 | emitter.emit(updatedDummyNotes) 80 | 81 | assertThat(viewModel.notesFlow.take(1).single()) 82 | .isEqualTo(State.Idle(updatedDummyNotes)) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /samples/demo-rx/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /samples/demo-rx/shrinker-rules.pro: -------------------------------------------------------------------------------- 1 | -verbose 2 | 3 | # Keep annotations with RUNTIME retention and their defaults. 4 | -keepattributes RuntimeVisible*Annotations, AnnotationDefault 5 | 6 | # For crash reporting 7 | -keepattributes LineNumberTable, SourceFile 8 | 9 | # For native methods, see http://proguard.sourceforge.net/manual/examples.html#native 10 | -keepclasseswithmembernames class * { 11 | native ; 12 | } 13 | 14 | # Enum.valueOf(Class, String) and others invoke this method reflectively. 15 | -keepclassmembers,allowoptimization enum * { 16 | public static **[] values(); 17 | } 18 | 19 | # Parcelable CREATOR fields are looked up reflectively when de-parceling. 20 | -keepclassmembers class * implements android.os.Parcelable { 21 | public static final android.os.Parcelable$Creator CREATOR; 22 | } 23 | -------------------------------------------------------------------------------- /samples/demo-rx/src/androidTest/kotlin/reactivecircus/blueprint/demo/RxBaseScreenTest.kt: -------------------------------------------------------------------------------- 1 | package reactivecircus.blueprint.demo 2 | 3 | import android.app.Activity 4 | import androidx.test.core.app.ActivityScenario 5 | import androidx.test.core.app.ApplicationProvider 6 | import androidx.test.core.app.launchActivity 7 | import androidx.test.espresso.Espresso 8 | import androidx.test.espresso.intent.Intents 9 | import org.junit.After 10 | import org.junit.Before 11 | import reactivecircus.blueprint.demo.data.cache.NoteCache 12 | 13 | abstract class RxBaseScreenTest { 14 | 15 | val noteCache: NoteCache by lazy { 16 | (ApplicationProvider.getApplicationContext() as RxScreenTestApp).injector.provideNoteCache() 17 | } 18 | 19 | @Before 20 | open fun setUp() { 21 | Intents.init() 22 | } 23 | 24 | @After 25 | open fun tearDown() { 26 | Intents.release() 27 | } 28 | 29 | inline fun launchActivityScenario( 30 | intent: android.content.Intent? = null 31 | ): ActivityScenario { 32 | return launchActivity(intent).also { 33 | Espresso.onIdle() 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /samples/demo-rx/src/androidTest/kotlin/reactivecircus/blueprint/demo/RxScreenTestApp.kt: -------------------------------------------------------------------------------- 1 | package reactivecircus.blueprint.demo 2 | 3 | class RxScreenTestApp : BlueprintRxDemoApp() { 4 | 5 | override val injector: RxAppInjector = RxScreenTestAppInjector() 6 | } 7 | -------------------------------------------------------------------------------- /samples/demo-rx/src/androidTest/kotlin/reactivecircus/blueprint/demo/RxScreenTestAppInjector.kt: -------------------------------------------------------------------------------- 1 | package reactivecircus.blueprint.demo 2 | 3 | import android.os.AsyncTask 4 | import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers 5 | import io.reactivex.rxjava3.schedulers.Schedulers 6 | import reactivecircus.blueprint.async.rx3.SchedulerProvider 7 | 8 | class RxScreenTestAppInjector : RxAppInjector() { 9 | 10 | private val testSchedulerProvider: SchedulerProvider by lazy { 11 | SchedulerProvider( 12 | io = Schedulers.from(AsyncTask.THREAD_POOL_EXECUTOR), 13 | computation = Schedulers.computation(), 14 | ui = AndroidSchedulers.mainThread() 15 | ) 16 | } 17 | 18 | override fun provideSchedulerProvider(): SchedulerProvider { 19 | return testSchedulerProvider 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /samples/demo-rx/src/androidTest/kotlin/reactivecircus/blueprint/demo/RxScreenTestRunner.kt: -------------------------------------------------------------------------------- 1 | package reactivecircus.blueprint.demo 2 | 3 | import android.app.Application 4 | import android.content.Context 5 | import androidx.test.runner.AndroidJUnitRunner 6 | 7 | class RxScreenTestRunner : AndroidJUnitRunner() { 8 | 9 | @Throws(InstantiationException::class, IllegalAccessException::class, ClassNotFoundException::class) 10 | override fun newApplication(cl: ClassLoader, className: String, context: Context): Application { 11 | return super.newApplication(cl, RxScreenTestApp::class.java.name, context) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /samples/demo-rx/src/androidTest/kotlin/reactivecircus/blueprint/demo/noteslist/RxNotesListScreenTest.kt: -------------------------------------------------------------------------------- 1 | package reactivecircus.blueprint.demo.noteslist 2 | 3 | import androidx.test.filters.LargeTest 4 | import org.junit.Test 5 | import reactivecircus.blueprint.demo.RxBaseScreenTest 6 | import reactivecircus.blueprint.demo.enternote.RxEnterNoteActivity 7 | import reactivecircus.blueprint.demo.testNotes 8 | import reactivecircus.blueprint.testing.assertion.activityLaunched 9 | 10 | @LargeTest 11 | class RxNotesListScreenTest : RxBaseScreenTest() { 12 | 13 | override fun tearDown() { 14 | super.tearDown() 15 | noteCache.deleteAllNotes() 16 | } 17 | 18 | @Test 19 | fun openNotesListScreenWithExistingNotes_notesDisplayed() { 20 | notesListScreen { 21 | given { 22 | noteCache.addNotes(testNotes) 23 | } 24 | perform { 25 | launchActivityScenario() 26 | } 27 | check { 28 | createNoteButtonDisplayed() 29 | notesDisplayed(testNotes) 30 | } 31 | } 32 | } 33 | 34 | @Test 35 | fun openNotesListScreenWithoutExistingNotes_emptyStateDisplayed() { 36 | notesListScreen { 37 | given { 38 | noteCache.deleteAllNotes() 39 | } 40 | perform { 41 | launchActivityScenario() 42 | } 43 | check { 44 | createNoteButtonDisplayed() 45 | emptyStateDisplayed() 46 | } 47 | } 48 | } 49 | 50 | @Test 51 | fun clickNote_enterNoteScreenLaunched() { 52 | notesListScreen { 53 | given { 54 | noteCache.addNotes(testNotes) 55 | } 56 | perform { 57 | launchActivityScenario() 58 | // select the first note 59 | clickNoteAt(0) 60 | } 61 | check { 62 | activityLaunched() 63 | } 64 | } 65 | } 66 | 67 | @Test 68 | fun clickCreateNoteButton_enterNoteScreenLaunched() { 69 | notesListScreen { 70 | perform { 71 | launchActivityScenario() 72 | clickCreateNoteButton() 73 | } 74 | check { 75 | activityLaunched() 76 | } 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /samples/demo-rx/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 14 | 15 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /samples/demo-rx/src/main/ic_launcher-web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReactiveCircus/blueprint/5098538a9e5ccc9e2c503444d3e5c35a47ecbe68/samples/demo-rx/src/main/ic_launcher-web.png -------------------------------------------------------------------------------- /samples/demo-rx/src/main/kotlin/reactivecircus/blueprint/demo/BlueprintRxDemoApp.kt: -------------------------------------------------------------------------------- 1 | package reactivecircus.blueprint.demo 2 | 3 | import android.app.Application 4 | import timber.log.Timber 5 | 6 | open class BlueprintRxDemoApp : Application() { 7 | 8 | override fun onCreate() { 9 | super.onCreate() 10 | Timber.plant(Timber.DebugTree()) 11 | } 12 | 13 | open val injector: RxAppInjector = RxAppInjector() 14 | } 15 | -------------------------------------------------------------------------------- /samples/demo-rx/src/main/kotlin/reactivecircus/blueprint/demo/RxAppInjector.kt: -------------------------------------------------------------------------------- 1 | package reactivecircus.blueprint.demo 2 | 3 | import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers 4 | import io.reactivex.rxjava3.schedulers.Schedulers 5 | import reactivecircus.blueprint.async.rx3.SchedulerProvider 6 | import reactivecircus.blueprint.demo.data.cache.InMemoryNoteCache 7 | import reactivecircus.blueprint.demo.data.cache.NoteCache 8 | import reactivecircus.blueprint.demo.data.repository.RxInMemoryNoteRepository 9 | import reactivecircus.blueprint.demo.domain.interactor.RxCreateNote 10 | import reactivecircus.blueprint.demo.domain.interactor.RxGetNoteByUuid 11 | import reactivecircus.blueprint.demo.domain.interactor.RxStreamAllNotes 12 | import reactivecircus.blueprint.demo.domain.interactor.RxUpdateNote 13 | import reactivecircus.blueprint.demo.domain.repository.RxNoteRepository 14 | import reactivecircus.blueprint.demo.enternote.RxEnterNoteViewModel 15 | import reactivecircus.blueprint.demo.noteslist.RxNotesListViewModel 16 | 17 | open class RxAppInjector { 18 | 19 | private val schedulerProvider: SchedulerProvider by lazy { 20 | SchedulerProvider( 21 | io = Schedulers.io(), 22 | computation = Schedulers.computation(), 23 | ui = AndroidSchedulers.mainThread() 24 | ) 25 | } 26 | 27 | private val noteCache: NoteCache by lazy { 28 | InMemoryNoteCache() 29 | } 30 | 31 | private val noteRepository: RxNoteRepository by lazy { 32 | RxInMemoryNoteRepository(noteCache) 33 | } 34 | 35 | private val streamAllNotes: RxStreamAllNotes by lazy { 36 | RxStreamAllNotes( 37 | noteRepository = noteRepository, 38 | schedulerProvider = schedulerProvider 39 | ) 40 | } 41 | 42 | private val getNoteByUuid: RxGetNoteByUuid by lazy { 43 | RxGetNoteByUuid( 44 | noteRepository = noteRepository, 45 | schedulerProvider = schedulerProvider 46 | ) 47 | } 48 | 49 | private val createNote: RxCreateNote by lazy { 50 | RxCreateNote( 51 | noteRepository = noteRepository, 52 | schedulerProvider = schedulerProvider 53 | ) 54 | } 55 | 56 | private val updateNote: RxUpdateNote by lazy { 57 | RxUpdateNote( 58 | noteRepository = noteRepository, 59 | schedulerProvider = schedulerProvider 60 | ) 61 | } 62 | 63 | open fun provideSchedulerProvider(): SchedulerProvider { 64 | return schedulerProvider 65 | } 66 | 67 | open fun provideNoteCache(): NoteCache { 68 | return noteCache 69 | } 70 | 71 | fun provideNotesListViewModel(): RxNotesListViewModel { 72 | return RxNotesListViewModel(streamAllNotes) 73 | } 74 | 75 | fun provideEnterNoteViewModel(noteUuid: String?): RxEnterNoteViewModel { 76 | return RxEnterNoteViewModel( 77 | noteUuid, 78 | getNoteByUuid, 79 | createNote, 80 | updateNote 81 | ) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /samples/demo-rx/src/main/kotlin/reactivecircus/blueprint/demo/data/repository/RxInMemoryNoteRepository.kt: -------------------------------------------------------------------------------- 1 | package reactivecircus.blueprint.demo.data.repository 2 | 3 | import io.reactivex.rxjava3.core.Completable 4 | import io.reactivex.rxjava3.core.Maybe 5 | import io.reactivex.rxjava3.core.Observable 6 | import io.reactivex.rxjava3.processors.BehaviorProcessor 7 | import reactivecircus.blueprint.demo.data.cache.NoteCache 8 | import reactivecircus.blueprint.demo.domain.model.Note 9 | import reactivecircus.blueprint.demo.domain.repository.RxNoteRepository 10 | 11 | class RxInMemoryNoteRepository( 12 | private val noteCache: NoteCache 13 | ) : RxNoteRepository { 14 | 15 | private val notesProcessor = BehaviorProcessor.createDefault(Unit).toSerialized() 16 | 17 | override fun streamAllNotes(): Observable> { 18 | return notesProcessor.map { noteCache.allNotes() } 19 | .toObservable() 20 | } 21 | 22 | override fun getNoteByUuid(uuid: String): Maybe { 23 | return Maybe.defer { 24 | val note = noteCache.findNote { it.uuid == uuid } 25 | if (note != null) { 26 | Maybe.just(note) 27 | } else { 28 | Maybe.empty() 29 | } 30 | } 31 | } 32 | 33 | override fun addNote(note: Note): Completable { 34 | return Completable.fromAction { 35 | noteCache.addNotes(listOf(note)) 36 | }.doOnComplete { 37 | // refresh all notes stream 38 | notesProcessor.onNext(Unit) 39 | } 40 | } 41 | 42 | override fun updateNote(note: Note): Completable { 43 | return Completable.fromAction { 44 | noteCache.updateNote(note) 45 | }.doOnComplete { 46 | // refresh all notes stream 47 | notesProcessor.onNext(Unit) 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /samples/demo-rx/src/main/kotlin/reactivecircus/blueprint/demo/domain/interactor/RxCreateNote.kt: -------------------------------------------------------------------------------- 1 | package reactivecircus.blueprint.demo.domain.interactor 2 | 3 | import io.reactivex.rxjava3.core.Completable 4 | import reactivecircus.blueprint.async.rx3.SchedulerProvider 5 | import reactivecircus.blueprint.demo.domain.model.Note 6 | import reactivecircus.blueprint.demo.domain.repository.RxNoteRepository 7 | import reactivecircus.blueprint.interactor.InteractorParams 8 | import reactivecircus.blueprint.interactor.rx3.CompletableInteractor 9 | 10 | class RxCreateNote( 11 | private val noteRepository: RxNoteRepository, 12 | schedulerProvider: SchedulerProvider 13 | ) : CompletableInteractor( 14 | ioScheduler = schedulerProvider.io, 15 | uiScheduler = schedulerProvider.ui 16 | ) { 17 | override fun createInteractor(params: Params): Completable { 18 | return noteRepository.addNote(params.note) 19 | } 20 | 21 | class Params(internal val note: Note) : InteractorParams 22 | } 23 | -------------------------------------------------------------------------------- /samples/demo-rx/src/main/kotlin/reactivecircus/blueprint/demo/domain/interactor/RxGetNoteByUuid.kt: -------------------------------------------------------------------------------- 1 | package reactivecircus.blueprint.demo.domain.interactor 2 | 3 | import io.reactivex.rxjava3.core.Maybe 4 | import io.reactivex.rxjava3.core.Single 5 | import reactivecircus.blueprint.async.rx3.SchedulerProvider 6 | import reactivecircus.blueprint.demo.domain.model.Note 7 | import reactivecircus.blueprint.demo.domain.repository.RxNoteRepository 8 | import reactivecircus.blueprint.interactor.InteractorParams 9 | import reactivecircus.blueprint.interactor.rx3.SingleInteractor 10 | 11 | class RxGetNoteByUuid( 12 | private val noteRepository: RxNoteRepository, 13 | schedulerProvider: SchedulerProvider 14 | ) : SingleInteractor( 15 | ioScheduler = schedulerProvider.io, 16 | uiScheduler = schedulerProvider.ui 17 | ) { 18 | override fun createInteractor(params: Params): Single { 19 | return noteRepository.getNoteByUuid(params.uuid) 20 | .switchIfEmpty( 21 | Maybe.error(IllegalStateException("Could not find note by uuid.")) 22 | ) 23 | .toSingle() 24 | } 25 | 26 | class Params(internal val uuid: String) : InteractorParams 27 | } 28 | -------------------------------------------------------------------------------- /samples/demo-rx/src/main/kotlin/reactivecircus/blueprint/demo/domain/interactor/RxStreamAllNotes.kt: -------------------------------------------------------------------------------- 1 | package reactivecircus.blueprint.demo.domain.interactor 2 | 3 | import io.reactivex.rxjava3.core.Observable 4 | import reactivecircus.blueprint.async.rx3.SchedulerProvider 5 | import reactivecircus.blueprint.demo.domain.model.Note 6 | import reactivecircus.blueprint.demo.domain.repository.RxNoteRepository 7 | import reactivecircus.blueprint.interactor.InteractorParams 8 | import reactivecircus.blueprint.interactor.rx3.ObservableInteractor 9 | 10 | class RxStreamAllNotes( 11 | private val noteRepository: RxNoteRepository, 12 | schedulerProvider: SchedulerProvider 13 | ) : ObservableInteractor>( 14 | ioScheduler = schedulerProvider.io, 15 | uiScheduler = schedulerProvider.ui 16 | ) { 17 | override fun createInteractor(params: Params): Observable> { 18 | return noteRepository.streamAllNotes() 19 | .map { notes -> 20 | if (params.sortedBy === SortedBy.TIME_CREATED) { 21 | notes.sortedByDescending { it.timeCreated } 22 | } else { 23 | notes.sortedByDescending { it.timeLastUpdated } 24 | } 25 | } 26 | } 27 | 28 | class Params(internal val sortedBy: SortedBy) : InteractorParams 29 | 30 | enum class SortedBy { 31 | TIME_CREATED, 32 | TIME_LAST_UPDATED 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /samples/demo-rx/src/main/kotlin/reactivecircus/blueprint/demo/domain/interactor/RxUpdateNote.kt: -------------------------------------------------------------------------------- 1 | package reactivecircus.blueprint.demo.domain.interactor 2 | 3 | import io.reactivex.rxjava3.core.Completable 4 | import reactivecircus.blueprint.async.rx3.SchedulerProvider 5 | import reactivecircus.blueprint.demo.domain.model.Note 6 | import reactivecircus.blueprint.demo.domain.repository.RxNoteRepository 7 | import reactivecircus.blueprint.interactor.InteractorParams 8 | import reactivecircus.blueprint.interactor.rx3.CompletableInteractor 9 | 10 | class RxUpdateNote( 11 | private val noteRepository: RxNoteRepository, 12 | schedulerProvider: SchedulerProvider 13 | ) : CompletableInteractor( 14 | ioScheduler = schedulerProvider.io, 15 | uiScheduler = schedulerProvider.ui 16 | ) { 17 | override fun createInteractor(params: Params): Completable { 18 | return noteRepository.updateNote(params.note) 19 | } 20 | 21 | class Params(internal val note: Note) : InteractorParams 22 | } 23 | -------------------------------------------------------------------------------- /samples/demo-rx/src/main/kotlin/reactivecircus/blueprint/demo/domain/repository/RxNoteRepository.kt: -------------------------------------------------------------------------------- 1 | package reactivecircus.blueprint.demo.domain.repository 2 | 3 | import io.reactivex.rxjava3.core.Completable 4 | import io.reactivex.rxjava3.core.Maybe 5 | import io.reactivex.rxjava3.core.Observable 6 | import reactivecircus.blueprint.demo.domain.model.Note 7 | 8 | interface RxNoteRepository { 9 | 10 | fun streamAllNotes(): Observable> 11 | 12 | fun getNoteByUuid(uuid: String): Maybe 13 | 14 | fun addNote(note: Note): Completable 15 | 16 | fun updateNote(note: Note): Completable 17 | } 18 | -------------------------------------------------------------------------------- /samples/demo-rx/src/main/kotlin/reactivecircus/blueprint/demo/enternote/RxEnterNoteViewModel.kt: -------------------------------------------------------------------------------- 1 | package reactivecircus.blueprint.demo.enternote 2 | 3 | import androidx.lifecycle.LiveData 4 | import androidx.lifecycle.MutableLiveData 5 | import androidx.lifecycle.ViewModel 6 | import io.reactivex.rxjava3.disposables.CompositeDisposable 7 | import io.reactivex.rxjava3.kotlin.plusAssign 8 | import io.reactivex.rxjava3.kotlin.subscribeBy 9 | import reactivecircus.blueprint.demo.domain.interactor.RxCreateNote 10 | import reactivecircus.blueprint.demo.domain.interactor.RxGetNoteByUuid 11 | import reactivecircus.blueprint.demo.domain.interactor.RxUpdateNote 12 | import reactivecircus.blueprint.demo.domain.model.Note 13 | import timber.log.Timber 14 | 15 | data class State(val note: Note?) 16 | 17 | class RxEnterNoteViewModel( 18 | noteUuid: String?, 19 | getNoteByUuid: RxGetNoteByUuid, 20 | private val createNote: RxCreateNote, 21 | private val updateNote: RxUpdateNote 22 | ) : ViewModel() { 23 | 24 | private val mutableNoteLiveData = MutableLiveData() 25 | 26 | val noteLiveData: LiveData get() = mutableNoteLiveData 27 | 28 | private val disposable = CompositeDisposable() 29 | 30 | init { 31 | if (noteUuid != null) { 32 | disposable += getNoteByUuid.buildSingle(RxGetNoteByUuid.Params(noteUuid)) 33 | .subscribeBy( 34 | onSuccess = { note -> 35 | mutableNoteLiveData.value = State(note) 36 | }, 37 | onError = { 38 | Timber.e(it) 39 | } 40 | ) 41 | } else { 42 | mutableNoteLiveData.value = State(null) 43 | } 44 | } 45 | 46 | fun createNote(content: String) { 47 | val time = System.currentTimeMillis() 48 | val newNote = Note( 49 | content = content, 50 | timeCreated = time, 51 | timeLastUpdated = time 52 | ) 53 | 54 | disposable += createNote.buildCompletable(RxCreateNote.Params(newNote)).subscribeBy( 55 | onError = { 56 | Timber.e(it) 57 | } 58 | ) 59 | } 60 | 61 | fun updateNote(updatedNote: Note) { 62 | disposable += updateNote.buildCompletable(RxUpdateNote.Params(updatedNote)).subscribeBy( 63 | onError = { 64 | Timber.e(it) 65 | } 66 | ) 67 | } 68 | 69 | override fun onCleared() { 70 | disposable.clear() 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /samples/demo-rx/src/main/kotlin/reactivecircus/blueprint/demo/noteslist/RxNotesListViewModel.kt: -------------------------------------------------------------------------------- 1 | package reactivecircus.blueprint.demo.noteslist 2 | 3 | import androidx.lifecycle.LiveData 4 | import androidx.lifecycle.MutableLiveData 5 | import androidx.lifecycle.ViewModel 6 | import io.reactivex.rxjava3.disposables.CompositeDisposable 7 | import io.reactivex.rxjava3.kotlin.plusAssign 8 | import io.reactivex.rxjava3.kotlin.subscribeBy 9 | import reactivecircus.blueprint.demo.domain.interactor.RxStreamAllNotes 10 | import reactivecircus.blueprint.demo.domain.model.Note 11 | import timber.log.Timber 12 | 13 | sealed class State { 14 | object LoadingNotes : State() 15 | data class Idle(val notes: List) : State() 16 | } 17 | 18 | class RxNotesListViewModel( 19 | streamAllNotes: RxStreamAllNotes 20 | ) : ViewModel() { 21 | 22 | private val mutableNotesLiveData = MutableLiveData() 23 | 24 | val notesLiveData: LiveData get() = mutableNotesLiveData 25 | 26 | private val disposable = CompositeDisposable() 27 | 28 | init { 29 | disposable += streamAllNotes 30 | .buildObservable( 31 | RxStreamAllNotes.Params(RxStreamAllNotes.SortedBy.TIME_LAST_UPDATED) 32 | ) 33 | .map { State.Idle(it) } 34 | .startWithItem(State.LoadingNotes) 35 | .subscribeBy( 36 | onNext = { 37 | mutableNotesLiveData.value = it 38 | }, 39 | onError = { 40 | Timber.e(it) 41 | } 42 | ) 43 | } 44 | 45 | override fun onCleared() { 46 | disposable.clear() 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /samples/demo-rx/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Blueprint RxJava Demo 3 | 4 | -------------------------------------------------------------------------------- /samples/demo-rx/src/test/kotlin/reactivecircus/blueprint/demo/domain/interactor/RxCreateNoteTest.kt: -------------------------------------------------------------------------------- 1 | package reactivecircus.blueprint.demo.domain.interactor 2 | 3 | import com.google.common.truth.Truth.assertThat 4 | import io.mockk.every 5 | import io.mockk.mockk 6 | import io.mockk.slot 7 | import io.mockk.verify 8 | import io.reactivex.rxjava3.core.Completable 9 | import io.reactivex.rxjava3.schedulers.Schedulers 10 | import org.junit.Test 11 | import reactivecircus.blueprint.async.rx3.SchedulerProvider 12 | import reactivecircus.blueprint.demo.domain.model.Note 13 | import reactivecircus.blueprint.demo.domain.repository.RxNoteRepository 14 | 15 | class RxCreateNoteTest { 16 | 17 | private val noteRepository = mockk { 18 | every { addNote(any()) } returns Completable.complete() 19 | } 20 | 21 | private val schedulerProvider = SchedulerProvider( 22 | io = Schedulers.trampoline(), 23 | computation = Schedulers.trampoline(), 24 | ui = Schedulers.trampoline() 25 | ) 26 | 27 | private val createNote = RxCreateNote( 28 | noteRepository = noteRepository, 29 | schedulerProvider = schedulerProvider 30 | ) 31 | 32 | @Test 33 | fun `add note through repository`() { 34 | val dummyNote = Note( 35 | content = "note", 36 | timeCreated = System.currentTimeMillis(), 37 | timeLastUpdated = System.currentTimeMillis() 38 | ) 39 | 40 | val testObserver = createNote.buildCompletable(RxCreateNote.Params(dummyNote)).test() 41 | 42 | val slot = slot() 43 | 44 | verify { noteRepository.addNote(note = capture(slot)) } 45 | 46 | assertThat(slot.captured) 47 | .isEqualTo(dummyNote) 48 | 49 | testObserver.assertComplete() 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /samples/demo-rx/src/test/kotlin/reactivecircus/blueprint/demo/domain/interactor/RxGetNoteByUuidTest.kt: -------------------------------------------------------------------------------- 1 | package reactivecircus.blueprint.demo.domain.interactor 2 | 3 | import io.mockk.every 4 | import io.mockk.mockk 5 | import io.mockk.verify 6 | import io.reactivex.rxjava3.core.Maybe 7 | import io.reactivex.rxjava3.schedulers.Schedulers 8 | import org.junit.Test 9 | import reactivecircus.blueprint.async.rx3.SchedulerProvider 10 | import reactivecircus.blueprint.demo.domain.model.Note 11 | import reactivecircus.blueprint.demo.domain.repository.RxNoteRepository 12 | 13 | class RxGetNoteByUuidTest { 14 | 15 | private val noteRepository = mockk() 16 | 17 | private val schedulerProvider = SchedulerProvider( 18 | io = Schedulers.trampoline(), 19 | computation = Schedulers.trampoline(), 20 | ui = Schedulers.trampoline() 21 | ) 22 | 23 | private val getNoteByUuid = RxGetNoteByUuid( 24 | noteRepository = noteRepository, 25 | schedulerProvider = schedulerProvider 26 | ) 27 | 28 | @Test 29 | fun `get note by uuid from repository`() { 30 | val dummyNote = Note( 31 | content = "note", 32 | timeCreated = System.currentTimeMillis(), 33 | timeLastUpdated = System.currentTimeMillis() 34 | ) 35 | 36 | every { noteRepository.getNoteByUuid(any()) } returns Maybe.just(dummyNote) 37 | 38 | val testObserver = getNoteByUuid.buildSingle(RxGetNoteByUuid.Params("uuid")).test() 39 | 40 | verify { noteRepository.getNoteByUuid(any()) } 41 | 42 | testObserver.assertValue(dummyNote) 43 | } 44 | 45 | @Test 46 | fun `throw exception when note cannot be found from repository`() { 47 | every { noteRepository.getNoteByUuid(any()) } returns Maybe.empty() 48 | 49 | val testObserver = getNoteByUuid.buildSingle(RxGetNoteByUuid.Params("uuid")).test() 50 | 51 | verify { noteRepository.getNoteByUuid(any()) } 52 | 53 | testObserver.assertError(IllegalStateException::class.java) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /samples/demo-rx/src/test/kotlin/reactivecircus/blueprint/demo/domain/interactor/RxUpdateNoteTest.kt: -------------------------------------------------------------------------------- 1 | package reactivecircus.blueprint.demo.domain.interactor 2 | 3 | import com.google.common.truth.Truth.assertThat 4 | import io.mockk.every 5 | import io.mockk.mockk 6 | import io.mockk.slot 7 | import io.mockk.verify 8 | import io.reactivex.rxjava3.core.Completable 9 | import io.reactivex.rxjava3.schedulers.Schedulers 10 | import org.junit.Test 11 | import reactivecircus.blueprint.async.rx3.SchedulerProvider 12 | import reactivecircus.blueprint.demo.domain.model.Note 13 | import reactivecircus.blueprint.demo.domain.repository.RxNoteRepository 14 | 15 | class RxUpdateNoteTest { 16 | 17 | private val noteRepository = mockk { 18 | every { updateNote(any()) } returns Completable.complete() 19 | } 20 | 21 | private val schedulerProvider = SchedulerProvider( 22 | io = Schedulers.trampoline(), 23 | computation = Schedulers.trampoline(), 24 | ui = Schedulers.trampoline() 25 | ) 26 | 27 | private val updateNote = RxUpdateNote( 28 | noteRepository = noteRepository, 29 | schedulerProvider = schedulerProvider 30 | ) 31 | 32 | @Test 33 | fun `update note in repository`() { 34 | val dummyNote = Note( 35 | content = "note", 36 | timeCreated = System.currentTimeMillis(), 37 | timeLastUpdated = System.currentTimeMillis() 38 | ) 39 | 40 | val testObserver = updateNote.buildCompletable(RxUpdateNote.Params(dummyNote)).test() 41 | 42 | val slot = slot() 43 | 44 | verify { noteRepository.updateNote(note = capture(slot)) } 45 | 46 | assertThat(slot.captured) 47 | .isEqualTo(dummyNote) 48 | 49 | testObserver.assertComplete() 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /samples/demo-rx/src/test/kotlin/reactivecircus/blueprint/demo/noteslist/RxNotesListViewModelTest.kt: -------------------------------------------------------------------------------- 1 | package reactivecircus.blueprint.demo.noteslist 2 | 3 | import androidx.arch.core.executor.testing.InstantTaskExecutorRule 4 | import androidx.lifecycle.Observer 5 | import io.mockk.every 6 | import io.mockk.mockk 7 | import io.mockk.verify 8 | import io.reactivex.rxjava3.core.Observable 9 | import io.reactivex.rxjava3.subjects.PublishSubject 10 | import org.junit.Rule 11 | import org.junit.Test 12 | import reactivecircus.blueprint.demo.domain.interactor.RxStreamAllNotes 13 | import reactivecircus.blueprint.demo.domain.model.Note 14 | 15 | class RxNotesListViewModelTest { 16 | 17 | @get:Rule 18 | val instantTaskExecutorRule = InstantTaskExecutorRule() 19 | 20 | private val dummyNotes = listOf( 21 | Note( 22 | content = "Note 1", 23 | timeCreated = System.currentTimeMillis(), 24 | timeLastUpdated = System.currentTimeMillis() 25 | ), 26 | Note( 27 | content = "Note 2", 28 | timeCreated = System.currentTimeMillis(), 29 | timeLastUpdated = System.currentTimeMillis() 30 | ) 31 | ) 32 | 33 | private val streamAllNotes = mockk() 34 | 35 | private val stateObserver = mockk>(relaxed = true) 36 | 37 | private val viewModel: RxNotesListViewModel by lazy { 38 | RxNotesListViewModel(streamAllNotes) 39 | } 40 | 41 | @Test 42 | fun `emit State#LoadingNotes when initialized`() { 43 | every { streamAllNotes.buildObservable(any()) } returns Observable.empty() 44 | 45 | viewModel.notesLiveData.observeForever(stateObserver) 46 | 47 | verify(exactly = 1) { 48 | stateObserver.onChanged( 49 | State.LoadingNotes 50 | ) 51 | } 52 | } 53 | 54 | @Test 55 | fun `emit State#Idle with notes when streamAllNotes emits`() { 56 | val emitter = PublishSubject.create>().toSerialized() 57 | every { streamAllNotes.buildObservable(any()) } returns emitter 58 | 59 | viewModel.notesLiveData.observeForever(stateObserver) 60 | 61 | verify(exactly = 1) { 62 | streamAllNotes.buildObservable(any()) 63 | } 64 | 65 | verify(exactly = 1) { 66 | stateObserver.onChanged( 67 | State.LoadingNotes 68 | ) 69 | } 70 | 71 | emitter.onNext(dummyNotes) 72 | 73 | verify(exactly = 1) { 74 | stateObserver.onChanged( 75 | State.Idle(dummyNotes) 76 | ) 77 | } 78 | 79 | val updatedDummyNotes = dummyNotes + listOf( 80 | Note( 81 | content = "Note 3", 82 | timeCreated = System.currentTimeMillis(), 83 | timeLastUpdated = System.currentTimeMillis() 84 | ) 85 | ) 86 | 87 | emitter.onNext(updatedDummyNotes) 88 | 89 | verify(exactly = 1) { 90 | stateObserver.onChanged( 91 | State.Idle(updatedDummyNotes) 92 | ) 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /samples/demo-testing-common/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /samples/demo-testing-common/README.md: -------------------------------------------------------------------------------- 1 | # Blueprint Demo Testing Infrastructure 2 | 3 | This library module has common UI testing infra code shared by the 2 apps, including the **screen robots** and **test data**. 4 | 5 | A **Screen Robot** implementation uses the predefined view actions, view assertions and Robot DSL from the [blueprint-testing-robot][testing-robot] artifact to provide a layer of abstraction on top of the more general and primitive instrumentation commands. 6 | 7 | An example of a Robot implementation: 8 | 9 | ```kotlin 10 | fun enterNoteScreen(block: EnterNoteRobot.() -> Unit) = 11 | EnterNoteRobot().apply { block() } 12 | 13 | class EnterNoteRobot : 14 | ScreenRobot( 15 | EnterNoteRobotActions(), EnterNoteRobotAssertions() 16 | ) 17 | 18 | class EnterNoteRobotActions : RobotActions { 19 | 20 | fun enterNote(note: String) { 21 | replaceTextInView(R.id.edit_text_note, note) 22 | } 23 | 24 | fun clickSaveButton() { 25 | clickView(R.id.action_save) 26 | } 27 | } 28 | 29 | class EnterNoteRobotAssertions : RobotAssertions { 30 | 31 | fun createNoteScreenTitleDisplayed() { 32 | toolbarHasTitle(R.string.title_create_note) 33 | } 34 | 35 | fun updateNoteScreenTitleDisplayed() { 36 | toolbarHasTitle(R.string.title_update_note) 37 | } 38 | 39 | fun noteDisplayed(note: String) { 40 | viewHasText(R.id.edit_text_note, note) 41 | } 42 | } 43 | ``` 44 | 45 | If the built-in robot actions and robot assertions are not sufficient, you can roll your custom actions or assertions directly using [Espresso][espresso]. Have a look at the custom **Robot Action** example in [blueprint-testing-robot][custom-robot-action]. 46 | 47 | [testing-robot]: ../../blueprint-testing-robot/ 48 | [custom-robot-action]: ../../blueprint-testing-robot#building-custom-robot-actions-and-robot-assertions 49 | [espresso]: https://developer.android.com/training/testing/espresso 50 | -------------------------------------------------------------------------------- /samples/demo-testing-common/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | `blueprint-plugin` 3 | id("com.android.library") 4 | kotlin("android") 5 | } 6 | 7 | android { 8 | namespace = "reactivecircus.blueprint" 9 | } 10 | 11 | dependencies { 12 | implementation(project(":demo-common")) 13 | implementation(project(":blueprint-testing-robot")) 14 | 15 | // Espresso 16 | implementation(libs.androidx.espresso.core) 17 | implementation(libs.androidx.espresso.contrib) 18 | implementation(libs.androidx.espresso.intents) 19 | } 20 | -------------------------------------------------------------------------------- /samples/demo-testing-common/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /samples/demo-testing-common/src/main/kotlin/reactivecircus/blueprint/demo/TestData.kt: -------------------------------------------------------------------------------- 1 | package reactivecircus.blueprint.demo 2 | 3 | import reactivecircus.blueprint.demo.domain.model.Note 4 | 5 | @Suppress("MagicNumber") 6 | val testNotes: List by lazy { 7 | val timeCreated = System.currentTimeMillis() 8 | val timeUpdated = System.currentTimeMillis() + 10 9 | 10 | listOf( 11 | Note( 12 | content = "Architectural frameworks and toolkits for bootstrapping modern Android codebases.", 13 | timeCreated = timeCreated, 14 | timeLastUpdated = timeUpdated 15 | ), 16 | Note( 17 | content = "Wrapper API for doing async work with Kotlin CoroutineDispatcher", 18 | timeCreated = timeCreated - 10, 19 | timeLastUpdated = timeUpdated - 10 20 | ), 21 | Note( 22 | content = "Wrapper API for doing async work with RxJava's Schedulers", 23 | timeCreated = timeCreated - 20, 24 | timeLastUpdated = timeUpdated - 20 25 | ), 26 | Note( 27 | content = "Interactors (use case in Clean Architecture) based on Kotlin Coroutines", 28 | timeCreated = timeCreated - 30, 29 | timeLastUpdated = timeUpdated - 30 30 | ), 31 | Note( 32 | content = "Interactors (use case in Clean Architecture) based on RxJava", 33 | timeCreated = timeCreated - 40, 34 | timeLastUpdated = timeUpdated - 40 35 | ), 36 | Note( 37 | content = "Common APIs for all Blueprint Interactor implementations", 38 | timeCreated = timeCreated - 50, 39 | timeLastUpdated = timeUpdated - 50 40 | ), 41 | Note( 42 | content = "Android UI extensions, utilities and widgets", 43 | timeCreated = timeCreated - 60, 44 | timeLastUpdated = timeUpdated - 60 45 | ), 46 | Note( 47 | content = "Android UI testing framework with Testing Robot DSL", 48 | timeCreated = timeCreated - 70, 49 | timeLastUpdated = timeUpdated - 70 50 | ) 51 | ) 52 | } 53 | -------------------------------------------------------------------------------- /samples/demo-testing-common/src/main/kotlin/reactivecircus/blueprint/demo/enternote/EnterNoteRobot.kt: -------------------------------------------------------------------------------- 1 | package reactivecircus.blueprint.demo.enternote 2 | 3 | import reactivecircus.blueprint.common.R 4 | import reactivecircus.blueprint.testing.RobotActions 5 | import reactivecircus.blueprint.testing.RobotAssertions 6 | import reactivecircus.blueprint.testing.ScreenRobot 7 | import reactivecircus.blueprint.testing.action.clickView 8 | import reactivecircus.blueprint.testing.action.replaceTextInView 9 | import reactivecircus.blueprint.testing.assertion.toolbarHasTitle 10 | import reactivecircus.blueprint.testing.assertion.viewHasText 11 | 12 | fun enterNoteScreen(block: EnterNoteRobot.() -> Unit) = 13 | EnterNoteRobot().apply { block() } 14 | 15 | class EnterNoteRobot : 16 | ScreenRobot( 17 | EnterNoteRobotActions(), 18 | EnterNoteRobotAssertions() 19 | ) 20 | 21 | class EnterNoteRobotActions : RobotActions { 22 | 23 | fun enterNote(note: String) { 24 | replaceTextInView(R.id.edit_text_note, note) 25 | } 26 | 27 | fun clickSaveButton() { 28 | clickView(R.id.action_save) 29 | } 30 | } 31 | 32 | class EnterNoteRobotAssertions : RobotAssertions { 33 | 34 | fun createNoteScreenTitleDisplayed() { 35 | toolbarHasTitle(R.string.title_create_note) 36 | } 37 | 38 | fun updateNoteScreenTitleDisplayed() { 39 | toolbarHasTitle(R.string.title_update_note) 40 | } 41 | 42 | fun noteDisplayed(note: String) { 43 | viewHasText(R.id.edit_text_note, note) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "Blueprint" 2 | 3 | @Suppress("UnstableApiUsage") 4 | dependencyResolutionManagement { 5 | repositories { 6 | mavenCentral() 7 | google() 8 | } 9 | } 10 | 11 | include(":blueprint-interactor-common") 12 | include(":blueprint-interactor-coroutines") 13 | include(":blueprint-interactor-rx2") 14 | include(":blueprint-interactor-rx3") 15 | include(":blueprint-async-coroutines") 16 | include(":blueprint-async-rx2") 17 | include(":blueprint-async-rx3") 18 | include(":blueprint-ui") 19 | include(":blueprint-testing-robot") 20 | includeProject(":demo-coroutines", "samples/demo-coroutines") 21 | includeProject(":demo-rx", "samples/demo-rx") 22 | includeProject(":demo-common", "samples/demo-common") 23 | includeProject(":demo-testing-common", "samples/demo-testing-common") 24 | include(":test-utils") 25 | 26 | fun includeProject(name: String, filePath: String) { 27 | include(name) 28 | project(name).projectDir = File(filePath) 29 | } 30 | -------------------------------------------------------------------------------- /test-utils/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /test-utils/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | `blueprint-plugin` 3 | kotlin("jvm") 4 | } 5 | 6 | dependencies { 7 | implementation(libs.junit) 8 | implementation(libs.truth) 9 | implementation(libs.kotlinx.coroutines.core) 10 | implementation(libs.kotlinx.coroutines.test) 11 | } 12 | -------------------------------------------------------------------------------- /test-utils/src/main/kotlin/reactivecircus/blueprint/testutils/CoroutinesTestRule.kt: -------------------------------------------------------------------------------- 1 | package reactivecircus.blueprint.testutils 2 | 3 | import kotlinx.coroutines.Dispatchers 4 | import kotlinx.coroutines.ExperimentalCoroutinesApi 5 | import kotlinx.coroutines.test.StandardTestDispatcher 6 | import kotlinx.coroutines.test.TestDispatcher 7 | import kotlinx.coroutines.test.resetMain 8 | import kotlinx.coroutines.test.setMain 9 | import org.junit.rules.TestWatcher 10 | import org.junit.runner.Description 11 | 12 | /** 13 | * A test rule that sets the Main coroutine dispatcher for unit testing. 14 | */ 15 | @ExperimentalCoroutinesApi 16 | class CoroutinesTestRule( 17 | val testDispatcher: TestDispatcher = StandardTestDispatcher() 18 | ) : TestWatcher() { 19 | 20 | override fun starting(description: Description) { 21 | super.starting(description) 22 | Dispatchers.setMain(testDispatcher) 23 | } 24 | 25 | override fun finished(description: Description) { 26 | super.finished(description) 27 | Dispatchers.resetMain() 28 | } 29 | } 30 | --------------------------------------------------------------------------------