├── app ├── .gitignore ├── src │ └── main │ │ ├── res │ │ ├── drawable │ │ │ ├── ic_launcher.xml │ │ │ └── ic_launcher_foreground.xml │ │ ├── drawable-v26 │ │ │ └── ic_launcher.xml │ │ └── drawable-v24 │ │ │ └── ic_launcher_foreground.xml │ │ ├── AndroidManifest.xml │ │ └── java │ │ └── dev │ │ └── yankew │ │ └── sample │ │ └── cleanarch │ │ ├── MainApplication.kt │ │ ├── crashlytics │ │ ├── CrashlyticsHelper.kt │ │ └── CrashlyticsTree.kt │ │ ├── AppModule.kt │ │ └── AppBaseInitializerImpl.kt ├── proguard-rules.pro ├── google-services.json └── build.gradle.kts ├── domain ├── .gitignore ├── src │ └── main │ │ └── java │ │ └── dev │ │ └── yankew │ │ └── sample │ │ └── domain │ │ ├── event │ │ ├── AnalyticsOptionEvent.kt │ │ ├── CrashlyticsOptionEvent.kt │ │ ├── EventHandler.kt │ │ ├── AppLaunchedEvent.kt │ │ ├── EventDispatcher.kt │ │ └── EventModule.kt │ │ ├── util │ │ ├── Optional.kt │ │ └── Logger.kt │ │ ├── model │ │ ├── AppVariant.kt │ │ └── DataResult.kt │ │ ├── config │ │ ├── NewFeatureParam.kt │ │ ├── Parameters.kt │ │ ├── ConfigModule.kt │ │ ├── ConfigValue.kt │ │ └── ConfigParam.kt │ │ ├── repository │ │ ├── AppRepository.kt │ │ └── FeatureRepository.kt │ │ ├── settings │ │ ├── SettingOption.kt │ │ └── SettingsQualifiers.kt │ │ ├── analytics │ │ ├── AnalyticsTracker.kt │ │ ├── ClickTrackingEvent.kt │ │ ├── AnalyticsEventHandlers.kt │ │ ├── AnalyticsEventDispatcher.kt │ │ ├── AnalyticsUserProperties.kt │ │ ├── AnalyticsModule.kt │ │ └── SettingsChangedTrackingEvent.kt │ │ ├── coroutine │ │ ├── CoroutinesQualifiers.kt │ │ └── CoroutineModule.kt │ │ └── interactor │ │ ├── GetAppVariantUseCase.kt │ │ ├── FlowUseCase.kt │ │ ├── GetElapsedMinutesUseCase.kt │ │ └── GetNewFeatureEnabledUseCase.kt └── build.gradle.kts ├── comp ├── core │ ├── .gitignore │ ├── consumer-rules.pro │ ├── src │ │ └── main │ │ │ ├── AndroidManifest.xml │ │ │ └── java │ │ │ └── dev │ │ │ └── yankew │ │ │ └── sample │ │ │ └── core │ │ │ ├── CoreModule.kt │ │ │ └── LoggerImpl.kt │ └── build.gradle.kts ├── download │ ├── .gitignore │ ├── consumer-rules.pro │ ├── src │ │ └── main │ │ │ └── AndroidManifest.xml │ └── build.gradle.kts ├── feature │ ├── .gitignore │ ├── consumer-rules.pro │ ├── src │ │ └── main │ │ │ ├── AndroidManifest.xml │ │ │ └── java │ │ │ └── dev │ │ │ └── yankew │ │ │ └── sample │ │ │ └── feature │ │ │ ├── FeatureRepositoryImpl.kt │ │ │ ├── FeatureEventHandlers.kt │ │ │ ├── ElapseDataSource.kt │ │ │ └── FeatureModule.kt │ └── build.gradle.kts ├── sample-data │ ├── .gitignore │ ├── consumer-rules.pro │ ├── src │ │ └── main │ │ │ ├── AndroidManifest.xml │ │ │ └── java │ │ │ └── dev │ │ │ └── yankew │ │ │ └── sample │ │ │ └── data │ │ │ └── sample │ │ │ ├── SampleModule.kt │ │ │ └── SampleFeatureRepository.kt │ └── build.gradle.kts ├── settings │ ├── .gitignore │ ├── consumer-rules.pro │ ├── src │ │ └── main │ │ │ ├── AndroidManifest.xml │ │ │ └── java │ │ │ └── dev │ │ │ └── yankew │ │ │ └── sample │ │ │ └── settings │ │ │ ├── SettingsModule.kt │ │ │ └── SettingsStore.kt │ └── build.gradle.kts ├── analytics-debug │ ├── .gitignore │ ├── consumer-rules.pro │ ├── src │ │ └── main │ │ │ ├── AndroidManifest.xml │ │ │ └── java │ │ │ └── dev │ │ │ └── yankew │ │ │ └── sample │ │ │ └── analytics │ │ │ └── debug │ │ │ ├── DebugAnalyticsModule.kt │ │ │ └── DebugAnalyticsTracker.kt │ └── build.gradle.kts ├── config-firebase │ ├── .gitignore │ ├── consumer-rules.pro │ ├── src │ │ └── main │ │ │ ├── res │ │ │ └── xml │ │ │ │ └── remote_config_defaults.xml │ │ │ ├── AndroidManifest.xml │ │ │ └── java │ │ │ └── dev │ │ │ └── yankew │ │ │ └── sample │ │ │ └── config │ │ │ └── firebase │ │ │ ├── RemoteConfig.kt │ │ │ ├── RemoteConfigInitializer.kt │ │ │ ├── RemoteParams.kt │ │ │ ├── RemoteParam.kt │ │ │ ├── RemoteConfigModule.kt │ │ │ ├── RemoteConfigHelper.kt │ │ │ ├── RemoteConfigImpl.kt │ │ │ └── RemoteConfigFetchWorker.kt │ └── build.gradle.kts ├── config-local │ ├── .gitignore │ ├── consumer-rules.pro │ ├── src │ │ └── main │ │ │ ├── AndroidManifest.xml │ │ │ └── java │ │ │ └── dev │ │ │ └── yankew │ │ │ └── sample │ │ │ └── config │ │ │ └── local │ │ │ ├── LocalParams.kt │ │ │ └── LocalConfigModule.kt │ └── build.gradle.kts ├── notification │ ├── .gitignore │ ├── consumer-rules.pro │ ├── src │ │ └── main │ │ │ └── AndroidManifest.xml │ └── build.gradle.kts └── analytics-firebase │ ├── .gitignore │ ├── consumer-rules.pro │ ├── src │ └── main │ │ ├── AndroidManifest.xml │ │ └── java │ │ └── dev │ │ └── yankew │ │ └── sample │ │ └── analytics │ │ └── firebase │ │ ├── AnalyticsOptionHandler.kt │ │ ├── FirebaseAnalyticsModule.kt │ │ ├── FirebaseAnalyticsTracker.kt │ │ └── FirebaseAnalyticsInitializer.kt │ └── build.gradle.kts ├── lib ├── android │ ├── .gitignore │ ├── consumer-rules.pro │ ├── src │ │ └── main │ │ │ ├── AndroidManifest.xml │ │ │ └── java │ │ │ └── dev │ │ │ └── yankew │ │ │ └── sample │ │ │ └── android │ │ │ └── FlowBroadcastReceiver.kt │ └── build.gradle.kts ├── compose │ ├── .gitignore │ ├── consumer-rules.pro │ ├── src │ │ └── main │ │ │ ├── AndroidManifest.xml │ │ │ └── java │ │ │ └── dev │ │ │ └── yankew │ │ │ └── sample │ │ │ └── compose │ │ │ ├── theme │ │ │ ├── Type.kt │ │ │ ├── Shape.kt │ │ │ ├── Color.kt │ │ │ └── Theme.kt │ │ │ ├── ActivityViewModel.kt │ │ │ └── FlowWithLifecycle.kt │ └── build.gradle.kts ├── init │ ├── .gitignore │ ├── consumer-rules.pro │ ├── src │ │ └── main │ │ │ ├── AndroidManifest.xml │ │ │ └── java │ │ │ └── dev │ │ │ └── yankew │ │ │ └── sample │ │ │ └── init │ │ │ ├── AppBaseInitializer.kt │ │ │ ├── ComponentInitializer.kt │ │ │ ├── InitModule.kt │ │ │ └── AppBaseInitializerWrapper.kt │ └── build.gradle.kts └── strings │ ├── .gitignore │ ├── consumer-rules.pro │ ├── src │ └── main │ │ ├── AndroidManifest.xml │ │ └── res │ │ └── values │ │ └── strings.xml │ └── build.gradle.kts ├── apps └── demo-mobile │ ├── .gitignore │ ├── src │ └── main │ │ ├── res │ │ ├── drawable │ │ │ ├── ic_launcher.xml │ │ │ └── ic_launcher_foreground.xml │ │ ├── drawable-v26 │ │ │ └── ic_launcher.xml │ │ └── drawable-v24 │ │ │ └── ic_launcher_foreground.xml │ │ ├── AndroidManifest.xml │ │ └── java │ │ └── dev │ │ └── yankew │ │ └── sample │ │ └── demo │ │ ├── DemoBaseInitializer.kt │ │ ├── DemoModule.kt │ │ └── DemoApplication.kt │ ├── proguard-rules.pro │ └── build.gradle.kts ├── ui ├── ui-mobile │ ├── .gitignore │ ├── consumer-rules.pro │ ├── build.gradle.kts │ └── src │ │ └── main │ │ ├── AndroidManifest.xml │ │ └── java │ │ └── dev │ │ └── yankew │ │ └── sample │ │ └── ui │ │ ├── navigation │ │ ├── NavDestinations.kt │ │ └── MainNavGraph.kt │ │ ├── MainActivity.kt │ │ ├── about │ │ ├── AboutViewModel.kt │ │ ├── DeveloperViewModel.kt │ │ ├── DeveloperScreen.kt │ │ └── AboutScreen.kt │ │ ├── MainViewModel.kt │ │ ├── home │ │ └── HomeViewModel.kt │ │ ├── new │ │ └── NewFeatureScreen.kt │ │ ├── settings │ │ ├── SettingsViewModel.kt │ │ └── SettingsSwitch.kt │ │ ├── MainApp.kt │ │ └── Scaffolds.kt └── ui-widget │ ├── .gitignore │ ├── consumer-rules.pro │ ├── src │ └── main │ │ └── AndroidManifest.xml │ └── build.gradle.kts ├── .github ├── debug.keystore ├── ci-gradle.properties └── workflows │ └── Check.yaml ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .editorconfig ├── .gitignore ├── doc ├── mobile-demo-modules.puml ├── mobile-app-modules.puml ├── all-modules.puml └── build-scripts.puml ├── .spotless └── copyright.kt ├── gradle.properties ├── settings.gradle.kts ├── depconstraints └── build.gradle.kts ├── README.md └── gradlew.bat /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /domain/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /comp/core/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /comp/core/consumer-rules.pro: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/android/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /lib/compose/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /lib/init/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /lib/init/consumer-rules.pro: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/strings/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /apps/demo-mobile/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /comp/download/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /comp/download/consumer-rules.pro: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /comp/feature/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /comp/feature/consumer-rules.pro: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /comp/sample-data/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /comp/settings/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /comp/settings/consumer-rules.pro: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/android/consumer-rules.pro: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/compose/consumer-rules.pro: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/strings/consumer-rules.pro: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/ui-mobile/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /ui/ui-mobile/consumer-rules.pro: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/ui-widget/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /ui/ui-widget/consumer-rules.pro: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /comp/analytics-debug/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /comp/config-firebase/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /comp/config-local/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /comp/config-local/consumer-rules.pro: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /comp/notification/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /comp/notification/consumer-rules.pro: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /comp/sample-data/consumer-rules.pro: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /comp/analytics-debug/consumer-rules.pro: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /comp/analytics-firebase/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /comp/analytics-firebase/consumer-rules.pro: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /comp/config-firebase/consumer-rules.pro: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/debug.keystore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yacca/android-clean-arch/HEAD/.github/debug.keystore -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yacca/android-clean-arch/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /.github/ci-gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.daemon=false 2 | org.gradle.jvmargs=-Xmx4096m 3 | org.gradle.parallel=true 4 | 5 | kotlin.incremental=false 6 | kotlin.compiler.execution.strategy=in-process 7 | -------------------------------------------------------------------------------- /comp/core/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /lib/android/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /lib/compose/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /lib/strings/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /ui/ui-widget/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /comp/download/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /comp/feature/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /comp/settings/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /comp/config-local/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /comp/notification/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /comp/sample-data/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /comp/analytics-debug/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /comp/config-firebase/src/main/res/xml/remote_config_defaults.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | new_feature_enabled 5 | false 6 | 7 | 8 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig: http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | insert_final_newline = true 6 | 7 | [*.{yml, json}] 8 | indent_style = space 9 | indent_size = 2 10 | 11 | [*.{kt, kts, java}] 12 | indent_size = 4 13 | max_line_length = 100 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Gradle files 2 | .gradle/ 3 | build/ 4 | 5 | # Local configuration file (sdk path, etc) 6 | local.properties 7 | 8 | # OS files 9 | .DS_Store 10 | 11 | # Android Studio generated files and folders 12 | captures/ 13 | .externalNativeBuild 14 | .cxx 15 | 16 | # Intellij 17 | *.iml 18 | .idea/ -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /apps/demo-mobile/src/main/res/drawable/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /apps/demo-mobile/src/main/res/drawable-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /apps/demo-mobile/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /doc/mobile-demo-modules.puml: -------------------------------------------------------------------------------- 1 | @startuml 2 | folder "apps" { 3 | [demo-mobile] 4 | } 5 | 6 | folder "ui" { 7 | [ui-mobile] 8 | [ui-widget] 9 | } 10 | 11 | folder "comp" { 12 | [analytics-debug] 13 | [config-local] 14 | [core] 15 | [notification] 16 | [sample-data] 17 | [settings] 18 | } 19 | 20 | [demo-mobile] ...> [analytics-debug] 21 | [demo-mobile] ...> [config-local] 22 | [demo-mobile] ...> [core] 23 | [demo-mobile] ...> [notification] 24 | [demo-mobile] ...> [sample-data] 25 | [demo-mobile] ...> [settings] 26 | [demo-mobile] .up.> [ui-mobile] 27 | [demo-mobile] .up.> [ui-widget] 28 | 29 | [demo-mobile] -right--> [domain] 30 | @enduml 31 | -------------------------------------------------------------------------------- /.spotless/copyright.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright $YEAR WANG Yanke 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | -------------------------------------------------------------------------------- /doc/mobile-app-modules.puml: -------------------------------------------------------------------------------- 1 | @startuml 2 | folder "ui" { 3 | [ui-mobile] 4 | [ui-widget] 5 | } 6 | 7 | folder "comp" { 8 | [analytics-debug] 9 | [analytics-firebase] 10 | [config-firebase] 11 | [config-local] 12 | [core] 13 | [download] 14 | [feature] 15 | [notification] 16 | [settings] 17 | } 18 | 19 | [app] ...> [analytics-debug] 20 | [app] ...> [analytics-firebase] 21 | [app] ...> [config-firebase] 22 | [app] ...> [config-local] 23 | [app] ...> [core] 24 | [app] ...> [download] 25 | [app] ...> [feature] 26 | [app] ...> [notification] 27 | [app] ...> [settings] 28 | [app] .up.> [ui-mobile] 29 | [app] .up.> [ui-widget] 30 | 31 | [app] -right--> [domain] 32 | @enduml 33 | -------------------------------------------------------------------------------- /comp/core/build.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 WANG Yanke 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | plugins { 18 | id("common.main-component") 19 | } 20 | -------------------------------------------------------------------------------- /comp/download/build.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 WANG Yanke 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | plugins { 18 | id("common.main-component") 19 | } 20 | -------------------------------------------------------------------------------- /comp/sample-data/build.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 WANG Yanke 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | plugins { 18 | id("common.main-component") 19 | } 20 | -------------------------------------------------------------------------------- /lib/compose/build.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 WANG Yanke 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | plugins { 18 | id("common.compose-library") 19 | } 20 | -------------------------------------------------------------------------------- /lib/init/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 12 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /comp/analytics-debug/build.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 WANG Yanke 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | plugins { 18 | id("common.main-component") 19 | } 20 | -------------------------------------------------------------------------------- /lib/android/build.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 WANG Yanke 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | plugins { 18 | id("common.base-android-library") 19 | } 20 | -------------------------------------------------------------------------------- /lib/strings/build.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 WANG Yanke 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | plugins { 18 | id("common.base-android-library") 19 | } 20 | -------------------------------------------------------------------------------- /comp/config-firebase/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 13 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /comp/analytics-firebase/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 12 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /lib/init/src/main/java/dev/yankew/sample/init/AppBaseInitializer.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 WANG Yanke 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.yankew.sample.init 18 | 19 | interface AppBaseInitializer { 20 | fun init() 21 | } 22 | -------------------------------------------------------------------------------- /ui/ui-widget/build.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 WANG Yanke 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | plugins { 18 | id("common.ui-component") 19 | } 20 | 21 | dependencies { 22 | implementation(project(":lib:compose")) 23 | } 24 | -------------------------------------------------------------------------------- /comp/notification/build.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 WANG Yanke 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | plugins { 18 | id("common.main-component") 19 | } 20 | 21 | dependencies { 22 | implementation(project(":lib:strings")) 23 | } 24 | -------------------------------------------------------------------------------- /lib/init/build.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 WANG Yanke 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | plugins { 18 | id("common.hilt-library") 19 | } 20 | 21 | dependencies { 22 | // App Startup 23 | implementation(Libs.STARTUP) 24 | } 25 | -------------------------------------------------------------------------------- /domain/src/main/java/dev/yankew/sample/domain/event/AnalyticsOptionEvent.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 WANG Yanke 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.yankew.sample.domain.event 18 | 19 | data class AnalyticsOptionEvent(val enabled: Boolean) 20 | -------------------------------------------------------------------------------- /domain/src/main/java/dev/yankew/sample/domain/event/CrashlyticsOptionEvent.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 WANG Yanke 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.yankew.sample.domain.event 18 | 19 | data class CrashlyticsOptionEvent(val enabled: Boolean) 20 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /domain/src/main/java/dev/yankew/sample/domain/event/EventHandler.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 WANG Yanke 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.yankew.sample.domain.event 18 | 19 | fun interface EventHandler { 20 | suspend fun handle(event: T) 21 | } 22 | -------------------------------------------------------------------------------- /domain/src/main/java/dev/yankew/sample/domain/util/Optional.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 WANG Yanke 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.yankew.sample.domain.util 18 | 19 | import java.util.Optional 20 | 21 | fun Optional.orNull(): T? = orElse(null) 22 | -------------------------------------------------------------------------------- /apps/demo-mobile/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 -------------------------------------------------------------------------------- /comp/settings/build.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 WANG Yanke 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | plugins { 18 | id("common.main-component") 19 | } 20 | 21 | dependencies { 22 | // Datastore Preferences 23 | implementation(Libs.DATA_STORE_PREFERENCES) 24 | } 25 | -------------------------------------------------------------------------------- /comp/config-local/build.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 WANG Yanke 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | plugins { 18 | id("common.main-component") 19 | } 20 | 21 | dependencies { 22 | // Datastore Preferences 23 | implementation(Libs.DATA_STORE_PREFERENCES) 24 | } 25 | -------------------------------------------------------------------------------- /ui/ui-mobile/build.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 WANG Yanke 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | plugins { 18 | id("common.ui-component") 19 | } 20 | 21 | dependencies { 22 | implementation(project(":lib:compose")) 23 | 24 | implementation(Libs.ANNOTATION) 25 | } 26 | -------------------------------------------------------------------------------- /domain/src/main/java/dev/yankew/sample/domain/model/AppVariant.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 WANG Yanke 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.yankew.sample.domain.model 18 | 19 | enum class AppVariant { 20 | MOBILE, 21 | MOBILE_DEMO, 22 | TV, 23 | TV_DEMO 24 | } 25 | -------------------------------------------------------------------------------- /ui/ui-mobile/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /lib/compose/src/main/java/dev/yankew/sample/compose/theme/Type.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 WANG Yanke 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.yankew.sample.compose.theme 18 | 19 | import androidx.compose.material.Typography 20 | 21 | val typography = Typography( 22 | // Use default Typography 23 | ) 24 | -------------------------------------------------------------------------------- /domain/src/main/java/dev/yankew/sample/domain/config/NewFeatureParam.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 WANG Yanke 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.yankew.sample.domain.config 18 | 19 | import javax.inject.Qualifier 20 | 21 | @Retention(AnnotationRetention.BINARY) 22 | @Qualifier 23 | annotation class NewFeatureParam 24 | -------------------------------------------------------------------------------- /comp/feature/build.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 WANG Yanke 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | plugins { 18 | id("common.main-component") 19 | } 20 | 21 | dependencies { 22 | implementation(project(":lib:android")) 23 | 24 | // Datastore Preferences 25 | implementation(Libs.DATA_STORE_PREFERENCES) 26 | } 27 | -------------------------------------------------------------------------------- /domain/src/main/java/dev/yankew/sample/domain/repository/AppRepository.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 WANG Yanke 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.yankew.sample.domain.repository 18 | 19 | import dev.yankew.sample.domain.model.AppVariant 20 | 21 | interface AppRepository { 22 | fun getAppVariant(): AppVariant 23 | } 24 | -------------------------------------------------------------------------------- /domain/src/main/java/dev/yankew/sample/domain/repository/FeatureRepository.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 WANG Yanke 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.yankew.sample.domain.repository 18 | 19 | import kotlinx.coroutines.flow.Flow 20 | 21 | interface FeatureRepository { 22 | fun getElapsedMinutes(): Flow 23 | } 24 | -------------------------------------------------------------------------------- /domain/src/main/java/dev/yankew/sample/domain/settings/SettingOption.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 WANG Yanke 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.yankew.sample.domain.settings 18 | 19 | import kotlinx.coroutines.flow.Flow 20 | 21 | interface SettingOption { 22 | fun getValue(): Flow 23 | suspend fun setValue(value: T) 24 | } 25 | -------------------------------------------------------------------------------- /domain/src/main/java/dev/yankew/sample/domain/util/Logger.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 WANG Yanke 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.yankew.sample.domain.util 18 | 19 | interface Logger { 20 | fun e(message: String) 21 | fun w(message: String) 22 | fun i(message: String) 23 | fun d(message: String) 24 | fun v(message: String) 25 | } 26 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 11 | 12 | 17 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /domain/src/main/java/dev/yankew/sample/domain/event/AppLaunchedEvent.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 WANG Yanke 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.yankew.sample.domain.event 18 | 19 | import dev.yankew.sample.domain.model.AppVariant 20 | 21 | /** 22 | * This event happens in onCreate of Application when all initializations completed 23 | */ 24 | data class AppLaunchedEvent(val variant: AppVariant) 25 | -------------------------------------------------------------------------------- /comp/config-firebase/src/main/java/dev/yankew/sample/config/firebase/RemoteConfig.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 WANG Yanke 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.yankew.sample.config.firebase 18 | 19 | import dev.yankew.sample.domain.config.ConfigValue 20 | import kotlinx.coroutines.flow.Flow 21 | 22 | interface RemoteConfig { 23 | operator fun get(param: RemoteParam): Flow> 24 | } 25 | -------------------------------------------------------------------------------- /lib/init/src/main/java/dev/yankew/sample/init/ComponentInitializer.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 WANG Yanke 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.yankew.sample.init 18 | 19 | import androidx.startup.Initializer 20 | 21 | abstract class ComponentInitializer : Initializer { 22 | override fun dependencies(): List>> = 23 | listOf(AppBaseInitializerWrapper::class.java) 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 6 | 11 | 12 | -------------------------------------------------------------------------------- /apps/demo-mobile/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 6 | 11 | 12 | -------------------------------------------------------------------------------- /comp/analytics-firebase/build.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 WANG Yanke 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | plugins { 18 | id("common.main-component") 19 | } 20 | 21 | dependencies { 22 | implementation(project(":lib:init")) 23 | 24 | // Firebase 25 | implementation(platform(Libs.FIREBASE_BOM)) 26 | implementation(Libs.FIREBASE_ANALYTICS) 27 | 28 | // Startup initialization 29 | implementation(Libs.STARTUP) 30 | } 31 | -------------------------------------------------------------------------------- /ui/ui-mobile/src/main/java/dev/yankew/sample/ui/navigation/NavDestinations.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 WANG Yanke 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.yankew.sample.ui.navigation 18 | 19 | object NavDestinations { 20 | const val ROUTE_HOME = "home" 21 | const val ROUTE_NEW_FEATURE = "new-feature" 22 | const val ROUTE_SETTINGS = "settings" 23 | const val ROUTE_ABOUT = "about" 24 | const val ROUTE_DEVELOPER = "developer" 25 | } 26 | -------------------------------------------------------------------------------- /domain/src/main/java/dev/yankew/sample/domain/analytics/AnalyticsTracker.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 WANG Yanke 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.yankew.sample.domain.analytics 18 | 19 | typealias AnalyticsTrackers = Set<@JvmSuppressWildcards AnalyticsTracker> 20 | 21 | interface AnalyticsTracker { 22 | suspend fun isAnalyticsEnabled(): Boolean 23 | fun logEvent(name: String, params: Map) 24 | fun setUserProperty(key: String, value: String) 25 | } 26 | -------------------------------------------------------------------------------- /doc/all-modules.puml: -------------------------------------------------------------------------------- 1 | @startuml 2 | folder "apps" { 3 | [app-tv] 4 | [demo-mobile] 5 | [demo-tv] 6 | } 7 | 8 | folder "ui" { 9 | [ui-mobile] 10 | [ui-tv] 11 | [ui-widget] 12 | } 13 | 14 | folder "comp" { 15 | [analytics-firebase] 16 | [analytics-debug] 17 | [config-firebase] 18 | [config-local] 19 | [core] 20 | [download] 21 | [feature] 22 | [notification] 23 | [sample-data] 24 | [settings] 25 | } 26 | 27 | folder "lib" { 28 | [android] 29 | [compose] 30 | [strings] 31 | } 32 | 33 | [app] --> [domain] 34 | 35 | [app-tv] --> [domain] 36 | [demo-mobile] --> [domain] 37 | [demo-tv] --> [domain] 38 | 39 | [ui-mobile] --> [domain] 40 | [ui-tv] --> [domain] 41 | [ui-widget] --> [domain] 42 | 43 | [analytics-firebase] -up-> [domain] 44 | [analytics-debug] -up-> [domain] 45 | [config-firebase] -up-> [domain] 46 | [config-local] -up-> [domain] 47 | [core] -up---> [domain] 48 | [download] -up-> [domain] 49 | [feature] -up-> [domain] 50 | [notification] -up-> [domain] 51 | [sample-data] -up-> [domain] 52 | [settings] -up-> [domain] 53 | @enduml 54 | -------------------------------------------------------------------------------- /lib/init/src/main/java/dev/yankew/sample/init/InitModule.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 WANG Yanke 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.yankew.sample.init 18 | 19 | import dagger.BindsOptionalOf 20 | import dagger.Module 21 | import dagger.hilt.InstallIn 22 | import dagger.hilt.components.SingletonComponent 23 | 24 | @Module 25 | @InstallIn(SingletonComponent::class) 26 | interface InitModule { 27 | @BindsOptionalOf 28 | fun optionalInitializer(): AppBaseInitializer 29 | } 30 | -------------------------------------------------------------------------------- /lib/compose/src/main/java/dev/yankew/sample/compose/theme/Shape.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 WANG Yanke 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.yankew.sample.compose.theme 18 | 19 | import androidx.compose.foundation.shape.RoundedCornerShape 20 | import androidx.compose.material.Shapes 21 | import androidx.compose.ui.unit.dp 22 | 23 | val shapes = Shapes( 24 | small = RoundedCornerShape(0.dp), 25 | medium = RoundedCornerShape(0.dp), 26 | large = RoundedCornerShape(0.dp) 27 | ) 28 | -------------------------------------------------------------------------------- /domain/src/main/java/dev/yankew/sample/domain/config/Parameters.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 WANG Yanke 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.yankew.sample.domain.config 18 | 19 | /** 20 | * The implementation of the parameter is optional. Always use Optional during injection. 21 | */ 22 | typealias NewFeatureLocalParam = LocalConfigParam 23 | 24 | /** 25 | * The implementation of the parameter is optional. Always use Optional during injection. 26 | */ 27 | typealias NewFeatureRemoteParam = RemoteConfigParam 28 | -------------------------------------------------------------------------------- /domain/src/main/java/dev/yankew/sample/domain/coroutine/CoroutinesQualifiers.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 WANG Yanke 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.yankew.sample.domain.coroutine 18 | 19 | import javax.inject.Qualifier 20 | 21 | @Retention(AnnotationRetention.BINARY) 22 | @Qualifier 23 | annotation class DefaultDispatcher 24 | 25 | @Retention(AnnotationRetention.BINARY) 26 | @Qualifier 27 | annotation class IoDispatcher 28 | 29 | @Retention(AnnotationRetention.BINARY) 30 | @Qualifier 31 | annotation class ApplicationScope 32 | -------------------------------------------------------------------------------- /domain/src/main/java/dev/yankew/sample/domain/settings/SettingsQualifiers.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 WANG Yanke 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.yankew.sample.domain.settings 18 | 19 | import javax.inject.Qualifier 20 | 21 | @Retention(AnnotationRetention.BINARY) 22 | @Qualifier 23 | annotation class AnalyticsOption 24 | 25 | @Retention(AnnotationRetention.BINARY) 26 | @Qualifier 27 | annotation class CrashlyticsOption 28 | 29 | @Retention(AnnotationRetention.BINARY) 30 | @Qualifier 31 | annotation class NotificationOption 32 | -------------------------------------------------------------------------------- /domain/src/main/java/dev/yankew/sample/domain/interactor/GetAppVariantUseCase.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 WANG Yanke 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.yankew.sample.domain.interactor 18 | 19 | import dev.yankew.sample.domain.model.AppVariant 20 | import dev.yankew.sample.domain.repository.AppRepository 21 | import javax.inject.Inject 22 | 23 | class GetAppVariantUseCase @Inject constructor( 24 | private val appRepository: AppRepository 25 | ) { 26 | operator fun invoke(): AppVariant = appRepository.getAppVariant() 27 | } 28 | -------------------------------------------------------------------------------- /comp/core/src/main/java/dev/yankew/sample/core/CoreModule.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 WANG Yanke 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.yankew.sample.core 18 | 19 | import dagger.Binds 20 | import dagger.Module 21 | import dagger.Reusable 22 | import dagger.hilt.InstallIn 23 | import dagger.hilt.components.SingletonComponent 24 | import dev.yankew.sample.domain.util.Logger 25 | 26 | @Module 27 | @InstallIn(SingletonComponent::class) 28 | interface CoreModule { 29 | @Reusable 30 | @Binds 31 | fun logger(logger: LoggerImpl): Logger 32 | } 33 | -------------------------------------------------------------------------------- /comp/config-firebase/src/main/java/dev/yankew/sample/config/firebase/RemoteConfigInitializer.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 WANG Yanke 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.yankew.sample.config.firebase 18 | 19 | import android.content.Context 20 | import dev.yankew.sample.init.ComponentInitializer 21 | import timber.log.Timber 22 | 23 | class RemoteConfigInitializer : ComponentInitializer() { 24 | override fun create(context: Context) { 25 | Timber.i("Initialize Firebase Remote Config") 26 | RemoteConfigFetchWorker.enqueue(context) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /comp/sample-data/src/main/java/dev/yankew/sample/data/sample/SampleModule.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 WANG Yanke 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.yankew.sample.data.sample 18 | 19 | import dagger.Module 20 | import dagger.Provides 21 | import dagger.hilt.InstallIn 22 | import dagger.hilt.components.SingletonComponent 23 | import dev.yankew.sample.domain.repository.FeatureRepository 24 | 25 | @Module 26 | @InstallIn(SingletonComponent::class) 27 | object SampleModule { 28 | @Provides 29 | fun featureRepository(): FeatureRepository = SampleFeatureRepository 30 | } 31 | -------------------------------------------------------------------------------- /app/google-services.json: -------------------------------------------------------------------------------- 1 | { 2 | "project_info": { 3 | "project_number": "310996819373", 4 | "project_id": "clean-arch-sample", 5 | "storage_bucket": "clean-arch-sample.appspot.com" 6 | }, 7 | "client": [ 8 | { 9 | "client_info": { 10 | "mobilesdk_app_id": "1:310996819373:android:9a199b44b233d06e009524", 11 | "android_client_info": { 12 | "package_name": "dev.yankew.cleanarch" 13 | } 14 | }, 15 | "oauth_client": [ 16 | { 17 | "client_id": "310996819373-7c14frv82atmppvd3ihjchrdiki04lb8.apps.googleusercontent.com", 18 | "client_type": 3 19 | } 20 | ], 21 | "api_key": [ 22 | { 23 | "current_key": "AIzaSyBmR_gDj3Cbw49g6-zduGWk2y8JfXZ_Wxw" 24 | } 25 | ], 26 | "services": { 27 | "appinvite_service": { 28 | "other_platform_oauth_client": [ 29 | { 30 | "client_id": "310996819373-7c14frv82atmppvd3ihjchrdiki04lb8.apps.googleusercontent.com", 31 | "client_type": 3 32 | } 33 | ] 34 | } 35 | } 36 | } 37 | ], 38 | "configuration_version": "1" 39 | } -------------------------------------------------------------------------------- /domain/src/main/java/dev/yankew/sample/domain/event/EventDispatcher.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 WANG Yanke 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.yankew.sample.domain.event 18 | 19 | import javax.inject.Inject 20 | import javax.inject.Provider 21 | 22 | typealias EventHandlers = Set<@JvmSuppressWildcards EventHandler> 23 | 24 | class EventDispatcher @Inject constructor( 25 | private val handlers: Provider> 26 | ) { 27 | suspend fun dispatch(event: T) { 28 | handlers.get().forEach { 29 | it.handle(event) 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /lib/compose/src/main/java/dev/yankew/sample/compose/theme/Color.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 WANG Yanke 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.yankew.sample.compose.theme 18 | 19 | import androidx.compose.ui.graphics.Color 20 | 21 | val Blue200 = Color(0xff90caf9) 22 | val Blue300 = Color(0xff64b5f6) 23 | val Blue700 = Color(0xff1976d2) 24 | val Blue800 = Color(0xff1565c0) 25 | val Blue900 = Color(0xff0d47a1) 26 | 27 | val Amber200 = Color(0xffffe082) 28 | val Amber300 = Color(0xffffd54f) 29 | val Amber700 = Color(0xffffa000) 30 | val Amber800 = Color(0xffff8f00) 31 | val Amber900 = Color(0xffff6f00) 32 | -------------------------------------------------------------------------------- /ui/ui-mobile/src/main/java/dev/yankew/sample/ui/MainActivity.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 WANG Yanke 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.yankew.sample.ui 18 | 19 | import android.os.Bundle 20 | import androidx.activity.ComponentActivity 21 | import androidx.activity.compose.setContent 22 | import dagger.hilt.android.AndroidEntryPoint 23 | 24 | @AndroidEntryPoint 25 | class MainActivity : ComponentActivity() { 26 | override fun onCreate(savedInstanceState: Bundle?) { 27 | super.onCreate(savedInstanceState) 28 | setContent { 29 | MainApp() 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /apps/demo-mobile/src/main/java/dev/yankew/sample/demo/DemoBaseInitializer.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 WANG Yanke 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.yankew.sample.demo 18 | 19 | import dev.yankew.sample.init.AppBaseInitializer 20 | import timber.log.Timber 21 | import javax.inject.Inject 22 | 23 | class DemoBaseInitializer @Inject constructor() : AppBaseInitializer { 24 | override fun init() { 25 | if (BuildConfig.BUILD_TYPE != "release") { 26 | Timber.plant(Timber.DebugTree()) 27 | } 28 | Timber.i("Base initialization of demo version for mobile") 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /comp/feature/src/main/java/dev/yankew/sample/feature/FeatureRepositoryImpl.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 WANG Yanke 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.yankew.sample.feature 18 | 19 | import dev.yankew.sample.domain.repository.FeatureRepository 20 | import kotlinx.coroutines.flow.Flow 21 | import kotlinx.coroutines.flow.map 22 | import javax.inject.Inject 23 | 24 | class FeatureRepositoryImpl @Inject constructor( 25 | private val elapseDataSource: ElapseDataSource 26 | ) : FeatureRepository { 27 | override fun getElapsedMinutes(): Flow { 28 | return elapseDataSource.getElapsedSeconds().map { it / 60 } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /.github/workflows/Check.yaml: -------------------------------------------------------------------------------- 1 | name: Check 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | timeout-minutes: 30 13 | 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v2 17 | 18 | - name: Copy CI gradle.properties 19 | run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties 20 | 21 | - name: Set up JDK 11 22 | uses: actions/setup-java@v2 23 | with: 24 | distribution: 'temurin' 25 | java-version: '11' 26 | 27 | - name: Check and build project 28 | run: ./gradlew spotlessCheck lintDebug assembleDebug 29 | 30 | - name: Upload build outputs 31 | uses: actions/upload-artifact@v2 32 | with: 33 | name: build-outputs 34 | path: | 35 | app/build/outputs 36 | apps/demo-mobile/build/outputs 37 | 38 | - name: Upload build reports 39 | if: always() 40 | uses: actions/upload-artifact@v2 41 | with: 42 | name: build-reports 43 | path: | 44 | app/build/reports 45 | apps/demo-mobile/build/reports 46 | -------------------------------------------------------------------------------- /comp/config-firebase/src/main/java/dev/yankew/sample/config/firebase/RemoteParams.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 WANG Yanke 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.yankew.sample.config.firebase 18 | 19 | import dev.yankew.sample.domain.config.NewFeatureRemoteParam 20 | import javax.inject.Inject 21 | 22 | class RemoteParams @Inject constructor( 23 | private val remoteConfig: RemoteConfig 24 | ) { 25 | val newFeatureParam = NewFeatureRemoteParam { 26 | remoteConfig[BooleanRemoteParam(KEY_NEW_FEATURE_ENABLED)] 27 | } 28 | 29 | companion object { 30 | private const val KEY_NEW_FEATURE_ENABLED = "new_feature_enabled" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /domain/src/main/java/dev/yankew/sample/domain/config/ConfigModule.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 WANG Yanke 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.yankew.sample.domain.config 18 | 19 | import dagger.BindsOptionalOf 20 | import dagger.Module 21 | import dagger.hilt.InstallIn 22 | import dagger.hilt.components.SingletonComponent 23 | 24 | @Module 25 | @InstallIn(SingletonComponent::class) 26 | interface ConfigModule { 27 | @BindsOptionalOf 28 | @NewFeatureParam 29 | fun optionalNewFeatureLocalParam(): NewFeatureLocalParam 30 | 31 | @BindsOptionalOf 32 | @NewFeatureParam 33 | fun optionalNewFeatureRemoteParam(): NewFeatureRemoteParam 34 | } 35 | -------------------------------------------------------------------------------- /comp/feature/src/main/java/dev/yankew/sample/feature/FeatureEventHandlers.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 WANG Yanke 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.yankew.sample.feature 18 | 19 | import dev.yankew.sample.domain.event.AppLaunchedEvent 20 | import dev.yankew.sample.domain.event.EventHandler 21 | import timber.log.Timber 22 | import javax.inject.Inject 23 | 24 | class FeatureEventHandlers @Inject constructor( 25 | elapseDataSource: ElapseDataSource 26 | ) { 27 | val appLaunchedHandler = EventHandler { 28 | Timber.i("AppLaunchedEvent received by FeatureEventHandlers") 29 | elapseDataSource.saveLaunchTime() 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /ui/ui-mobile/src/main/java/dev/yankew/sample/ui/about/AboutViewModel.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 WANG Yanke 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.yankew.sample.ui.about 18 | 19 | import androidx.lifecycle.ViewModel 20 | import dagger.hilt.android.lifecycle.HiltViewModel 21 | import dev.yankew.sample.domain.config.NewFeatureLocalParam 22 | import dev.yankew.sample.domain.config.NewFeatureParam 23 | import java.util.Optional 24 | import javax.inject.Inject 25 | 26 | @HiltViewModel 27 | class AboutViewModel @Inject constructor( 28 | @NewFeatureParam private val localNewFeatureParam: Optional, 29 | ) : ViewModel() { 30 | val isLocalConfigPresent = localNewFeatureParam.isPresent 31 | } 32 | -------------------------------------------------------------------------------- /comp/analytics-debug/src/main/java/dev/yankew/sample/analytics/debug/DebugAnalyticsModule.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 WANG Yanke 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.yankew.sample.analytics.debug 18 | 19 | import dagger.Binds 20 | import dagger.Module 21 | import dagger.hilt.InstallIn 22 | import dagger.hilt.components.SingletonComponent 23 | import dagger.multibindings.IntoSet 24 | import dev.yankew.sample.domain.analytics.AnalyticsTracker 25 | import javax.inject.Singleton 26 | 27 | @Module 28 | @InstallIn(SingletonComponent::class) 29 | interface DebugAnalyticsModule { 30 | @Binds 31 | @IntoSet 32 | @Singleton 33 | fun debugAnalyticsTracker(tracker: DebugAnalyticsTracker): AnalyticsTracker 34 | } 35 | -------------------------------------------------------------------------------- /domain/src/main/java/dev/yankew/sample/domain/event/EventModule.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 WANG Yanke 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.yankew.sample.domain.event 18 | 19 | import dagger.Module 20 | import dagger.hilt.InstallIn 21 | import dagger.hilt.components.SingletonComponent 22 | import dagger.multibindings.Multibinds 23 | 24 | @Module 25 | @InstallIn(SingletonComponent::class) 26 | interface EventModule { 27 | @Multibinds 28 | fun analyticsOptionHandlers(): EventHandlers 29 | 30 | @Multibinds 31 | fun appLaunchedHandlers(): EventHandlers 32 | 33 | @Multibinds 34 | fun crashlyticsOptionHandlers(): EventHandlers 35 | } 36 | -------------------------------------------------------------------------------- /comp/sample-data/src/main/java/dev/yankew/sample/data/sample/SampleFeatureRepository.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 WANG Yanke 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.yankew.sample.data.sample 18 | 19 | import dev.yankew.sample.domain.repository.FeatureRepository 20 | import kotlinx.coroutines.delay 21 | import kotlinx.coroutines.flow.Flow 22 | import kotlinx.coroutines.flow.flow 23 | import timber.log.Timber 24 | 25 | object SampleFeatureRepository : FeatureRepository { 26 | override fun getElapsedMinutes(): Flow { 27 | return flow { 28 | (0L..100L).forEach { 29 | emit(it) 30 | Timber.i("$it emitted") 31 | delay(1000) 32 | } 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /domain/src/main/java/dev/yankew/sample/domain/analytics/ClickTrackingEvent.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 WANG Yanke 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.yankew.sample.domain.analytics 18 | 19 | import javax.inject.Inject 20 | 21 | class ClickTrackingEvent @Inject constructor( 22 | private val analytics: AnalyticsEventDispatcher, 23 | ) { 24 | fun logHelloClicked() = log(VALUE_BUTTON_HELLO) 25 | 26 | private fun log(button: String) = 27 | analytics.logEvent(NAME, PARAM_BUTTON_NAME to button) 28 | 29 | companion object { 30 | private const val NAME = "button_clicked" 31 | private const val PARAM_BUTTON_NAME = "button_name" 32 | private const val VALUE_BUTTON_HELLO = "hello_clicked" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /domain/src/main/java/dev/yankew/sample/domain/analytics/AnalyticsEventHandlers.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 WANG Yanke 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.yankew.sample.domain.analytics 18 | 19 | import dev.yankew.sample.domain.event.AppLaunchedEvent 20 | import dev.yankew.sample.domain.event.EventHandler 21 | import dev.yankew.sample.domain.util.Logger 22 | import javax.inject.Inject 23 | 24 | class AnalyticsEventHandlers @Inject constructor( 25 | analyticsUserProperties: AnalyticsUserProperties, 26 | logger: Logger, 27 | ) { 28 | val appLaunchedHandler = EventHandler { 29 | logger.i("AppLaunchedEvent received by AnalyticsEventHandlers") 30 | analyticsUserProperties.setAppVariant(it.variant) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /comp/core/src/main/java/dev/yankew/sample/core/LoggerImpl.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 WANG Yanke 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.yankew.sample.core 18 | 19 | import dev.yankew.sample.domain.util.Logger 20 | import timber.log.Timber 21 | import javax.inject.Inject 22 | 23 | class LoggerImpl @Inject constructor() : Logger { 24 | override fun e(message: String) { 25 | Timber.e(message) 26 | } 27 | 28 | override fun w(message: String) { 29 | Timber.w(message) 30 | } 31 | 32 | override fun i(message: String) { 33 | Timber.i(message) 34 | } 35 | 36 | override fun d(message: String) { 37 | Timber.d(message) 38 | } 39 | 40 | override fun v(message: String) { 41 | Timber.v(message) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /domain/src/main/java/dev/yankew/sample/domain/interactor/FlowUseCase.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 WANG Yanke 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.yankew.sample.domain.interactor 18 | 19 | import dev.yankew.sample.domain.model.DataResult 20 | import kotlinx.coroutines.CoroutineDispatcher 21 | import kotlinx.coroutines.flow.Flow 22 | import kotlinx.coroutines.flow.catch 23 | import kotlinx.coroutines.flow.flowOn 24 | 25 | abstract class FlowUseCase(private val coroutineDispatcher: CoroutineDispatcher) { 26 | operator fun invoke(parameters: P): Flow> = execute(parameters) 27 | .catch { emit(DataResult.Error(it)) } 28 | .flowOn(coroutineDispatcher) 29 | 30 | protected abstract fun execute(parameters: P): Flow> 31 | } 32 | -------------------------------------------------------------------------------- /comp/config-firebase/build.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 WANG Yanke 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | plugins { 18 | id("common.main-component") 19 | } 20 | 21 | dependencies { 22 | implementation(project(":lib:init")) 23 | 24 | // Startup initialization 25 | implementation(Libs.STARTUP) 26 | 27 | // WorkManager 28 | implementation(Libs.WORK_RUNTIME_KTX) 29 | implementation(Libs.HILT_WORK) 30 | kapt(Libs.HILT_ANDROIDX_COMPILER) 31 | 32 | // Firebase 33 | implementation(platform(Libs.FIREBASE_BOM)) 34 | implementation(Libs.FIREBASE_ANALYTICS) 35 | implementation(Libs.FIREBASE_CONFIG) 36 | implementation(Libs.COROUTINES_PLAY_SERVICE) 37 | 38 | // Serialization for parsing JSON values 39 | implementation(Libs.KOTLINX_SERIALIZATION) 40 | } 41 | -------------------------------------------------------------------------------- /comp/config-firebase/src/main/java/dev/yankew/sample/config/firebase/RemoteParam.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 WANG Yanke 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.yankew.sample.config.firebase 18 | 19 | import kotlinx.serialization.KSerializer 20 | 21 | sealed class RemoteParam(open val key: String) 22 | 23 | data class StringRemoteParam(override val key: String) : RemoteParam(key) 24 | 25 | data class BooleanRemoteParam(override val key: String) : RemoteParam(key) 26 | 27 | data class LongRemoteParam(override val key: String) : RemoteParam(key) 28 | 29 | data class DoubleRemoteParam(override val key: String) : RemoteParam(key) 30 | 31 | data class JsonRemoteParam( 32 | override val key: String, 33 | val serializer: KSerializer 34 | ) : RemoteParam(key) 35 | -------------------------------------------------------------------------------- /domain/src/main/java/dev/yankew/sample/domain/config/ConfigValue.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 WANG Yanke 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.yankew.sample.domain.config 18 | 19 | import kotlinx.coroutines.flow.Flow 20 | import kotlinx.coroutines.flow.catch 21 | import kotlinx.coroutines.flow.flatMapLatest 22 | import kotlinx.coroutines.flow.flowOf 23 | 24 | sealed class ConfigValue { 25 | data class ConfiguredValue(val data: T) : ConfigValue() 26 | object Unset : ConfigValue() 27 | } 28 | 29 | fun Flow>.value(default: () -> Flow): Flow = 30 | catch { default() }.flatMapLatest { 31 | when (it) { 32 | is ConfigValue.ConfiguredValue -> flowOf(it.data) 33 | is ConfigValue.Unset -> default() 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /comp/analytics-firebase/src/main/java/dev/yankew/sample/analytics/firebase/AnalyticsOptionHandler.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 WANG Yanke 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.yankew.sample.analytics.firebase 18 | 19 | import com.google.firebase.analytics.ktx.analytics 20 | import com.google.firebase.ktx.Firebase 21 | import dev.yankew.sample.domain.event.AnalyticsOptionEvent 22 | import dev.yankew.sample.domain.event.EventHandler 23 | import timber.log.Timber 24 | import javax.inject.Inject 25 | 26 | class AnalyticsOptionHandler @Inject constructor() : EventHandler { 27 | override suspend fun handle(event: AnalyticsOptionEvent) { 28 | Timber.i("AnalyticsOptionEvent received by AnalyticsOptionHandler") 29 | Firebase.analytics.setAnalyticsCollectionEnabled(event.enabled) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /doc/build-scripts.puml: -------------------------------------------------------------------------------- 1 | @startuml 2 | object "base-android-app" as baseAppPlugin #skyblue 3 | object "base-android-library" as baseLibPlugin #skyblue 4 | object "compose-library" as composeLibPlugin #skyblue 5 | object "hilt-library" as hiltLibPlugin #skyblue 6 | object "main-component" as mainCompPlugin #skyblue 7 | object "ui-component" as uiCompPlugin #skyblue 8 | object app 9 | object "apps/demo-mobile" as mobileDemoApp 10 | object "lib/android" as androidLib 11 | object "lib/compose" as composeLib 12 | object "lib/init" as initLib 13 | object "lib/strings" as stringsLib 14 | object "ui/ui-mobile" as mobileUi 15 | object "ui/ui-widget" as widget 16 | map Components { 17 | 1 => analytics-debug 18 | 2 => analytics-firebase 19 | 3 => config-local 20 | 4 => config-remote 21 | 5 => core 22 | 6 => download 23 | 7 => feature 24 | 8 => notification 25 | 9 => sample-data 26 | 10 => settings 27 | } 28 | 29 | composeLibPlugin --> baseLibPlugin 30 | hiltLibPlugin --> baseLibPlugin 31 | mainCompPlugin --> hiltLibPlugin 32 | uiCompPlugin --> mainCompPlugin 33 | uiCompPlugin --> composeLibPlugin 34 | Components --> mainCompPlugin 35 | 36 | androidLib --> baseLibPlugin 37 | composeLib --> composeLibPlugin 38 | initLib --> hiltLibPlugin 39 | stringsLib --> baseLibPlugin 40 | 41 | mobileUi --> uiCompPlugin 42 | widget --> uiCompPlugin 43 | 44 | app --> baseAppPlugin 45 | mobileDemoApp --> baseAppPlugin 46 | @enduml 47 | -------------------------------------------------------------------------------- /domain/src/main/java/dev/yankew/sample/domain/analytics/AnalyticsEventDispatcher.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 WANG Yanke 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.yankew.sample.domain.analytics 18 | 19 | import dev.yankew.sample.domain.coroutine.ApplicationScope 20 | import kotlinx.coroutines.CoroutineScope 21 | import kotlinx.coroutines.launch 22 | import javax.inject.Inject 23 | 24 | class AnalyticsEventDispatcher @Inject constructor( 25 | private val trackers: AnalyticsTrackers, 26 | @ApplicationScope private val applicationScope: CoroutineScope, 27 | ) { 28 | fun logEvent(name: String, vararg params: Pair) { 29 | applicationScope.launch { 30 | trackers.filter { it.isAnalyticsEnabled() }.forEach { 31 | it.logEvent(name, mapOf(*params)) 32 | } 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /domain/build.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 WANG Yanke 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import Versions.javaVersion 18 | 19 | plugins { 20 | `java-library` 21 | kotlin("jvm") 22 | kotlin("kapt") 23 | } 24 | 25 | val compileKotlin: org.jetbrains.kotlin.gradle.tasks.KotlinCompile by tasks 26 | 27 | compileKotlin.kotlinOptions { 28 | jvmTarget = javaVersion.toString() 29 | allWarningsAsErrors = true 30 | } 31 | 32 | java { 33 | sourceCompatibility = javaVersion 34 | targetCompatibility = javaVersion 35 | } 36 | 37 | dependencies { 38 | api(platform(project(":depconstraints"))) 39 | kapt(platform(project(":depconstraints"))) 40 | implementation(Libs.KOTLIN_STDLIB) 41 | implementation(Libs.COROUTINES) 42 | 43 | // Dagger Hilt 44 | implementation(Libs.HILT_CORE) 45 | kapt(Libs.HILT_COMPILER) 46 | } 47 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx4096m -Dfile.encoding=UTF-8 10 | org.gradle.caching=true 11 | # When configured, Gradle will run in incubating parallel mode. 12 | # This option should only be used with decoupled projects. More details, visit 13 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 14 | org.gradle.parallel=true 15 | # AndroidX package structure to make it clearer which packages are bundled with the 16 | # Android operating system, and which are packaged with your app"s APK 17 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 18 | android.useAndroidX=true 19 | # Kotlin code style for this project: "official" or "obsolete": 20 | kotlin.code.style=official 21 | # Enables namespacing of each library's R class so that its R class includes only the 22 | # resources declared in the library itself and none from the library's dependencies, 23 | # thereby reducing the size of the R class for that library 24 | android.nonTransitiveRClass=true 25 | -------------------------------------------------------------------------------- /ui/ui-mobile/src/main/java/dev/yankew/sample/ui/MainViewModel.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 WANG Yanke 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.yankew.sample.ui 18 | 19 | import androidx.lifecycle.ViewModel 20 | import dagger.hilt.android.lifecycle.HiltViewModel 21 | import dev.yankew.sample.domain.interactor.GetAppVariantUseCase 22 | import dev.yankew.sample.domain.interactor.GetNewFeatureEnabledUseCase 23 | import dev.yankew.sample.domain.model.successOr 24 | import kotlinx.coroutines.flow.map 25 | import javax.inject.Inject 26 | 27 | @HiltViewModel 28 | class MainViewModel @Inject constructor( 29 | getAppVariantUseCase: GetAppVariantUseCase, 30 | getNewFeatureEnabledUseCase: GetNewFeatureEnabledUseCase, 31 | ) : ViewModel() { 32 | val appVariant = getAppVariantUseCase() 33 | 34 | val newFeatureEnabled = getNewFeatureEnabledUseCase(Unit).map { it.successOr(false) } 35 | } 36 | -------------------------------------------------------------------------------- /comp/config-firebase/src/main/java/dev/yankew/sample/config/firebase/RemoteConfigModule.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 WANG Yanke 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.yankew.sample.config.firebase 18 | 19 | import dagger.Binds 20 | import dagger.Module 21 | import dagger.Provides 22 | import dagger.Reusable 23 | import dagger.hilt.InstallIn 24 | import dagger.hilt.components.SingletonComponent 25 | import dev.yankew.sample.domain.config.NewFeatureParam 26 | 27 | @Module 28 | @InstallIn(SingletonComponent::class) 29 | object RemoteConfigModule { 30 | @Provides 31 | @NewFeatureParam 32 | fun newFeatureParam(remoteParams: RemoteParams) = remoteParams.newFeatureParam 33 | } 34 | 35 | @Module 36 | @InstallIn(SingletonComponent::class) 37 | interface RemoteConfigInterfaceModule { 38 | @Binds 39 | @Reusable 40 | fun remoteConfig(remoteConfigImpl: RemoteConfigImpl): RemoteConfig 41 | } 42 | -------------------------------------------------------------------------------- /ui/ui-mobile/src/main/java/dev/yankew/sample/ui/home/HomeViewModel.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 WANG Yanke 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.yankew.sample.ui.home 18 | 19 | import androidx.lifecycle.ViewModel 20 | import dagger.hilt.android.lifecycle.HiltViewModel 21 | import dev.yankew.sample.domain.analytics.ClickTrackingEvent 22 | import dev.yankew.sample.domain.interactor.GetElapsedMinutesUseCase 23 | import dev.yankew.sample.domain.model.data 24 | import kotlinx.coroutines.flow.mapNotNull 25 | import javax.inject.Inject 26 | 27 | @HiltViewModel 28 | class HomeViewModel @Inject constructor( 29 | getElapsedMinutesUseCase: GetElapsedMinutesUseCase, 30 | private val clickTrackingEvent: ClickTrackingEvent 31 | ) : ViewModel() { 32 | val elapsedMinutes = getElapsedMinutesUseCase(Unit).mapNotNull { it.data } 33 | 34 | fun logHello() { 35 | clickTrackingEvent.logHelloClicked() 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /comp/analytics-firebase/src/main/java/dev/yankew/sample/analytics/firebase/FirebaseAnalyticsModule.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 WANG Yanke 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.yankew.sample.analytics.firebase 18 | 19 | import dagger.Binds 20 | import dagger.Module 21 | import dagger.hilt.InstallIn 22 | import dagger.hilt.components.SingletonComponent 23 | import dagger.multibindings.IntoSet 24 | import dev.yankew.sample.domain.analytics.AnalyticsTracker 25 | import dev.yankew.sample.domain.event.AnalyticsOptionEvent 26 | import dev.yankew.sample.domain.event.EventHandler 27 | 28 | @Module 29 | @InstallIn(SingletonComponent::class) 30 | interface FirebaseAnalyticsModule { 31 | @Binds 32 | @IntoSet 33 | fun analyticsOptionHandler(handler: AnalyticsOptionHandler): EventHandler 34 | 35 | @Binds 36 | @IntoSet 37 | fun firebaseAnalyticsTracker(tracker: FirebaseAnalyticsTracker): AnalyticsTracker 38 | } 39 | -------------------------------------------------------------------------------- /domain/src/main/java/dev/yankew/sample/domain/analytics/AnalyticsUserProperties.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 WANG Yanke 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.yankew.sample.domain.analytics 18 | 19 | import dev.yankew.sample.domain.model.AppVariant 20 | import javax.inject.Inject 21 | 22 | class AnalyticsUserProperties @Inject constructor( 23 | private val trackers: AnalyticsTrackers, 24 | ) { 25 | suspend fun setAppVariant(variant: AppVariant) { 26 | setProperties(PARAM_APP_VARIANT to variant.toString()) 27 | } 28 | 29 | private suspend fun setProperties(vararg properties: Pair) { 30 | trackers.filter { it.isAnalyticsEnabled() }.forEach { 31 | properties.forEach { (key, value) -> 32 | it.setUserProperty(key, value) 33 | } 34 | } 35 | } 36 | 37 | companion object { 38 | private const val PARAM_APP_VARIANT = "app_variant" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /lib/strings/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | CleanArch 4 | 5 | 6 | Open menu 7 | Go back 8 | 9 | 10 | Home 11 | Hello 12 | Opened %1$d minutes ago 13 | 14 | 15 | New feature 16 | This is an experimental new feature. 17 | 18 | 19 | Settings 20 | Allow Clean Arch Sample to notify you updates. 21 | Allow Clean Arch Sample to collect anonymous usage information. 22 | Allow Clean Arch Sample to collect the crash reports to improve the app. 23 | 24 | 25 | About 26 | This is a Clean Architecture sample of Android application. 27 | Developer options 28 | 29 | 30 | Developer options 31 | Enable the new feature. 32 | 33 | -------------------------------------------------------------------------------- /domain/src/main/java/dev/yankew/sample/domain/interactor/GetElapsedMinutesUseCase.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 WANG Yanke 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.yankew.sample.domain.interactor 18 | 19 | import dev.yankew.sample.domain.coroutine.IoDispatcher 20 | import dev.yankew.sample.domain.model.DataResult 21 | import dev.yankew.sample.domain.repository.FeatureRepository 22 | import kotlinx.coroutines.CoroutineDispatcher 23 | import kotlinx.coroutines.flow.Flow 24 | import kotlinx.coroutines.flow.map 25 | import javax.inject.Inject 26 | 27 | class GetElapsedMinutesUseCase @Inject constructor( 28 | private val featureRepository: FeatureRepository, 29 | @IoDispatcher ioDispatcher: CoroutineDispatcher 30 | ) : FlowUseCase(ioDispatcher) { 31 | override fun execute(parameters: Unit): Flow> { 32 | return featureRepository.getElapsedMinutes().map { 33 | DataResult.Success(it) 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 WANG Yanke 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | include(":app") 18 | include(":apps:demo-mobile") 19 | include(":comp:analytics-debug") 20 | include(":comp:analytics-firebase") 21 | include(":comp:config-firebase") 22 | include(":comp:config-local") 23 | include(":comp:core") 24 | include(":comp:download") 25 | include(":comp:feature") 26 | include(":comp:notification") 27 | include(":comp:sample-data") 28 | include(":comp:settings") 29 | include(":depconstraints") 30 | include(":domain") 31 | include(":lib:android") 32 | include(":lib:compose") 33 | include(":lib:init") 34 | include(":lib:strings") 35 | include(":ui:ui-mobile") 36 | include(":ui:ui-widget") 37 | 38 | @Suppress("UnstableApiUsage") 39 | dependencyResolutionManagement { 40 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 41 | repositories { 42 | google() 43 | mavenCentral() 44 | } 45 | } 46 | 47 | rootProject.name = "android-clean-arch" 48 | -------------------------------------------------------------------------------- /domain/src/main/java/dev/yankew/sample/domain/analytics/AnalyticsModule.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 WANG Yanke 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.yankew.sample.domain.analytics 18 | 19 | import dagger.Module 20 | import dagger.Provides 21 | import dagger.hilt.InstallIn 22 | import dagger.hilt.components.SingletonComponent 23 | import dagger.multibindings.IntoSet 24 | import dagger.multibindings.Multibinds 25 | import dev.yankew.sample.domain.event.AppLaunchedEvent 26 | import dev.yankew.sample.domain.event.EventHandler 27 | 28 | @Module 29 | @InstallIn(SingletonComponent::class) 30 | object AnalyticsModule { 31 | @Provides 32 | @IntoSet 33 | fun appLaunchedHandler( 34 | handlers: AnalyticsEventHandlers 35 | ): EventHandler = handlers.appLaunchedHandler 36 | } 37 | 38 | @Module 39 | @InstallIn(SingletonComponent::class) 40 | interface AnalyticsInterfaceModule { 41 | @Multibinds 42 | fun analyticsTrackers(): AnalyticsTrackers 43 | } 44 | -------------------------------------------------------------------------------- /apps/demo-mobile/build.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 WANG Yanke 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | plugins { 18 | id("common.base-android-app") 19 | } 20 | 21 | android { 22 | defaultConfig { 23 | applicationId = "dev.yankew.cleanarch.demo" 24 | versionCode = Versions.versionCodeMobile 25 | versionName = Versions.versionName 26 | } 27 | } 28 | 29 | dependencies { 30 | implementation(project(":domain")) 31 | implementation(project(":lib:init")) 32 | implementation(project(":lib:strings")) 33 | 34 | // Dependencies for injection 35 | implementation(project(":comp:analytics-debug")) 36 | implementation(project(":comp:config-local")) 37 | implementation(project(":comp:core")) 38 | implementation(project(":comp:notification")) 39 | implementation(project(":comp:sample-data")) 40 | implementation(project(":comp:settings")) 41 | implementation(project(":ui:ui-mobile")) 42 | implementation(project(":ui:ui-widget")) 43 | } 44 | -------------------------------------------------------------------------------- /domain/src/main/java/dev/yankew/sample/domain/model/DataResult.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 WANG Yanke 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.yankew.sample.domain.model 18 | 19 | sealed class DataResult { 20 | data class Success(val data: T) : DataResult() 21 | data class Updating(val data: T) : DataResult() 22 | data class Error(val exception: Throwable) : DataResult() 23 | object Loading : DataResult() 24 | 25 | override fun toString(): String { 26 | return when (this) { 27 | is Success -> "Success[data=$data]" 28 | is Updating -> "Updating[data=$data]" 29 | is Error -> "Error[exception=$exception]" 30 | Loading -> "Loading" 31 | } 32 | } 33 | } 34 | 35 | val DataResult.data: T? 36 | get() = (this as? DataResult.Success)?.data ?: (this as? DataResult.Updating)?.data 37 | 38 | fun DataResult.successOr(fallback: T): T { 39 | return data ?: fallback 40 | } 41 | -------------------------------------------------------------------------------- /apps/demo-mobile/src/main/java/dev/yankew/sample/demo/DemoModule.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 WANG Yanke 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.yankew.sample.demo 18 | 19 | import dagger.Binds 20 | import dagger.Module 21 | import dagger.Provides 22 | import dagger.hilt.InstallIn 23 | import dagger.hilt.components.SingletonComponent 24 | import dev.yankew.sample.domain.model.AppVariant 25 | import dev.yankew.sample.domain.repository.AppRepository 26 | import dev.yankew.sample.init.AppBaseInitializer 27 | import javax.inject.Singleton 28 | 29 | @Module 30 | @InstallIn(SingletonComponent::class) 31 | object DemoModule { 32 | @Provides 33 | fun appRepository(): AppRepository = object : AppRepository { 34 | override fun getAppVariant(): AppVariant = AppVariant.MOBILE_DEMO 35 | } 36 | } 37 | 38 | @Module 39 | @InstallIn(SingletonComponent::class) 40 | interface DemoInterfaceModule { 41 | @Singleton 42 | @Binds 43 | fun appBaseInitializer(initializer: DemoBaseInitializer): AppBaseInitializer 44 | } 45 | -------------------------------------------------------------------------------- /app/src/main/java/dev/yankew/sample/cleanarch/MainApplication.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 WANG Yanke 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.yankew.sample.cleanarch 18 | 19 | import android.app.Application 20 | import dagger.hilt.android.HiltAndroidApp 21 | import dev.yankew.sample.domain.coroutine.ApplicationScope 22 | import dev.yankew.sample.domain.event.AppLaunchedEvent 23 | import dev.yankew.sample.domain.event.EventDispatcher 24 | import dev.yankew.sample.domain.model.AppVariant 25 | import kotlinx.coroutines.CoroutineScope 26 | import kotlinx.coroutines.launch 27 | import javax.inject.Inject 28 | 29 | @HiltAndroidApp 30 | class MainApplication : Application() { 31 | @Inject 32 | @ApplicationScope 33 | lateinit var applicationScope: CoroutineScope 34 | 35 | @Inject 36 | lateinit var eventDispatcher: EventDispatcher 37 | 38 | override fun onCreate() { 39 | super.onCreate() 40 | applicationScope.launch { 41 | eventDispatcher.dispatch(AppLaunchedEvent(AppVariant.MOBILE)) 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /apps/demo-mobile/src/main/java/dev/yankew/sample/demo/DemoApplication.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 WANG Yanke 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.yankew.sample.demo 18 | 19 | import android.app.Application 20 | import dagger.hilt.android.HiltAndroidApp 21 | import dev.yankew.sample.domain.coroutine.ApplicationScope 22 | import dev.yankew.sample.domain.event.AppLaunchedEvent 23 | import dev.yankew.sample.domain.event.EventDispatcher 24 | import dev.yankew.sample.domain.model.AppVariant 25 | import kotlinx.coroutines.CoroutineScope 26 | import kotlinx.coroutines.launch 27 | import javax.inject.Inject 28 | 29 | @HiltAndroidApp 30 | class DemoApplication : Application() { 31 | @Inject 32 | @ApplicationScope 33 | lateinit var applicationScope: CoroutineScope 34 | 35 | @Inject 36 | lateinit var eventDispatcher: EventDispatcher 37 | 38 | override fun onCreate() { 39 | super.onCreate() 40 | applicationScope.launch { 41 | eventDispatcher.dispatch(AppLaunchedEvent(AppVariant.MOBILE_DEMO)) 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /domain/src/main/java/dev/yankew/sample/domain/coroutine/CoroutineModule.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 WANG Yanke 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.yankew.sample.domain.coroutine 18 | 19 | import dagger.Module 20 | import dagger.Provides 21 | import dagger.hilt.InstallIn 22 | import dagger.hilt.components.SingletonComponent 23 | import kotlinx.coroutines.CoroutineDispatcher 24 | import kotlinx.coroutines.CoroutineScope 25 | import kotlinx.coroutines.Dispatchers 26 | import kotlinx.coroutines.SupervisorJob 27 | import javax.inject.Singleton 28 | 29 | @Module 30 | @InstallIn(SingletonComponent::class) 31 | object CoroutineModule { 32 | @ApplicationScope 33 | @Singleton 34 | @Provides 35 | fun provideApplicationScope( 36 | @DefaultDispatcher defaultDispatcher: CoroutineDispatcher 37 | ): CoroutineScope = CoroutineScope(SupervisorJob() + defaultDispatcher) 38 | 39 | @DefaultDispatcher 40 | @Provides 41 | fun provideDefaultDispatcher(): CoroutineDispatcher = Dispatchers.Default 42 | 43 | @IoDispatcher 44 | @Provides 45 | fun provideIoDispatcher(): CoroutineDispatcher = Dispatchers.IO 46 | } 47 | -------------------------------------------------------------------------------- /ui/ui-mobile/src/main/java/dev/yankew/sample/ui/about/DeveloperViewModel.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 WANG Yanke 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.yankew.sample.ui.about 18 | 19 | import androidx.lifecycle.ViewModel 20 | import androidx.lifecycle.viewModelScope 21 | import dagger.hilt.android.lifecycle.HiltViewModel 22 | import dev.yankew.sample.domain.config.NewFeatureLocalParam 23 | import dev.yankew.sample.domain.config.NewFeatureParam 24 | import dev.yankew.sample.domain.config.getConfiguredValue 25 | import dev.yankew.sample.domain.util.orNull 26 | import kotlinx.coroutines.flow.flowOf 27 | import kotlinx.coroutines.launch 28 | import java.util.Optional 29 | import javax.inject.Inject 30 | 31 | @HiltViewModel 32 | class DeveloperViewModel @Inject constructor( 33 | @NewFeatureParam private val localNewFeatureParam: Optional, 34 | ) : ViewModel() { 35 | val newFeatureLocalEnabled = localNewFeatureParam.getConfiguredValue { flowOf(false) } 36 | 37 | fun setNewFeatureLocalEnabled(enabled: Boolean) { 38 | viewModelScope.launch { 39 | localNewFeatureParam.orNull()?.setValue(enabled) 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /domain/src/main/java/dev/yankew/sample/domain/config/ConfigParam.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 WANG Yanke 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.yankew.sample.domain.config 18 | 19 | import dev.yankew.sample.domain.util.orNull 20 | import kotlinx.coroutines.flow.Flow 21 | import kotlinx.coroutines.flow.flowOf 22 | import java.util.Optional 23 | 24 | fun interface ConfigParam { 25 | fun getValue(): Flow> 26 | } 27 | 28 | interface LocalConfigParam : ConfigParam { 29 | suspend fun setValue(value: T) 30 | } 31 | 32 | fun interface RemoteConfigParam : ConfigParam 33 | 34 | fun Optional>.getConfiguredValue( 35 | default: () -> Flow 36 | ): Flow { 37 | return orNull()?.getValue()?.value { default() } ?: default() 38 | } 39 | 40 | fun Optional>.getConfiguredValue(): Flow> { 41 | return orNull()?.getValue() ?: flowOf(ConfigValue.Unset) 42 | } 43 | 44 | fun Optional>.getConfiguredValue( 45 | remote: Optional>, 46 | default: () -> Flow 47 | ): Flow { 48 | return getConfiguredValue { remote.getConfiguredValue(default) } 49 | } 50 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /apps/demo-mobile/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /app/src/main/java/dev/yankew/sample/cleanarch/crashlytics/CrashlyticsHelper.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 WANG Yanke 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.yankew.sample.cleanarch.crashlytics 18 | 19 | import com.google.firebase.crashlytics.FirebaseCrashlytics 20 | import dev.yankew.sample.cleanarch.BuildConfig 21 | import dev.yankew.sample.domain.event.CrashlyticsOptionEvent 22 | import dev.yankew.sample.domain.event.EventHandler 23 | import dev.yankew.sample.domain.settings.CrashlyticsOption 24 | import dev.yankew.sample.domain.settings.SettingOption 25 | import kotlinx.coroutines.flow.first 26 | import timber.log.Timber 27 | import javax.inject.Inject 28 | 29 | class CrashlyticsHelper @Inject constructor( 30 | @CrashlyticsOption private val crashlyticsOption: SettingOption, 31 | ) { 32 | val crashlyticsOptionHandler = EventHandler { 33 | Timber.i("CrashlyticsOptionEvent received by CrashlyticsHelper") 34 | setUpCrashlytics() 35 | } 36 | 37 | suspend fun setUpCrashlytics() { 38 | if (BuildConfig.BUILD_TYPE == "release") { 39 | val enabled = crashlyticsOption.getValue().first() 40 | FirebaseCrashlytics.getInstance().setCrashlyticsCollectionEnabled(enabled) 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /comp/config-firebase/src/main/java/dev/yankew/sample/config/firebase/RemoteConfigHelper.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 WANG Yanke 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.yankew.sample.config.firebase 18 | 19 | import com.google.firebase.ktx.Firebase 20 | import com.google.firebase.remoteconfig.ktx.remoteConfig 21 | import com.google.firebase.remoteconfig.ktx.remoteConfigSettings 22 | import kotlinx.coroutines.channels.BufferOverflow 23 | import kotlinx.coroutines.flow.MutableSharedFlow 24 | import kotlinx.coroutines.flow.asSharedFlow 25 | import javax.inject.Inject 26 | import javax.inject.Singleton 27 | 28 | @Singleton 29 | class RemoteConfigHelper @Inject constructor() { 30 | val remoteConfig = Firebase.remoteConfig.apply { 31 | setConfigSettingsAsync( 32 | remoteConfigSettings { 33 | minimumFetchIntervalInSeconds = if (BuildConfig.DEBUG) 60 else 3600 34 | } 35 | ) 36 | setDefaultsAsync(R.xml.remote_config_defaults) 37 | } 38 | 39 | private val _updateEvent = MutableSharedFlow( 40 | extraBufferCapacity = 1, 41 | onBufferOverflow = BufferOverflow.DROP_OLDEST 42 | ) 43 | val updateEvent = _updateEvent.asSharedFlow() 44 | 45 | suspend fun notifyConfigUpdated() { 46 | _updateEvent.emit(Unit) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /ui/ui-mobile/src/main/java/dev/yankew/sample/ui/new/NewFeatureScreen.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 WANG Yanke 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.yankew.sample.ui.new 18 | 19 | import androidx.compose.foundation.layout.Column 20 | import androidx.compose.foundation.layout.fillMaxSize 21 | import androidx.compose.foundation.layout.padding 22 | import androidx.compose.material.Text 23 | import androidx.compose.runtime.Composable 24 | import androidx.compose.ui.Alignment 25 | import androidx.compose.ui.Modifier 26 | import androidx.compose.ui.res.stringResource 27 | import androidx.compose.ui.unit.dp 28 | import dev.yankew.sample.strings.R 29 | import dev.yankew.sample.ui.MainScreenScaffold 30 | 31 | /** 32 | * Stateless composable of New Feature Screen 33 | */ 34 | @Composable 35 | fun NewFeatureScreen( 36 | openDrawer: () -> Unit, 37 | ) { 38 | MainScreenScaffold( 39 | titleResId = R.string.new_feature_title, 40 | openDrawer = openDrawer, 41 | ) { innerPaddings -> 42 | Column( 43 | modifier = Modifier 44 | .fillMaxSize() 45 | .padding(innerPaddings) 46 | .padding(16.dp), 47 | horizontalAlignment = Alignment.CenterHorizontally 48 | ) { 49 | Text(stringResource(R.string.new_feature_text)) 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /app/src/main/java/dev/yankew/sample/cleanarch/crashlytics/CrashlyticsTree.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 WANG Yanke 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.yankew.sample.cleanarch.crashlytics 18 | 19 | import android.util.Log 20 | import com.google.firebase.crashlytics.FirebaseCrashlytics 21 | import com.google.firebase.crashlytics.ktx.setCustomKeys 22 | import timber.log.Timber 23 | 24 | class CrashlyticsTree : Timber.DebugTree() { 25 | override fun log(priority: Int, tag: String?, message: String, t: Throwable?) { 26 | if (priority == Log.VERBOSE || priority == Log.DEBUG || priority == Log.INFO) { 27 | return 28 | } 29 | FirebaseCrashlytics.getInstance().run { 30 | setCustomKeys { 31 | key(KEY_PRIORITY, getPriorityString(priority)) 32 | key(KEY_TAG, tag ?: "unknown") 33 | key(KEY_MESSAGE, message) 34 | } 35 | log(t?.message ?: message) 36 | } 37 | } 38 | 39 | private fun getPriorityString(priority: Int) = when (priority) { 40 | Log.WARN -> "Warn($priority)" 41 | else -> "Error($priority)" 42 | } 43 | 44 | companion object { 45 | private const val KEY_PRIORITY = "priority" 46 | private const val KEY_TAG = "tag" 47 | private const val KEY_MESSAGE = "message" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /comp/config-local/src/main/java/dev/yankew/sample/config/local/LocalParams.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 WANG Yanke 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.yankew.sample.config.local 18 | 19 | import androidx.datastore.core.DataStore 20 | import androidx.datastore.preferences.core.Preferences 21 | import androidx.datastore.preferences.core.booleanPreferencesKey 22 | import androidx.datastore.preferences.core.edit 23 | import dev.yankew.sample.domain.config.ConfigValue 24 | import dev.yankew.sample.domain.config.NewFeatureLocalParam 25 | import kotlinx.coroutines.flow.Flow 26 | import kotlinx.coroutines.flow.map 27 | import javax.inject.Inject 28 | 29 | class LocalParams @Inject constructor( 30 | @LocalConfigDataStore private val dataStore: DataStore, 31 | ) { 32 | val newFeatureParam = object : NewFeatureLocalParam { 33 | override fun getValue(): Flow> = dataStore.data.map { preferences -> 34 | preferences[KEY_NEW_FEATURE_ENABLED]?.let { enabled -> 35 | ConfigValue.ConfiguredValue(enabled) 36 | } ?: ConfigValue.Unset 37 | } 38 | 39 | override suspend fun setValue(value: Boolean) { 40 | dataStore.edit { it[KEY_NEW_FEATURE_ENABLED] = value } 41 | } 42 | } 43 | 44 | companion object { 45 | private val KEY_NEW_FEATURE_ENABLED = booleanPreferencesKey("new_feature_enabled") 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /app/src/main/java/dev/yankew/sample/cleanarch/AppModule.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 WANG Yanke 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.yankew.sample.cleanarch 18 | 19 | import dagger.Binds 20 | import dagger.Module 21 | import dagger.Provides 22 | import dagger.hilt.InstallIn 23 | import dagger.hilt.components.SingletonComponent 24 | import dagger.multibindings.IntoSet 25 | import dev.yankew.sample.cleanarch.crashlytics.CrashlyticsHelper 26 | import dev.yankew.sample.domain.event.CrashlyticsOptionEvent 27 | import dev.yankew.sample.domain.event.EventHandler 28 | import dev.yankew.sample.domain.model.AppVariant 29 | import dev.yankew.sample.domain.repository.AppRepository 30 | import dev.yankew.sample.init.AppBaseInitializer 31 | import javax.inject.Singleton 32 | 33 | @Module 34 | @InstallIn(SingletonComponent::class) 35 | object AppModule { 36 | @Provides 37 | fun appRepository(): AppRepository = object : AppRepository { 38 | override fun getAppVariant(): AppVariant = AppVariant.MOBILE 39 | } 40 | 41 | @Provides 42 | @IntoSet 43 | fun crashlyticsOptionHandler( 44 | helper: CrashlyticsHelper 45 | ): EventHandler = helper.crashlyticsOptionHandler 46 | } 47 | 48 | @Module 49 | @InstallIn(SingletonComponent::class) 50 | interface AppInterfaceModule { 51 | @Singleton 52 | @Binds 53 | fun appBaseInitializer(initializer: AppBaseInitializerImpl): AppBaseInitializer 54 | } 55 | -------------------------------------------------------------------------------- /domain/src/main/java/dev/yankew/sample/domain/interactor/GetNewFeatureEnabledUseCase.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 WANG Yanke 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.yankew.sample.domain.interactor 18 | 19 | import dev.yankew.sample.domain.config.NewFeatureLocalParam 20 | import dev.yankew.sample.domain.config.NewFeatureParam 21 | import dev.yankew.sample.domain.config.NewFeatureRemoteParam 22 | import dev.yankew.sample.domain.config.getConfiguredValue 23 | import dev.yankew.sample.domain.coroutine.IoDispatcher 24 | import dev.yankew.sample.domain.model.DataResult 25 | import dev.yankew.sample.domain.util.Logger 26 | import kotlinx.coroutines.CoroutineDispatcher 27 | import kotlinx.coroutines.flow.Flow 28 | import kotlinx.coroutines.flow.flowOf 29 | import kotlinx.coroutines.flow.map 30 | import java.util.Optional 31 | import javax.inject.Inject 32 | 33 | class GetNewFeatureEnabledUseCase @Inject constructor( 34 | @NewFeatureParam private val localParam: Optional, 35 | @NewFeatureParam private val remoteParam: Optional, 36 | private val logger: Logger, 37 | @IoDispatcher ioDispatcher: CoroutineDispatcher 38 | ) : FlowUseCase(ioDispatcher) { 39 | override fun execute(parameters: Unit): Flow> { 40 | return localParam.getConfiguredValue(remoteParam) { 41 | logger.i("Use local default value for new feature enabled") 42 | flowOf(false) 43 | }.map { DataResult.Success(it) } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /domain/src/main/java/dev/yankew/sample/domain/analytics/SettingsChangedTrackingEvent.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 WANG Yanke 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.yankew.sample.domain.analytics 18 | 19 | import javax.inject.Inject 20 | 21 | class SettingsChangedTrackingEvent @Inject constructor( 22 | private val analytics: AnalyticsEventDispatcher, 23 | ) { 24 | fun logAnalyticsChanged(enabled: Boolean) = 25 | log(if (enabled) VALUE_ANALYTICS_ENABLED else VALUE_ANALYTICS_DISABLED) 26 | 27 | fun logCrashlyticsChanged(enabled: Boolean) = 28 | log(if (enabled) VALUE_CRASHLYTICS_ENABLED else VALUE_CRASHLYTICS_DISABLED) 29 | 30 | fun logNotificationChanged(enabled: Boolean) = 31 | log(if (enabled) VALUE_NOTIFICATION_ENABLED else VALUE_NOTIFICATION_DISABLED) 32 | 33 | private fun log(value: String) = analytics.logEvent(EVENT_NAME, PARAM_SETTING_NAME to value) 34 | 35 | companion object { 36 | private const val EVENT_NAME = "settings_changed" 37 | private const val PARAM_SETTING_NAME = "setting_name" 38 | private const val VALUE_ANALYTICS_ENABLED = "analytics_enabled" 39 | private const val VALUE_ANALYTICS_DISABLED = "analytics_disabled" 40 | private const val VALUE_CRASHLYTICS_ENABLED = "crashlytics_enabled" 41 | private const val VALUE_CRASHLYTICS_DISABLED = "crashlytics_disabled" 42 | private const val VALUE_NOTIFICATION_ENABLED = "notification_enabled" 43 | private const val VALUE_NOTIFICATION_DISABLED = "notification_disabled" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /ui/ui-mobile/src/main/java/dev/yankew/sample/ui/settings/SettingsViewModel.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 WANG Yanke 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.yankew.sample.ui.settings 18 | 19 | import androidx.lifecycle.ViewModel 20 | import androidx.lifecycle.viewModelScope 21 | import dagger.hilt.android.lifecycle.HiltViewModel 22 | import dev.yankew.sample.domain.settings.AnalyticsOption 23 | import dev.yankew.sample.domain.settings.CrashlyticsOption 24 | import dev.yankew.sample.domain.settings.NotificationOption 25 | import dev.yankew.sample.domain.settings.SettingOption 26 | import kotlinx.coroutines.launch 27 | import javax.inject.Inject 28 | 29 | @HiltViewModel 30 | class SettingsViewModel @Inject constructor( 31 | @AnalyticsOption private val analyticsOption: SettingOption, 32 | @CrashlyticsOption private val crashlyticsOption: SettingOption, 33 | @NotificationOption private val notificationOption: SettingOption, 34 | ) : ViewModel() { 35 | val analytics = analyticsOption.getValue() 36 | val crashlytics = crashlyticsOption.getValue() 37 | val notification = notificationOption.getValue() 38 | 39 | fun setAnalytics(enabled: Boolean) { 40 | viewModelScope.launch { analyticsOption.setValue(enabled) } 41 | } 42 | 43 | fun setCrashlytics(enabled: Boolean) { 44 | viewModelScope.launch { crashlyticsOption.setValue(enabled) } 45 | } 46 | 47 | fun setNotification(enabled: Boolean) { 48 | viewModelScope.launch { notificationOption.setValue(enabled) } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 WANG Yanke 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | plugins { 18 | id("common.base-android-app") 19 | id("com.google.gms.google-services") 20 | id("com.google.firebase.crashlytics") 21 | } 22 | 23 | android { 24 | defaultConfig { 25 | applicationId = "dev.yankew.cleanarch" 26 | versionCode = Versions.versionCodeMobile 27 | versionName = Versions.versionName 28 | } 29 | } 30 | 31 | dependencies { 32 | implementation(project(":domain")) 33 | implementation(project(":lib:init")) 34 | implementation(project(":lib:strings")) 35 | 36 | // Dependencies for injection 37 | debugImplementation(project(":comp:analytics-debug")) 38 | implementation(project(":comp:analytics-firebase")) 39 | implementation(project(":comp:config-firebase")) 40 | implementation(project(":comp:config-local")) 41 | implementation(project(":comp:core")) 42 | implementation(project(":comp:download")) 43 | implementation(project(":comp:feature")) 44 | implementation(project(":comp:notification")) 45 | implementation(project(":comp:settings")) 46 | implementation(project(":ui:ui-mobile")) 47 | implementation(project(":ui:ui-widget")) 48 | 49 | // Initialization 50 | // App Startup 51 | implementation(Libs.STARTUP) 52 | // Init WorkManager 53 | implementation(Libs.HILT_WORK) 54 | implementation(Libs.WORK_RUNTIME_KTX) 55 | 56 | // Firebase crashlytics 57 | implementation(platform(Libs.FIREBASE_BOM)) 58 | implementation(Libs.FIREBASE_CRASHLYTICS) 59 | } 60 | -------------------------------------------------------------------------------- /comp/analytics-firebase/src/main/java/dev/yankew/sample/analytics/firebase/FirebaseAnalyticsTracker.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 WANG Yanke 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.yankew.sample.analytics.firebase 18 | 19 | import com.google.firebase.analytics.ktx.analytics 20 | import com.google.firebase.analytics.ktx.logEvent 21 | import com.google.firebase.ktx.Firebase 22 | import dev.yankew.sample.domain.analytics.AnalyticsTracker 23 | import dev.yankew.sample.domain.settings.AnalyticsOption 24 | import dev.yankew.sample.domain.settings.SettingOption 25 | import kotlinx.coroutines.flow.first 26 | import javax.inject.Inject 27 | import javax.inject.Provider 28 | 29 | class FirebaseAnalyticsTracker @Inject constructor( 30 | @AnalyticsOption private val analyticsOption: Provider>, 31 | ) : AnalyticsTracker { 32 | override suspend fun isAnalyticsEnabled(): Boolean { 33 | return analyticsOption.get().getValue().first() 34 | } 35 | 36 | override fun logEvent(name: String, params: Map) { 37 | Firebase.analytics.logEvent(name) { 38 | params.forEach { (key, value) -> 39 | when (value) { 40 | is String -> param(key, value) 41 | is Int -> param(key, value.toLong()) 42 | is Long -> param(key, value) 43 | else -> param(key, value.toString()) 44 | } 45 | } 46 | } 47 | } 48 | 49 | override fun setUserProperty(key: String, value: String) { 50 | Firebase.analytics.setUserProperty(key, value) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /comp/config-local/src/main/java/dev/yankew/sample/config/local/LocalConfigModule.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 WANG Yanke 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.yankew.sample.config.local 18 | 19 | import android.content.Context 20 | import androidx.datastore.core.DataStore 21 | import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler 22 | import androidx.datastore.preferences.core.PreferenceDataStoreFactory 23 | import androidx.datastore.preferences.core.Preferences 24 | import androidx.datastore.preferences.core.emptyPreferences 25 | import androidx.datastore.preferences.preferencesDataStoreFile 26 | import dagger.Module 27 | import dagger.Provides 28 | import dagger.hilt.InstallIn 29 | import dagger.hilt.android.qualifiers.ApplicationContext 30 | import dagger.hilt.components.SingletonComponent 31 | import dev.yankew.sample.domain.config.NewFeatureParam 32 | import javax.inject.Qualifier 33 | import javax.inject.Singleton 34 | 35 | @Retention(AnnotationRetention.BINARY) 36 | @Qualifier 37 | annotation class LocalConfigDataStore 38 | 39 | @Module 40 | @InstallIn(SingletonComponent::class) 41 | object LocalConfigModule { 42 | @Provides 43 | @Singleton 44 | @LocalConfigDataStore 45 | fun preferenceDataStore(@ApplicationContext context: Context): DataStore = 46 | PreferenceDataStoreFactory.create( 47 | corruptionHandler = ReplaceFileCorruptionHandler { emptyPreferences() } 48 | ) { context.preferencesDataStoreFile("local-config") } 49 | 50 | @Provides 51 | @NewFeatureParam 52 | fun newFeatureParam(localParams: LocalParams) = localParams.newFeatureParam 53 | } 54 | -------------------------------------------------------------------------------- /lib/compose/src/main/java/dev/yankew/sample/compose/ActivityViewModel.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 WANG Yanke 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.yankew.sample.compose 18 | 19 | import android.app.Activity 20 | import android.content.ContextWrapper 21 | import androidx.activity.ComponentActivity 22 | import androidx.compose.runtime.Composable 23 | import androidx.compose.ui.platform.LocalContext 24 | import androidx.lifecycle.ViewModel 25 | import androidx.lifecycle.ViewModelStoreOwner 26 | import androidx.lifecycle.viewmodel.compose.viewModel 27 | 28 | /** 29 | * Get or create a Activity scope ViewModel instance. 30 | * 31 | * This function is usually used with Hilt because the Activity generated by Hilt implements 32 | * HasDefaultViewModelProviderFactory and the viewModel method can use that factory to create 33 | * the ViewModel. 34 | */ 35 | @Composable 36 | inline fun activityViewModel(): VM = viewModel( 37 | getActivityViewModelStoreOwner() 38 | ) 39 | 40 | @Composable 41 | fun getActivityViewModelStoreOwner(): ViewModelStoreOwner { 42 | return LocalContext.current.let { 43 | var ctx = it 44 | while (ctx is ContextWrapper) { 45 | if (ctx is Activity) { 46 | if (ctx is ComponentActivity) { 47 | return@let ctx 48 | } else { 49 | break 50 | } 51 | } 52 | ctx = ctx.baseContext 53 | } 54 | throw IllegalStateException( 55 | "Expected a ComponentActivity context as a ViewModelStoreOwner but instead found: $ctx" 56 | ) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /app/src/main/java/dev/yankew/sample/cleanarch/AppBaseInitializerImpl.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 WANG Yanke 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.yankew.sample.cleanarch 18 | 19 | import android.content.Context 20 | import androidx.hilt.work.HiltWorkerFactory 21 | import androidx.work.Configuration 22 | import androidx.work.WorkManager 23 | import dagger.hilt.android.qualifiers.ApplicationContext 24 | import dev.yankew.sample.cleanarch.crashlytics.CrashlyticsHelper 25 | import dev.yankew.sample.cleanarch.crashlytics.CrashlyticsTree 26 | import dev.yankew.sample.domain.coroutine.ApplicationScope 27 | import dev.yankew.sample.init.AppBaseInitializer 28 | import kotlinx.coroutines.CoroutineScope 29 | import kotlinx.coroutines.launch 30 | import timber.log.Timber 31 | import javax.inject.Inject 32 | 33 | class AppBaseInitializerImpl @Inject constructor( 34 | @ApplicationContext private val context: Context, 35 | @ApplicationScope private val applicationScope: CoroutineScope, 36 | private val workerFactory: HiltWorkerFactory, 37 | private val crashlyticsHelper: CrashlyticsHelper, 38 | ) : AppBaseInitializer { 39 | override fun init() { 40 | if (BuildConfig.BUILD_TYPE == "release") { 41 | Timber.plant(CrashlyticsTree()) 42 | } else { 43 | Timber.plant(CrashlyticsTree(), Timber.DebugTree()) 44 | } 45 | Timber.i("App base initialization") 46 | 47 | applicationScope.launch { 48 | crashlyticsHelper.setUpCrashlytics() 49 | } 50 | 51 | WorkManager.initialize( 52 | context, 53 | Configuration.Builder().setWorkerFactory(workerFactory).build() 54 | ) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /lib/compose/src/main/java/dev/yankew/sample/compose/FlowWithLifecycle.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 WANG Yanke 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.yankew.sample.compose 18 | 19 | import androidx.compose.runtime.Composable 20 | import androidx.compose.runtime.LaunchedEffect 21 | import androidx.compose.runtime.State 22 | import androidx.compose.runtime.mutableStateOf 23 | import androidx.compose.runtime.remember 24 | import androidx.compose.ui.platform.LocalLifecycleOwner 25 | import androidx.lifecycle.Lifecycle 26 | import androidx.lifecycle.repeatOnLifecycle 27 | import kotlinx.coroutines.flow.Flow 28 | import kotlinx.coroutines.flow.StateFlow 29 | 30 | /** 31 | * Collect the flow as Compose State when the lifecycle is at least at STARTED state. 32 | * 33 | * @param initial The initial value of the returned Compose State 34 | */ 35 | @Composable 36 | fun Flow.collectAsStateWhenStarted( 37 | initial: R 38 | ): State = collectAsStateWhenStarted { initial } 39 | 40 | /** 41 | * Collect the flow as Compose State when the lifecycle is at least at STARTED state. 42 | * 43 | * No initial parameter is required here because StateFlow has. 44 | */ 45 | @Composable 46 | fun StateFlow.collectAsStateWhenStarted(): State = collectAsStateWhenStarted { value } 47 | 48 | @Composable 49 | private fun Flow.collectAsStateWhenStarted(initial: () -> R): State { 50 | val lifecycle: Lifecycle = LocalLifecycleOwner.current.lifecycle 51 | val state = remember(this) { mutableStateOf(initial()) } 52 | LaunchedEffect(this) { 53 | lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { 54 | collect { state.value = it } 55 | } 56 | } 57 | return state 58 | } 59 | -------------------------------------------------------------------------------- /ui/ui-mobile/src/main/java/dev/yankew/sample/ui/navigation/MainNavGraph.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 WANG Yanke 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.yankew.sample.ui 18 | 19 | import androidx.compose.runtime.Composable 20 | import androidx.navigation.NavHostController 21 | import androidx.navigation.compose.NavHost 22 | import androidx.navigation.compose.composable 23 | import androidx.navigation.compose.rememberNavController 24 | import dev.yankew.sample.ui.about.AboutScreen 25 | import dev.yankew.sample.ui.about.DeveloperScreen 26 | import dev.yankew.sample.ui.home.HomeScreen 27 | import dev.yankew.sample.ui.navigation.NavDestinations 28 | import dev.yankew.sample.ui.new.NewFeatureScreen 29 | import dev.yankew.sample.ui.settings.SettingsScreen 30 | 31 | @Composable 32 | fun MainNavGraph( 33 | navController: NavHostController = rememberNavController(), 34 | openDrawer: () -> Unit, 35 | ) { 36 | NavHost( 37 | navController = navController, 38 | startDestination = NavDestinations.ROUTE_HOME, 39 | ) { 40 | composable(route = NavDestinations.ROUTE_HOME) { 41 | HomeScreen(openDrawer) 42 | } 43 | composable(route = NavDestinations.ROUTE_NEW_FEATURE) { 44 | NewFeatureScreen(openDrawer) 45 | } 46 | composable(route = NavDestinations.ROUTE_SETTINGS) { 47 | SettingsScreen(openDrawer) 48 | } 49 | composable(route = NavDestinations.ROUTE_ABOUT) { 50 | AboutScreen( 51 | openDeveloper = { navController.navigate(NavDestinations.ROUTE_DEVELOPER) }, 52 | openDrawer = openDrawer 53 | ) 54 | } 55 | composable(route = NavDestinations.ROUTE_DEVELOPER) { 56 | DeveloperScreen { navController.navigateUp() } 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /ui/ui-mobile/src/main/java/dev/yankew/sample/ui/MainApp.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 WANG Yanke 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.yankew.sample.ui 18 | 19 | import androidx.activity.compose.BackHandler 20 | import androidx.compose.material.DrawerValue 21 | import androidx.compose.material.ModalDrawer 22 | import androidx.compose.material.rememberDrawerState 23 | import androidx.compose.runtime.Composable 24 | import androidx.compose.runtime.rememberCoroutineScope 25 | import androidx.navigation.compose.rememberNavController 26 | import dev.yankew.sample.compose.activityViewModel 27 | import dev.yankew.sample.compose.theme.MyTheme 28 | import dev.yankew.sample.domain.model.AppVariant 29 | import dev.yankew.sample.ui.navigation.AppDrawer 30 | import kotlinx.coroutines.launch 31 | 32 | @Composable 33 | fun MainApp() { 34 | val isDemo = activityViewModel().appVariant == AppVariant.MOBILE_DEMO 35 | MyTheme(isDemo = isDemo) { 36 | val navController = rememberNavController() 37 | val coroutineScope = rememberCoroutineScope() 38 | val drawerState = rememberDrawerState(DrawerValue.Closed) 39 | val openDrawer: () -> Unit = { coroutineScope.launch { drawerState.open() } } 40 | val closeDrawer: () -> Unit = { coroutineScope.launch { drawerState.close() } } 41 | if (drawerState.isOpen) { 42 | BackHandler { closeDrawer() } 43 | } 44 | ModalDrawer( 45 | drawerContent = { 46 | AppDrawer( 47 | navController = navController, 48 | closeDrawer = closeDrawer 49 | ) 50 | }, 51 | drawerState = drawerState, 52 | gesturesEnabled = true 53 | ) { 54 | MainNavGraph(navController, openDrawer) 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /lib/android/src/main/java/dev/yankew/sample/android/FlowBroadcastReceiver.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 WANG Yanke 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.yankew.sample.android 18 | 19 | import android.content.BroadcastReceiver 20 | import android.content.Context 21 | import android.content.Intent 22 | import android.content.IntentFilter 23 | import kotlinx.coroutines.channels.Channel 24 | import kotlinx.coroutines.channels.consumeEach 25 | import kotlinx.coroutines.flow.Flow 26 | import kotlinx.coroutines.flow.flow 27 | 28 | /** 29 | * Register broadcast receiver to convert data in broadcast to Flow. 30 | * The receiver will be unregistered if the returned Flow is cancelled. 31 | * 32 | * @param filter Selects the Intent broadcasts to be received. 33 | * @param onStartListening A function that will run right after the registration of the receiver. 34 | * It is useful when you do two-way communication via broadcast. The requesting data broadcast 35 | * can be sent in this function. 36 | * @param extractData The function to extract data from the broadcast 37 | */ 38 | fun Context.observeBroadcast( 39 | filter: IntentFilter, 40 | onStartListening: () -> Unit = {}, 41 | extractData: (Context, Intent) -> T 42 | ): Flow { 43 | return flow { 44 | val channel = Channel(Channel.UNLIMITED) 45 | val receiver = object : BroadcastReceiver() { 46 | override fun onReceive(context: Context, intent: Intent) { 47 | channel.trySend(extractData(context, intent)) 48 | } 49 | } 50 | registerReceiver(receiver, filter) 51 | onStartListening() 52 | try { 53 | channel.consumeEach { 54 | emit(it) 55 | } 56 | } finally { 57 | unregisterReceiver(receiver) 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /comp/feature/src/main/java/dev/yankew/sample/feature/ElapseDataSource.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 WANG Yanke 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.yankew.sample.feature 18 | 19 | import android.content.Context 20 | import android.content.Intent 21 | import android.content.IntentFilter 22 | import androidx.datastore.core.DataStore 23 | import androidx.datastore.preferences.core.Preferences 24 | import androidx.datastore.preferences.core.edit 25 | import androidx.datastore.preferences.core.longPreferencesKey 26 | import dagger.hilt.android.qualifiers.ApplicationContext 27 | import dev.yankew.sample.android.observeBroadcast 28 | import kotlinx.coroutines.flow.Flow 29 | import kotlinx.coroutines.flow.combine 30 | import kotlinx.coroutines.flow.mapNotNull 31 | import kotlinx.coroutines.flow.onStart 32 | import java.time.Instant 33 | import javax.inject.Inject 34 | 35 | interface ElapseDataSource { 36 | fun getElapsedSeconds(): Flow 37 | suspend fun saveLaunchTime() 38 | } 39 | 40 | class ElapseDataSourceImpl @Inject constructor( 41 | @ApplicationContext private val context: Context, 42 | @FeatureDataStore private val dataStore: DataStore, 43 | ) : ElapseDataSource { 44 | override fun getElapsedSeconds(): Flow = IntentFilter().apply { 45 | addAction(Intent.ACTION_TIME_TICK) 46 | }.let { filter -> 47 | context.observeBroadcast(filter) { _, _ -> Instant.now().epochSecond } 48 | }.onStart { 49 | emit(Instant.now().epochSecond) 50 | }.combine( 51 | dataStore.data.mapNotNull { it[KEY_LAUNCH_EPOCH] } 52 | ) { now, launch -> now - launch } 53 | 54 | override suspend fun saveLaunchTime() { 55 | dataStore.edit { 56 | it[KEY_LAUNCH_EPOCH] = Instant.now().epochSecond 57 | } 58 | } 59 | 60 | companion object { 61 | private val KEY_LAUNCH_EPOCH = longPreferencesKey("launch_epoch") 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /lib/init/src/main/java/dev/yankew/sample/init/AppBaseInitializerWrapper.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 WANG Yanke 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.yankew.sample.init 18 | 19 | import android.content.Context 20 | import androidx.startup.Initializer 21 | import dagger.hilt.EntryPoint 22 | import dagger.hilt.InstallIn 23 | import dagger.hilt.android.EntryPointAccessors 24 | import dagger.hilt.components.SingletonComponent 25 | import timber.log.Timber 26 | import java.util.Optional 27 | 28 | /** 29 | * This class is used to overcome the limitation of App Startup: the dependencies should be known 30 | * before create() method and the dependencies can't be provided by injection in create(). 31 | * The app module can implement AppBaseInitializer interface to do some basic initialization before 32 | * all others. And feature modules can depend this initializer wrapper to make sure their 33 | * initializations happen after the base initialization. 34 | */ 35 | class AppBaseInitializerWrapper : Initializer { 36 | override fun create(context: Context) { 37 | AppBaseInitializerEntryPoint.getBaseInitializer(context).orElse(null)?.init() 38 | Timber.i("Base initialization completed.") 39 | } 40 | 41 | override fun dependencies(): List>> = emptyList() 42 | } 43 | 44 | @EntryPoint 45 | @InstallIn(SingletonComponent::class) 46 | private interface AppBaseInitializerEntryPoint { 47 | fun getBaseInitializer(): Optional 48 | 49 | companion object { 50 | fun getBaseInitializer(context: Context): Optional { 51 | val appContext = context.applicationContext ?: throw IllegalStateException() 52 | return EntryPointAccessors.fromApplication( 53 | appContext, 54 | AppBaseInitializerEntryPoint::class.java 55 | ).getBaseInitializer() 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /comp/settings/src/main/java/dev/yankew/sample/settings/SettingsModule.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 WANG Yanke 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.yankew.sample.settings 18 | 19 | import android.content.Context 20 | import androidx.datastore.core.DataStore 21 | import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler 22 | import androidx.datastore.preferences.core.PreferenceDataStoreFactory 23 | import androidx.datastore.preferences.core.Preferences 24 | import androidx.datastore.preferences.core.emptyPreferences 25 | import androidx.datastore.preferences.preferencesDataStoreFile 26 | import dagger.Module 27 | import dagger.Provides 28 | import dagger.hilt.InstallIn 29 | import dagger.hilt.android.qualifiers.ApplicationContext 30 | import dagger.hilt.components.SingletonComponent 31 | import dev.yankew.sample.domain.settings.AnalyticsOption 32 | import dev.yankew.sample.domain.settings.CrashlyticsOption 33 | import dev.yankew.sample.domain.settings.NotificationOption 34 | import javax.inject.Qualifier 35 | import javax.inject.Singleton 36 | 37 | @Retention(AnnotationRetention.BINARY) 38 | @Qualifier 39 | annotation class SettingsDataStore 40 | 41 | @Module 42 | @InstallIn(SingletonComponent::class) 43 | object SettingsModule { 44 | @Provides 45 | @Singleton 46 | @SettingsDataStore 47 | fun preferenceDataStore(@ApplicationContext context: Context): DataStore = 48 | PreferenceDataStoreFactory.create( 49 | corruptionHandler = ReplaceFileCorruptionHandler { emptyPreferences() } 50 | ) { context.preferencesDataStoreFile("settings") } 51 | 52 | @Provides 53 | @AnalyticsOption 54 | fun analyticsOption(settingsStore: SettingsStore) = settingsStore.analyticsOption 55 | 56 | @Provides 57 | @CrashlyticsOption 58 | fun crashlyticsOption(settingsStore: SettingsStore) = settingsStore.crashlyticsOption 59 | 60 | @Provides 61 | @NotificationOption 62 | fun notificationOption(settingsStore: SettingsStore) = settingsStore.notificationOption 63 | } 64 | -------------------------------------------------------------------------------- /comp/analytics-firebase/src/main/java/dev/yankew/sample/analytics/firebase/FirebaseAnalyticsInitializer.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 WANG Yanke 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.yankew.sample.analytics.firebase 18 | 19 | import android.content.Context 20 | import com.google.firebase.analytics.ktx.analytics 21 | import com.google.firebase.ktx.Firebase 22 | import dagger.hilt.EntryPoint 23 | import dagger.hilt.InstallIn 24 | import dagger.hilt.android.EntryPointAccessors 25 | import dagger.hilt.components.SingletonComponent 26 | import dev.yankew.sample.domain.coroutine.ApplicationScope 27 | import dev.yankew.sample.init.ComponentInitializer 28 | import kotlinx.coroutines.CoroutineScope 29 | import kotlinx.coroutines.launch 30 | import timber.log.Timber 31 | import javax.inject.Inject 32 | 33 | class FirebaseAnalyticsInitializer : ComponentInitializer() { 34 | @Inject 35 | lateinit var firebaseAnalyticsTracker: FirebaseAnalyticsTracker 36 | 37 | @Inject 38 | @ApplicationScope 39 | lateinit var applicationScope: CoroutineScope 40 | 41 | override fun create(context: Context) { 42 | Timber.i("Initialize Firebase Analytics") 43 | FirebaseAnalyticsInitializerEntryPoint.resolve(context).inject(this) 44 | applicationScope.launch { 45 | val enabled = firebaseAnalyticsTracker.isAnalyticsEnabled() 46 | Firebase.analytics.setAnalyticsCollectionEnabled(enabled) 47 | } 48 | } 49 | } 50 | 51 | @EntryPoint 52 | @InstallIn(SingletonComponent::class) 53 | interface FirebaseAnalyticsInitializerEntryPoint { 54 | fun inject(initializer: FirebaseAnalyticsInitializer) 55 | 56 | companion object { 57 | fun resolve(context: Context): FirebaseAnalyticsInitializerEntryPoint { 58 | val appContext = context.applicationContext ?: throw IllegalStateException() 59 | return EntryPointAccessors.fromApplication( 60 | appContext, 61 | FirebaseAnalyticsInitializerEntryPoint::class.java 62 | ) 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /comp/config-firebase/src/main/java/dev/yankew/sample/config/firebase/RemoteConfigImpl.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 WANG Yanke 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.yankew.sample.config.firebase 18 | 19 | import com.google.firebase.remoteconfig.FirebaseRemoteConfig 20 | import com.google.firebase.remoteconfig.ktx.get 21 | import dev.yankew.sample.domain.config.ConfigValue 22 | import kotlinx.coroutines.flow.Flow 23 | import kotlinx.coroutines.flow.map 24 | import kotlinx.coroutines.flow.onStart 25 | import kotlinx.serialization.json.Json 26 | import timber.log.Timber 27 | import javax.inject.Inject 28 | 29 | class RemoteConfigImpl @Inject constructor( 30 | private val remoteConfigHelper: RemoteConfigHelper 31 | ) : RemoteConfig { 32 | override operator fun get(param: RemoteParam): Flow> { 33 | return remoteConfigHelper.updateEvent.map { getValue(param) } 34 | .onStart { emit(getValue(param)) } 35 | } 36 | 37 | private fun getValue(param: RemoteParam): ConfigValue { 38 | val value = remoteConfigHelper.remoteConfig[param.key] 39 | when (value.source) { 40 | FirebaseRemoteConfig.VALUE_SOURCE_STATIC -> return ConfigValue.Unset 41 | FirebaseRemoteConfig.VALUE_SOURCE_DEFAULT -> { 42 | Timber.i("The value of key <${param.key}> is retrieved from the defaults") 43 | } 44 | FirebaseRemoteConfig.VALUE_SOURCE_REMOTE -> { 45 | Timber.i("The value of key <${param.key}> is retrieved from Firebase Remote Config") 46 | } 47 | } 48 | @Suppress("UNCHECKED_CAST") 49 | return when (param) { 50 | is StringRemoteParam -> value.asString() as T 51 | is BooleanRemoteParam -> value.asBoolean() as T 52 | is LongRemoteParam -> value.asLong() as T 53 | is DoubleRemoteParam -> value.asDouble() as T 54 | is JsonRemoteParam -> Json.decodeFromString(param.serializer, value.asString()) 55 | }.let { ConfigValue.ConfiguredValue(it) } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /comp/config-firebase/src/main/java/dev/yankew/sample/config/firebase/RemoteConfigFetchWorker.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 WANG Yanke 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.yankew.sample.config.firebase 18 | 19 | import android.content.Context 20 | import androidx.hilt.work.HiltWorker 21 | import androidx.work.Constraints 22 | import androidx.work.CoroutineWorker 23 | import androidx.work.ExistingWorkPolicy 24 | import androidx.work.NetworkType 25 | import androidx.work.OneTimeWorkRequestBuilder 26 | import androidx.work.WorkManager 27 | import androidx.work.WorkerParameters 28 | import dagger.assisted.Assisted 29 | import dagger.assisted.AssistedInject 30 | import kotlinx.coroutines.tasks.await 31 | import timber.log.Timber 32 | 33 | @HiltWorker 34 | class RemoteConfigFetchWorker @AssistedInject constructor( 35 | @Assisted context: Context, 36 | @Assisted params: WorkerParameters, 37 | private val remoteConfigHelper: RemoteConfigHelper, 38 | ) : CoroutineWorker(context, params) { 39 | companion object { 40 | private const val UNIQUE_WORK_NAME = "RemoteConfigFetch" 41 | 42 | fun enqueue(context: Context) { 43 | val constraints = Constraints.Builder() 44 | .setRequiredNetworkType(NetworkType.CONNECTED) 45 | .build() 46 | val requestBuilder = OneTimeWorkRequestBuilder() 47 | .setConstraints(constraints) 48 | WorkManager.getInstance(context).enqueueUniqueWork( 49 | UNIQUE_WORK_NAME, 50 | ExistingWorkPolicy.KEEP, 51 | requestBuilder.build() 52 | ) 53 | } 54 | } 55 | 56 | override suspend fun doWork(): Result { 57 | return try { 58 | if (remoteConfigHelper.remoteConfig.fetchAndActivate().await()) { 59 | remoteConfigHelper.notifyConfigUpdated() 60 | } 61 | Result.success() 62 | } catch (e: Exception) { 63 | Timber.w("Fetch remote config failed: ${e.message}") 64 | Result.retry() 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /ui/ui-mobile/src/main/java/dev/yankew/sample/ui/settings/SettingsSwitch.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 WANG Yanke 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.yankew.sample.ui.settings 18 | 19 | import androidx.compose.foundation.clickable 20 | import androidx.compose.foundation.layout.Box 21 | import androidx.compose.foundation.layout.Row 22 | import androidx.compose.foundation.layout.Spacer 23 | import androidx.compose.foundation.layout.fillMaxWidth 24 | import androidx.compose.foundation.layout.height 25 | import androidx.compose.foundation.layout.padding 26 | import androidx.compose.foundation.layout.width 27 | import androidx.compose.material.MaterialTheme 28 | import androidx.compose.material.Switch 29 | import androidx.compose.material.Text 30 | import androidx.compose.runtime.Composable 31 | import androidx.compose.ui.Alignment 32 | import androidx.compose.ui.Modifier 33 | import androidx.compose.ui.unit.dp 34 | 35 | @Composable 36 | fun SettingsSwitch( 37 | title: String, 38 | checked: Boolean, 39 | onCheckedChange: (Boolean) -> Unit, 40 | enabled: Boolean = true 41 | ) { 42 | Row( 43 | modifier = Modifier 44 | .fillMaxWidth() 45 | .clickable(enabled = enabled) { onCheckedChange(!checked) }, 46 | verticalAlignment = Alignment.CenterVertically 47 | ) { 48 | Spacer( 49 | modifier = Modifier 50 | .width(72.dp) 51 | .height(56.dp) 52 | ) 53 | Text( 54 | text = title, 55 | modifier = Modifier 56 | .weight(1f) 57 | .padding(vertical = 16.dp), 58 | style = MaterialTheme.typography.subtitle2 59 | ) 60 | Box( 61 | modifier = Modifier 62 | .width(72.dp) 63 | .height(56.dp), 64 | contentAlignment = Alignment.Center 65 | ) { 66 | Switch( 67 | enabled = enabled, 68 | checked = checked, 69 | onCheckedChange = { onCheckedChange(!checked) } 70 | ) 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /comp/feature/src/main/java/dev/yankew/sample/feature/FeatureModule.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 WANG Yanke 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.yankew.sample.feature 18 | 19 | import android.content.Context 20 | import androidx.datastore.core.DataStore 21 | import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler 22 | import androidx.datastore.preferences.core.PreferenceDataStoreFactory 23 | import androidx.datastore.preferences.core.Preferences 24 | import androidx.datastore.preferences.core.emptyPreferences 25 | import androidx.datastore.preferences.preferencesDataStoreFile 26 | import dagger.Binds 27 | import dagger.Module 28 | import dagger.Provides 29 | import dagger.hilt.InstallIn 30 | import dagger.hilt.android.qualifiers.ApplicationContext 31 | import dagger.hilt.components.SingletonComponent 32 | import dagger.multibindings.IntoSet 33 | import dev.yankew.sample.domain.event.AppLaunchedEvent 34 | import dev.yankew.sample.domain.event.EventHandler 35 | import dev.yankew.sample.domain.repository.FeatureRepository 36 | import javax.inject.Qualifier 37 | import javax.inject.Singleton 38 | 39 | @Retention(AnnotationRetention.BINARY) 40 | @Qualifier 41 | annotation class FeatureDataStore 42 | 43 | @Module 44 | @InstallIn(SingletonComponent::class) 45 | object FeatureModule { 46 | @Provides 47 | @IntoSet 48 | fun appLaunchedHandler( 49 | handlers: FeatureEventHandlers 50 | ): EventHandler = handlers.appLaunchedHandler 51 | 52 | @Provides 53 | @Singleton 54 | @FeatureDataStore 55 | fun preferenceDataStore(@ApplicationContext context: Context): DataStore = 56 | PreferenceDataStoreFactory.create( 57 | corruptionHandler = ReplaceFileCorruptionHandler { emptyPreferences() } 58 | ) { context.preferencesDataStoreFile("feature") } 59 | } 60 | 61 | @Module 62 | @InstallIn(SingletonComponent::class) 63 | interface FeatureInterfaceModule { 64 | @Binds 65 | fun elapseDataSource(dataSource: ElapseDataSourceImpl): ElapseDataSource 66 | 67 | @Binds 68 | fun featureRepository(repository: FeatureRepositoryImpl): FeatureRepository 69 | } 70 | -------------------------------------------------------------------------------- /ui/ui-mobile/src/main/java/dev/yankew/sample/ui/about/DeveloperScreen.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 WANG Yanke 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.yankew.sample.ui.about 18 | 19 | import androidx.compose.foundation.layout.Column 20 | import androidx.compose.foundation.layout.padding 21 | import androidx.compose.foundation.rememberScrollState 22 | import androidx.compose.foundation.verticalScroll 23 | import androidx.compose.runtime.Composable 24 | import androidx.compose.ui.Modifier 25 | import androidx.compose.ui.res.stringResource 26 | import androidx.hilt.navigation.compose.hiltViewModel 27 | import dev.yankew.sample.compose.collectAsStateWhenStarted 28 | import dev.yankew.sample.strings.R 29 | import dev.yankew.sample.ui.SubScreenScaffold 30 | import dev.yankew.sample.ui.settings.SettingsSwitch 31 | 32 | /** 33 | * Stateful composable of Developer Screen 34 | */ 35 | @Composable 36 | fun DeveloperScreen( 37 | navigateUp: () -> Unit, 38 | ) { 39 | val viewModel = hiltViewModel() 40 | val newFeatureEnabled = viewModel.newFeatureLocalEnabled.collectAsStateWhenStarted(false).value 41 | DeveloperScreen( 42 | navigateUp = navigateUp, 43 | newFeatureEnabled = newFeatureEnabled, 44 | setNewFeatureEnabled = viewModel::setNewFeatureLocalEnabled 45 | ) 46 | } 47 | 48 | /** 49 | * Stateless composable of Developer Screen 50 | */ 51 | @Composable 52 | fun DeveloperScreen( 53 | navigateUp: () -> Unit, 54 | newFeatureEnabled: Boolean, 55 | setNewFeatureEnabled: (Boolean) -> Unit 56 | ) { 57 | SubScreenScaffold( 58 | titleResId = R.string.developer_title, 59 | navigateUp = navigateUp, 60 | ) { innerPaddings -> 61 | Column( 62 | modifier = Modifier 63 | .padding(innerPaddings) 64 | .verticalScroll(state = rememberScrollState()) 65 | ) { 66 | SettingsSwitch( 67 | title = stringResource(R.string.enable_new_feature), 68 | checked = newFeatureEnabled, 69 | onCheckedChange = setNewFeatureEnabled 70 | ) 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /depconstraints/build.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 WANG Yanke 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | plugins { 18 | id("java-platform") 19 | } 20 | 21 | val activity = "1.4.0" 22 | val annotation = "1.3.0" 23 | val compose = Versions.COMPOSE 24 | val coroutines = "1.6.0" 25 | val dataStore = "1.0.0" 26 | val firebaseBom = "29.1.0" 27 | val hilt = Versions.HILT 28 | val hiltJetPack = "1.0.0" 29 | val lifecycle = "2.4.1" 30 | val navigation = "2.4.1" 31 | val serialization = "1.3.2" 32 | val startup = "1.1.1" 33 | val timber = "5.0.1" 34 | val workManager = "2.7.1" 35 | 36 | dependencies { 37 | constraints { 38 | api("${Libs.ACTIVITY_COMPOSE}:$activity") 39 | api("${Libs.ACTIVITY_KTX}:$activity") 40 | api("${Libs.ANNOTATION}:$annotation") 41 | api("${Libs.COMPOSE_ANIMATION}:$compose") 42 | api("${Libs.COMPOSE_MATERIAL}:$compose") 43 | api("${Libs.COMPOSE_RUNTIME}:$compose") 44 | api("${Libs.COMPOSE_THEME_ADAPTER}:$compose") 45 | api("${Libs.COMPOSE_TOOLING}:$compose") 46 | api("${Libs.COMPOSE_UI}:$compose") 47 | api("${Libs.COROUTINES}:$coroutines") 48 | api("${Libs.COROUTINES_PLAY_SERVICE}:$coroutines") 49 | api("${Libs.DATA_STORE_PREFERENCES}:$dataStore") 50 | api("${Libs.FIREBASE_BOM}:$firebaseBom") 51 | api("${Libs.HILT_ANDROID}:$hilt") 52 | api("${Libs.HILT_ANDROID_COMPILER}:$hilt") 53 | api("${Libs.HILT_ANDROIDX_COMPILER}:$hiltJetPack") 54 | api("${Libs.HILT_COMPILER}:$hilt") 55 | api("${Libs.HILT_CORE}:$hilt") 56 | api("${Libs.HILT_NAVIGATION_COMPOSE}:$hiltJetPack") 57 | api("${Libs.HILT_WORK}:$hiltJetPack") 58 | api("${Libs.KOTLIN_STDLIB}:${Versions.KOTLIN}") 59 | api("${Libs.KOTLINX_SERIALIZATION}:$serialization") 60 | api("${Libs.LIFECYCLE_RUNTIME_KTX}:$lifecycle") 61 | api("${Libs.LIFECYCLE_VIEWMODEL_COMPOSE}:$lifecycle") 62 | api("${Libs.LIFECYCLE_VIEWMODEL_KTX}:$lifecycle") 63 | api("${Libs.NAVIGATION_COMPOSE}:$navigation") 64 | api("${Libs.STARTUP}:$startup") 65 | api("${Libs.TIMBER}:$timber") 66 | api("${Libs.WORK_RUNTIME_KTX}:$workManager") 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /ui/ui-mobile/src/main/java/dev/yankew/sample/ui/about/AboutScreen.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 WANG Yanke 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.yankew.sample.ui.about 18 | 19 | import androidx.compose.foundation.layout.Column 20 | import androidx.compose.foundation.layout.Spacer 21 | import androidx.compose.foundation.layout.fillMaxSize 22 | import androidx.compose.foundation.layout.height 23 | import androidx.compose.foundation.layout.padding 24 | import androidx.compose.material.Button 25 | import androidx.compose.material.Text 26 | import androidx.compose.runtime.Composable 27 | import androidx.compose.ui.Alignment 28 | import androidx.compose.ui.Modifier 29 | import androidx.compose.ui.res.stringResource 30 | import androidx.compose.ui.unit.dp 31 | import androidx.hilt.navigation.compose.hiltViewModel 32 | import dev.yankew.sample.strings.R 33 | import dev.yankew.sample.ui.MainScreenScaffold 34 | 35 | /** 36 | * Stateful composable of About Screen 37 | */ 38 | @Composable 39 | fun AboutScreen( 40 | openDeveloper: () -> Unit, 41 | openDrawer: () -> Unit, 42 | ) { 43 | AboutScreen( 44 | hasDeveloper = hiltViewModel().isLocalConfigPresent, 45 | openDeveloper = openDeveloper, 46 | openDrawer = openDrawer 47 | ) 48 | } 49 | 50 | /** 51 | * Stateless composable of About Screen 52 | */ 53 | @Composable 54 | fun AboutScreen( 55 | hasDeveloper: Boolean, 56 | openDeveloper: () -> Unit, 57 | openDrawer: () -> Unit, 58 | ) { 59 | MainScreenScaffold( 60 | titleResId = R.string.about_title, 61 | openDrawer = openDrawer, 62 | ) { innerPaddings -> 63 | Column( 64 | modifier = Modifier 65 | .fillMaxSize() 66 | .padding(innerPaddings) 67 | .padding(16.dp), 68 | horizontalAlignment = Alignment.CenterHorizontally 69 | ) { 70 | Text(stringResource(R.string.about_text)) 71 | if (hasDeveloper) { 72 | Spacer(modifier = Modifier.height(16.dp)) 73 | Button(onClick = openDeveloper) { 74 | Text(stringResource(R.string.button_developer)) 75 | } 76 | } 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /ui/ui-mobile/src/main/java/dev/yankew/sample/ui/Scaffolds.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 WANG Yanke 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.yankew.sample.ui 18 | 19 | import androidx.annotation.StringRes 20 | import androidx.compose.foundation.layout.PaddingValues 21 | import androidx.compose.material.Icon 22 | import androidx.compose.material.IconButton 23 | import androidx.compose.material.Scaffold 24 | import androidx.compose.material.Text 25 | import androidx.compose.material.TopAppBar 26 | import androidx.compose.material.icons.Icons 27 | import androidx.compose.material.icons.filled.ArrowBack 28 | import androidx.compose.material.icons.filled.Menu 29 | import androidx.compose.runtime.Composable 30 | import androidx.compose.ui.res.stringResource 31 | import dev.yankew.sample.strings.R 32 | 33 | @Composable 34 | fun MainScreenScaffold( 35 | @StringRes titleResId: Int, 36 | openDrawer: () -> Unit, 37 | content: @Composable (PaddingValues) -> Unit 38 | ) { 39 | Scaffold( 40 | topBar = { 41 | TopAppBar( 42 | title = { Text(stringResource(titleResId)) }, 43 | navigationIcon = { 44 | IconButton(onClick = { openDrawer() }) { 45 | Icon( 46 | Icons.Default.Menu, 47 | contentDescription = stringResource(R.string.open_menu_desc) 48 | ) 49 | } 50 | } 51 | ) 52 | }, 53 | content = content 54 | ) 55 | } 56 | 57 | @Composable 58 | fun SubScreenScaffold( 59 | @StringRes titleResId: Int, 60 | navigateUp: () -> Unit, 61 | content: @Composable (PaddingValues) -> Unit 62 | ) { 63 | Scaffold( 64 | topBar = { 65 | TopAppBar( 66 | title = { Text(stringResource(titleResId)) }, 67 | navigationIcon = { 68 | IconButton(onClick = navigateUp) { 69 | Icon( 70 | imageVector = Icons.Default.ArrowBack, 71 | contentDescription = stringResource(R.string.go_upward_desc) 72 | ) 73 | } 74 | }, 75 | ) 76 | }, 77 | content = content 78 | ) 79 | } 80 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Android Clean Architecture Sample 2 | [![Check](https://github.com/yacca/android-clean-arch/actions/workflows/Check.yaml/badge.svg)](https://github.com/yacca/android-clean-arch/actions/workflows/Check.yaml) 3 | 4 | A sample project demonstrating clean architecture in an Android app 5 | 6 | ## Tech stack 7 | - [Kotlin](https://kotlinlang.org/) 8 | - [Coroutines](https://kotlinlang.org/docs/coroutines-overview.html) 9 | - [Flow](https://kotlinlang.org/docs/flow.html) 10 | - [Jetpack](https://developer.android.com/jetpack) 11 | - [App Startup](https://developer.android.com/topic/libraries/app-startup) 12 | - [Compose](https://developer.android.com/jetpack/compose) 13 | - [DataStore](https://developer.android.com/topic/libraries/architecture/datastore) 14 | - [Hilt](https://dagger.dev/hilt/) 15 | - [Hilt Android](https://developer.android.com/training/dependency-injection/hilt-android) 16 | - [Lifecycle](https://developer.android.com/topic/libraries/architecture/lifecycle) 17 | - [Navigation Compose](https://developer.android.com/jetpack/compose/navigation) 18 | - [WorkManager](https://developer.android.com/topic/libraries/architecture/workmanager) 19 | - [Firebase](https://firebase.google.com/) 20 | - [Analytics](https://firebase.google.com/products/analytics) 21 | - [Crashlytics](https://firebase.google.com/products/crashlytics) 22 | - [Remote Config](https://firebase.google.com/products/remote-config) 23 | - Others 24 | - [Gradle](https://gradle.org/) 25 | - [Gradle Versions Plugin](https://github.com/ben-manes/gradle-versions-plugin) 26 | - [PlantUML](https://plantuml.com/) 27 | - [Spotless](https://github.com/diffplug/spotless) 28 | - [Timber](https://github.com/JakeWharton/timber) 29 | 30 | ## Modules and their dependencies 31 | 32 | All modules 33 | ![All modules](http://www.plantuml.com/plantuml/proxy?cache=no&src=https://raw.githubusercontent.com/yacca/android-clean-arch/main/doc/all-modules.puml) 34 | 35 | Modules of mobile app 36 | ![Modules of mobile app](http://www.plantuml.com/plantuml/proxy?cache=no&src=https://raw.githubusercontent.com/yacca/android-clean-arch/main/doc/mobile-app-modules.puml) 37 | 38 | Modules of demo mobile app 39 | ![Modules of demo mobile app](http://www.plantuml.com/plantuml/proxy?cache=no&src=https://raw.githubusercontent.com/yacca/android-clean-arch/main/doc/mobile-demo-modules.puml) 40 | 41 | ## License 42 | ``` 43 | Copyright 2022 WANG Yanke 44 | 45 | Licensed under the Apache License, Version 2.0 (the "License"); 46 | you may not use this file except in compliance with the License. 47 | You may obtain a copy of the License at 48 | 49 | https://www.apache.org/licenses/LICENSE-2.0 50 | 51 | Unless required by applicable law or agreed to in writing, software 52 | distributed under the License is distributed on an "AS IS" BASIS, 53 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 54 | See the License for the specific language governing permissions and 55 | limitations under the License. 56 | ``` 57 | -------------------------------------------------------------------------------- /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 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /comp/analytics-debug/src/main/java/dev/yankew/sample/analytics/debug/DebugAnalyticsTracker.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 WANG Yanke 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.yankew.sample.analytics.debug 18 | 19 | import dev.yankew.sample.domain.analytics.AnalyticsTracker 20 | import dev.yankew.sample.domain.settings.AnalyticsOption 21 | import dev.yankew.sample.domain.settings.SettingOption 22 | import kotlinx.coroutines.flow.first 23 | import timber.log.Timber 24 | import javax.inject.Inject 25 | import javax.inject.Provider 26 | 27 | class DebugAnalyticsTracker @Inject constructor( 28 | @AnalyticsOption private val analyticsOption: Provider>, 29 | ) : AnalyticsTracker { 30 | private val userProperties: MutableMap = mutableMapOf() 31 | 32 | override suspend fun isAnalyticsEnabled(): Boolean { 33 | return analyticsOption.get().getValue().first() 34 | } 35 | 36 | override fun logEvent(name: String, params: Map) { 37 | val eventNameString = "Event Name: $name" 38 | val title = " Debug Analytics " 39 | val maxKeyLen = params.keys.maxOfOrNull { it.length } ?: 4 40 | val maxValueLen = params.values.maxOfOrNull { it.toString().length } ?: 4 41 | val dividerLineLen = (maxKeyLen + maxValueLen + 4).coerceAtLeast(eventNameString.length) 42 | val dividerLen = ((dividerLineLen - title.length + 1) / 2).coerceAtLeast(4) 43 | val parameters = params.toString(maxKeyLen) 44 | val maxPropertyKeyLen = userProperties.keys.maxOfOrNull { it.length } ?: 4 45 | val properties = userProperties.toString(maxPropertyKeyLen) 46 | val topDivider = "=".repeat(dividerLen) 47 | val bottomDivider = "-".repeat(dividerLen) 48 | Timber.v( 49 | """ 50 | || 51 | |$topDivider$title$topDivider 52 | |$eventNameString 53 | |Params: 54 | |$parameters 55 | |User Properties: 56 | |$properties 57 | |$bottomDivider$title$bottomDivider 58 | || 59 | """.trimMargin() 60 | ) 61 | } 62 | 63 | override fun setUserProperty(key: String, value: String) { 64 | userProperties[key] = value 65 | } 66 | 67 | private fun Map.toString(maxKeyLen: Int): String { 68 | val keyFormat = "%-${maxKeyLen}s" 69 | return entries.joinToString("\n") { 70 | val keyString = keyFormat.format(it.key) 71 | " $keyString: ${it.value}" 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /comp/settings/src/main/java/dev/yankew/sample/settings/SettingsStore.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 WANG Yanke 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.yankew.sample.settings 18 | 19 | import androidx.datastore.core.DataStore 20 | import androidx.datastore.preferences.core.Preferences 21 | import androidx.datastore.preferences.core.booleanPreferencesKey 22 | import androidx.datastore.preferences.core.edit 23 | import dev.yankew.sample.domain.analytics.SettingsChangedTrackingEvent 24 | import dev.yankew.sample.domain.event.AnalyticsOptionEvent 25 | import dev.yankew.sample.domain.event.CrashlyticsOptionEvent 26 | import dev.yankew.sample.domain.event.EventDispatcher 27 | import dev.yankew.sample.domain.settings.SettingOption 28 | import kotlinx.coroutines.flow.Flow 29 | import kotlinx.coroutines.flow.map 30 | import javax.inject.Inject 31 | 32 | class SettingsStore @Inject constructor( 33 | @SettingsDataStore private val dataStore: DataStore, 34 | analyticsOptionEventDispatcher: EventDispatcher, 35 | crashlyticsOptionEventDispatcher: EventDispatcher, 36 | settingsChangedTrackingEvent: SettingsChangedTrackingEvent, 37 | ) { 38 | val analyticsOption = getBooleanOption(KEY_ANALYTICS, true) { 39 | // Only 'enabled event' can be logged 40 | settingsChangedTrackingEvent.logAnalyticsChanged(it) 41 | analyticsOptionEventDispatcher.dispatch(AnalyticsOptionEvent(it)) 42 | } 43 | val crashlyticsOption = getBooleanOption(KEY_CRASHLYTICS, true) { 44 | settingsChangedTrackingEvent.logCrashlyticsChanged(it) 45 | crashlyticsOptionEventDispatcher.dispatch(CrashlyticsOptionEvent(it)) 46 | } 47 | val notificationOption = getBooleanOption(KEY_NOTIFICATION, true) { 48 | settingsChangedTrackingEvent.logNotificationChanged(it) 49 | } 50 | 51 | private fun getBooleanOption( 52 | key: String, 53 | default: Boolean, 54 | dispatch: suspend (Boolean) -> Unit = {} 55 | ) = object : SettingOption { 56 | override fun getValue(): Flow { 57 | return dataStore.data.map { it[booleanPreferencesKey(key)] ?: default } 58 | } 59 | 60 | override suspend fun setValue(value: Boolean) { 61 | dataStore.edit { it[booleanPreferencesKey(key)] = value } 62 | dispatch(value) 63 | } 64 | } 65 | 66 | private companion object { 67 | const val KEY_NOTIFICATION = "notification" 68 | const val KEY_ANALYTICS = "analytics" 69 | const val KEY_CRASHLYTICS = "crashlytics" 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /lib/compose/src/main/java/dev/yankew/sample/compose/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 WANG Yanke 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.yankew.sample.compose.theme 18 | 19 | import androidx.compose.foundation.isSystemInDarkTheme 20 | import androidx.compose.material.MaterialTheme 21 | import androidx.compose.material.Surface 22 | import androidx.compose.material.darkColors 23 | import androidx.compose.material.lightColors 24 | import androidx.compose.runtime.Composable 25 | import androidx.compose.ui.graphics.Color 26 | 27 | private val LightThemeColors = lightColors( 28 | primary = Blue700, 29 | primaryVariant = Blue900, 30 | onPrimary = Color.White, 31 | secondary = Blue700, 32 | secondaryVariant = Blue900, 33 | onSecondary = Color.White, 34 | error = Blue800, 35 | onBackground = Color.Black, 36 | 37 | ) 38 | 39 | private val DarkThemeColors = darkColors( 40 | primary = Blue300, 41 | primaryVariant = Blue700, 42 | onPrimary = Color.Black, 43 | secondary = Blue300, 44 | onSecondary = Color.Black, 45 | error = Blue200, 46 | onBackground = Color.White 47 | ) 48 | 49 | private val DemoLightThemeColors = lightColors( 50 | primary = Amber700, 51 | primaryVariant = Amber900, 52 | onPrimary = Color.White, 53 | secondary = Amber700, 54 | secondaryVariant = Amber900, 55 | onSecondary = Color.White, 56 | error = Amber800, 57 | onBackground = Color.Black, 58 | 59 | ) 60 | 61 | private val DemoDarkThemeColors = darkColors( 62 | primary = Amber300, 63 | primaryVariant = Amber700, 64 | onPrimary = Color.Black, 65 | secondary = Amber300, 66 | onSecondary = Color.Black, 67 | error = Amber200, 68 | onBackground = Color.White 69 | ) 70 | 71 | @Composable 72 | fun MyTheme( 73 | darkTheme: Boolean = isSystemInDarkTheme(), 74 | isDemo: Boolean = false, 75 | content: @Composable () -> Unit 76 | ) { 77 | val colors = if (!isDemo) { 78 | if (darkTheme) DarkThemeColors else LightThemeColors 79 | } else { 80 | if (darkTheme) DemoDarkThemeColors else DemoLightThemeColors 81 | } 82 | MaterialTheme( 83 | colors = colors, 84 | typography = typography, 85 | shapes = shapes, 86 | content = content 87 | ) 88 | } 89 | 90 | @Composable 91 | fun PreviewTheme( 92 | darkTheme: Boolean = isSystemInDarkTheme(), 93 | content: @Composable () -> Unit 94 | ) { 95 | MyTheme(darkTheme = darkTheme) { 96 | Surface(color = MaterialTheme.colors.background) { 97 | content() 98 | } 99 | } 100 | } 101 | --------------------------------------------------------------------------------