├── common ├── .gitignore ├── src │ └── main │ │ └── java │ │ └── common │ │ ├── AppScope.kt │ │ ├── ActivityScope.kt │ │ ├── AppContext.kt │ │ ├── ActivityContext.kt │ │ ├── ActivityRetainedScope.kt │ │ ├── ImmediateDispatcher.kt │ │ ├── IoDispatcher.kt │ │ └── MainDispatcher.kt └── build.gradle ├── domain ├── .gitignore ├── src │ ├── main │ │ └── java │ │ │ └── app │ │ │ └── boletinhos │ │ │ └── domain │ │ │ ├── bill │ │ │ ├── BillStatus.kt │ │ │ ├── error │ │ │ │ ├── BillInvalidDueDateException.kt │ │ │ │ ├── BillsIsAlreadyPaidException.kt │ │ │ │ ├── BillValidationException.kt │ │ │ │ └── BillValidationErrorType.kt │ │ │ ├── BillGateway.kt │ │ │ ├── BillService.kt │ │ │ ├── CreateBill.kt │ │ │ ├── PayBill.kt │ │ │ ├── Bill.kt │ │ │ └── BillValidator.kt │ │ │ ├── summary │ │ │ ├── SummaryService.kt │ │ │ ├── SummaryPreferences.kt │ │ │ ├── Summary.kt │ │ │ └── FetchSummary.kt │ │ │ └── currency │ │ │ └── CurrencyMachine.kt │ └── test │ │ └── java │ │ └── app │ │ └── boletinhos │ │ └── domain │ │ ├── summary │ │ ├── FakeSummaryPreferences.kt │ │ └── FakeSummaryService.kt │ │ ├── bill │ │ ├── FakeBillGateway.kt │ │ ├── PayBillTest.kt │ │ └── CreateBillTest.kt │ │ └── currency │ │ └── CurrencyMachineTest.kt └── build.gradle ├── android-ui ├── .gitignore ├── src │ ├── main │ │ ├── res │ │ │ ├── drawable-hdpi │ │ │ │ └── ic_barcode.png │ │ │ ├── drawable-mdpi │ │ │ │ └── ic_barcode.png │ │ │ ├── drawable-xhdpi │ │ │ │ └── ic_barcode.png │ │ │ ├── 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 │ │ │ ├── drawable-xxhdpi │ │ │ │ └── ic_barcode.png │ │ │ ├── drawable-xxxhdpi │ │ │ │ └── ic_barcode.png │ │ │ ├── mipmap-xxxhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── layout │ │ │ │ ├── wip_view.xml │ │ │ │ ├── summary_view_error.xml │ │ │ │ ├── bill_add.xml │ │ │ │ ├── error_view.xml │ │ │ │ ├── summary_item_view.xml │ │ │ │ ├── welcome_view.xml │ │ │ │ └── summary_view.xml │ │ │ ├── values │ │ │ │ ├── attrs.xml │ │ │ │ ├── dimens.xml │ │ │ │ ├── themes.xml │ │ │ │ ├── styles.xml │ │ │ │ ├── colors.xml │ │ │ │ ├── shapes.xml │ │ │ │ ├── strings.xml │ │ │ │ └── baseline.xml │ │ │ ├── values-v23 │ │ │ │ └── themes.xml │ │ │ ├── mipmap-anydpi-v26 │ │ │ │ ├── ic_launcher.xml │ │ │ │ └── ic_launcher_round.xml │ │ │ ├── font │ │ │ │ ├── fira_sans.xml │ │ │ │ ├── vidaloka.xml │ │ │ │ ├── fira_sans_bold.xml │ │ │ │ ├── fira_sans_light.xml │ │ │ │ └── fira_sans_medium.xml │ │ │ ├── drawable-anydpi-v23 │ │ │ │ ├── splash.xml │ │ │ │ └── ic_barcode.xml │ │ │ ├── drawable │ │ │ │ ├── ic_arrow_back.xml │ │ │ │ ├── splash.xml │ │ │ │ ├── ic_add.xml │ │ │ │ ├── ic_check.xml │ │ │ │ ├── ic_hourglass.xml │ │ │ │ ├── ic_calendar.xml │ │ │ │ ├── ic_summary.xml │ │ │ │ ├── ic_launcher_foreground.xml │ │ │ │ └── ic_paid.xml │ │ │ └── animator-v21 │ │ │ │ └── appbar_state_list_animator.xml │ │ ├── java │ │ │ └── app │ │ │ │ └── boletinhos │ │ │ │ ├── summary │ │ │ │ ├── SummaryViewEvent.kt │ │ │ │ ├── SummaryViewState.kt │ │ │ │ ├── SummaryViewKey.kt │ │ │ │ ├── SummaryDecoration.kt │ │ │ │ ├── SummaryAdapter.kt │ │ │ │ └── SummaryItemCardView.kt │ │ │ │ ├── messaging │ │ │ │ └── UiEvent.kt │ │ │ │ ├── bill │ │ │ │ └── add │ │ │ │ │ ├── AddBillViewInput.kt │ │ │ │ │ ├── ExceptionHandler.kt │ │ │ │ │ ├── AddBillViewKey.kt │ │ │ │ │ └── AddBillView.kt │ │ │ │ ├── error │ │ │ │ ├── ErrorViewModel.kt │ │ │ │ └── ErrorView.kt │ │ │ │ ├── wip │ │ │ │ └── WipViewKey.kt │ │ │ │ ├── welcome │ │ │ │ ├── injection │ │ │ │ │ └── WelcomeModule.kt │ │ │ │ ├── WelcomeView.kt │ │ │ │ ├── WelcomeViewModel.kt │ │ │ │ └── WelcomeViewKey.kt │ │ │ │ ├── crashcat │ │ │ │ ├── injection │ │ │ │ │ └── CrashlyticsModule.kt │ │ │ │ └── CrashCat.kt │ │ │ │ ├── main │ │ │ │ ├── injection │ │ │ │ │ ├── ActivityRetainedServicesFactory.kt │ │ │ │ │ ├── ActivityRetainedService.kt │ │ │ │ │ └── ActivityRetainedComponent.kt │ │ │ │ └── MainActivity.kt │ │ │ │ ├── application │ │ │ │ ├── MainApplication.kt │ │ │ │ └── injection │ │ │ │ │ ├── AppModule.kt │ │ │ │ │ └── AppComponent.kt │ │ │ │ └── theming │ │ │ │ └── ThemeAwareDrawable.kt │ │ └── AndroidManifest.xml │ ├── androidTest │ │ └── java │ │ │ └── app │ │ │ └── boletinhos │ │ │ ├── testutil │ │ │ ├── TestActivity.kt │ │ │ ├── TextInputMatcher.kt │ │ │ ├── DateInputMatcher.kt │ │ │ ├── CurrencyInputMatcher.kt │ │ │ ├── RecyclerViewMatcher.kt │ │ │ ├── TextInputLayoutMatcher.kt │ │ │ └── FakeBillsFactory.kt │ │ │ ├── runner │ │ │ └── TestRunner.kt │ │ │ ├── navigation │ │ │ └── FakeView.kt │ │ │ ├── application │ │ │ ├── TestApplication.kt │ │ │ └── injection │ │ │ │ ├── TestDatabaseModule.kt │ │ │ │ ├── TestAppComponent.kt │ │ │ │ └── TestModule.kt │ │ │ ├── WorkflowExampleTest.kt │ │ │ ├── rule │ │ │ └── UsesDatabaseRule.kt │ │ │ ├── welcome │ │ │ ├── WelcomeViewTest.kt │ │ │ └── WitnessRobot.kt │ │ │ ├── widget │ │ │ ├── date │ │ │ │ ├── DateInputTest.kt │ │ │ │ └── FifteenthRobot.kt │ │ │ └── currency │ │ │ │ ├── CurrencyInputTest.kt │ │ │ │ └── QuindimRobot.kt │ │ │ └── lifecycle │ │ │ └── ActivityRetainedCoroutineScopeTest.kt │ ├── test │ │ └── java │ │ │ └── app │ │ │ └── boletinhos │ │ │ ├── lifecycle │ │ │ └── ViewModelScope.kt │ │ │ ├── testutil │ │ │ └── TestKey.kt │ │ │ ├── welcome │ │ │ └── WelcomeViewModelTest.kt │ │ │ └── summary │ │ │ └── SummaryTestUtil.kt │ └── debug │ │ └── AndroidManifest.xml ├── proguard-rules.pro ├── google-services.json └── build.gradle ├── preferences ├── .gitignore ├── src │ ├── main │ │ ├── AndroidManifest.xml │ │ └── java │ │ │ └── app │ │ │ └── boletinhos │ │ │ └── preferences │ │ │ ├── injection │ │ │ └── UserPreferencesModule.kt │ │ │ └── UserPreferences.kt │ └── test │ │ └── java │ │ └── app │ │ └── boletinhos │ │ └── preferences │ │ ├── UserPreferencesTest.kt │ │ └── FakePreferences.kt ├── build.gradle └── proguard-rules.pro ├── testutil ├── .gitignore ├── build.gradle └── src │ └── main │ └── java │ └── testutil │ └── MainCoroutineRule.kt ├── android-core ├── .gitignore ├── src │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── res │ │ │ ├── values │ │ │ │ └── ids.xml │ │ │ └── layout │ │ │ │ ├── text_input_content.xml │ │ │ │ └── modal_bottom_sheet.xml │ │ └── java │ │ │ └── app │ │ │ └── boletinhos │ │ │ ├── lifecycle │ │ │ ├── LifecycleAwareCoroutineScope.kt │ │ │ ├── injection │ │ │ │ └── LifecycleCoroutineScopeModule.kt │ │ │ └── ActivityRetainedCoroutineScope.kt │ │ │ ├── widget │ │ │ ├── recyclerview │ │ │ │ ├── BindableViewHolder.kt │ │ │ │ ├── ListAdapter.kt │ │ │ │ └── VerticalMarginDecoration.kt │ │ │ ├── text │ │ │ │ └── TextInput.kt │ │ │ ├── date │ │ │ │ ├── DateInput.kt │ │ │ │ └── DateTextWatcher.kt │ │ │ └── currency │ │ │ │ ├── CurrencyInput.kt │ │ │ │ └── CurrencyTextWatcher.kt │ │ │ ├── navigation │ │ │ ├── ViewCoroutineScope.kt │ │ │ ├── ViewKey.kt │ │ │ └── StateChange.kt │ │ │ ├── time │ │ │ └── LocalDateParser.kt │ │ │ └── ext │ │ │ └── view │ │ │ └── ViewExt.kt │ └── test │ │ └── java │ │ └── app │ │ └── boletinhos │ │ └── lifecycle │ │ ├── FakeCoroutineScopeClient.kt │ │ └── LifecycleCoroutineScopeTest.kt └── build.gradle ├── bills-service ├── .gitignore ├── src │ ├── main │ │ ├── AndroidManifest.xml │ │ └── java │ │ │ └── app │ │ │ └── boletinhos │ │ │ ├── bill │ │ │ ├── InDatabaseBillGateway.kt │ │ │ ├── BillEntity.kt │ │ │ ├── injection │ │ │ │ └── BillServiceModule.kt │ │ │ └── InDatabaseBillService.kt │ │ │ ├── typeconverter │ │ │ ├── BillStatusTypeConverter.kt │ │ │ └── LocalDateTypeConverter.kt │ │ │ ├── database │ │ │ ├── injection │ │ │ │ └── AppDatabaseModule.kt │ │ │ └── AppDatabase.kt │ │ │ └── summary │ │ │ ├── UserSummaryPreferences.kt │ │ │ ├── injection │ │ │ └── SummaryServiceModule.kt │ │ │ └── InDatabaseSummaryService.kt │ └── test │ │ └── java │ │ └── app │ │ └── boletinhos │ │ ├── testutil │ │ ├── CoroutineTest.kt │ │ └── AppDatabaseTest.kt │ │ ├── summary │ │ ├── UserSummaryPreferencesTest.kt │ │ ├── InDatabaseSummaryServiceTest.kt │ │ └── FakePreferences.kt │ │ ├── typeconverter │ │ ├── LocalDateTypeConverterTest.kt │ │ └── BillStatusTypeConverterTest.kt │ │ ├── bill │ │ ├── InDatabaseBillGatewayTest.kt │ │ └── InDatabaseBillServiceTest.kt │ │ └── fakes │ │ ├── SummaryFactory.kt │ │ └── BillsFactory.kt ├── build.gradle └── schemas │ ├── app.boletinhos.database.AppDatabase │ └── 1.json │ └── app.boletinhos.storage.database.AppDatabase │ └── 1.json ├── .github ├── branding │ └── cover.png └── workflows │ └── code_analysis.yml ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── detekt.gradle ├── settings.gradle ├── LICENSE ├── gradle.properties ├── .gitignore ├── gradlew.bat └── jacoco.gradle /common/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /domain/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /android-ui/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /preferences/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /testutil/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /android-core/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /bills-service/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /android-core/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /preferences/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/branding/cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lcszc/Boletinhos/HEAD/.github/branding/cover.png -------------------------------------------------------------------------------- /bills-service/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | / 4 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lcszc/Boletinhos/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /common/src/main/java/common/AppScope.kt: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import javax.inject.Scope 4 | 5 | @Scope @Retention annotation class AppScope 6 | -------------------------------------------------------------------------------- /android-ui/src/main/res/drawable-hdpi/ic_barcode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lcszc/Boletinhos/HEAD/android-ui/src/main/res/drawable-hdpi/ic_barcode.png -------------------------------------------------------------------------------- /android-ui/src/main/res/drawable-mdpi/ic_barcode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lcszc/Boletinhos/HEAD/android-ui/src/main/res/drawable-mdpi/ic_barcode.png -------------------------------------------------------------------------------- /android-ui/src/main/res/drawable-xhdpi/ic_barcode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lcszc/Boletinhos/HEAD/android-ui/src/main/res/drawable-xhdpi/ic_barcode.png -------------------------------------------------------------------------------- /android-ui/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lcszc/Boletinhos/HEAD/android-ui/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android-ui/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lcszc/Boletinhos/HEAD/android-ui/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android-ui/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lcszc/Boletinhos/HEAD/android-ui/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android-ui/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lcszc/Boletinhos/HEAD/android-ui/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /common/src/main/java/common/ActivityScope.kt: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import javax.inject.Scope 4 | 5 | @Scope @Retention annotation class ActivityScope 6 | -------------------------------------------------------------------------------- /common/src/main/java/common/AppContext.kt: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import javax.inject.Qualifier 4 | 5 | @Qualifier @Retention annotation class AppContext 6 | -------------------------------------------------------------------------------- /android-ui/src/main/res/drawable-xxhdpi/ic_barcode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lcszc/Boletinhos/HEAD/android-ui/src/main/res/drawable-xxhdpi/ic_barcode.png -------------------------------------------------------------------------------- /android-ui/src/main/res/drawable-xxxhdpi/ic_barcode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lcszc/Boletinhos/HEAD/android-ui/src/main/res/drawable-xxxhdpi/ic_barcode.png -------------------------------------------------------------------------------- /android-ui/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lcszc/Boletinhos/HEAD/android-ui/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android-ui/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lcszc/Boletinhos/HEAD/android-ui/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android-ui/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lcszc/Boletinhos/HEAD/android-ui/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android-ui/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lcszc/Boletinhos/HEAD/android-ui/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /common/src/main/java/common/ActivityContext.kt: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import javax.inject.Qualifier 4 | 5 | @Qualifier @Retention annotation class ActivityContext 6 | -------------------------------------------------------------------------------- /detekt.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'io.gitlab.arturbosch.detekt' 2 | 3 | detekt { 4 | config = files("$rootDir/config/detekt/detekt.yml") 5 | autoCorrect = true 6 | } -------------------------------------------------------------------------------- /android-ui/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lcszc/Boletinhos/HEAD/android-ui/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android-ui/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lcszc/Boletinhos/HEAD/android-ui/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /common/src/main/java/common/ActivityRetainedScope.kt: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import javax.inject.Scope 4 | 5 | @Scope @Retention annotation class ActivityRetainedScope 6 | -------------------------------------------------------------------------------- /common/src/main/java/common/ImmediateDispatcher.kt: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import javax.inject.Qualifier 4 | 5 | @Qualifier @Retention annotation class ImmediateDispatcher 6 | -------------------------------------------------------------------------------- /common/src/main/java/common/IoDispatcher.kt: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import javax.inject.Qualifier 4 | 5 | @Qualifier @MustBeDocumented @Retention annotation class IoDispatcher 6 | -------------------------------------------------------------------------------- /common/src/main/java/common/MainDispatcher.kt: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import javax.inject.Qualifier 4 | 5 | @Qualifier @MustBeDocumented @Retention annotation class MainDispatcher 6 | -------------------------------------------------------------------------------- /domain/src/main/java/app/boletinhos/domain/bill/BillStatus.kt: -------------------------------------------------------------------------------- 1 | package app.boletinhos.domain.bill 2 | 3 | enum class BillStatus { 4 | UNPAID, 5 | PAID, 6 | OVERDUE 7 | } 8 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':common' 2 | include ':android-ui' 3 | include ':android-core' 4 | include ':domain' 5 | include ':bills-service' 6 | include ':preferences' 7 | include ':testutil' 8 | -------------------------------------------------------------------------------- /domain/src/main/java/app/boletinhos/domain/bill/error/BillInvalidDueDateException.kt: -------------------------------------------------------------------------------- 1 | package app.boletinhos.domain.bill.error 2 | 3 | object BillInvalidDueDateException : IllegalStateException() 4 | -------------------------------------------------------------------------------- /domain/src/main/java/app/boletinhos/domain/bill/error/BillsIsAlreadyPaidException.kt: -------------------------------------------------------------------------------- 1 | package app.boletinhos.domain.bill.error 2 | 3 | object BillsIsAlreadyPaidException : IllegalStateException() 4 | -------------------------------------------------------------------------------- /domain/src/main/java/app/boletinhos/domain/bill/BillGateway.kt: -------------------------------------------------------------------------------- 1 | package app.boletinhos.domain.bill 2 | 3 | interface BillGateway { 4 | suspend fun create(bill: Bill) 5 | suspend fun pay(bill: Bill) 6 | } 7 | -------------------------------------------------------------------------------- /android-core/src/main/res/values/ids.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /common/build.gradle: -------------------------------------------------------------------------------- 1 | import config.DI 2 | import config.Kotlinx 3 | 4 | apply from: "$rootDir/build-system/kotlin.gradle" 5 | 6 | dependencies { 7 | implementation(DI.javax) 8 | implementation(Kotlinx.Coroutines.core) 9 | } -------------------------------------------------------------------------------- /domain/src/main/java/app/boletinhos/domain/bill/error/BillValidationException.kt: -------------------------------------------------------------------------------- 1 | package app.boletinhos.domain.bill.error 2 | 3 | data class BillValidationException(val errors: List) : RuntimeException() 4 | -------------------------------------------------------------------------------- /android-ui/src/main/java/app/boletinhos/summary/SummaryViewEvent.kt: -------------------------------------------------------------------------------- 1 | package app.boletinhos.summary 2 | 3 | sealed class SummaryViewEvent { 4 | object FetchData : SummaryViewEvent() 5 | object OnClickInAddBill : SummaryViewEvent() 6 | } 7 | -------------------------------------------------------------------------------- /android-ui/src/main/res/layout/wip_view.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /android-ui/src/main/java/app/boletinhos/messaging/UiEvent.kt: -------------------------------------------------------------------------------- 1 | package app.boletinhos.messaging 2 | 3 | import androidx.annotation.StringRes 4 | 5 | sealed class UiEvent { 6 | data class ResourceMessage(@StringRes val messageRes: Int) : UiEvent() 7 | } 8 | -------------------------------------------------------------------------------- /domain/src/main/java/app/boletinhos/domain/summary/SummaryService.kt: -------------------------------------------------------------------------------- 1 | package app.boletinhos.domain.summary 2 | 3 | import kotlinx.coroutines.flow.Flow 4 | 5 | interface SummaryService { 6 | suspend fun hasSummary(): Boolean 7 | fun getSummaries(): Flow> 8 | } 9 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Apr 17 00:06:33 BRT 2020 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip 7 | -------------------------------------------------------------------------------- /android-ui/src/main/res/values/attrs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /domain/build.gradle: -------------------------------------------------------------------------------- 1 | import config.DI 2 | import config.Kotlinx 3 | 4 | apply from: "$rootDir/build-system/kotlin.gradle" 5 | apply plugin: 'kotlin-kapt' 6 | 7 | dependencies { 8 | implementation(Kotlinx.Coroutines.core) 9 | implementation(DI.dagger) 10 | kapt(DI.compiler) 11 | } -------------------------------------------------------------------------------- /testutil/build.gradle: -------------------------------------------------------------------------------- 1 | import config.Kotlinx 2 | import config.Testing 3 | 4 | apply from: "$rootDir/build-system/kotlin.gradle" 5 | 6 | dependencies { 7 | implementation(Kotlinx.Coroutines.core) 8 | implementation(Kotlinx.Coroutines.test) 9 | implementation(Testing.junit) 10 | } -------------------------------------------------------------------------------- /android-ui/src/main/java/app/boletinhos/bill/add/AddBillViewInput.kt: -------------------------------------------------------------------------------- 1 | package app.boletinhos.bill.add 2 | 3 | import java.time.LocalDate 4 | 5 | data class AddBillViewInput( 6 | val value: Long, 7 | val name: String, 8 | val description: String, 9 | val dueDate: LocalDate? 10 | ) 11 | -------------------------------------------------------------------------------- /android-ui/src/main/res/values-v23/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | -------------------------------------------------------------------------------- /android-ui/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /android-ui/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /domain/src/main/java/app/boletinhos/domain/bill/BillService.kt: -------------------------------------------------------------------------------- 1 | package app.boletinhos.domain.bill 2 | 3 | import kotlinx.coroutines.flow.Flow 4 | 5 | interface BillService { 6 | suspend fun getById(id: Long): Bill 7 | fun getAll(): Flow> 8 | fun getByStatus(status: BillStatus): Flow> 9 | } 10 | -------------------------------------------------------------------------------- /android-core/src/main/res/layout/text_input_content.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | -------------------------------------------------------------------------------- /android-core/src/main/java/app/boletinhos/lifecycle/LifecycleAwareCoroutineScope.kt: -------------------------------------------------------------------------------- 1 | package app.boletinhos.lifecycle 2 | 3 | import kotlinx.coroutines.CoroutineDispatcher 4 | import kotlinx.coroutines.CoroutineScope 5 | 6 | interface LifecycleAwareCoroutineScope : CoroutineScope { 7 | val io: CoroutineDispatcher 8 | val main: CoroutineDispatcher 9 | } 10 | -------------------------------------------------------------------------------- /android-ui/src/main/java/app/boletinhos/error/ErrorViewModel.kt: -------------------------------------------------------------------------------- 1 | package app.boletinhos.error 2 | 3 | import android.os.Parcelable 4 | import androidx.annotation.StringRes 5 | import kotlinx.android.parcel.Parcelize 6 | 7 | @Parcelize 8 | data class ErrorViewModel( 9 | @StringRes val titleRes: Int, 10 | @StringRes val messageRes: Int 11 | ) : Parcelable 12 | -------------------------------------------------------------------------------- /android-ui/src/main/java/app/boletinhos/wip/WipViewKey.kt: -------------------------------------------------------------------------------- 1 | package app.boletinhos.wip 2 | 3 | import app.boletinhos.navigation.ViewKey 4 | import kotlinx.android.parcel.Parcelize 5 | import app.boletinhos.R.layout as Layouts 6 | 7 | @Parcelize 8 | data class WipViewKey(val title: String = "") : ViewKey { 9 | override fun layout() = Layouts.wip_view 10 | } 11 | -------------------------------------------------------------------------------- /android-ui/src/main/java/app/boletinhos/bill/add/ExceptionHandler.kt: -------------------------------------------------------------------------------- 1 | package app.boletinhos.bill.add 2 | 3 | import kotlinx.coroutines.CoroutineExceptionHandler 4 | 5 | inline fun addBillExceptionHandler( 6 | crossinline handler: (AddBillViewError) -> Unit 7 | ) = CoroutineExceptionHandler { _, throwable -> 8 | handler(AddBillViewError(throwable)) 9 | } 10 | -------------------------------------------------------------------------------- /domain/src/main/java/app/boletinhos/domain/summary/SummaryPreferences.kt: -------------------------------------------------------------------------------- 1 | package app.boletinhos.domain.summary 2 | 3 | interface SummaryPreferences { 4 | fun actualSummaryId(): Long 5 | fun actualSummary(id: Long) 6 | 7 | companion object { 8 | const val NO_SUMMARY: Long = -1 9 | const val ACTUAL_SUMMARY_ID = "actual_summary_id" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /android-ui/src/main/res/layout/summary_view_error.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /preferences/build.gradle: -------------------------------------------------------------------------------- 1 | import config.DI 2 | import config.Kotlinx 3 | 4 | apply plugin: 'com.android.library' 5 | apply from: "$rootDir/build-system/android.gradle" 6 | 7 | apply plugin: 'kotlin-kapt' 8 | 9 | dependencies { 10 | api project(":common") 11 | 12 | implementation(DI.dagger) 13 | kapt(DI.compiler) 14 | 15 | testImplementation(Kotlinx.Coroutines.test) 16 | } -------------------------------------------------------------------------------- /android-ui/src/main/res/font/fira_sans.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | -------------------------------------------------------------------------------- /android-ui/src/main/res/font/vidaloka.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | -------------------------------------------------------------------------------- /android-ui/src/main/res/drawable-anydpi-v23/splash.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 11 | -------------------------------------------------------------------------------- /android-ui/src/main/res/drawable/ic_arrow_back.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /android-ui/src/main/res/font/fira_sans_bold.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | -------------------------------------------------------------------------------- /android-ui/src/main/res/layout/bill_add.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /android-ui/src/main/res/font/fira_sans_light.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | -------------------------------------------------------------------------------- /android-ui/src/main/res/font/fira_sans_medium.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | -------------------------------------------------------------------------------- /domain/src/test/java/app/boletinhos/domain/summary/FakeSummaryPreferences.kt: -------------------------------------------------------------------------------- 1 | package app.boletinhos.domain.summary 2 | 3 | class FakeSummaryPreferences : SummaryPreferences { 4 | private var summaryId: Long? = null 5 | 6 | override fun actualSummaryId(): Long { 7 | return summaryId ?: SummaryPreferences.NO_SUMMARY 8 | } 9 | 10 | override fun actualSummary(id: Long) { 11 | summaryId = id 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /android-ui/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8dp 5 | 16dp 6 | 7 | @dimen/app_margin_2x 8 | 9 | 10 | 0.3 11 | 0.16 12 | 13 | -------------------------------------------------------------------------------- /android-ui/src/main/java/app/boletinhos/welcome/injection/WelcomeModule.kt: -------------------------------------------------------------------------------- 1 | package app.boletinhos.welcome.injection 2 | 3 | import app.boletinhos.bill.add.AddBillViewModel 4 | import app.boletinhos.welcome.WelcomeViewModel 5 | import dagger.Binds 6 | import dagger.Module 7 | 8 | @Module 9 | abstract class WelcomeModule { 10 | @Binds abstract fun provideOnBillCreatedListener( 11 | impl: WelcomeViewModel 12 | ): AddBillViewModel.OnBillCreatedListener 13 | } 14 | -------------------------------------------------------------------------------- /domain/src/test/java/app/boletinhos/domain/summary/FakeSummaryService.kt: -------------------------------------------------------------------------------- 1 | package app.boletinhos.domain.summary 2 | 3 | import kotlinx.coroutines.flow.Flow 4 | import kotlinx.coroutines.flow.flowOf 5 | 6 | class FakeSummaryService(private val db: List) : SummaryService { 7 | override suspend fun hasSummary(): Boolean { 8 | return db.isNotEmpty() 9 | } 10 | 11 | override fun getSummaries(): Flow> { 12 | return flowOf(db) 13 | } 14 | } -------------------------------------------------------------------------------- /android-core/src/main/java/app/boletinhos/widget/recyclerview/BindableViewHolder.kt: -------------------------------------------------------------------------------- 1 | package app.boletinhos.widget.recyclerview 2 | 3 | import android.view.View 4 | import androidx.recyclerview.widget.RecyclerView 5 | import androidx.viewbinding.ViewBinding 6 | 7 | abstract class BindableViewHolder : RecyclerView.ViewHolder { 8 | constructor(view: View) : super(view) 9 | constructor(viewBinding: ViewBinding) : super(viewBinding.root) 10 | abstract fun bind(model: T) 11 | } 12 | -------------------------------------------------------------------------------- /android-ui/src/main/java/app/boletinhos/crashcat/injection/CrashlyticsModule.kt: -------------------------------------------------------------------------------- 1 | package app.boletinhos.crashcat.injection 2 | 3 | import com.google.firebase.crashlytics.FirebaseCrashlytics 4 | import common.AppScope 5 | import dagger.Module 6 | import dagger.Provides 7 | 8 | @Module 9 | object CrashlyticsModule { 10 | @Provides 11 | @AppScope 12 | internal fun provideCrashlytics(): FirebaseCrashlytics { 13 | return FirebaseCrashlytics.getInstance() 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /android-ui/src/main/res/drawable/splash.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 12 | 13 | -------------------------------------------------------------------------------- /android-ui/src/main/java/app/boletinhos/summary/SummaryViewState.kt: -------------------------------------------------------------------------------- 1 | package app.boletinhos.summary 2 | 3 | import android.os.Parcelable 4 | import app.boletinhos.error.ErrorViewModel 5 | import kotlinx.android.parcel.Parcelize 6 | 7 | @Parcelize 8 | data class SummaryViewState( 9 | val isLoading: Boolean = false, 10 | val isActionAndSummaryVisible: Boolean = false, 11 | val summary: List? = null, 12 | val error: ErrorViewModel? = null 13 | ) : Parcelable 14 | -------------------------------------------------------------------------------- /android-ui/src/androidTest/java/app/boletinhos/testutil/TestActivity.kt: -------------------------------------------------------------------------------- 1 | package app.boletinhos.testutil 2 | 3 | import android.app.Activity 4 | import android.os.Bundle 5 | import android.widget.FrameLayout 6 | import android.R.id as AndroidIds 7 | 8 | class TestActivity : Activity() { 9 | lateinit var rootView: FrameLayout 10 | private set 11 | 12 | override fun onCreate(savedInstanceState: Bundle?) { 13 | super.onCreate(savedInstanceState) 14 | rootView = findViewById(AndroidIds.content) 15 | } 16 | } -------------------------------------------------------------------------------- /bills-service/src/main/java/app/boletinhos/bill/InDatabaseBillGateway.kt: -------------------------------------------------------------------------------- 1 | package app.boletinhos.bill 2 | 3 | import androidx.room.Dao 4 | import androidx.room.Insert 5 | import androidx.room.Update 6 | import app.boletinhos.domain.bill.Bill 7 | import app.boletinhos.domain.bill.BillGateway 8 | 9 | @Dao internal interface InDatabaseBillGateway : BillGateway { 10 | @Insert(entity = BillEntity::class) 11 | override suspend fun create(bill: Bill) 12 | 13 | @Update(entity = BillEntity::class) 14 | override suspend fun pay(bill: Bill) 15 | } 16 | -------------------------------------------------------------------------------- /android-core/src/main/java/app/boletinhos/lifecycle/injection/LifecycleCoroutineScopeModule.kt: -------------------------------------------------------------------------------- 1 | package app.boletinhos.lifecycle.injection 2 | 3 | import app.boletinhos.lifecycle.ActivityRetainedCoroutineScope 4 | import app.boletinhos.lifecycle.LifecycleAwareCoroutineScope 5 | import kotlinx.coroutines.CoroutineScope 6 | 7 | @dagger.Module 8 | object LifecycleCoroutineScopeModule { 9 | @dagger.Provides 10 | internal fun provideCoroutineScope(impl: ActivityRetainedCoroutineScope): LifecycleAwareCoroutineScope { 11 | return impl 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /android-ui/src/test/java/app/boletinhos/lifecycle/ViewModelScope.kt: -------------------------------------------------------------------------------- 1 | package app.boletinhos.lifecycle 2 | 3 | import kotlinx.coroutines.CoroutineDispatcher 4 | import kotlinx.coroutines.test.TestCoroutineDispatcher 5 | import kotlin.coroutines.CoroutineContext 6 | 7 | val viewModelScope = object : LifecycleAwareCoroutineScope { 8 | override val coroutineContext: CoroutineContext = TestCoroutineDispatcher() 9 | override val io: CoroutineDispatcher = TestCoroutineDispatcher() 10 | override val main: CoroutineDispatcher = TestCoroutineDispatcher() 11 | } 12 | -------------------------------------------------------------------------------- /bills-service/src/main/java/app/boletinhos/bill/BillEntity.kt: -------------------------------------------------------------------------------- 1 | package app.boletinhos.bill 2 | 3 | import androidx.room.Entity 4 | import androidx.room.PrimaryKey 5 | import app.boletinhos.domain.bill.BillStatus 6 | import java.time.LocalDate 7 | 8 | @Entity(tableName = "bills") 9 | data class BillEntity( 10 | @PrimaryKey(autoGenerate = true) 11 | var id: Long = 0, 12 | val name: String, 13 | val description: String, 14 | val value: Long, 15 | val paymentDate: LocalDate?, 16 | val dueDate: LocalDate, 17 | val status: BillStatus 18 | ) 19 | -------------------------------------------------------------------------------- /android-ui/src/androidTest/java/app/boletinhos/runner/TestRunner.kt: -------------------------------------------------------------------------------- 1 | package app.boletinhos.runner 2 | 3 | import android.app.Application 4 | import android.content.Context 5 | import androidx.test.runner.AndroidJUnitRunner 6 | import app.boletinhos.application.TestApplication 7 | 8 | class TestRunner : AndroidJUnitRunner() { 9 | override fun newApplication( 10 | cl: ClassLoader?, 11 | className: String?, 12 | context: Context? 13 | ): Application { 14 | return super.newApplication(cl, TestApplication::class.java.name, context) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /domain/src/main/java/app/boletinhos/domain/bill/CreateBill.kt: -------------------------------------------------------------------------------- 1 | package app.boletinhos.domain.bill 2 | 3 | import javax.inject.Inject 4 | 5 | class CreateBill @Inject constructor( 6 | private val gateway: BillGateway, 7 | private val validator: BillValidator 8 | ) { 9 | suspend operator fun invoke(bill: Bill) { 10 | validator.validate(bill) 11 | 12 | val status = if (bill.isOverdue()) BillStatus.OVERDUE else bill.status 13 | val newBill = bill.copy(status = status) 14 | 15 | newBill.id = bill.id 16 | gateway.create(newBill) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /bills-service/src/main/java/app/boletinhos/typeconverter/BillStatusTypeConverter.kt: -------------------------------------------------------------------------------- 1 | package app.boletinhos.typeconverter 2 | 3 | import androidx.room.TypeConverter 4 | import app.boletinhos.domain.bill.BillStatus 5 | 6 | object BillStatusTypeConverter { 7 | @TypeConverter 8 | @JvmStatic 9 | fun toStatus(value: String?): BillStatus? { 10 | if (value == null) return null 11 | return BillStatus.valueOf(value) 12 | } 13 | 14 | @TypeConverter 15 | @JvmStatic 16 | fun fromStatus(status: BillStatus?): String? { 17 | return status?.name 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /android-core/src/main/java/app/boletinhos/widget/recyclerview/ListAdapter.kt: -------------------------------------------------------------------------------- 1 | package app.boletinhos.widget.recyclerview 2 | 3 | import androidx.recyclerview.widget.RecyclerView 4 | 5 | abstract class ListAdapter : RecyclerView.Adapter>() { 6 | var items: List = emptyList() 7 | set(value) { 8 | field = value 9 | notifyDataSetChanged() 10 | } 11 | 12 | override fun getItemCount() = items.size 13 | 14 | override fun onBindViewHolder(holder: BindableViewHolder, position: Int) { 15 | holder.bind(items[position]) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.github/workflows/code_analysis.yml: -------------------------------------------------------------------------------- 1 | name: Static Code Analysis 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - 'main' 7 | paths-ignore: 8 | - '*.md' 9 | 10 | jobs: 11 | code_analysis: 12 | name: Detekt 13 | runs-on: ubuntu-18.04 14 | if: "!contains(github.event.head_commit.message, '[skip-ci]')" 15 | 16 | steps: 17 | - name: Checkout repo 18 | uses: actions/checkout@v2 19 | 20 | - name: Set up JDK 1.8 21 | uses: actions/setup-java@v1 22 | with: 23 | java-version: 1.8 24 | 25 | - name: Run DeteKt check 26 | run: bash ./gradlew detekt -------------------------------------------------------------------------------- /android-ui/src/androidTest/java/app/boletinhos/navigation/FakeView.kt: -------------------------------------------------------------------------------- 1 | package app.boletinhos.navigation 2 | 3 | import android.content.Context 4 | import android.widget.FrameLayout 5 | import android.widget.TextView 6 | import kotlinx.coroutines.launch 7 | 8 | class FakeView(context: Context, private val textContent: String) : FrameLayout(context) { 9 | override fun onAttachedToWindow() { 10 | super.onAttachedToWindow() 11 | 12 | val textView = TextView(context) 13 | addView(textView) 14 | 15 | viewScope.launch { 16 | textView.text = textContent 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /android-ui/src/androidTest/java/app/boletinhos/application/TestApplication.kt: -------------------------------------------------------------------------------- 1 | package app.boletinhos.application 2 | 3 | import app.boletinhos.application.injection.AppComponent 4 | import app.boletinhos.application.injection.DaggerTestAppComponent 5 | 6 | class TestApplication : MainApplication() { 7 | private lateinit var appComponent: AppComponent 8 | 9 | override fun onCreate() { 10 | super.onCreate() 11 | appComponent = DaggerTestAppComponent.factory() 12 | .create(this) 13 | } 14 | 15 | override fun appComponent(): AppComponent { 16 | return appComponent 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /domain/src/main/java/app/boletinhos/domain/bill/PayBill.kt: -------------------------------------------------------------------------------- 1 | package app.boletinhos.domain.bill 2 | 3 | import app.boletinhos.domain.bill.error.BillsIsAlreadyPaidException 4 | import java.time.LocalDate 5 | import javax.inject.Inject 6 | 7 | class PayBill @Inject constructor(private val gateway: BillGateway) { 8 | suspend operator fun invoke(bill: Bill) { 9 | if (bill.isPaid()) throw BillsIsAlreadyPaidException 10 | 11 | val billToPay = bill.copy( 12 | status = BillStatus.PAID, 13 | paymentDate = LocalDate.now() 14 | ) 15 | 16 | billToPay.id = bill.id 17 | gateway.pay(billToPay) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /android-ui/src/test/java/app/boletinhos/testutil/TestKey.kt: -------------------------------------------------------------------------------- 1 | package app.boletinhos.testutil 2 | 3 | import android.os.Parcelable 4 | import com.zhuinden.simplestack.navigator.DefaultViewKey 5 | import com.zhuinden.simplestack.navigator.ViewChangeHandler 6 | import com.zhuinden.simplestack.navigator.changehandlers.NoOpViewChangeHandler 7 | import kotlinx.android.parcel.Parcelize 8 | 9 | @Parcelize 10 | open class TestKey : DefaultViewKey, Parcelable { 11 | override fun layout(): Int = 0 12 | 13 | override fun viewChangeHandler(): ViewChangeHandler { 14 | return NoOpViewChangeHandler() 15 | } 16 | 17 | override fun describeContents(): Int = 0 18 | } 19 | -------------------------------------------------------------------------------- /preferences/src/main/java/app/boletinhos/preferences/injection/UserPreferencesModule.kt: -------------------------------------------------------------------------------- 1 | package app.boletinhos.preferences.injection 2 | 3 | import android.content.Context 4 | import android.content.SharedPreferences 5 | import common.AppContext 6 | import common.AppScope 7 | import dagger.Module 8 | import dagger.Provides 9 | 10 | @Module 11 | object UserPreferencesModule { 12 | private const val NAME = "app_prefs" 13 | 14 | @Provides 15 | @AppScope 16 | internal fun provideSharedPreferences( 17 | @AppContext context: Context 18 | ): SharedPreferences { 19 | return context.getSharedPreferences(NAME, Context.MODE_PRIVATE) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /android-ui/src/androidTest/java/app/boletinhos/WorkflowExampleTest.kt: -------------------------------------------------------------------------------- 1 | package app.boletinhos 2 | 3 | import androidx.test.core.app.ApplicationProvider 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | import app.boletinhos.application.MainApplication 6 | import assertk.assertThat 7 | import assertk.assertions.isEqualTo 8 | import org.junit.Test 9 | import org.junit.runner.RunWith 10 | 11 | @RunWith(AndroidJUnit4::class) 12 | class WorkflowExampleTest { 13 | @Test fun testIsRunningBoletinhoApp() { 14 | val context = ApplicationProvider.getApplicationContext() 15 | assertThat(context.packageName).isEqualTo("app.boletinhos") 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /bills-service/src/main/java/app/boletinhos/database/injection/AppDatabaseModule.kt: -------------------------------------------------------------------------------- 1 | package app.boletinhos.database.injection 2 | 3 | import android.content.Context 4 | import androidx.room.Room 5 | import app.boletinhos.database.AppDatabase 6 | import common.AppContext 7 | import common.AppScope 8 | import dagger.Module 9 | import dagger.Provides 10 | 11 | @Module 12 | object AppDatabaseModule { 13 | @Provides 14 | @AppScope 15 | internal fun provideAppDatabase( 16 | @AppContext context: Context 17 | ): AppDatabase { 18 | return Room 19 | .databaseBuilder(context, AppDatabase::class.java, AppDatabase.DATABASE_NAME) 20 | .build() 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /android-core/src/main/java/app/boletinhos/navigation/ViewCoroutineScope.kt: -------------------------------------------------------------------------------- 1 | package app.boletinhos.navigation 2 | 3 | import android.view.View 4 | import android.view.ViewGroup 5 | import app.boletinhos.core.R.id as Ids 6 | import kotlinx.coroutines.CoroutineScope 7 | 8 | private tailrec fun View.findTopMostParentWithCoroutineScope(): CoroutineScope { 9 | val scope = getTag(Ids.view_coroutine_scope) as CoroutineScope? 10 | return scope ?: checkNotNull(parent as ViewGroup) { 11 | "ViewCoroutineScope wasn't been set for this view tree hierarchy." 12 | }.findTopMostParentWithCoroutineScope() 13 | } 14 | 15 | val View.viewScope: CoroutineScope get() = findTopMostParentWithCoroutineScope() 16 | -------------------------------------------------------------------------------- /android-core/src/test/java/app/boletinhos/lifecycle/FakeCoroutineScopeClient.kt: -------------------------------------------------------------------------------- 1 | package app.boletinhos.lifecycle 2 | 3 | import kotlinx.coroutines.CoroutineScope 4 | import kotlinx.coroutines.async 5 | import kotlinx.coroutines.delay 6 | import kotlinx.coroutines.launch 7 | 8 | internal class FakeCoroutineScopeClient( 9 | coroutineScope: CoroutineScope 10 | ) : CoroutineScope by coroutineScope { 11 | fun job1() = launch { delay(100) } 12 | 13 | fun job2() = launch { delay(200) } 14 | 15 | fun job3() = launch { delay(300) } 16 | 17 | fun job4Async() = async { delay(999) } 18 | 19 | fun job5Async() = async { throw IllegalStateException("An expected error occurred. Bye.") } 20 | } 21 | -------------------------------------------------------------------------------- /android-ui/src/main/java/app/boletinhos/main/injection/ActivityRetainedServicesFactory.kt: -------------------------------------------------------------------------------- 1 | package app.boletinhos.main.injection 2 | 3 | import com.zhuinden.simplestack.Backstack 4 | import com.zhuinden.simplestack.GlobalServices 5 | import com.zhuinden.simplestackextensions.servicesktx.add 6 | import javax.inject.Inject 7 | 8 | class ActivityRetainedServicesFactory @Inject constructor( 9 | private val service: ActivityRetainedService 10 | ) : GlobalServices.Factory { 11 | override fun create(backstack: Backstack): GlobalServices { 12 | service.createComponent(backstack) 13 | 14 | return GlobalServices.builder() 15 | .add(service) 16 | .build() 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /bills-service/src/main/java/app/boletinhos/bill/injection/BillServiceModule.kt: -------------------------------------------------------------------------------- 1 | package app.boletinhos.bill.injection 2 | 3 | import app.boletinhos.database.AppDatabase 4 | import app.boletinhos.domain.bill.BillGateway 5 | import app.boletinhos.domain.bill.BillService 6 | import common.AppScope 7 | import dagger.Module 8 | import dagger.Provides 9 | 10 | @Module 11 | object BillServiceModule { 12 | @Provides 13 | @AppScope 14 | internal fun provideBillService( 15 | database: AppDatabase 16 | ): BillService = database.billService() 17 | 18 | @Provides 19 | @AppScope 20 | internal fun provideBillGateway( 21 | database: AppDatabase 22 | ): BillGateway = database.billGateway() 23 | } 24 | -------------------------------------------------------------------------------- /android-core/src/main/java/app/boletinhos/time/LocalDateParser.kt: -------------------------------------------------------------------------------- 1 | package app.boletinhos.time 2 | 3 | import app.boletinhos.widget.date.DateInput 4 | import java.time.DateTimeException 5 | import java.time.LocalDate 6 | import java.time.format.DateTimeFormatter 7 | 8 | val String.dateOrNull: LocalDate? 9 | get() = parseTextAsLocalDateInternal() 10 | 11 | private val defaultFormatter = DateTimeFormatter.ofPattern("d/MM/yyyy") 12 | 13 | private fun String.parseTextAsLocalDateInternal(): LocalDate? { 14 | if (isEmpty() || length < DateInput.MAX_INPUT_SIZE) return null 15 | 16 | return try { 17 | LocalDate.parse(this, defaultFormatter) 18 | } catch (e: DateTimeException) { 19 | null 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /preferences/src/main/java/app/boletinhos/preferences/UserPreferences.kt: -------------------------------------------------------------------------------- 1 | package app.boletinhos.preferences 2 | 3 | import android.content.SharedPreferences 4 | import androidx.core.content.edit 5 | import common.AppScope 6 | import javax.inject.Inject 7 | 8 | @AppScope 9 | class UserPreferences @Inject constructor( 10 | private val prefs: SharedPreferences 11 | ) { 12 | var isCrashReportEnabled: Boolean get() = prefs.getBoolean(CRASH_REPORTING_ENABLED, false) 13 | set(value) { 14 | prefs.edit { 15 | putBoolean(CRASH_REPORTING_ENABLED, value) 16 | } 17 | } 18 | 19 | companion object { 20 | private const val CRASH_REPORTING_ENABLED = "crash_reporting_enabled" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /android-ui/src/androidTest/java/app/boletinhos/testutil/TextInputMatcher.kt: -------------------------------------------------------------------------------- 1 | package app.boletinhos.testutil 2 | 3 | import android.view.View 4 | import androidx.test.espresso.matcher.BoundedMatcher 5 | import app.boletinhos.widget.text.TextInput 6 | import org.hamcrest.Description 7 | import org.hamcrest.Matcher 8 | 9 | fun textInputHasTextValue(text: String): Matcher { 10 | return object : BoundedMatcher(TextInput::class.java) { 11 | override fun matchesSafely(item: TextInput): Boolean { 12 | return text == item.value.trim() 13 | } 14 | 15 | override fun describeTo(description: Description?) { 16 | description?.appendText("has text $text in TextInput") 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /bills-service/src/main/java/app/boletinhos/bill/InDatabaseBillService.kt: -------------------------------------------------------------------------------- 1 | package app.boletinhos.bill 2 | 3 | import androidx.room.Dao 4 | import androidx.room.Query 5 | import app.boletinhos.domain.bill.Bill 6 | import app.boletinhos.domain.bill.BillService 7 | import app.boletinhos.domain.bill.BillStatus 8 | import kotlinx.coroutines.flow.Flow 9 | 10 | @Dao internal interface InDatabaseBillService : BillService { 11 | @Query("SELECT * FROM bills WHERE id = :id") 12 | override suspend fun getById(id: Long): Bill 13 | 14 | @Query("SELECT * FROM bills") 15 | override fun getAll(): Flow> 16 | 17 | @Query("SELECT * FROM bills WHERE status = :status") 18 | override fun getByStatus(status: BillStatus): Flow> 19 | } 20 | -------------------------------------------------------------------------------- /domain/src/test/java/app/boletinhos/domain/bill/FakeBillGateway.kt: -------------------------------------------------------------------------------- 1 | package app.boletinhos.domain.bill 2 | 3 | class FakeBillGateway : BillGateway { 4 | private val _bills = mutableMapOf() 5 | val bills: Map = _bills 6 | 7 | override suspend fun create(bill: Bill) { 8 | if (bills.keys.contains(bill.id)) { 9 | throw IllegalStateException("Bill already exists.") 10 | } 11 | 12 | _bills += bill.id to bill 13 | } 14 | 15 | override suspend fun pay(bill: Bill) { 16 | if (!bills.keys.contains(bill.id)) { 17 | throw NullPointerException("This bill does not exists. We cant mark it as paid.") 18 | } 19 | 20 | _bills[bill.id] = bill 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /android-ui/src/main/java/app/boletinhos/welcome/WelcomeView.kt: -------------------------------------------------------------------------------- 1 | package app.boletinhos.welcome 2 | 3 | import android.content.Context 4 | import android.util.AttributeSet 5 | import android.widget.LinearLayout 6 | import app.boletinhos.databinding.WelcomeViewBinding 7 | import app.boletinhos.ext.view.service 8 | 9 | class WelcomeView(context: Context, attrs: AttributeSet? = null): LinearLayout(context, attrs) { 10 | private val binding by lazy { WelcomeViewBinding.bind(this) } 11 | private val viewModel by service() 12 | 13 | override fun onAttachedToWindow() { 14 | super.onAttachedToWindow() 15 | binding.actionAddBill.setOnClickListener { 16 | viewModel.onAddBillClick() 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /android-ui/src/main/res/drawable/ic_add.xml: -------------------------------------------------------------------------------- 1 | 6 | 11 | 14 | 15 | -------------------------------------------------------------------------------- /bills-service/src/test/java/app/boletinhos/testutil/CoroutineTest.kt: -------------------------------------------------------------------------------- 1 | package app.boletinhos.testutil 2 | 3 | import kotlinx.coroutines.cancelAndJoin 4 | import kotlinx.coroutines.flow.Flow 5 | import kotlinx.coroutines.flow.collect 6 | import kotlinx.coroutines.launch 7 | import org.junit.Rule 8 | import testutil.MainCoroutineRule 9 | import testutil.runBlocking 10 | 11 | abstract class CoroutineTest { 12 | @get:Rule val mainCoroutineRule = MainCoroutineRule() 13 | 14 | fun Flow.test(block: (actual: T) -> Unit) { 15 | mainCoroutineRule.runBlocking { scope -> 16 | scope.launch { 17 | collect { actual -> 18 | block(actual) 19 | } 20 | }.cancelAndJoin() 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /android-ui/src/androidTest/java/app/boletinhos/testutil/DateInputMatcher.kt: -------------------------------------------------------------------------------- 1 | package app.boletinhos.testutil 2 | 3 | import android.view.View 4 | import androidx.test.espresso.matcher.BoundedMatcher 5 | import app.boletinhos.widget.date.DateInput 6 | import org.hamcrest.Description 7 | import org.hamcrest.Matcher 8 | import java.time.LocalDate 9 | 10 | fun dateInputHasDate(date: LocalDate?) : Matcher { 11 | return object : BoundedMatcher(DateInput::class.java) { 12 | override fun matchesSafely(item: DateInput): Boolean { 13 | return date == item.date 14 | } 15 | 16 | override fun describeTo(description: Description?) { 17 | description?.appendText("date is $date in DateInput") 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /android-ui/src/androidTest/java/app/boletinhos/testutil/CurrencyInputMatcher.kt: -------------------------------------------------------------------------------- 1 | package app.boletinhos.testutil 2 | 3 | import android.view.View 4 | import androidx.test.espresso.matcher.BoundedMatcher 5 | import app.boletinhos.widget.currency.CurrencyInput 6 | import org.hamcrest.Description 7 | import org.hamcrest.Matcher 8 | 9 | fun currencyInputHasRawValue(value: Long) : Matcher { 10 | return object : BoundedMatcher(CurrencyInput::class.java) { 11 | override fun matchesSafely(item: CurrencyInput): Boolean { 12 | return value == item.rawValue 13 | } 14 | 15 | override fun describeTo(description: Description?) { 16 | description?.appendText("has value $value in CurrencyInput") 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /android-core/src/main/java/app/boletinhos/navigation/ViewKey.kt: -------------------------------------------------------------------------------- 1 | package app.boletinhos.navigation 2 | 3 | import android.os.Parcelable 4 | import com.zhuinden.simplestack.navigator.DefaultViewKey 5 | import com.zhuinden.simplestack.navigator.ViewChangeHandler 6 | import com.zhuinden.simplestack.navigator.changehandlers.FadeViewChangeHandler 7 | 8 | @SuppressWarnings("MagicNumber") 9 | interface ViewKey : DefaultViewKey, Parcelable { 10 | override fun viewChangeHandler(): ViewChangeHandler { 11 | return FadeViewChangeHandler() 12 | } 13 | 14 | interface ModalBottomSheet : ViewKey { 15 | val dimAmount: Float get() = 0.8f 16 | 17 | override fun viewChangeHandler(): ViewChangeHandler { 18 | return ModalBottomSheetChangeHandler() 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /bills-service/src/main/java/app/boletinhos/summary/UserSummaryPreferences.kt: -------------------------------------------------------------------------------- 1 | package app.boletinhos.summary 2 | 3 | import android.content.SharedPreferences 4 | import androidx.core.content.edit 5 | import app.boletinhos.domain.summary.SummaryPreferences 6 | import app.boletinhos.domain.summary.SummaryPreferences.Companion.ACTUAL_SUMMARY_ID 7 | import javax.inject.Inject 8 | 9 | @common.AppScope 10 | internal class UserSummaryPreferences @Inject constructor( 11 | private val prefs: SharedPreferences 12 | ) : SummaryPreferences { 13 | override fun actualSummaryId(): Long { 14 | return prefs.getLong(ACTUAL_SUMMARY_ID, -1) 15 | } 16 | 17 | override fun actualSummary(id: Long) { 18 | prefs.edit { 19 | putLong(ACTUAL_SUMMARY_ID, id) 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /android-ui/src/main/res/drawable/ic_check.xml: -------------------------------------------------------------------------------- 1 | 6 | 11 | 14 | 15 | -------------------------------------------------------------------------------- /bills-service/src/main/java/app/boletinhos/summary/injection/SummaryServiceModule.kt: -------------------------------------------------------------------------------- 1 | package app.boletinhos.summary.injection 2 | 3 | import app.boletinhos.database.AppDatabase 4 | import app.boletinhos.domain.summary.SummaryPreferences 5 | import app.boletinhos.domain.summary.SummaryService 6 | import app.boletinhos.summary.UserSummaryPreferences 7 | import common.AppScope 8 | import dagger.Module 9 | import dagger.Provides 10 | 11 | @Module 12 | object SummaryServiceModule { 13 | @Provides 14 | @AppScope 15 | internal fun provideSummaryService( 16 | database: AppDatabase 17 | ): SummaryService = database.summaryService() 18 | 19 | @Provides 20 | internal fun provideSummaryPreferences( 21 | userSummaryPreferences: UserSummaryPreferences 22 | ): SummaryPreferences = userSummaryPreferences 23 | } 24 | -------------------------------------------------------------------------------- /android-ui/src/main/java/app/boletinhos/welcome/WelcomeViewModel.kt: -------------------------------------------------------------------------------- 1 | package app.boletinhos.welcome 2 | 3 | import app.boletinhos.bill.add.AddBillViewKey 4 | import app.boletinhos.bill.add.AddBillViewModel 5 | import app.boletinhos.summary.SummaryViewKey 6 | import com.zhuinden.simplestack.Backstack 7 | import com.zhuinden.simplestack.History 8 | import com.zhuinden.simplestack.StateChange 9 | import javax.inject.Inject 10 | 11 | class WelcomeViewModel @Inject constructor( 12 | private val backStack: Backstack 13 | ) : AddBillViewModel.OnBillCreatedListener { 14 | override fun onBillCreated() { 15 | backStack.setHistory( 16 | History.single(SummaryViewKey()), 17 | StateChange.REPLACE 18 | ) 19 | } 20 | 21 | fun onAddBillClick() { 22 | backStack.goTo(AddBillViewKey()) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /android-ui/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /preferences/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /android-core/src/main/java/app/boletinhos/navigation/StateChange.kt: -------------------------------------------------------------------------------- 1 | package app.boletinhos.navigation 2 | 3 | import app.boletinhos.navigation.ViewKey.ModalBottomSheet 4 | import com.zhuinden.simplestack.StateChange 5 | 6 | internal val StateChange.newKey get() = topNewKey() 7 | 8 | internal val StateChange.previousKey get() = topPreviousKey() 9 | 10 | internal val StateChange.isGoingBackward get() = direction == StateChange.BACKWARD 11 | 12 | internal val StateChange.isGoingForward get() = direction in StateChange.REPLACE..StateChange.FORWARD 13 | 14 | internal val StateChange.isNavigatingFromBottomSheetToView 15 | get() = newKey !is ModalBottomSheet && previousKey is ModalBottomSheet 16 | 17 | internal val StateChange.isNavigatingFromViewToBottomSheet 18 | get() = isGoingBackward && (previousKey !is ModalBottomSheet && newKey is ModalBottomSheet) 19 | 20 | -------------------------------------------------------------------------------- /android-core/src/main/java/app/boletinhos/widget/recyclerview/VerticalMarginDecoration.kt: -------------------------------------------------------------------------------- 1 | package app.boletinhos.widget.recyclerview 2 | 3 | import android.graphics.Rect 4 | import android.view.View 5 | import androidx.annotation.Dimension 6 | import androidx.recyclerview.widget.RecyclerView 7 | 8 | open class VerticalMarginDecoration( 9 | @Dimension private val margin: Int 10 | ) : RecyclerView.ItemDecoration() { 11 | override fun getItemOffsets( 12 | outRect: Rect, 13 | view: View, 14 | parent: RecyclerView, 15 | state: RecyclerView.State 16 | ) { 17 | val viewPosition = parent.getChildLayoutPosition(view) 18 | 19 | val top = if(viewPosition == 0) margin else 0 20 | val bottom = margin 21 | 22 | val start = margin 23 | val end = margin 24 | 25 | outRect.set(start, top, end, bottom) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /android-ui/src/main/java/app/boletinhos/error/ErrorView.kt: -------------------------------------------------------------------------------- 1 | package app.boletinhos.error 2 | 3 | import android.content.Context 4 | import android.util.AttributeSet 5 | import android.widget.LinearLayout 6 | import app.boletinhos.databinding.ErrorViewBinding 7 | import app.boletinhos.ext.view.inflater 8 | 9 | class ErrorView(context: Context, attrs: AttributeSet? = null) : LinearLayout(context, attrs) { 10 | init { 11 | orientation = VERTICAL 12 | ErrorViewBinding.inflate(inflater, this) 13 | } 14 | 15 | private val viewBinding = ErrorViewBinding.bind(this) 16 | 17 | fun bindWith(model: ErrorViewModel, onTryAgain: () -> Unit) = viewBinding.run { 18 | textErrorTitle.setText(model.titleRes) 19 | textErrorMessage.setText(model.messageRes) 20 | actionTryAgain.setOnClickListener { 21 | onTryAgain() 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /android-ui/src/main/java/app/boletinhos/crashcat/CrashCat.kt: -------------------------------------------------------------------------------- 1 | package app.boletinhos.crashcat 2 | 3 | import app.boletinhos.preferences.UserPreferences 4 | import com.google.firebase.crashlytics.FirebaseCrashlytics 5 | import javax.inject.Inject 6 | 7 | /* 8 | * CrashCat is utility class to configure crashlytics 9 | * crashs collection based on user preferences. 10 | * 11 | * 'CrashCat' class name is based on `DataDog`. If they're a dog, we're a cat. 12 | * This class doesn't have much responsibilities, but in the future it can be a wrap 13 | * over Crashlytics and any other tool for analytics. 14 | */ 15 | class CrashCat @Inject constructor( 16 | private val userPreferences: UserPreferences, 17 | private val crashlytics: FirebaseCrashlytics 18 | ) { 19 | fun configure() { 20 | crashlytics.setCrashlyticsCollectionEnabled(userPreferences.isCrashReportEnabled) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /android-ui/src/main/java/app/boletinhos/bill/add/AddBillViewKey.kt: -------------------------------------------------------------------------------- 1 | package app.boletinhos.bill.add 2 | 3 | import app.boletinhos.main.injection.activityRetainedComponent 4 | import app.boletinhos.navigation.ViewKey 5 | import com.zhuinden.simplestack.ServiceBinder 6 | import com.zhuinden.simplestackextensions.services.DefaultServiceProvider.HasServices 7 | import com.zhuinden.simplestackextensions.servicesktx.add 8 | import kotlinx.android.parcel.Parcelize 9 | import app.boletinhos.R.layout as Layouts 10 | 11 | @Parcelize 12 | class AddBillViewKey : ViewKey, HasServices { 13 | override fun layout(): Int = Layouts.bill_add 14 | 15 | override fun getScopeTag(): String = javaClass.name 16 | 17 | override fun bindServices(serviceBinder: ServiceBinder) { 18 | val component = serviceBinder.activityRetainedComponent 19 | serviceBinder.add(component.addBillViewModel()) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /android-core/src/main/java/app/boletinhos/ext/view/ViewExt.kt: -------------------------------------------------------------------------------- 1 | package app.boletinhos.ext.view 2 | 3 | import android.view.LayoutInflater 4 | import android.view.View 5 | import androidx.annotation.StringRes 6 | import com.zhuinden.simplestackextensions.navigatorktx.backstack 7 | import com.zhuinden.simplestackextensions.servicesktx.lookup 8 | 9 | inline fun View.service(serviceTag: String = T::class.java.name): Lazy { 10 | return lazy(LazyThreadSafetyMode.NONE) { 11 | return@lazy backstack.lookup(serviceTag) 12 | } 13 | } 14 | 15 | val View.inflater: LayoutInflater get() = LayoutInflater.from(context) 16 | 17 | fun View.getString(@StringRes resId: Int): String { 18 | return context.getString(resId) 19 | } 20 | 21 | @Suppress("SpreadOperator") 22 | fun View.getString(@StringRes resId: Int, vararg value: Any): String { 23 | return context.getString(resId, *value) 24 | } 25 | -------------------------------------------------------------------------------- /android-ui/src/main/java/app/boletinhos/welcome/WelcomeViewKey.kt: -------------------------------------------------------------------------------- 1 | package app.boletinhos.welcome 2 | 3 | import app.boletinhos.main.injection.activityRetainedComponent 4 | import app.boletinhos.navigation.ViewKey 5 | import com.zhuinden.simplestack.ServiceBinder 6 | import com.zhuinden.simplestackextensions.services.DefaultServiceProvider 7 | import com.zhuinden.simplestackextensions.servicesktx.add 8 | import kotlinx.android.parcel.Parcelize 9 | import app.boletinhos.R.layout as Layouts 10 | 11 | @Parcelize 12 | class WelcomeViewKey : ViewKey, DefaultServiceProvider.HasServices { 13 | override fun layout(): Int = Layouts.welcome_view 14 | 15 | override fun getScopeTag() = javaClass.name 16 | 17 | override fun bindServices(serviceBinder: ServiceBinder) { 18 | val component = serviceBinder.activityRetainedComponent 19 | serviceBinder.add(component.welcomeViewModel()) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /android-core/src/main/java/app/boletinhos/lifecycle/ActivityRetainedCoroutineScope.kt: -------------------------------------------------------------------------------- 1 | package app.boletinhos.lifecycle 2 | 3 | import common.ActivityRetainedScope 4 | import common.IoDispatcher 5 | import kotlinx.coroutines.CoroutineDispatcher 6 | import kotlinx.coroutines.SupervisorJob 7 | import javax.inject.Inject 8 | import kotlin.coroutines.CoroutineContext 9 | 10 | @ActivityRetainedScope 11 | internal class ActivityRetainedCoroutineScope @Inject constructor( 12 | @common.ImmediateDispatcher dispatcher: CoroutineDispatcher, 13 | @common.MainDispatcher mainDispatcher: CoroutineDispatcher, 14 | @common.IoDispatcher ioDispatcher: CoroutineDispatcher 15 | ): LifecycleAwareCoroutineScope { 16 | override val coroutineContext: CoroutineContext = SupervisorJob() + dispatcher 17 | 18 | override val io: CoroutineDispatcher = ioDispatcher 19 | override val main: CoroutineDispatcher = mainDispatcher 20 | } 21 | -------------------------------------------------------------------------------- /android-ui/src/main/java/app/boletinhos/summary/SummaryViewKey.kt: -------------------------------------------------------------------------------- 1 | package app.boletinhos.summary 2 | 3 | import app.boletinhos.R 4 | import app.boletinhos.main.injection.activityRetainedComponent 5 | import app.boletinhos.navigation.ViewKey 6 | import com.zhuinden.simplestack.ServiceBinder 7 | import com.zhuinden.simplestackextensions.services.DefaultServiceProvider 8 | import com.zhuinden.simplestackextensions.servicesktx.add 9 | import kotlinx.android.parcel.Parcelize 10 | 11 | @Parcelize 12 | class SummaryViewKey : ViewKey, DefaultServiceProvider.HasServices { 13 | override fun layout() = R.layout.summary_view 14 | 15 | override fun getScopeTag() = javaClass.name 16 | 17 | override fun bindServices(serviceBinder: ServiceBinder) { 18 | val activityRetainedComponent = serviceBinder.activityRetainedComponent 19 | serviceBinder.add(activityRetainedComponent.summaryViewModel()) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /bills-service/src/main/java/app/boletinhos/typeconverter/LocalDateTypeConverter.kt: -------------------------------------------------------------------------------- 1 | package app.boletinhos.typeconverter 2 | 3 | import androidx.room.TypeConverter 4 | import java.time.LocalDate 5 | import java.time.Month 6 | import java.time.format.DateTimeFormatter 7 | 8 | object LocalDateTypeConverter { 9 | private val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd") 10 | 11 | @TypeConverter 12 | @JvmStatic 13 | fun toLocalDateTime(value: String?): LocalDate? { 14 | if (value == null) return null 15 | return formatter.parse(value, LocalDate::from) 16 | } 17 | 18 | @TypeConverter 19 | @JvmStatic 20 | fun fromLocalDateTime(date: LocalDate?) = date?.format(formatter) 21 | 22 | @TypeConverter 23 | @JvmStatic 24 | fun toMonth(value: String?): Month? { 25 | val month = value?.toIntOrNull() ?: return null 26 | return Month.of(month) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /domain/src/main/java/app/boletinhos/domain/bill/Bill.kt: -------------------------------------------------------------------------------- 1 | package app.boletinhos.domain.bill 2 | 3 | import java.time.LocalDate 4 | 5 | data class Bill( 6 | val name: String, 7 | val description: String, 8 | val value: Long, 9 | val paymentDate: LocalDate?, 10 | val dueDate: LocalDate, 11 | val status: BillStatus 12 | ) { 13 | var id: Long = 0L 14 | 15 | fun isOverdue(): Boolean { 16 | if (isPaid()) return false 17 | return dueDate.isBefore(LocalDate.now()) 18 | } 19 | 20 | fun isPaid() = paymentDate != null 21 | 22 | companion object { 23 | const val MINIMUM_VALUE = 10_00L /* 10 */ 24 | const val MAXIMUM_VALUE = 250_000_00L /* 250k */ 25 | const val MINIMUM_NAME_COUNT = 5 26 | const val MAXIMUM_NAME_COUNT = 48 27 | const val MINIMUM_DESCRIPTION_COUNT = 8 28 | const val MAXIMUM_DESCRIPTION_COUNT = 148 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /domain/src/test/java/app/boletinhos/domain/currency/CurrencyMachineTest.kt: -------------------------------------------------------------------------------- 1 | package app.boletinhos.domain.currency 2 | 3 | import assertk.assertThat 4 | import assertk.assertions.isEqualTo 5 | import org.junit.Test 6 | import java.util.Locale 7 | 8 | class CurrencyMachineTest { 9 | @Test fun `should format raw value and keep currency symbol`() { 10 | val expected = "$250,000.00" 11 | val rawValue = 250_000_00L 12 | val actual = CurrencyMachine.formatFromRawValue(rawValue = rawValue, locale = Locale.US) 13 | 14 | assertThat(actual).isEqualTo(expected) 15 | } 16 | 17 | @Test fun `should format from raw value and remove currency symbol`() { 18 | val expected = "250.000,00" /* ptbr */ 19 | val rawValue = 250_000_00L 20 | val actual = CurrencyMachine.formatFromRawValueAndRemoveSymbol(rawValue = rawValue, locale = Locale("pt", "Br")) 21 | assertThat(actual.trim()).isEqualTo(expected) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /android-ui/src/androidTest/java/app/boletinhos/rule/UsesDatabaseRule.kt: -------------------------------------------------------------------------------- 1 | package app.boletinhos.rule 2 | 3 | import androidx.test.core.app.ApplicationProvider 4 | import app.boletinhos.application.TestApplication 5 | import app.boletinhos.application.injection.TestAppComponent 6 | import app.boletinhos.database.AppDatabase 7 | import org.junit.rules.TestWatcher 8 | import org.junit.runner.Description 9 | import javax.inject.Inject 10 | 11 | class UsesDatabaseRule : TestWatcher() { 12 | @Inject lateinit var database: AppDatabase 13 | 14 | override fun starting(description: Description?) { 15 | super.starting(description) 16 | val component = (ApplicationProvider.getApplicationContext() 17 | .appComponent() as TestAppComponent) 18 | 19 | component.inject(this) 20 | } 21 | 22 | override fun finished(description: Description?) { 23 | super.finished(description) 24 | database.clearAllTables() 25 | } 26 | } -------------------------------------------------------------------------------- /android-ui/src/main/java/app/boletinhos/main/injection/ActivityRetainedService.kt: -------------------------------------------------------------------------------- 1 | package app.boletinhos.main.injection 2 | 3 | import android.app.Application 4 | import app.boletinhos.application.MainApplication 5 | import com.zhuinden.simplestack.Backstack 6 | import com.zhuinden.simplestack.ScopedServices 7 | import kotlinx.coroutines.cancel 8 | import javax.inject.Inject 9 | 10 | class ActivityRetainedService @Inject constructor( 11 | private val application: Application 12 | ) : ScopedServices.Registered { 13 | lateinit var component: ActivityRetainedComponent 14 | private set 15 | 16 | fun createComponent(backstack: Backstack) { 17 | component = (application as MainApplication).appComponent() 18 | .activityRetainedComponentFactory() 19 | .create(backstack) 20 | } 21 | 22 | override fun onServiceRegistered() = Unit 23 | 24 | override fun onServiceUnregistered() { 25 | component.coroutineScope().cancel() 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /android-core/src/main/java/app/boletinhos/widget/text/TextInput.kt: -------------------------------------------------------------------------------- 1 | package app.boletinhos.widget.text 2 | 3 | import android.content.Context 4 | import android.util.AttributeSet 5 | import app.boletinhos.core.databinding.TextInputContentBinding 6 | import app.boletinhos.ext.view.inflater 7 | import com.google.android.material.textfield.TextInputLayout 8 | 9 | open class TextInput( 10 | context: Context, 11 | attrs: AttributeSet? = null 12 | ) : TextInputLayout(context, attrs) { 13 | internal lateinit var inputBinding: TextInputContentBinding 14 | private set 15 | 16 | open var value: String 17 | get() = inputBinding.input.text?.toString().orEmpty() 18 | set(value) = setTextInternal(value) 19 | 20 | override fun onAttachedToWindow() { 21 | super.onAttachedToWindow() 22 | inputBinding = TextInputContentBinding.inflate(inflater, this, true) 23 | } 24 | 25 | private fun setTextInternal(value: String) { 26 | inputBinding.input.setText(value) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /domain/src/main/java/app/boletinhos/domain/bill/error/BillValidationErrorType.kt: -------------------------------------------------------------------------------- 1 | package app.boletinhos.domain.bill.error 2 | 3 | import app.boletinhos.domain.bill.Bill.Companion.MAXIMUM_DESCRIPTION_COUNT 4 | import app.boletinhos.domain.bill.Bill.Companion.MAXIMUM_NAME_COUNT 5 | import app.boletinhos.domain.bill.Bill.Companion.MAXIMUM_VALUE 6 | import app.boletinhos.domain.bill.Bill.Companion.MINIMUM_DESCRIPTION_COUNT 7 | import app.boletinhos.domain.bill.Bill.Companion.MINIMUM_NAME_COUNT 8 | import app.boletinhos.domain.bill.Bill.Companion.MINIMUM_VALUE 9 | 10 | enum class BillValidationErrorType(val rawValue: Long) { 11 | VALUE_MIN_REQUIRED(rawValue = MINIMUM_VALUE), 12 | VALUE_MAX_EXCEEDED(rawValue = MAXIMUM_VALUE), 13 | NAME_MIN_REQUIRED(rawValue = MINIMUM_NAME_COUNT.toLong()), 14 | NAME_MAX_EXCEEDED(rawValue = MAXIMUM_NAME_COUNT.toLong()), 15 | DESCRIPTION_MIN_REQUIRED(rawValue = MINIMUM_DESCRIPTION_COUNT.toLong()), 16 | DESCRIPTION_MAX_EXCEEDED(rawValue = MAXIMUM_DESCRIPTION_COUNT.toLong()) 17 | } 18 | -------------------------------------------------------------------------------- /android-core/src/main/res/layout/modal_bottom_sheet.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 14 | 15 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /android-ui/src/androidTest/java/app/boletinhos/testutil/RecyclerViewMatcher.kt: -------------------------------------------------------------------------------- 1 | package app.boletinhos.testutil 2 | 3 | import android.view.View 4 | import androidx.recyclerview.widget.RecyclerView 5 | import androidx.test.espresso.matcher.BoundedMatcher 6 | import org.hamcrest.Description 7 | import org.hamcrest.Matcher 8 | 9 | fun atPositionOnView( 10 | position: Int, 11 | target: Int, 12 | matcher: Matcher 13 | ): Matcher? { 14 | return object : BoundedMatcher(RecyclerView::class.java) { 15 | override fun describeTo(description: Description) { 16 | description.appendText("has view id $matcher at position $position") 17 | } 18 | 19 | override fun matchesSafely(recyclerView: RecyclerView): Boolean { 20 | val viewHolder = recyclerView.findViewHolderForAdapterPosition(position) 21 | val targetView: View = viewHolder!!.itemView.findViewById(target) 22 | return matcher.matches(targetView) 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /android-ui/src/main/java/app/boletinhos/application/MainApplication.kt: -------------------------------------------------------------------------------- 1 | package app.boletinhos.application 2 | 3 | import android.app.Application 4 | import androidx.appcompat.app.AppCompatDelegate 5 | import app.boletinhos.application.injection.AppComponent 6 | import app.boletinhos.application.injection.DaggerAppComponent 7 | import app.boletinhos.crashcat.CrashCat 8 | import javax.inject.Inject 9 | 10 | open class MainApplication : Application() { 11 | private lateinit var component: AppComponent 12 | 13 | private fun injector(): AppComponent { 14 | return DaggerAppComponent.factory() 15 | .create(this) 16 | .also { component -> component.inject(this) } 17 | } 18 | 19 | @Inject lateinit var crashCat: CrashCat 20 | 21 | override fun onCreate() { 22 | super.onCreate() 23 | AppCompatDelegate.setCompatVectorFromResourcesEnabled(true) 24 | 25 | component = injector() 26 | crashCat.configure() 27 | } 28 | 29 | open fun appComponent() = component 30 | } 31 | -------------------------------------------------------------------------------- /bills-service/src/main/java/app/boletinhos/summary/InDatabaseSummaryService.kt: -------------------------------------------------------------------------------- 1 | package app.boletinhos.summary 2 | 3 | import androidx.room.Dao 4 | import androidx.room.Query 5 | import app.boletinhos.domain.summary.Summary 6 | import app.boletinhos.domain.summary.SummaryService 7 | import kotlinx.coroutines.flow.Flow 8 | 9 | @Dao internal interface InDatabaseSummaryService : SummaryService { 10 | @Query( 11 | """ 12 | SELECT 13 | strftime('%m', dueDate) AS month, 14 | strftime('%Y', dueDate) AS year, 15 | SUM(value) AS totalValue, 16 | SUM(status = 'PAID') AS paids, 17 | SUM(status = 'UNPAID') AS unpaids, 18 | SUM(status = 'OVERDUE') AS overdue 19 | FROM bills 20 | GROUP BY strftime('%m-%Y', dueDate) 21 | ORDER BY dueDate DESC 22 | """ 23 | ) 24 | override fun getSummaries(): Flow> 25 | 26 | @Query("SELECT EXISTS(SELECT * FROM bills)") 27 | override suspend fun hasSummary(): Boolean 28 | } 29 | -------------------------------------------------------------------------------- /android-ui/src/main/res/drawable/ic_hourglass.xml: -------------------------------------------------------------------------------- 1 | 6 | 11 | 12 | 18 | 19 | 23 | 24 | -------------------------------------------------------------------------------- /android-ui/src/main/res/layout/error_view.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | 11 | 18 | 19 |