├── .gitignore
├── README.md
├── app
├── .gitignore
├── build.gradle
├── proguard-rules.pro
└── src
│ ├── androidTest
│ └── java
│ │ └── dk
│ │ └── nodes
│ │ └── template
│ │ └── ExampleInstrumentedTest.kt
│ ├── debug
│ └── res
│ │ └── xml
│ │ └── network_security_config.xml
│ ├── main
│ ├── AndroidManifest.xml
│ ├── java
│ │ └── dk
│ │ │ └── nodes
│ │ │ └── template
│ │ │ ├── App.kt
│ │ │ ├── inititializers
│ │ │ └── AppInitializer.kt
│ │ │ └── injection
│ │ │ ├── components
│ │ │ └── AppComponent.kt
│ │ │ └── modules
│ │ │ ├── AppModule.kt
│ │ │ ├── InteractorModule.kt
│ │ │ ├── OAuthModule.kt
│ │ │ ├── RestModule.kt
│ │ │ ├── RestRepositoryBinding.kt
│ │ │ └── StorageBindingModule.kt
│ └── res
│ │ └── values
│ │ └── nstack_keys.xml
│ └── test
│ └── java
│ └── dk
│ └── nodes
│ └── template
│ └── ExampleUnitTest.kt
├── build.gradle
├── data
├── .gitignore
├── build.gradle
└── src
│ └── main
│ ├── AndroidManifest.xml
│ └── java
│ └── dk
│ └── nodes
│ └── template
│ └── data
│ ├── network
│ ├── Api.kt
│ ├── RestPostRepository.kt
│ ├── oauth
│ │ ├── OAuthCallbackImpl.kt
│ │ └── OAuthPreferencesRepository.kt
│ └── util
│ │ ├── BufferedSourceConverterFactory.kt
│ │ ├── DateDeserializer.kt
│ │ └── ItemTypeAdapterFactory.kt
│ └── storage
│ └── PrefManagerImpl.kt
├── docs
├── HOWWEWORK.md
└── images
│ └── arch.png
├── domain
├── .gitignore
├── build.gradle
└── src
│ └── main
│ ├── AndroidManifest.xml
│ └── java
│ └── dk
│ └── nodes
│ └── template
│ └── domain
│ ├── entities
│ ├── Post.kt
│ └── Theme.kt
│ ├── extensions
│ └── Extensions.kt
│ ├── interactors
│ ├── Interactor.kt
│ ├── InteractorExtensions.kt
│ ├── InteractorResult.kt
│ ├── PostFlowInteractor.kt
│ ├── PostsInteractor.kt
│ └── SwitchThemeInteractor.kt
│ ├── managers
│ ├── PrefManager.kt
│ ├── ThemeManager.kt
│ └── ThemeManagerImpl.kt
│ └── repositories
│ ├── PostRepository.kt
│ └── RepositoryException.kt
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── presentation
├── .gitignore
├── build.gradle
├── proguard-rules.pro
└── src
│ ├── androidTest
│ └── java
│ │ └── dk
│ │ └── nodes
│ │ └── template
│ │ └── presentation
│ │ └── ExampleInstrumentedTest.java
│ ├── main
│ ├── AndroidManifest.xml
│ ├── assets
│ │ ├── translations_0_da-DK.json
│ │ ├── translations_1_en-GB.json
│ │ ├── translations_2_es-MX.json
│ │ └── translations_3_fr-FR.json
│ ├── java
│ │ └── dk
│ │ │ └── nodes
│ │ │ └── template
│ │ │ └── presentation
│ │ │ ├── extensions
│ │ │ ├── LiveDataExtensions.kt
│ │ │ └── ViewModelStoreOwnerExtensions.kt
│ │ │ ├── injection
│ │ │ ├── DaggerViewModelFactory.kt
│ │ │ ├── PresentationModule.kt
│ │ │ ├── ViewModelBuilder.kt
│ │ │ └── ViewModelKey.kt
│ │ │ ├── nstack
│ │ │ ├── NStackPresenter.kt
│ │ │ ├── RateReminderActions.kt
│ │ │ └── Translation.java
│ │ │ ├── ui
│ │ │ ├── base
│ │ │ │ ├── BaseActivity.kt
│ │ │ │ ├── BaseFragment.kt
│ │ │ │ └── BaseViewModel.kt
│ │ │ ├── main
│ │ │ │ ├── MainActivity+NStack.kt
│ │ │ │ ├── MainActivity.kt
│ │ │ │ ├── MainActivityBuilder.kt
│ │ │ │ ├── MainActivityViewModel.kt
│ │ │ │ └── MainActivityViewState.kt
│ │ │ ├── sample
│ │ │ │ ├── SampleAdapter.kt
│ │ │ │ ├── SampleBuilder.kt
│ │ │ │ ├── SampleFragment.kt
│ │ │ │ ├── SampleViewModel.kt
│ │ │ │ └── SampleViewState.kt
│ │ │ └── splash
│ │ │ │ ├── SplashActivity.kt
│ │ │ │ ├── SplashBuilder.kt
│ │ │ │ ├── SplashFragment.kt
│ │ │ │ ├── SplashViewModel.kt
│ │ │ │ └── SplashViewState.kt
│ │ │ └── util
│ │ │ ├── SharedElementHelper.kt
│ │ │ ├── SingleEvent.kt
│ │ │ ├── ThemeHelper.kt
│ │ │ ├── ViewError.kt
│ │ │ └── ViewErrorController.kt
│ └── res
│ │ ├── anim
│ │ ├── slide_left_enter.xml
│ │ ├── slide_left_exit.xml
│ │ ├── slide_right_enter.xml
│ │ └── slide_right_exit.xml
│ │ ├── color
│ │ └── selector_primary.xml
│ │ ├── drawable-v24
│ │ └── ic_launcher_foreground.xml
│ │ ├── drawable
│ │ └── ic_half_moon_and_sun.xml
│ │ ├── font
│ │ └── qwigley.ttf
│ │ ├── layout
│ │ ├── activity_main.xml
│ │ ├── activity_splash.xml
│ │ ├── fragment_sample.xml
│ │ ├── fragment_splash.xml
│ │ ├── item_sample_list.xml
│ │ └── row_sample.xml
│ │ ├── menu
│ │ └── menu_main.xml
│ │ ├── mipmap-anydpi-v26
│ │ ├── ic_launcher.xml
│ │ └── ic_launcher_round.xml
│ │ ├── mipmap-hdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-mdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxxhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── navigation
│ │ ├── main_nav_graph.xml
│ │ └── splash_nav_graph.xml
│ │ ├── values-night-v27
│ │ └── themes.xml
│ │ ├── values-night
│ │ └── themes.xml
│ │ ├── values-v27
│ │ └── themes.xml
│ │ └── values
│ │ ├── attrs.xml
│ │ ├── colors.xml
│ │ ├── dimens.xml
│ │ ├── ic_launcher_background.xml
│ │ ├── nstack_keys.xml
│ │ ├── shape.xml
│ │ ├── strings.xml
│ │ ├── styles.xml
│ │ ├── themes.xml
│ │ └── type.xml
│ └── test
│ └── java
│ └── dk
│ └── nodes
│ └── template
│ └── presentation
│ └── ExampleUnitTest.java
└── settings.gradle
/.gitignore:
--------------------------------------------------------------------------------
1 | # Built application files
2 | *.apk
3 | *.ap_
4 |
5 | # Files for the ART/Dalvik VM
6 | *.dex
7 |
8 | # Java class files
9 | *.class
10 |
11 | # Generated files
12 | bin/
13 | gen/
14 | out/
15 |
16 | # Gradle files
17 | .gradle/
18 | build/
19 |
20 | # Local configuration file (sdk path, etc)
21 | local.properties
22 |
23 | # Log Files
24 | *.log
25 |
26 | # Android Studio Navigation editor temp files
27 | .navigation/
28 |
29 | # Android Studio captures folder
30 | captures/
31 |
32 | # Intellij
33 | *.iml
34 | .idea/
35 | projectFilesBackup/
36 |
37 | # External native build folder generated in Android Studio 2.2 and later
38 | .externalNativeBuild
39 |
40 | # Windows thumbnail db
41 | Thumbs.db
42 |
43 | # OSX files
44 | .DS_Store
45 |
46 | #NDK
47 | obj/
48 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## 📖 Project description
2 |
3 |
4 | ## 🔧 Installation
5 | 1. `git clone `
6 | 2. Run Gradle Sync
7 | 3. Rebuild to generate Dagger classes
8 |
9 | ## 🔗 Useful links
10 | * [Jira board]()
11 |
12 | ## 🌲 Branches
13 | * `master` - Latest version on Google Play.
14 | * `develop` - Updated work. Feature branches are merged in when complete and then deleted.
15 |
16 | ## ⚠️ Things to know
17 |
18 |
19 | ## 💻 Developers
20 | - [Name](https://github.com/) (@slackname)
21 |
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 | apply plugin: 'kotlin-android'
3 | apply plugin: 'kotlin-android-extensions'
4 | apply plugin: 'com.github.gfx.ribbonizer'
5 | apply plugin: 'kotlin-kapt'
6 |
7 | android {
8 | compileSdkVersion sdks.compileSdkVersion
9 | buildToolsVersion sdks.buildToolsVersion
10 | flavorDimensions "default"
11 |
12 | defaultConfig {
13 | applicationId "dk.nodes.template"
14 | minSdkVersion sdks.minSdkVersion
15 | targetSdkVersion sdks.targetSdkVersion
16 | multiDexEnabled true
17 | versionCode 1
18 | versionName "2.0.3"
19 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
20 |
21 | manifestPlaceholders = [
22 | appId : keys.appId,
23 | apiKey: keys.apiKey
24 | ]
25 |
26 | packagingOptions {
27 | pickFirst("META-INF/atomicfu.kotlin_module")
28 | }
29 | }
30 |
31 | // Uncomment the following if you include signingConfigs
32 | /*
33 | signingConfigs {
34 | staging {
35 | keyAlias 'androiddebugkey'
36 | keyPassword 'android'
37 | storeFile file('debug.keystore')
38 | storePassword 'android'
39 | }
40 | production {
41 | keyAlias 'androiddebugkey'
42 | keyPassword 'android'
43 | storeFile file('debug.keystore')
44 | storePassword 'android'
45 | }
46 | }
47 | */
48 |
49 | buildTypes {
50 | debug {
51 | // uncomment the following line if you add a flavorbased signingConfig for debug builds
52 | // signingConfig null
53 | }
54 | release {
55 | buildConfigField "String", "API_URL", "\"https://jsonplaceholder.typicode.com\""
56 | minifyEnabled false
57 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
58 | }
59 | }
60 |
61 | productFlavors {
62 | // The dev flavor is intended to be the same as staging, but with LeakCanary enabled
63 | dev {
64 | dimension "default"
65 | applicationIdSuffix ".dev"
66 | manifestPlaceholders = [APP_NAME: "KotlinTemplateDev"]
67 | buildConfigField "String", "API_URL", "\"https://jsonplaceholder.typicode.com\""
68 | }
69 | staging {
70 | dimension "default"
71 | applicationIdSuffix ".staging"
72 | //signingConfig signingConfigs.staging
73 | manifestPlaceholders = [APP_NAME: "KotlinTemplateStaging"]
74 | buildConfigField "String", "API_URL", "\"https://jsonplaceholder.typicode.com\""
75 | }
76 | production {
77 | dimension "default"
78 | applicationIdSuffix ".production"
79 | //signingConfig signingConfigs.production
80 | manifestPlaceholders = [APP_NAME: "KotlinTemplate"]
81 | buildConfigField "String", "API_URL", "\"https://jsonplaceholder.typicode.com\""
82 | }
83 | }
84 |
85 | compileOptions {
86 | coreLibraryDesugaringEnabled true
87 | sourceCompatibility JavaVersion.VERSION_1_8
88 | targetCompatibility JavaVersion.VERSION_1_8
89 | }
90 | kotlinOptions {
91 | jvmTarget = "1.8"
92 | }
93 | }
94 |
95 | repositories {
96 | maven { url "https://jitpack.io" }
97 | mavenLocal()
98 | }
99 |
100 | dependencies {
101 |
102 | fileTree(dir: "libs", include: ["*.jar"])
103 | androidTestImplementation("androidx.test.espresso:espresso-core:${versions.espresso}", {
104 | exclude group: "com.android.support", module: "support-annotations"
105 | })
106 | implementation project (':domain')
107 | implementation project (':presentation')
108 | implementation project (':data')
109 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:${versions.kotlin}"
110 | implementation "org.jetbrains.kotlin:kotlin-reflect:${versions.kotlin}"
111 | implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:${versions.coroutines}"
112 | implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:${versions.coroutines}"
113 |
114 | implementation "androidx.appcompat:appcompat:$versions.appcompat"
115 | implementation 'androidx.recyclerview:recyclerview:1.1.0'
116 | implementation "com.google.android.material:material:$versions.material"
117 | implementation 'androidx.cardview:cardview:1.0.0'
118 |
119 | implementation "com.google.dagger:dagger-android:${versions.dagger}"
120 | implementation "com.google.dagger:dagger-android-support:${versions.dagger}"
121 | kapt "com.google.dagger:dagger-compiler:${versions.dagger}"
122 | kapt "com.google.dagger:dagger-android-processor:${versions.dagger}"
123 |
124 | implementation "com.jakewharton.timber:timber:${versions.timber}"
125 |
126 | implementation("com.squareup.retrofit2:retrofit:${versions.retrofit}")
127 | implementation("com.squareup.retrofit2:converter-gson:${versions.retrofit}") {
128 | exclude module: "retrofit:${versions.retrofit}"
129 | }
130 |
131 | // Nodes OkHTTP utilities
132 | implementation "dk.nodes.utils:okhttp:${versions.nodes_utils_okhttp}"
133 |
134 | implementation "com.squareup.okhttp3:okhttp:${versions.okhttp}"
135 | implementation("com.squareup.okhttp3:logging-interceptor:${versions.okhttp}")
136 |
137 | implementation "dk.nodes.nstack:nstack-kotlin:${versions.nstack}"
138 | // implementation "dk.nodes.arch:base:${versions.nodes_arch}"
139 |
140 | // Testing
141 | testImplementation "junit:junit:${versions.junit}"
142 | // required if you want to use Mockito for unit tests
143 | testImplementation "org.mockito:mockito-core:${versions.mockito}"
144 | // required if you want to use Mockito for Android tests
145 | androidTestImplementation "org.mockito:mockito-android:${versions.mockito}"
146 |
147 | // For Espresso UI testing
148 | androidTestImplementation "androidx.test.espresso:espresso-core:${versions.espresso}"
149 | androidTestImplementation "androidx.test.espresso:espresso-intents:${versions.espresso}"
150 | androidTestImplementation "androidx.test.espresso:espresso-contrib:${versions.espresso}"
151 |
152 | // The following section is only used to force the latest version to resolve conflicts
153 | implementation "androidx.arch.core:core-common:$versions.archCore"
154 | implementation "androidx.arch.core:core-runtime:$versions.archCore"
155 | implementation "androidx.lifecycle:lifecycle-livedata-core:${versions.lifecycle}"
156 | implementation "androidx.lifecycle:lifecycle-runtime:${versions.lifecycle}"
157 | implementation "androidx.lifecycle:lifecycle-common-java8:${versions.lifecycle}"
158 | implementation "androidx.lifecycle:lifecycle-extensions:${versions.lifecycle}"
159 | implementation "androidx.lifecycle:lifecycle-viewmodel:${versions.lifecycle}"
160 | implementation "androidx.lifecycle:lifecycle-livedata:${versions.lifecycle}"
161 | implementation "androidx.core:core-ktx:${versions.ktx}"
162 |
163 | implementation "androidx.preference:preference-ktx:${versions.preferences}"
164 |
165 | // Chucker (like Charles but on device)
166 | devImplementation "com.github.ChuckerTeam.Chucker:library-no-op:${versions.chucker}"
167 | stagingImplementation "com.github.ChuckerTeam.Chucker:library:${versions.chucker}"
168 | productionImplementation "com.github.ChuckerTeam.Chucker:library-no-op:${versions.chucker}"
169 |
170 | // LeakCanary
171 | devImplementation "com.squareup.leakcanary:leakcanary-android:${versions.leak_canary}"
172 |
173 | // Java 8+ API desugaring support
174 | coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:$versions.desugaring"
175 |
176 | }
177 |
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # By default, the flags in this file are appended to flags specified
3 | # in D:\programs\Android\sdk/tools/proguard/proguard-android.txt
4 | # You can edit the include path and order by changing the proguardFiles
5 | # directive in build.gradle.
6 | #
7 | # For more details, see
8 | # http://developer.android.com/guide/developing/tools/proguard.html
9 |
10 | # Add any project specific keep options here:
11 |
12 | # If your project uses WebView with JS, uncomment the following
13 | # and specify the fully qualified class name to the JavaScript interface
14 | # class:
15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
16 | # public *;
17 | #}
18 |
19 | # Uncomment this to preserve the line number information for
20 | # debugging stack traces.
21 | #-keepattributes SourceFile,LineNumberTable
22 |
23 | # If you keep the line number information, uncomment this to
24 | # hide the original source file name.
25 | #-renamesourcefileattribute SourceFile
26 |
27 | -keep class androidx.core.app.CoreComponentFactory { *; }
28 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/dk/nodes/template/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package dk.nodes.template
2 |
3 | import androidx.test.InstrumentationRegistry
4 | import androidx.test.runner.AndroidJUnit4
5 |
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | import org.junit.Assert.assertEquals
10 |
11 | /**
12 | * Instrumentation test, which will execute on an Android device.
13 | *
14 | * @see [Testing documentation](http://d.android.com/tools/testing)
15 | */
16 | @RunWith(AndroidJUnit4::class)
17 | class ExampleInstrumentedTest {
18 | @Test
19 | @Throws(Exception::class)
20 | fun useAppContext() {
21 | // Context of the app under test.
22 | val appContext = InstrumentationRegistry.getTargetContext()
23 | assertEquals("dk.bison.wt", appContext.packageName)
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/app/src/debug/res/xml/network_security_config.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
10 |
11 |
19 |
20 |
21 |
25 |
29 |
30 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
47 |
48 |
--------------------------------------------------------------------------------
/app/src/main/java/dk/nodes/template/App.kt:
--------------------------------------------------------------------------------
1 | package dk.nodes.template
2 |
3 | import dagger.android.AndroidInjector
4 | import dagger.android.DaggerApplication
5 | import dk.nodes.template.inititializers.AppInitializer
6 | import dk.nodes.template.injection.components.DaggerAppComponent
7 | import javax.inject.Inject
8 |
9 | class App : DaggerApplication() {
10 |
11 | @Inject lateinit var initializer: AppInitializer
12 | override fun onCreate() {
13 | super.onCreate()
14 | initializer.init(this)
15 | }
16 |
17 | override fun applicationInjector(): AndroidInjector {
18 | return DaggerAppComponent.factory().create(this)
19 | }
20 | }
--------------------------------------------------------------------------------
/app/src/main/java/dk/nodes/template/inititializers/AppInitializer.kt:
--------------------------------------------------------------------------------
1 | package dk.nodes.template.inititializers
2 |
3 | import android.app.Application
4 | import android.util.Log
5 | import com.chuckerteam.chucker.api.ChuckerCollector
6 | import dk.nodes.nstack.kotlin.NStack
7 | import dk.nodes.template.BuildConfig
8 | import dk.nodes.template.domain.managers.ThemeManager
9 | import dk.nodes.template.presentation.nstack.Translation
10 | import dk.nodes.template.presentation.util.ThemeHelper
11 | import timber.log.Timber
12 | import javax.inject.Inject
13 |
14 | interface AppInitializer {
15 | fun init(app: Application)
16 | }
17 |
18 | class AppInitializerImpl @Inject constructor(
19 | private val themeManager: ThemeManager,
20 | private val chuckerCollector: ChuckerCollector
21 | ) : AppInitializer {
22 | override fun init(app: Application) {
23 | NStack.translationClass = Translation::class.java
24 | NStack.init(app, BuildConfig.DEBUG)
25 | if (BuildConfig.DEBUG) {
26 | NStack.enableLiveEdit(app)
27 | Timber.plant(Timber.DebugTree(), chuckerTree())
28 | }
29 | ThemeHelper.applyTheme(themeManager.theme)
30 | }
31 |
32 | private fun chuckerTree() = object : Timber.Tree() {
33 | override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
34 | if (priority >= Log.ERROR) {
35 | chuckerCollector.onError(
36 | tag ?: BuildConfig.APPLICATION_ID,
37 | t ?: Throwable("Unknown Error")
38 | )
39 | }
40 | }
41 | }
42 |
43 | }
--------------------------------------------------------------------------------
/app/src/main/java/dk/nodes/template/injection/components/AppComponent.kt:
--------------------------------------------------------------------------------
1 | package dk.nodes.template.injection.components
2 |
3 | import dagger.Component
4 | import dagger.android.AndroidInjector
5 | import dagger.android.support.AndroidSupportInjectionModule
6 | import dk.nodes.template.App
7 | import dk.nodes.template.injection.modules.*
8 | import dk.nodes.template.presentation.injection.PresentationModule
9 | import dk.nodes.template.presentation.injection.ViewModelBuilder
10 | import javax.inject.Singleton
11 |
12 | @Component(
13 | modules = [
14 | AndroidSupportInjectionModule::class,
15 | ViewModelBuilder::class,
16 | PresentationModule::class,
17 | AppModule::class,
18 | InteractorModule::class,
19 | RestModule::class,
20 | RestRepositoryBinding::class,
21 | StorageBindingModule::class,
22 | OAuthModule::class
23 | ]
24 | )
25 | @Singleton
26 | interface AppComponent : AndroidInjector {
27 | @Component.Factory
28 | abstract class Factory : AndroidInjector.Factory
29 | }
--------------------------------------------------------------------------------
/app/src/main/java/dk/nodes/template/injection/modules/AppModule.kt:
--------------------------------------------------------------------------------
1 | package dk.nodes.template.injection.modules
2 |
3 | import android.content.Context
4 | import dagger.Binds
5 | import dagger.Module
6 | import dagger.Provides
7 | import dk.nodes.template.App
8 | import dk.nodes.template.inititializers.AppInitializer
9 | import dk.nodes.template.inititializers.AppInitializerImpl
10 |
11 | @Module
12 | abstract class AppModule {
13 |
14 | @Binds
15 | abstract fun bindAppInitalizer(initializer: AppInitializerImpl): AppInitializer
16 |
17 | @Module
18 | companion object {
19 | @JvmStatic
20 | @Provides
21 | fun provideContext(application: App): Context = application.applicationContext
22 | }
23 | }
--------------------------------------------------------------------------------
/app/src/main/java/dk/nodes/template/injection/modules/InteractorModule.kt:
--------------------------------------------------------------------------------
1 | package dk.nodes.template.injection.modules
2 |
3 | import dagger.Module
4 |
5 | @Module
6 | class InteractorModule
--------------------------------------------------------------------------------
/app/src/main/java/dk/nodes/template/injection/modules/OAuthModule.kt:
--------------------------------------------------------------------------------
1 | package dk.nodes.template.injection.modules
2 |
3 | import dagger.Binds
4 | import dagger.Module
5 | import dagger.Provides
6 | import dk.nodes.okhttputils.oauth.OAuthAuthenticator
7 | import dk.nodes.okhttputils.oauth.OAuthCallback
8 | import dk.nodes.okhttputils.oauth.OAuthInterceptor
9 | import dk.nodes.okhttputils.oauth.OAuthRepository
10 | import dk.nodes.okhttputils.oauth.entities.OAuthHeader
11 | import dk.nodes.template.data.network.oauth.OAuthCallbackImpl
12 | import dk.nodes.template.data.network.oauth.OAuthPreferencesRepository
13 | import javax.inject.Singleton
14 |
15 | @Module(includes = [OAuthModule.BindingModule::class])
16 | class OAuthModule {
17 |
18 | @Module
19 | interface BindingModule {
20 |
21 | @Binds
22 | @Singleton
23 | fun bindOAuthRepository(repository: OAuthPreferencesRepository): OAuthRepository
24 |
25 | @Binds
26 | @Singleton
27 | fun bindOAuthCallback(oAuthCallback: OAuthCallbackImpl): OAuthCallback
28 | }
29 |
30 | @Provides
31 | @Singleton
32 | fun provideOAuthHeader(): OAuthHeader {
33 | // modify header type & name here
34 | return OAuthHeader()
35 | }
36 |
37 | @Provides
38 | @Singleton
39 | fun provideOAuthInterceptor(repository: OAuthRepository, oAuthHeader: OAuthHeader): OAuthInterceptor {
40 | return OAuthInterceptor(repository, oAuthHeader)
41 | }
42 |
43 | @Provides
44 | @Singleton
45 | fun provideOAuthAuthenticator(
46 | repository: OAuthRepository,
47 | oAuthHeader: OAuthHeader,
48 | oAuthCallback: OAuthCallback
49 | ): OAuthAuthenticator {
50 | return OAuthAuthenticator(repository, oAuthCallback, oAuthHeader)
51 | }
52 | }
--------------------------------------------------------------------------------
/app/src/main/java/dk/nodes/template/injection/modules/RestModule.kt:
--------------------------------------------------------------------------------
1 | package dk.nodes.template.injection.modules
2 |
3 | import android.content.Context
4 | import android.os.Build
5 | import com.chuckerteam.chucker.api.ChuckerCollector
6 | import com.chuckerteam.chucker.api.ChuckerInterceptor
7 | import com.chuckerteam.chucker.api.RetentionManager
8 | import com.google.gson.Gson
9 | import com.google.gson.GsonBuilder
10 | import dagger.Module
11 | import dagger.Provides
12 | import dk.nodes.nstack.kotlin.NStack
13 | import dk.nodes.nstack.kotlin.provider.NMetaInterceptor
14 | import dk.nodes.okhttputils.oauth.OAuthAuthenticator
15 | import dk.nodes.okhttputils.oauth.OAuthInterceptor
16 | import dk.nodes.template.BuildConfig
17 | import dk.nodes.template.data.network.Api
18 | import dk.nodes.template.data.network.util.BufferedSourceConverterFactory
19 | import dk.nodes.template.data.network.util.DateDeserializer
20 | import dk.nodes.template.data.network.util.ItemTypeAdapterFactory
21 | import okhttp3.OkHttpClient
22 | import retrofit2.Converter
23 | import retrofit2.Retrofit
24 | import retrofit2.converter.gson.GsonConverterFactory
25 | import java.util.Date
26 | import java.util.concurrent.TimeUnit
27 | import javax.inject.Named
28 | import javax.inject.Singleton
29 |
30 | @Module
31 | class RestModule {
32 | @Provides
33 | fun provideTypeFactory(): ItemTypeAdapterFactory {
34 | return ItemTypeAdapterFactory()
35 | }
36 |
37 | @Provides
38 | fun provideDateDeserializer(): DateDeserializer {
39 | return DateDeserializer()
40 | }
41 |
42 | @Provides
43 | @Singleton
44 | fun provideGson(typeFactory: ItemTypeAdapterFactory, dateDeserializer: DateDeserializer): Gson {
45 | return GsonBuilder()
46 | .registerTypeAdapterFactory(typeFactory)
47 | .registerTypeAdapter(Date::class.java, dateDeserializer)
48 | .setDateFormat(DateDeserializer.DATE_FORMATS[0])
49 | .create()
50 | }
51 |
52 | @Provides
53 | @Named("NAME_BASE_URL")
54 | fun provideBaseUrlString(): String {
55 | return BuildConfig.API_URL
56 | }
57 |
58 | @Provides
59 | @Singleton
60 | fun provideGsonConverter(gson: Gson): Converter.Factory {
61 | return GsonConverterFactory.create(gson)
62 | }
63 |
64 | @Provides
65 | @Singleton
66 | fun provideChuckerCollector(context: Context) = ChuckerCollector(
67 | context = context,
68 | // Toggles visibility of the push notification
69 | showNotification = true,
70 | // Allows to customize the retention period of collected data
71 | retentionPeriod = RetentionManager.Period.ONE_HOUR
72 | )
73 |
74 | @Provides
75 | @Singleton
76 | fun provideChuckerInterceptor(
77 | context: Context,
78 | chuckerCollector: ChuckerCollector
79 | ) = ChuckerInterceptor(
80 | context = context,
81 | // The previously created Collector
82 | collector = chuckerCollector,
83 | // The max body content length in bytes, after this responses will be truncated.
84 | maxContentLength = 250000L
85 | // List of headers to replace with ** in the Chucker UI
86 | // headersToRedact = setOf("Auth-Token")
87 | )
88 |
89 | @Provides
90 | @Singleton
91 | fun provideHttpClient(chuckerInterceptor: ChuckerInterceptor): OkHttpClient {
92 | val clientBuilder = OkHttpClient.Builder()
93 | .connectTimeout(45, TimeUnit.SECONDS)
94 | .readTimeout(60, TimeUnit.SECONDS)
95 | .writeTimeout(60, TimeUnit.SECONDS)
96 | .addInterceptor(chuckerInterceptor)
97 | .addInterceptor(
98 | NMetaInterceptor(
99 | NStack.env,
100 | NStack.appClientInfo.versionName,
101 | Build.VERSION.RELEASE,
102 | Build.MODEL
103 | )
104 | )
105 |
106 | if (BuildConfig.DEBUG) {
107 | val logging = okhttp3.logging.HttpLoggingInterceptor()
108 | logging.level = okhttp3.logging.HttpLoggingInterceptor.Level.BODY
109 | clientBuilder.addInterceptor(logging)
110 | }
111 |
112 | return clientBuilder.build()
113 | }
114 |
115 | @Provides
116 | @Singleton
117 | fun provideRetrofit(
118 | client: OkHttpClient,
119 | converter: Converter.Factory,
120 | @Named("NAME_BASE_URL") baseUrl: String
121 | ): Retrofit {
122 | return Retrofit.Builder()
123 | .client(client)
124 | .baseUrl(baseUrl)
125 | .addConverterFactory(BufferedSourceConverterFactory())
126 | .addConverterFactory(converter)
127 | .build()
128 | }
129 |
130 | @Provides
131 | @Singleton
132 | fun provideApi(retrofit: Retrofit): Api {
133 | return retrofit.create(Api::class.java)
134 | }
135 | }
136 |
--------------------------------------------------------------------------------
/app/src/main/java/dk/nodes/template/injection/modules/RestRepositoryBinding.kt:
--------------------------------------------------------------------------------
1 | package dk.nodes.template.injection.modules
2 |
3 | import dagger.Binds
4 | import dagger.Module
5 | import dk.nodes.template.data.network.RestPostRepository
6 | import dk.nodes.template.domain.repositories.PostRepository
7 | import javax.inject.Singleton
8 |
9 | @Module
10 | abstract class RestRepositoryBinding {
11 | @Binds
12 | @Singleton
13 | abstract fun bindPostRepository(repository: RestPostRepository): PostRepository
14 | }
--------------------------------------------------------------------------------
/app/src/main/java/dk/nodes/template/injection/modules/StorageBindingModule.kt:
--------------------------------------------------------------------------------
1 | package dk.nodes.template.injection.modules
2 |
3 | import dagger.Binds
4 | import dagger.Module
5 | import dk.nodes.template.domain.managers.PrefManager
6 | import dk.nodes.template.domain.managers.ThemeManager
7 | import dk.nodes.template.domain.managers.ThemeManagerImpl
8 | import dk.nodes.template.data.storage.PrefManagerImpl
9 | import javax.inject.Singleton
10 |
11 | @Module
12 | abstract class StorageBindingModule {
13 |
14 | @Binds
15 | @Singleton
16 | abstract fun bindPrefManager(manager: PrefManagerImpl): PrefManager
17 |
18 | @Binds
19 | @Singleton
20 | abstract fun bindThemeManager(manager: ThemeManagerImpl): ThemeManager
21 | }
--------------------------------------------------------------------------------
/app/src/main/res/values/nstack_keys.xml:
--------------------------------------------------------------------------------
1 |
2 | {default_ok}
3 | {default_cancel}
4 | {default_no}
5 | {default_yes}
6 | {default_retry}
7 | {default_edit}
8 | {default_save}
9 | {default_back}
10 | {default_settings}
11 | {default_later}
12 | {default_next}
13 | {default_previous}
14 | {default_skip}
15 | {error_authenticationError}
16 | {error_connectionError}
17 | {error_errorTitle}
18 | {error_unknownError}
19 | {rateReminder_title}
20 | {rateReminder_body}
21 | {rateReminder_yesBtn}
22 | {rateReminder_laterBtn}
23 | {rateReminder_noBtn}
24 | {versionControl_updateHeader}
25 | {versionControl_forceHeader}
26 | {versionControl_negativeBtn}
27 | {versionControl_positiveBtn}
28 | {versionControl_newInVersionHeader}
29 | {versionControl_okBtn}
30 |
--------------------------------------------------------------------------------
/app/src/test/java/dk/nodes/template/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package dk.nodes.template
2 |
3 | import org.junit.Test
4 |
5 | /**
6 | * Example local unit test, which will execute on the development machine (host).
7 | *
8 | * @see [Testing documentation](http://d.android.com/tools/testing)
9 | */
10 | class ExampleUnitTest {
11 | @Test
12 | @Throws(Exception::class)
13 | fun addition_isCorrect() {
14 | org.junit.Assert.assertEquals(4, (2 + 2).toLong())
15 | }
16 | }
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 |
3 | buildscript {
4 |
5 | ext.versions = [
6 | kotlin : '1.3.72',
7 | coroutines : '1.3.7',
8 | constraint_layout : '2.0.0-beta6',
9 | dagger : '2.28',
10 | lifecycle : '2.2.0',
11 | timber : "4.7.1",
12 | junit : '4.13',
13 | espresso : '3.2.0',
14 | mockito : '3.3.3',
15 | nstack : '3.2.5',
16 | nodes_arch : '2.3.5',
17 | retrofit : '2.9.0',
18 | okhttp : '4.6.0',
19 | ktx : '1.3.0',
20 | material : '1.2.0-beta01',
21 | archCore : '2.1.0',
22 | navigation : '2.2.2',
23 | nodes_utils : '1.0.0',
24 | nodes_utils_okhttp : '0.12.2',
25 | preferences : '1.1.1',
26 | chucker : '3.2.0',
27 | leak_canary : '2.3',
28 | desugaring : '1.0.5'
29 | ]
30 |
31 | ext.keys = [
32 | appId : 'bOdrNuZd4syxuAz6gyCb3xwBCjA8U4h4IcQI',
33 | apiKey : 'X0ENl5QpKI51tS9CzKSt1PGwfZeq2gBMTU58',
34 | acceptHeader: "da-DK"
35 | ]
36 |
37 | ext.sdks = [
38 | compileSdkVersion: 29,
39 | buildToolsVersion: '29.0.0',
40 | minSdkVersion : 21,
41 | targetSdkVersion : 29
42 | ]
43 |
44 | repositories {
45 | mavenCentral()
46 | google()
47 | jcenter()
48 | mavenLocal()
49 | }
50 | dependencies {
51 | classpath 'com.android.tools.build:gradle:4.0.0'
52 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${versions.kotlin}"
53 | classpath "dk.nodes.nstack:translation:${versions.nstack}"
54 | classpath "com.github.gfx.ribbonizer:ribbonizer-plugin:2.1.0"
55 | }
56 | }
57 |
58 | plugins {
59 | id "com.diffplug.gradle.spotless" version "3.27.2"
60 | id 'com.github.ben-manes.versions' version "0.28.0"
61 | }
62 |
63 | allprojects {
64 | repositories {
65 | mavenCentral()
66 | maven { url 'https://maven.google.com' }
67 | jcenter()
68 | mavenLocal()
69 | }
70 | }
71 |
72 |
73 | subprojects {
74 | apply plugin: 'com.diffplug.gradle.spotless'
75 | spotless {
76 | format 'misc', {
77 | target '**/*.gradle'
78 | trimTrailingWhitespace()
79 | indentWithSpaces(4) // or spaces. Takes an integer argument if you don't like 4
80 | endWithNewline()
81 | }
82 | kotlin {
83 | target "**/*.kt"
84 | ktlint('0.33.0')
85 | targetExclude("**/RateReminderActions.kt")
86 | }
87 | java {
88 | target "**/*.java"
89 | targetExclude '**/Translation.java'
90 | googleJavaFormat('1.1').aosp()
91 | }
92 | }
93 |
94 | tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
95 | kotlinOptions {
96 | // Treat all Kotlin warnings as errors, don't go full retarded
97 | // allWarningsAsErrors = true
98 |
99 | // Enable experimental coroutines APIs, including Flow
100 | freeCompilerArgs += "-Xuse-experimental=kotlinx.coroutines.ExperimentalCoroutinesApi"
101 | freeCompilerArgs += "-Xuse-experimental=kotlinx.coroutines.FlowPreview"
102 |
103 | // Set JVM target to 1.8
104 | jvmTarget = "1.8"
105 | }
106 | }
107 | }
108 |
109 | dependencyUpdates.resolutionStrategy {
110 | componentSelection { rules ->
111 | rules.all { ComponentSelection selection ->
112 | boolean rejected = ['alpha', 'beta', 'cr', 'm', 'preview'].any { qualifier ->
113 | selection.candidate.version ==~ /(?i).*[.-]${qualifier}[.\d-]*/
114 | }
115 | if (rejected) {
116 | selection.reject('Release candidate')
117 | }
118 | }
119 | }
120 | }
--------------------------------------------------------------------------------
/data/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/data/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.library'
2 | apply plugin: 'kotlin-android'
3 |
4 | android {
5 | compileSdkVersion sdks.compileSdkVersion
6 | defaultConfig {
7 | minSdkVersion sdks.minSdkVersion
8 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
9 | }
10 |
11 | compileOptions {
12 | coreLibraryDesugaringEnabled true
13 | sourceCompatibility JavaVersion.VERSION_1_8
14 | targetCompatibility JavaVersion.VERSION_1_8
15 | }
16 | kotlinOptions {
17 | jvmTarget = "1.8"
18 | }
19 | }
20 |
21 | dependencies {
22 | implementation project (':domain')
23 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:${versions.kotlin}"
24 | implementation "androidx.core:core-ktx:${versions.ktx}"
25 | implementation "androidx.preference:preference-ktx:${versions.preferences}"
26 | implementation("com.squareup.retrofit2:retrofit:${versions.retrofit}")
27 | implementation("com.squareup.retrofit2:converter-gson:${versions.retrofit}") {
28 | exclude module: "retrofit:${versions.retrofit}"
29 | }
30 | // Nodes OkHTTP utilities
31 | implementation "dk.nodes.utils:okhttp:${versions.nodes_utils_okhttp}"
32 |
33 | implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:${versions.coroutines}"
34 | implementation group: 'javax.inject', name: 'javax.inject', version: '1'
35 | coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:$versions.desugaring"
36 | }
37 |
--------------------------------------------------------------------------------
/data/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/data/src/main/java/dk/nodes/template/data/network/Api.kt:
--------------------------------------------------------------------------------
1 | package dk.nodes.template.data.network
2 |
3 | import dk.nodes.template.domain.entities.Post
4 | import retrofit2.Response
5 | import retrofit2.http.GET
6 |
7 | interface Api {
8 | @GET("posts")
9 | suspend fun getPosts(): Response>
10 | }
11 |
--------------------------------------------------------------------------------
/data/src/main/java/dk/nodes/template/data/network/RestPostRepository.kt:
--------------------------------------------------------------------------------
1 | package dk.nodes.template.data.network
2 |
3 | import dk.nodes.template.domain.entities.Post
4 | import dk.nodes.template.domain.repositories.PostRepository
5 | import dk.nodes.template.domain.repositories.RepositoryException
6 | import kotlinx.coroutines.channels.BroadcastChannel
7 | import kotlinx.coroutines.channels.Channel
8 | import kotlinx.coroutines.flow.Flow
9 | import kotlinx.coroutines.flow.asFlow
10 | import javax.inject.Inject
11 |
12 | class RestPostRepository @Inject constructor(private val api: Api) : PostRepository {
13 |
14 | private val postsChannel = BroadcastChannel>(Channel.CONFLATED)
15 | .also {
16 | it.offer(listOf())
17 | }
18 |
19 | override fun getPostsFlow(): Flow> {
20 | return postsChannel.asFlow()
21 | }
22 |
23 | override suspend fun getPosts(): List {
24 | val response = api.getPosts()
25 | if (response.isSuccessful) {
26 |
27 | return response.body()
28 | ?.also {
29 | postsChannel.send(it)
30 | }
31 | ?: throw(RepositoryException(
32 | response.code(),
33 | response.errorBody()?.string(),
34 | response.message()
35 | ))
36 | } else {
37 | throw(RepositoryException(
38 | response.code(),
39 | response.errorBody()?.string(),
40 | response.message()
41 | ))
42 | }
43 | }
44 | }
--------------------------------------------------------------------------------
/data/src/main/java/dk/nodes/template/data/network/oauth/OAuthCallbackImpl.kt:
--------------------------------------------------------------------------------
1 | package dk.nodes.template.data.network.oauth
2 |
3 | import dk.nodes.okhttputils.oauth.OAuthCallback
4 | import dk.nodes.okhttputils.oauth.entities.OAuthInfo
5 | import dk.nodes.okhttputils.oauth.entities.OAuthResult
6 | import javax.inject.Inject
7 |
8 | class OAuthCallbackImpl @Inject constructor() : OAuthCallback {
9 | override fun provideAuthInfo(refreshToken: String?): OAuthResult {
10 | return OAuthResult.Success(OAuthInfo(
11 | accessToken = "newAccessToken",
12 | refreshToken = "new/old refresh token"
13 | ))
14 | }
15 | }
--------------------------------------------------------------------------------
/data/src/main/java/dk/nodes/template/data/network/oauth/OAuthPreferencesRepository.kt:
--------------------------------------------------------------------------------
1 | package dk.nodes.template.data.network.oauth
2 |
3 | import dk.nodes.okhttputils.oauth.OAuthRepository
4 | import dk.nodes.template.domain.managers.PrefManager
5 | import javax.inject.Inject
6 |
7 | class OAuthPreferencesRepository @Inject constructor(
8 | private val prefManager: PrefManager
9 | ) : OAuthRepository {
10 |
11 | override fun getAccessToken(): String? {
12 | return prefManager.getString(PREF_ACCESS_TOKEN, null)
13 | }
14 |
15 | override fun getRefreshToken(): String? {
16 | return prefManager.getString(PREF_REFRESH_TOKEN, null)
17 | }
18 |
19 | override fun setAccessToken(accessToken: String?) {
20 | accessToken?.let { token ->
21 | prefManager.setString(PREF_ACCESS_TOKEN, token)
22 | }
23 | }
24 |
25 | override fun setRefreshToken(refreshToken: String?) {
26 | refreshToken?.let { token ->
27 | prefManager.setString(PREF_REFRESH_TOKEN, token)
28 | }
29 | }
30 |
31 | override fun clear() {
32 | prefManager.remove(PREF_ACCESS_TOKEN)
33 | prefManager.remove(PREF_REFRESH_TOKEN)
34 | }
35 |
36 | companion object {
37 | private const val PREF_REFRESH_TOKEN = "pref_oauth_refresh_token"
38 | private const val PREF_ACCESS_TOKEN = "pref_oauth_access_token"
39 | }
40 | }
--------------------------------------------------------------------------------
/data/src/main/java/dk/nodes/template/data/network/util/BufferedSourceConverterFactory.kt:
--------------------------------------------------------------------------------
1 | package dk.nodes.template.data.network.util
2 |
3 | import okhttp3.ResponseBody
4 | import okio.BufferedSource
5 | import retrofit2.Converter
6 | import retrofit2.Retrofit
7 | import java.lang.reflect.Type
8 |
9 | /*
10 | This is necessary to get general purpose disk caching from Store library to work
11 | with retrofit. (Store expects okio BufferedSource)
12 | */
13 | class BufferedSourceConverterFactory : Converter.Factory() {
14 | override fun responseBodyConverter(
15 | type: Type?,
16 | annotations: Array?,
17 | retrofit: Retrofit?
18 | ): Converter? {
19 | return if (BufferedSource::class.java != type) {
20 | null
21 | } else Converter { value -> value.source() }
22 | }
23 | }
--------------------------------------------------------------------------------
/data/src/main/java/dk/nodes/template/data/network/util/DateDeserializer.kt:
--------------------------------------------------------------------------------
1 | package dk.nodes.template.data.network.util
2 |
3 | import com.google.gson.JsonDeserializationContext
4 | import com.google.gson.JsonDeserializer
5 | import com.google.gson.JsonElement
6 | import com.google.gson.JsonParseException
7 | import java.lang.reflect.Type
8 | import java.text.ParseException
9 | import java.text.SimpleDateFormat
10 | import java.util.Arrays
11 | import java.util.Date
12 | import java.util.HashMap
13 | import java.util.Locale
14 |
15 | class DateDeserializer : JsonDeserializer {
16 | @Throws(JsonParseException::class)
17 | override fun deserialize(
18 | jsonElement: JsonElement,
19 | typeOF: Type,
20 | context: JsonDeserializationContext
21 | ): Date {
22 | for (format in DATE_FORMATS) {
23 | if (!formatters.containsKey(format)) {
24 | formatters.put(format, SimpleDateFormat(format, Locale.getDefault()))
25 | }
26 |
27 | try {
28 | return formatters[format]?.parse(jsonElement.asString) ?: Date()
29 | } catch (e: ParseException) {
30 | }
31 | }
32 | throw JsonParseException(
33 | "Unparseable date: \"" + jsonElement.asString +
34 | "\". Supported formats: " + Arrays.toString(DATE_FORMATS)
35 | )
36 | }
37 |
38 | // replacement for a static member
39 | companion object {
40 | private val formatters = HashMap()
41 | val DATE_FORMATS = arrayOf(
42 | "yyyy-MM-dd'T'HH:mm:ss.SSSZ",
43 | "yyyy-MM-dd'T'HH:mm:ss'Z'",
44 | "yyyy-MM-dd'T'HH:mm:ssZ",
45 | "yyyy-MM-dd",
46 | "MMM dd, yyyy hh:mm:ss aaa"
47 | )
48 | }
49 | }
--------------------------------------------------------------------------------
/data/src/main/java/dk/nodes/template/data/network/util/ItemTypeAdapterFactory.kt:
--------------------------------------------------------------------------------
1 | package dk.nodes.template.data.network.util
2 |
3 | import com.google.gson.Gson
4 | import com.google.gson.JsonElement
5 | import com.google.gson.TypeAdapter
6 | import com.google.gson.TypeAdapterFactory
7 | import com.google.gson.reflect.TypeToken
8 | import com.google.gson.stream.JsonReader
9 | import com.google.gson.stream.JsonWriter
10 | import java.io.IOException
11 |
12 | class ItemTypeAdapterFactory : TypeAdapterFactory {
13 | var rootContainerNames = listOf("data")
14 |
15 | override fun create(gson: Gson, type: TypeToken): TypeAdapter {
16 |
17 | val delegate = gson.getDelegateAdapter(this, type)
18 | val elementAdapter = gson.getAdapter(JsonElement::class.java)
19 |
20 | return object : TypeAdapter() {
21 |
22 | @Throws(IOException::class)
23 | override fun write(out: JsonWriter, value: T) {
24 | delegate.write(out, value)
25 | }
26 |
27 | @Throws(IOException::class)
28 | override fun read(`in`: JsonReader): T {
29 |
30 | var jsonElement = elementAdapter.read(`in`)
31 | if (jsonElement.isJsonObject) {
32 | // Log.e("debug", "parsing element " + jsonElement.toString())
33 | val jsonObject = jsonElement.asJsonObject
34 | val entry_set = jsonObject.entrySet()
35 | if (entry_set.size == 1) {
36 | val key: String = entry_set.iterator().next().key ?: ""
37 | val ele: JsonElement = entry_set.iterator().next().value
38 | if (rootContainerNames.contains(key)) {
39 | // Log.e("debug", "Doing deserialization workaround")
40 | return delegate.fromJsonTree(ele)
41 | }
42 | }
43 | }
44 | return delegate.fromJsonTree(jsonElement)
45 | }
46 | }.nullSafe()
47 | }
48 | }
--------------------------------------------------------------------------------
/data/src/main/java/dk/nodes/template/data/storage/PrefManagerImpl.kt:
--------------------------------------------------------------------------------
1 | package dk.nodes.template.data.storage
2 |
3 | import android.content.Context
4 | import android.content.SharedPreferences
5 | import androidx.core.content.edit
6 | import androidx.preference.PreferenceManager
7 | import dk.nodes.template.domain.managers.PrefManager
8 | import javax.inject.Inject
9 |
10 | class PrefManagerImpl @Inject constructor(context: Context) : PrefManager {
11 | private var sharedPrefs: SharedPreferences =
12 | PreferenceManager.getDefaultSharedPreferences(context)
13 |
14 | override fun getInt(key: String, defaultValue: Int): Int {
15 | return sharedPrefs.getInt(key, defaultValue)
16 | }
17 |
18 | override fun setInt(key: String, value: Int) {
19 | sharedPrefs.edit(commit = true) {
20 | putInt(key, value)
21 | }
22 | }
23 |
24 | override fun getLong(key: String, defaultValue: Long): Long =
25 | sharedPrefs.getLong(key, defaultValue)
26 |
27 | override fun setLong(key: String, value: Long) {
28 | sharedPrefs.edit(commit = true) {
29 | putLong(key, value)
30 | }
31 | }
32 |
33 | override fun getBoolean(key: String, defaultValue: Boolean): Boolean =
34 | sharedPrefs.getBoolean(key, defaultValue)
35 |
36 | override fun setBoolean(key: String, value: Boolean) {
37 | sharedPrefs.edit(commit = true) {
38 | putBoolean(key, value)
39 | }
40 | }
41 |
42 | override fun getFloat(key: String, defaultValue: Float): Float =
43 | sharedPrefs.getFloat(key, defaultValue)
44 |
45 | override fun setFloat(key: String, value: Float) {
46 | sharedPrefs.edit(commit = true) {
47 | putFloat(key, value)
48 | }
49 | }
50 |
51 | override fun getString(key: String, defaultValue: String?): String? =
52 | sharedPrefs.getString(key, defaultValue)
53 |
54 | override fun setString(key: String, value: String) {
55 | sharedPrefs.edit(commit = true) {
56 | putString(key, value)
57 | }
58 | }
59 |
60 | override fun remove(key: String) {
61 | sharedPrefs.edit(true) {
62 | remove(key)
63 | }
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/docs/HOWWEWORK.md:
--------------------------------------------------------------------------------
1 | # Kotlin Template
2 |
3 | [Short project description]
4 |
5 | ## Day-to-day work
6 |
7 | 1. All new work resigns in `feature/*` branches. If some new interfaces were introduced, remember to cover them with unit-tests.
8 | 2. After your work on the feature branch is complete, open the PR and ask someone who is working on this project (or worked previously) to review it.
9 | 3. When PR is opened, Bitrise will automatically run all the code quality checks and unit-tests and will generate a report.
10 | 4. When PR is reviewed, merged and all the checks have passed, remember to make a new builds.
11 |
12 | > Pro-tip: When opening pull-requests and/or creating branches remember to reference the JIRA issue/ticket
13 |
14 | ## Enterprise level projects
15 |
16 | ### Code quaulity
17 | Enterprise level projects also contain `lintrules` module with custom linting rules to maintain code quaulity. Moreover, each module uses `detect` and `klint` gradle plugins to maintain code style and cleanliness. Every pull request made, will launch these checks to make sure develop/master branches are always "clean" and release-ready. You can also run these checks manually using `gradle` commands like so:
18 | ```
19 | ./gradlew detekt
20 | ./gradlew lint
21 | ./gradlew ktlintCheck
22 | ```
23 |
24 | ### Unit-test coverage
25 | Enterprise level project are required to have **80% code-coverage** with the most important piece being application's **business logic** (100% coverage) and Presentation/UI layers. Android uses `JaCoCo` gradle plugin to generate coverage reports for all the modules and ties them all together. If there are some files you wish to exclude from coverage-report (Dagger files for example) you can update `jacoco.gradle` to with additional files like so:
26 | ```groovy
27 | def toExclude = ['**/R.class',
28 | '**/R$*.class',
29 | '**/*$$ViewBinder*.*',
30 | '**/injection/*',
31 | '**/**Builder.*'
32 | ```
33 | When it comes to writing unit-test itself please refer to our extensive guide for unit testing: https://github.com/nodes-android/guidelines/blob/master/unittesting.md
34 |
35 | ## Android Project
36 |
37 | Android project is based on our newest template for client projects that uses [Google's ViewModels](https://developer.android.com/topic/libraries/architecture/viewmodel) with a lightweight ViewState approach, similar to MVI and follows multi-modular approach to support clean architecture principles.
38 |
39 |
40 |
41 | ### Build Configuration
42 | Due to the modular architecture approach top-level `build.gradle` should provides all dependencies versions, android API level, and API keys that could be shared across different modules using extensions.
43 | ```groovy
44 |
45 | ext.sdks = [
46 | compileSdkVersion: [latest SDK version],
47 | buildToolsVersion: [latest Build Tools version]
48 | minSdkVersion : 21,
49 | targetSdkVersion : [latest SDK version]
50 | ]
51 |
52 | ext.versions = [
53 | kotlin : '[Latest Kotlin version]',
54 | coroutines : '[Latest Kotlin coroutines version]',
55 | ...
56 | ]
57 | ```
58 | When it comes to flavours, projects usually have two application variants, that are defined in the `app` build.gradle. Thats also the place where all flavour-dependent variables should be specified
59 | - `stating` - points to test environment, builds are debuggable
60 | - `production` - points to production environment, signed with the the release keystore
61 |
62 |
63 |
64 | ### Flow of control
65 |
66 |
67 |
68 |
69 |
70 |
71 | Example:
72 | 1. View subscribes to ViewModel's LiveData instance(s).
73 | 2. User clicks a button that loads a list of posts in a view.
74 | 3. OnClickListener executes a Interactor/UseCase asynchronously in the business logic layer.
75 | 4. The Interactor runs in the background accessing a post repository which fetches a list of posts
76 | 5. ViewModel gets result from the Interactor and updates local view state, which triggers a LiveData update
77 | 6. View is updated since it's observing the LiveData instance from our ViewModel.
78 |
79 |
80 |
81 | ### Modules
82 | Android project follows multi-modular approach to support clean architecture principles
83 |
84 | #### App
85 | Main entry point with shared Application logic
86 |
87 | #### Data
88 | Contains data class models, repositories and network logic. Retrofit2/OkHttp3 is used for network logic.
89 |
90 | ```kotlin
91 | class RestPostRepository @Inject constructor(private val api: Api) : PostRepository {
92 | @Throws(RepositoryException::class)
93 | override suspend fun getPosts(cached: Boolean): List {
94 | val response = api.getPosts().execute()
95 | if (response.isSuccessful) {
96 | return response.body()
97 | ?: throw(RepositoryException(
98 | response.code(),
99 | response.message()
100 | ))
101 | }
102 | throw(RepositoryException(response.code(), response.message()))
103 | }
104 | }
105 | ```
106 |
107 | #### Domain
108 | General shared business logic with interactors, extensions, managers, various utility code.
109 |
110 | An interactor usually returns a result via a suspend method. You can model the Result class as you like:
111 |
112 | ```kotlin
113 | sealed class Result {
114 | sealed class Success : Result() {
115 | data class StillFetching(val data: V) : Success()
116 | data class Cached(val data: V) : Success()
117 | data class FreshData(val data: V) : Success()
118 | object NoData() : Success()
119 | }
120 |
121 | // Error states
122 | }
123 | ```
124 | Or the more simple version:
125 | ```kotlin
126 | sealed class Result {
127 | data class Success(val data: SomeData) : Result()
128 | data class Error(val e: Exception): Result()
129 | }
130 | ```
131 |
132 | Interactors are the link to the outer layers of the domain layer, i.e. contacting the API or fetching/saving various state.
133 |
134 | ```kotlin
135 | class FetchPostsInteractor @Inject constructor(
136 | private val postRepository: PostRepository
137 | ) : BaseAsyncInteractor> {
138 |
139 | override suspend fun invoke(): List {
140 | return postRepository.getPosts()
141 | }
142 | }
143 | ```
144 |
145 | #### Presentation
146 | Unsurprisingly holds the UI with matching ViewModels. This is usually the module branched out from if needed. Presentation layer provides various extension functions for your interactors so you could use different approaches when updating your `viewState`.
147 |
148 | ##### Result Interactor
149 | `ResultInteractor` will handle exception handling and produce a `CompleteResult`
150 | ```kotlin
151 | // Some random ViewModel
152 |
153 | // Wrap your basic intractors as a Result Interactor
154 | private val resultInteractor = getPostsInteractor.asResult()
155 |
156 | fun fetchPosts() = viewModelScope.launch(Dispatchers.Main) {
157 | state = mapResult(Loading())
158 | val result = withContext(Dispatchers.IO) { resultInteractor.invoke() }
159 | state = mapResult(result)
160 | }
161 | ```
162 |
163 | ##### LiveData Interactor
164 | `LiveDataInteactor` will produce a `LiveData>` that you can add as an additional state source
165 |
166 | ```kotlin
167 | // Wrap your basic interactor as LiveData
168 | private val liveDataInteractor = postsInteractor.asLiveData()
169 |
170 | fun fetchPosts() = viewModelScope.launch(Dispatchers.Main) {
171 | addStateSource(resultInteractor.liveData) { state = mapResult(it) }
172 | withContext(Dispatchers.IO) {
173 | resultInteractor()
174 | }
175 | }
176 | ```
177 |
178 | ##### Flow Interactor
179 | `FlowInteractor` will produce a `Flow` to which you can subscribe to and map state accordingly
180 | ```kotlin
181 |
182 | // Wrap your basic interactor as a Flow
183 | private val flowInteractor = getPostsInteractor.asFlow()
184 |
185 | fun fetchPosts() = viewModelScope.launch(Dispatchers.Main) {
186 | resultInteractor.invoke()
187 | .flowOn(Dispatchers.IO)
188 | .collect { state = mapResult(it) }
189 | }
190 | ```
191 |
192 | ##### RxInteractor
193 | `RxInteractor` will produce a `Flowable` that you can observe using `RxJava/RxKotlin`
194 | ```kotlin
195 | private val rxInteractor = postsInteractor.asRx()
196 | private val cd = CompositeDisposable()
197 |
198 | fun fetchPosts() = viewModelScope.launch {
199 | cd.add(rxInteractor.observe()
200 | .observeOn(AndroidSchedulers.mainThread())
201 | .subscribeOn(Schedulers.io())
202 | .subscribe { state = mapResult(it) })
203 | rxInteractor.invoke()
204 | }
205 | ```
206 | > Don't forget to clear `CompositeDisposable` in `onCleared()`
207 |
208 |
209 | #### Error Handling
210 | `ViewErrorController` is used to handle exceptions received from the intractors in the `ViewModel` and present the human-readable error message in the ui layer (i.e `Fragment`/`Activity`).
211 | When using it in `ViewModel`, `ViewErrorController` can be used to map throwable to a `ViewError` instance. You can also tweak `ViewErrorController` implementation to provide your own exception handling and etc.
212 | ```kotlin
213 | private fun mapResult(result: InteractorResult>): SampleViewState {
214 | return when (result) {
215 | is Fail -> state.copy(
216 | viewError = SingleEvent(ViewErrorController.mapThrowable(result.throwable)),
217 | isLoading = false
218 | )
219 | }
220 | }
221 | ```
222 | `ViewError` object is then can be used in Fragment/Activity to display Error Dialog or a Snackbar
223 | ```kotlin
224 | private fun showErrorMessage(state: SampleViewState) {
225 | defaultErrorController.get().showErrorSnackbar(requireView(), state.viewError?.consume() ?: return) {
226 | viewModel.fetchPosts()
227 | }
228 | }
229 | ```
230 |
231 | Views are the consumers of the ViewModel's exposed LiveData. We want the view to be as dumb and small as possible, so only put UI code here
232 |
233 | ```kotlin
234 | override fun onCreate(savedInstanceState: Bundle?) {
235 | // ...
236 | viewModel.viewState.observeNonNull(this) { state ->
237 | showLoading(state)
238 | showPosts(state)
239 | showErrorMessage(state)
240 | }
241 | viewModel.loadPosts()
242 | }
243 |
244 | private fun showPosts(state: MainActivityViewState) {
245 | postsTextView.text = state.posts.joinToString { it.title + System.lineSeparator() }
246 | }
247 |
248 | private fun showLoading(state: MainActivityViewState) {
249 | postsProgressBar.isVisible = state.isLoading
250 | }
251 |
252 | private fun showErrorMessage(state: MainActivityViewState) {
253 | state.errorMessage?.let {
254 | if (it.consumed) return@let
255 | Snackbar.make(
256 | postsTextView,
257 | it.consume() ?: Translation.error.unknownError,
258 | Snackbar.LENGTH_SHORT
259 | )
260 | }
261 | }
262 | ```
263 |
264 | ## Injection
265 |
266 | This project is using Dagger for injection and scoping. Dagger is an annotation based dependency injection, which computes the dependency graph at compile time and verifies that everything is correctly injected at runtime.
267 |
268 | Dagger works by defining `@Component`s that hold the scope and lifetime of objects it creates. Each `@Component` can depend on other `@Component`s by being a `@Subcomponent`.
269 |
270 | ### Modules
271 |
272 | In Dagger Modules are the way to specify _how_ objects are created, where components are the once who decides the lifetime of those objects.
273 |
274 | There are different approaches to do this, given this class:
275 | ```kotlin
276 | class RestCityRepository @Inject constructor(
277 | private val api: Provider,
278 | private val gson: Gson
279 | ) : CityRepository {
280 | // ...
281 | }
282 | ```
283 |
284 | 1) Full declaration in module via `@Provides`
285 |
286 | ```kotlin
287 | @Provides
288 | fun provideCityRepository(val api: Provider, val gson: Gson): CityRepository {
289 | return RestCityRepository(api, gson)
290 | }
291 | ```
292 |
293 | 2) `@Bind`s and defining dependencies at implementation site
294 |
295 | ```kotlin
296 | @Binds
297 | abstract fun bindCityRepository(cityRepository: RestCityRepository): CityRepository
298 | ```
299 |
300 | By method 2 we avoid having to mirror constructor dependencies and only have to define what implementation of the `CityRepository` we want to inject where needed.
301 |
302 |
303 | ### Scoping
304 |
305 | We define scopes via the components _or_ via scope annotations. In Careem we have two modes - component scope and @AppScope, which is similar to a singleton.
306 |
307 | If we wanted RestCityRepository to be a singleton, all we had to do was mark it as @AppScope;
308 | ```kotlin
309 | @AppScope
310 | class RestCityRepository @Inject constructor(
311 | private val api: Provider,
312 | private val gson: Gson
313 | ) : CityRepository {
314 | // ...
315 | }
316 | ```
317 |
318 |
319 | ## Integrations
320 | List of important 3d party APIs and SDKs that are used in this project
321 |
322 |
323 | ## Live Templates
324 | The Kotlin template comes supported with a its own set of live templates which can be found at
325 |
326 | https://github.com/nodes-android/androidstudio-livetemplates
327 |
328 | It should make generating the boilerplate for activities and fragments easy.
329 |
330 | ## Nodes Architecture Library
331 | The Template uses components from our Architecture library so be sure to read up on how that is used as well
332 |
333 | https://github.com/nodes-android/nodes-architecture-android
334 |
335 | ## Inspired from the following sources:
336 | - [Clean Architecture by Uncle Bob](http://blog.8thlight.com/uncle-bob/2012/08/13/the-clean-architecture.html)
337 | - [Some dudes android implementation](https://medium.com/@dmilicic/a-detailed-guide-on-developing-android-apps-using-the-clean-architecture-pattern-d38d71e94029)
338 | - [Some other dudes implementation](https://fernandocejas.com/2014/09/03/architecting-android-the-clean-way)
339 |
--------------------------------------------------------------------------------
/docs/images/arch.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ml-archive/kotlin-template/6399bf37288bdb294a47e21fc73a621f7f356a0b/docs/images/arch.png
--------------------------------------------------------------------------------
/domain/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/domain/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'kotlin'
2 |
3 | dependencies {
4 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:${versions.kotlin}"
5 | implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:${versions.coroutines}"
6 | implementation group: 'javax.inject', name: 'javax.inject', version: '1'
7 | }
8 |
--------------------------------------------------------------------------------
/domain/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/domain/src/main/java/dk/nodes/template/domain/entities/Post.kt:
--------------------------------------------------------------------------------
1 | package dk.nodes.template.domain.entities
2 |
3 | data class Post(
4 | var userId: Int,
5 | var id: Int,
6 | var title: String,
7 | var body: String
8 | )
--------------------------------------------------------------------------------
/domain/src/main/java/dk/nodes/template/domain/entities/Theme.kt:
--------------------------------------------------------------------------------
1 | package dk.nodes.template.domain.entities
2 |
3 | enum class Theme { DARK, LIGHT, FOLLOW_SYSTEM }
--------------------------------------------------------------------------------
/domain/src/main/java/dk/nodes/template/domain/extensions/Extensions.kt:
--------------------------------------------------------------------------------
1 | package dk.nodes.template.domain.extensions
2 |
3 | inline fun T.guard(block: T.() -> Unit): T {
4 | if (this == null) block(); return this
5 | }
--------------------------------------------------------------------------------
/domain/src/main/java/dk/nodes/template/domain/interactors/Interactor.kt:
--------------------------------------------------------------------------------
1 | package dk.nodes.template.domain.interactors
2 |
3 | interface Interactor {
4 | suspend operator fun invoke(input: I): O
5 | }
6 |
7 | suspend operator fun NoInputInteractor.invoke() = invoke(Unit)
8 |
9 | typealias NoInputInteractor = Interactor
10 |
11 | typealias NoOutputInteractor = Interactor
12 |
13 | typealias EmptyInteractor = Interactor
--------------------------------------------------------------------------------
/domain/src/main/java/dk/nodes/template/domain/interactors/InteractorExtensions.kt:
--------------------------------------------------------------------------------
1 | package dk.nodes.template.domain.interactors
2 |
3 | import kotlinx.coroutines.CoroutineScope
4 | import kotlinx.coroutines.Dispatchers
5 | import kotlinx.coroutines.channels.BroadcastChannel
6 | import kotlinx.coroutines.channels.Channel
7 | import kotlinx.coroutines.flow.Flow
8 | import kotlinx.coroutines.flow.asFlow
9 | import kotlinx.coroutines.launch
10 | import kotlinx.coroutines.withContext
11 | import kotlin.coroutines.CoroutineContext
12 |
13 | interface ResultInteractor : Interactor>
14 |
15 | interface FlowInteractor : NoOutputInteractor {
16 | fun flow(): Flow
17 | }
18 |
19 | private class ResultInteractorImpl(private val interactor: Interactor) :
20 | ResultInteractor {
21 | override suspend fun invoke(input: I): CompleteResult {
22 | return try {
23 | Success(interactor(input))
24 | } catch (t: Throwable) {
25 | Fail(t)
26 | }
27 | }
28 | }
29 |
30 | private class FlowInteractorImpl(private val interactor: Interactor) :
31 | FlowInteractor> {
32 |
33 | private val channel = BroadcastChannel>(Channel.CONFLATED).also {
34 | it.offer(Uninitialized)
35 | }
36 |
37 | override fun flow(): Flow> {
38 | return channel.asFlow()
39 | }
40 |
41 | override suspend fun invoke(input: I) {
42 | channel.offer(Loading())
43 | try {
44 | channel.offer(Success(interactor(input)))
45 | } catch (t: Throwable) {
46 | channel.offer(Fail(t))
47 | }
48 | }
49 | }
50 |
51 | fun Interactor.asResult(): ResultInteractor {
52 | return ResultInteractorImpl(this)
53 | }
54 |
55 | fun Interactor.asFlow(): FlowInteractor> {
56 | return FlowInteractorImpl(this)
57 | }
58 |
59 | fun CoroutineScope.launchInteractor(
60 | interactor: NoOutputInteractor,
61 | input: I,
62 | coroutineContext: CoroutineContext = Dispatchers.IO
63 | ) {
64 | launch(coroutineContext) { interactor(input) }
65 | }
66 |
67 | fun CoroutineScope.launchInteractor(
68 | interactor: EmptyInteractor,
69 | coroutineContext: CoroutineContext = Dispatchers.IO
70 | ) {
71 | launchInteractor(interactor, Unit, coroutineContext)
72 | }
73 |
74 | suspend fun runInteractor(
75 | interactor: Interactor,
76 | input: I,
77 | coroutineContext: CoroutineContext = Dispatchers.IO
78 | ): T {
79 | return withContext(coroutineContext) { interactor(input) }
80 | }
81 |
82 | suspend fun runInteractor(
83 | interactor: NoInputInteractor,
84 | coroutineContext: CoroutineContext = Dispatchers.IO
85 | ): O {
86 | return withContext(coroutineContext) { interactor.invoke() }
87 | }
88 |
89 | fun InteractorResult.isSuccess(block: (T) -> R): InteractorResult {
90 | if (this is Success) {
91 | block(this.data)
92 | }
93 | return this
94 | }
95 |
96 | fun InteractorResult.isError(block: (throwable: Throwable) -> R): InteractorResult {
97 | if (this is Fail) {
98 | block(this.throwable)
99 | }
100 | return this
101 | }
--------------------------------------------------------------------------------
/domain/src/main/java/dk/nodes/template/domain/interactors/InteractorResult.kt:
--------------------------------------------------------------------------------
1 | package dk.nodes.template.domain.interactors
2 |
3 | sealed class InteractorResult {
4 |
5 | override fun toString(): String {
6 | return when (this) {
7 | is Success<*> -> "Success[data=$data]"
8 | is Fail -> "Fail[throwable=$throwable]"
9 | is Loading -> "Loading"
10 | Uninitialized -> "Uninitialized"
11 | }
12 | }
13 | }
14 |
15 | sealed class IncompleteResult : InteractorResult()
16 | sealed class CompleteResult : InteractorResult()
17 |
18 | data class Success(val data: T) : CompleteResult()
19 | data class Fail(val throwable: Throwable) : CompleteResult()
20 | object Uninitialized : IncompleteResult()
21 | class Loading : IncompleteResult()
--------------------------------------------------------------------------------
/domain/src/main/java/dk/nodes/template/domain/interactors/PostFlowInteractor.kt:
--------------------------------------------------------------------------------
1 | package dk.nodes.template.domain.interactors
2 |
3 | import dk.nodes.template.domain.entities.Post
4 | import dk.nodes.template.domain.repositories.PostRepository
5 | import kotlinx.coroutines.flow.Flow
6 | import javax.inject.Inject
7 |
8 | class PostFlowInteractor @Inject constructor(private val postRepository: PostRepository) :
9 | FlowInteractor> {
10 | override fun flow(): Flow> {
11 | return postRepository.getPostsFlow()
12 | }
13 |
14 | override suspend fun invoke(input: Unit) {
15 | postRepository.getPosts()
16 | }
17 | }
--------------------------------------------------------------------------------
/domain/src/main/java/dk/nodes/template/domain/interactors/PostsInteractor.kt:
--------------------------------------------------------------------------------
1 | package dk.nodes.template.domain.interactors
2 |
3 | import dk.nodes.template.domain.entities.Post
4 | import dk.nodes.template.domain.repositories.PostRepository
5 | import javax.inject.Inject
6 |
7 | class PostsInteractor @Inject constructor(
8 | private val postRepository: PostRepository
9 | ) : NoInputInteractor> {
10 |
11 | override suspend fun invoke(input: Unit): List {
12 | return postRepository.getPosts()
13 | }
14 | }
--------------------------------------------------------------------------------
/domain/src/main/java/dk/nodes/template/domain/interactors/SwitchThemeInteractor.kt:
--------------------------------------------------------------------------------
1 | package dk.nodes.template.domain.interactors
2 |
3 | import dk.nodes.template.domain.managers.ThemeManager
4 | import dk.nodes.template.domain.entities.Theme
5 | import javax.inject.Inject
6 |
7 | class SwitchThemeInteractor @Inject constructor(
8 | private val themeManager: ThemeManager
9 | ) : NoInputInteractor {
10 | override suspend fun invoke(input: Unit): Theme {
11 | themeManager.theme = if (themeManager.theme == Theme.LIGHT) Theme.DARK else Theme.LIGHT
12 | return themeManager.theme
13 | }
14 | }
--------------------------------------------------------------------------------
/domain/src/main/java/dk/nodes/template/domain/managers/PrefManager.kt:
--------------------------------------------------------------------------------
1 | package dk.nodes.template.domain.managers
2 |
3 | /**
4 | * Created by bison on 11/10/17.
5 | *
6 | * The clean version of JosoPrefs :D (inject into and use from interactors)
7 | */
8 | interface PrefManager {
9 | fun getInt(key: String, defaultValue: Int): Int
10 | fun setInt(key: String, value: Int)
11 | fun getLong(key: String, defaultValue: Long): Long
12 | fun setLong(key: String, value: Long)
13 | fun getBoolean(key: String, defaultValue: Boolean): Boolean
14 | fun setBoolean(key: String, value: Boolean)
15 | fun getFloat(key: String, defaultValue: Float): Float
16 | fun setFloat(key: String, value: Float)
17 | fun getString(key: String, defaultValue: String?): String?
18 | fun setString(key: String, value: String)
19 | fun remove(key: String)
20 | }
--------------------------------------------------------------------------------
/domain/src/main/java/dk/nodes/template/domain/managers/ThemeManager.kt:
--------------------------------------------------------------------------------
1 | package dk.nodes.template.domain.managers
2 |
3 | import dk.nodes.template.domain.entities.Theme
4 |
5 | interface ThemeManager {
6 | var theme: Theme
7 | }
--------------------------------------------------------------------------------
/domain/src/main/java/dk/nodes/template/domain/managers/ThemeManagerImpl.kt:
--------------------------------------------------------------------------------
1 | package dk.nodes.template.domain.managers
2 |
3 | import dk.nodes.template.domain.entities.Theme
4 | import javax.inject.Inject
5 |
6 | class ThemeManagerImpl @Inject constructor(private val prefManager: PrefManager) : ThemeManager {
7 |
8 | override var theme: Theme
9 | get() = getThemePref()
10 | set(value) {
11 | setThemePRef(value)
12 | }
13 |
14 | companion object {
15 | private const val PREF_THEME = "PREF_THEME"
16 | }
17 |
18 | private fun getThemePref(): Theme {
19 | val t = prefManager.getString(PREF_THEME, null)
20 | return if (t == null) Theme.LIGHT else Theme.valueOf(t)
21 | }
22 |
23 | private fun setThemePRef(theme: Theme) {
24 | prefManager.setString(PREF_THEME, theme.name)
25 | }
26 | }
--------------------------------------------------------------------------------
/domain/src/main/java/dk/nodes/template/domain/repositories/PostRepository.kt:
--------------------------------------------------------------------------------
1 | package dk.nodes.template.domain.repositories
2 |
3 | import dk.nodes.template.domain.entities.Post
4 | import kotlinx.coroutines.flow.Flow
5 |
6 | interface PostRepository {
7 | suspend fun getPosts(): List
8 | fun getPostsFlow(): Flow>
9 | }
--------------------------------------------------------------------------------
/domain/src/main/java/dk/nodes/template/domain/repositories/RepositoryException.kt:
--------------------------------------------------------------------------------
1 | package dk.nodes.template.domain.repositories
2 |
3 | data class RepositoryException(
4 | val code: Int,
5 | val errorBody: String?,
6 | val msg: String
7 | ) : RuntimeException(msg)
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 |
3 | # IDE (e.g. Android Studio) users:
4 | # Gradle settings configured through the IDE *will override*
5 | # any settings specified in this file.
6 |
7 | # For more details on how to configure your build environment visit
8 | # http://www.gradle.org/docs/current/userguide/build_environment.html
9 |
10 | # Specifies the JVM arguments used for the daemon process.
11 | # The setting is particularly useful for tweaking memory settings.
12 | org.gradle.jvmargs=-Xmx4g
13 |
14 | # When configured, Gradle will run in incubating parallel mode.
15 | # This option should only be used with decoupled projects. More details, visit
16 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
17 | # org.gradle.parallel=true
18 | android.useAndroidX=true
19 | android.enableJetifier=true
20 |
21 | org.gradle.parallel=true
22 | kapt.incremental.apt=true
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ml-archive/kotlin-template/6399bf37288bdb294a47e21fc73a621f7f356a0b/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Fri May 29 10:35:29 CEST 2020
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.4.1-all.zip
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
10 | DEFAULT_JVM_OPTS=""
11 |
12 | APP_NAME="Gradle"
13 | APP_BASE_NAME=`basename "$0"`
14 |
15 | # Use the maximum available, or set MAX_FD != -1 to use that value.
16 | MAX_FD="maximum"
17 |
18 | warn ( ) {
19 | echo "$*"
20 | }
21 |
22 | die ( ) {
23 | echo
24 | echo "$*"
25 | echo
26 | exit 1
27 | }
28 |
29 | # OS specific support (must be 'true' or 'false').
30 | cygwin=false
31 | msys=false
32 | darwin=false
33 | case "`uname`" in
34 | CYGWIN* )
35 | cygwin=true
36 | ;;
37 | Darwin* )
38 | darwin=true
39 | ;;
40 | MINGW* )
41 | msys=true
42 | ;;
43 | esac
44 |
45 | # Attempt to set APP_HOME
46 | # Resolve links: $0 may be a link
47 | PRG="$0"
48 | # Need this for relative symlinks.
49 | while [ -h "$PRG" ] ; do
50 | ls=`ls -ld "$PRG"`
51 | link=`expr "$ls" : '.*-> \(.*\)$'`
52 | if expr "$link" : '/.*' > /dev/null; then
53 | PRG="$link"
54 | else
55 | PRG=`dirname "$PRG"`"/$link"
56 | fi
57 | done
58 | SAVED="`pwd`"
59 | cd "`dirname \"$PRG\"`/" >/dev/null
60 | APP_HOME="`pwd -P`"
61 | cd "$SAVED" >/dev/null
62 |
63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
64 |
65 | # Determine the Java command to use to start the JVM.
66 | if [ -n "$JAVA_HOME" ] ; then
67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
68 | # IBM's JDK on AIX uses strange locations for the executables
69 | JAVACMD="$JAVA_HOME/jre/sh/java"
70 | else
71 | JAVACMD="$JAVA_HOME/bin/java"
72 | fi
73 | if [ ! -x "$JAVACMD" ] ; then
74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
75 |
76 | Please set the JAVA_HOME variable in your environment to match the
77 | location of your Java installation."
78 | fi
79 | else
80 | JAVACMD="java"
81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
82 |
83 | Please set the JAVA_HOME variable in your environment to match the
84 | location of your Java installation."
85 | fi
86 |
87 | # Increase the maximum file descriptors if we can.
88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
89 | MAX_FD_LIMIT=`ulimit -H -n`
90 | if [ $? -eq 0 ] ; then
91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
92 | MAX_FD="$MAX_FD_LIMIT"
93 | fi
94 | ulimit -n $MAX_FD
95 | if [ $? -ne 0 ] ; then
96 | warn "Could not set maximum file descriptor limit: $MAX_FD"
97 | fi
98 | else
99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
100 | fi
101 | fi
102 |
103 | # For Darwin, add options to specify how the application appears in the dock
104 | if $darwin; then
105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
106 | fi
107 |
108 | # For Cygwin, switch paths to Windows format before running java
109 | if $cygwin ; then
110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
112 | JAVACMD=`cygpath --unix "$JAVACMD"`
113 |
114 | # We build the pattern for arguments to be converted via cygpath
115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
116 | SEP=""
117 | for dir in $ROOTDIRSRAW ; do
118 | ROOTDIRS="$ROOTDIRS$SEP$dir"
119 | SEP="|"
120 | done
121 | OURCYGPATTERN="(^($ROOTDIRS))"
122 | # Add a user-defined pattern to the cygpath arguments
123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
125 | fi
126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
127 | i=0
128 | for arg in "$@" ; do
129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
131 |
132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
134 | else
135 | eval `echo args$i`="\"$arg\""
136 | fi
137 | i=$((i+1))
138 | done
139 | case $i in
140 | (0) set -- ;;
141 | (1) set -- "$args0" ;;
142 | (2) set -- "$args0" "$args1" ;;
143 | (3) set -- "$args0" "$args1" "$args2" ;;
144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
150 | esac
151 | fi
152 |
153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
154 | function splitJvmOpts() {
155 | JVM_OPTS=("$@")
156 | }
157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
159 |
160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
161 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
12 | set DEFAULT_JVM_OPTS=
13 |
14 | set DIRNAME=%~dp0
15 | if "%DIRNAME%" == "" set DIRNAME=.
16 | set APP_BASE_NAME=%~n0
17 | set APP_HOME=%DIRNAME%
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windowz variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 | if "%@eval[2+2]" == "4" goto 4NT_args
53 |
54 | :win9xME_args
55 | @rem Slurp the command line arguments.
56 | set CMD_LINE_ARGS=
57 | set _SKIP=2
58 |
59 | :win9xME_args_slurp
60 | if "x%~1" == "x" goto execute
61 |
62 | set CMD_LINE_ARGS=%*
63 | goto execute
64 |
65 | :4NT_args
66 | @rem Get arguments from the 4NT Shell from JP Software
67 | set CMD_LINE_ARGS=%$
68 |
69 | :execute
70 | @rem Setup the command line
71 |
72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if "%ERRORLEVEL%"=="0" goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
85 | exit /b 1
86 |
87 | :mainEnd
88 | if "%OS%"=="Windows_NT" endlocal
89 |
90 | :omega
91 |
--------------------------------------------------------------------------------
/presentation/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/presentation/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.library'
2 | apply plugin: 'kotlin-android'
3 | apply plugin: 'kotlin-kapt'
4 | apply plugin: 'kotlin-android-extensions'
5 | apply plugin: 'dk.nstack.translation.plugin'
6 |
7 | translation {
8 | appId = keys.appId
9 | apiKey = keys.apiKey
10 | acceptHeader = keys.acceptHeader
11 | }
12 |
13 | android {
14 | compileSdkVersion sdks.compileSdkVersion
15 | defaultConfig {
16 | minSdkVersion sdks.minSdkVersion
17 | targetSdkVersion sdks.targetSdkVersion
18 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
19 | }
20 |
21 | buildTypes {
22 | release {
23 | minifyEnabled false
24 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
25 | }
26 | }
27 | compileOptions {
28 | coreLibraryDesugaringEnabled true
29 | sourceCompatibility JavaVersion.VERSION_1_8
30 | targetCompatibility JavaVersion.VERSION_1_8
31 | }
32 | kotlinOptions {
33 | jvmTarget = "1.8"
34 | }
35 |
36 | }
37 |
38 | dependencies {
39 | implementation project(':domain')
40 | implementation "com.google.android.material:material:$versions.material"
41 | implementation "androidx.appcompat:appcompat:$versions.appcompat"
42 | implementation "androidx.arch.core:core-common:$versions.archCore"
43 | implementation "androidx.arch.core:core-runtime:$versions.archCore"
44 | implementation "androidx.constraintlayout:constraintlayout:${versions.constraint_layout}"
45 |
46 | implementation "androidx.lifecycle:lifecycle-livedata-core:${versions.lifecycle}"
47 | implementation "androidx.lifecycle:lifecycle-runtime:${versions.lifecycle}"
48 | implementation "androidx.lifecycle:lifecycle-common-java8:${versions.lifecycle}"
49 | implementation "androidx.lifecycle:lifecycle-extensions:${versions.lifecycle}"
50 | implementation "androidx.lifecycle:lifecycle-viewmodel:${versions.lifecycle}"
51 | implementation "androidx.lifecycle:lifecycle-livedata:${versions.lifecycle}"
52 | implementation "androidx.lifecycle:lifecycle-livedata-ktx:$versions.lifecycle"
53 | implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:${versions.lifecycle}"
54 |
55 | // Utils & Extensions
56 | implementation "dk.nodes.utils:kotlin:${versions.nodes_utils}"
57 | implementation "dk.nodes.utils:android:${versions.nodes_utils}"
58 |
59 | // implementation "dk.nodes.arch:base:${versions.nodes_arch}"
60 | implementation "androidx.core:core-ktx:${versions.ktx}"
61 | implementation "com.google.dagger:dagger-android:${versions.dagger}"
62 | implementation "com.google.dagger:dagger-android-support:${versions.dagger}"
63 | kapt "com.google.dagger:dagger-compiler:${versions.dagger}"
64 | kapt "com.google.dagger:dagger-android-processor:${versions.dagger}"
65 | implementation "dk.nodes.nstack:nstack-kotlin:$versions.nstack"
66 | implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:${versions.coroutines}"
67 | implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:${versions.coroutines}"
68 | testImplementation "junit:junit:$versions.junit"
69 | implementation "androidx.core:core-ktx:${versions.ktx}"
70 | implementation "com.jakewharton.timber:timber:${versions.timber}"
71 |
72 | implementation "androidx.navigation:navigation-fragment-ktx:${versions.navigation}"
73 | implementation "androidx.navigation:navigation-ui-ktx:${versions.navigation}"
74 |
75 | coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:$versions.desugaring"
76 | }
77 |
--------------------------------------------------------------------------------
/presentation/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
23 | -keep class androidx.core.app.CoreComponentFactory { *; }
24 |
--------------------------------------------------------------------------------
/presentation/src/androidTest/java/dk/nodes/template/presentation/ExampleInstrumentedTest.java:
--------------------------------------------------------------------------------
1 | package dk.nodes.template.presentation;
2 |
3 | import static org.junit.Assert.*;
4 |
5 | import android.content.Context;
6 | import android.support.test.InstrumentationRegistry;
7 | import android.support.test.runner.AndroidJUnit4;
8 | import org.junit.Test;
9 | import org.junit.runner.RunWith;
10 |
11 | /**
12 | * Instrumented test, which will execute on an Android device.
13 | *
14 | * @see Testing documentation
15 | */
16 | @RunWith(AndroidJUnit4.class)
17 | public class ExampleInstrumentedTest {
18 | @Test
19 | public void useAppContext() {
20 | // Context of the app under test.
21 | Context appContext = InstrumentationRegistry.getTargetContext();
22 |
23 | assertEquals("dk.nodes.template.presentation.test", appContext.getPackageName());
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/presentation/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
--------------------------------------------------------------------------------
/presentation/src/main/assets/translations_0_da-DK.json:
--------------------------------------------------------------------------------
1 | {"default":{"hi":"Hej","cancel":"Annuller","no":"nej","yes":"Ja","edit":"Redigere","next":"N\u00e6ste","on":"T\u00e6ndt","off":"af","ok":"Ok"},"error":{"errorRandom":"Helt tilf\u00e6ldig fejl","errorTitle":"Fejl","authenticationError":"Login er udl\u00f8bet, login venligst ind igen.","connectionError":"Ingen eller d\u00e5rlig forbindelse, pr\u00f8v igen!","unknownError":"Ukendt fejl, pr\u00f8v igen."},"test":{"title":"NStack Demo","message":"Bacon ipsum dolor amet magna meatball jerky in, shank sunt do burgdoggen spare ribs. Lorem boudin eiusmod short ribs pastrami. Sausage bresaola do turkey, dolor qui tail ground round culpa boudin nulla minim sunt beef ribs ham. Cillum in pastrami adipisicing swine lorem, velit sunt meatloaf bresaola short loin fugiat tri-tip boudin.","subTitle":"Subtitle demo","on":"on","off":"off"}}
--------------------------------------------------------------------------------
/presentation/src/main/assets/translations_1_en-GB.json:
--------------------------------------------------------------------------------
1 | {"default":{"hi":"Hi","cancel":"Cancel","no":"No","yes":"Yes","edit":"Edit","next":"Next","on":"On","off":"Off","ok":"Ok"},"error":{"errorRandom":"Totally random error","errorTitle":"Error","authenticationError":"Login expired, please login again.","connectionError":"No or bad connection, please try again.","unknownError":"Unknown error, please try again."},"test":{"title":"NStack Demo","message":"Bacon ipsum dolor amet magna meatball jerky in, shank sunt do burgdoggen spare ribs. Lorem boudin eiusmod short ribs pastrami. Sausage bresaola do turkey, dolor qui tail ground round culpa boudin nulla minim sunt beef ribs ham. Cillum in pastrami adipisicing swine lorem, velit sunt meatloaf bresaola short loin fugiat tri-tip boudin.","subTitle":"Subtitle demo","on":"on","off":"off"}}
--------------------------------------------------------------------------------
/presentation/src/main/assets/translations_2_es-MX.json:
--------------------------------------------------------------------------------
1 | {"default":{"hi":"Hola","cancel":"Cancelar","no":"no","yes":"Si","edit":"Editar","next":"Siguiente","on":"Apprendido","off":"Apagado","ok":"__ok"},"error":{"errorRandom":"__errorRandom","errorTitle":"__errorTitle","authenticationError":"__authenticationError","connectionError":"__connectionError","unknownError":"__unknownError"},"test":{"title":"__title","message":"__message","subTitle":"__subTitle","on":"__on","off":"__off"}}
--------------------------------------------------------------------------------
/presentation/src/main/assets/translations_3_fr-FR.json:
--------------------------------------------------------------------------------
1 | {"default":{"hi":"Salut","cancel":"Annuler","no":"Non","yes":"Oui","edit":"Modifier","next":"Prochain","on":"Allumez","off":"\u00c9teins","ok":"__ok"},"error":{"errorRandom":"__errorRandom","errorTitle":"__errorTitle","authenticationError":"__authenticationError","connectionError":"__connectionError","unknownError":"__unknownError"},"test":{"title":"__title","message":"__message","subTitle":"__subTitle","on":"__on","off":"__off"}}
--------------------------------------------------------------------------------
/presentation/src/main/java/dk/nodes/template/presentation/extensions/LiveDataExtensions.kt:
--------------------------------------------------------------------------------
1 | package dk.nodes.template.presentation.extensions
2 |
3 | import androidx.lifecycle.LifecycleOwner
4 | import androidx.lifecycle.LiveData
5 | import androidx.lifecycle.Observer
6 | import dk.nodes.template.presentation.util.EventObserver
7 | import dk.nodes.template.presentation.util.SingleEvent
8 |
9 | inline fun LiveData.observe(
10 | lifecycleOwner: LifecycleOwner,
11 | crossinline observer: (T?) -> Unit
12 | ) {
13 | this.observe(lifecycleOwner, Observer {
14 | observer(it)
15 | })
16 | }
17 |
18 | inline fun LiveData.observeNonNull(
19 | lifecycleOwner: LifecycleOwner,
20 | crossinline observer: (T) -> Unit
21 | ) {
22 | this.observe(lifecycleOwner, Observer {
23 | it?.let(observer)
24 | })
25 | }
26 |
27 | inline fun > LiveData.observeEvent(
28 | lifecycleOwner: LifecycleOwner,
29 | crossinline observer: (E) -> Unit
30 | ) {
31 | this.observe(lifecycleOwner, EventObserver {
32 | observer(it)
33 | })
34 | }
35 |
--------------------------------------------------------------------------------
/presentation/src/main/java/dk/nodes/template/presentation/extensions/ViewModelStoreOwnerExtensions.kt:
--------------------------------------------------------------------------------
1 | package dk.nodes.template.presentation.extensions
2 |
3 | import androidx.fragment.app.Fragment
4 | import androidx.lifecycle.Lifecycle
5 | import androidx.lifecycle.LifecycleObserver
6 | import androidx.lifecycle.LifecycleOwner
7 | import androidx.lifecycle.OnLifecycleEvent
8 | import androidx.lifecycle.ViewModel
9 | import androidx.lifecycle.ViewModelProvider
10 | import androidx.lifecycle.ViewModelStoreOwner
11 | import java.io.Serializable
12 |
13 | inline fun ViewModelStoreOwner.getViewModel(factory: ViewModelProvider.Factory): VM {
14 | return ViewModelProvider(this, factory).get(VM::class.java)
15 | }
16 |
17 | inline fun Fragment.getSharedViewModel(factory: ViewModelProvider.Factory): VM {
18 | return ViewModelProvider(requireActivity(), factory).get(VM::class.java)
19 | }
20 |
21 | private object UninitializedValue
22 |
23 | /**
24 | * This was copied from SynchronizedLazyImpl but modified to automatically initialize in ON_CREATE.
25 | */
26 | @Suppress("ClassName")
27 | class lifecycleAwareLazy(private val owner: LifecycleOwner, initializer: () -> T) : Lazy,
28 | Serializable {
29 | private var initializer: (() -> T)? = initializer
30 | @Volatile
31 | private var _value: Any? = UninitializedValue
32 | // final field is required to enable safe publication of constructed instance
33 | private val lock = this
34 |
35 | init {
36 | owner.lifecycle.addObserver(object : LifecycleObserver {
37 | @OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
38 | fun onStart() {
39 | if (!isInitialized()) value
40 | owner.lifecycle.removeObserver(this)
41 | }
42 | })
43 | }
44 |
45 | @Suppress("LocalVariableName")
46 | override val value: T
47 | get() {
48 | val _v1 = _value
49 | if (_v1 !== UninitializedValue) {
50 | @Suppress("UNCHECKED_CAST")
51 | return _v1 as T
52 | }
53 |
54 | return synchronized(lock) {
55 | val _v2 = _value
56 | if (_v2 !== UninitializedValue) {
57 | @Suppress("UNCHECKED_CAST") (_v2 as T)
58 | } else {
59 | val typedValue = initializer!!()
60 | _value = typedValue
61 | initializer = null
62 | typedValue
63 | }
64 | }
65 | }
66 |
67 | override fun isInitialized(): Boolean = _value !== UninitializedValue
68 |
69 | override fun toString(): String =
70 | if (isInitialized()) value.toString() else "Lazy value not initialized yet."
71 | }
--------------------------------------------------------------------------------
/presentation/src/main/java/dk/nodes/template/presentation/injection/DaggerViewModelFactory.kt:
--------------------------------------------------------------------------------
1 | package dk.nodes.template.presentation.injection
2 |
3 | import androidx.lifecycle.ViewModel
4 | import androidx.lifecycle.ViewModelProvider
5 | import javax.inject.Inject
6 | import javax.inject.Provider
7 |
8 | internal class DaggerViewModelFactory @Inject constructor(
9 | private val creators: @JvmSuppressWildcards Map, Provider>
10 | ) : ViewModelProvider.Factory {
11 | override fun create(modelClass: Class): T {
12 | var creator: Provider? = creators[modelClass]
13 | if (creator == null) {
14 | for ((key, value) in creators) {
15 | if (modelClass.isAssignableFrom(key)) {
16 | creator = value
17 | break
18 | }
19 | }
20 | }
21 | if (creator == null) {
22 | throw IllegalArgumentException("Unknown model class: $modelClass")
23 | }
24 | try {
25 | @Suppress("UNCHECKED_CAST")
26 | return creator.get() as T
27 | } catch (e: Exception) {
28 | throw RuntimeException(e)
29 | }
30 | }
31 | }
--------------------------------------------------------------------------------
/presentation/src/main/java/dk/nodes/template/presentation/injection/PresentationModule.kt:
--------------------------------------------------------------------------------
1 | package dk.nodes.template.presentation.injection
2 |
3 | import dagger.Module
4 | import dk.nodes.template.presentation.ui.main.MainActivityBuilder
5 | import dk.nodes.template.presentation.ui.splash.SplashBuilder
6 |
7 | @Module(includes = [
8 | MainActivityBuilder::class,
9 | SplashBuilder::class
10 | ])
11 | class PresentationModule
--------------------------------------------------------------------------------
/presentation/src/main/java/dk/nodes/template/presentation/injection/ViewModelBuilder.kt:
--------------------------------------------------------------------------------
1 | package dk.nodes.template.presentation.injection
2 |
3 | import androidx.lifecycle.ViewModelProvider
4 | import dagger.Binds
5 | import dagger.Module
6 |
7 | @Module
8 | abstract class ViewModelBuilder {
9 |
10 | @Binds
11 | internal abstract fun bindViewModelFactory(factory: DaggerViewModelFactory): ViewModelProvider.Factory
12 | }
--------------------------------------------------------------------------------
/presentation/src/main/java/dk/nodes/template/presentation/injection/ViewModelKey.kt:
--------------------------------------------------------------------------------
1 | package dk.nodes.template.presentation.injection
2 |
3 | import androidx.lifecycle.ViewModel
4 | import dagger.MapKey
5 | import kotlin.reflect.KClass
6 |
7 | @Target(
8 | AnnotationTarget.FUNCTION,
9 | AnnotationTarget.PROPERTY_GETTER,
10 | AnnotationTarget.PROPERTY_SETTER
11 | )
12 | @Retention(AnnotationRetention.RUNTIME)
13 | @MapKey
14 | annotation class ViewModelKey(val value: KClass)
--------------------------------------------------------------------------------
/presentation/src/main/java/dk/nodes/template/presentation/nstack/NStackPresenter.kt:
--------------------------------------------------------------------------------
1 | package dk.nodes.template.presentation.nstack
2 |
3 | import dk.nodes.nstack.kotlin.models.AppOpenData
4 | import dk.nodes.nstack.kotlin.models.AppUpdate
5 | import dk.nodes.nstack.kotlin.models.AppUpdateState
6 | import dk.nodes.nstack.kotlin.models.Message
7 | import dk.nodes.nstack.kotlin.models.RateReminder
8 | import dk.nodes.nstack.kotlin.models.state
9 | import javax.inject.Inject
10 | import javax.inject.Singleton
11 |
12 | @Singleton
13 | class NStackPresenter @Inject constructor() {
14 |
15 | private var appOpenData: AppOpenData? = null
16 |
17 | fun saveAppState(appUpdate: AppOpenData) {
18 | this.appOpenData = appUpdate
19 | }
20 |
21 | fun whenMessage(callback: (Message) -> Unit): NStackPresenter {
22 | appOpenData?.message?.let(callback)
23 | return this
24 | }
25 |
26 | fun whenChangelog(callback: (AppUpdate) -> Unit): NStackPresenter {
27 | if (appOpenData?.update?.state == AppUpdateState.CHANGELOG) {
28 | callback.invoke(appOpenData?.update ?: return this)
29 | }
30 | return this
31 | }
32 |
33 | fun whenRateReminder(callback: (RateReminder) -> Unit): NStackPresenter {
34 | appOpenData?.rateReminder?.let(callback)
35 | return this
36 | }
37 | }
--------------------------------------------------------------------------------
/presentation/src/main/java/dk/nodes/template/presentation/nstack/RateReminderActions.kt:
--------------------------------------------------------------------------------
1 | package dk.nodes.template.presentation.nstack
2 |
3 | import dk.nodes.nstack.kotlin.NStack
4 |
5 | /**
6 | * Generated by the NStack gradle plugin
7 | */
8 | object RateReminderActions {
9 |
10 | private const val TEST = "test"
11 |
12 | suspend fun test() {
13 | send(TEST)
14 | }
15 |
16 | private suspend fun send(action: String) {
17 | NStack.RateReminder.action(action)
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/presentation/src/main/java/dk/nodes/template/presentation/nstack/Translation.java:
--------------------------------------------------------------------------------
1 | package dk.nodes.template.presentation.nstack;
2 |
3 | /**
4 | * Created by nstack.io gradle translation plugin
5 | * Built from Accept Header: da-DK
6 | */
7 |
8 | public class Translation {
9 | public final static class defaultSection {
10 | public static String hi = "Hej";
11 | public static String cancel = "Annuller";
12 | public static String no = "nej";
13 | public static String yes = "Ja";
14 | public static String edit = "Redigere";
15 | public static String next = "N\u00E6ste";
16 | public static String on = "T\u00E6ndt";
17 | public static String off = "af";
18 | public static String ok = "Ok";
19 | }
20 | public final static class error {
21 | public static String errorRandom = "Helt tilf\u00E6ldig fejl";
22 | public static String errorTitle = "Fejl";
23 | public static String authenticationError = "Login er udl\u00F8bet, login venligst ind igen.";
24 | public static String connectionError = "Ingen eller d\u00E5rlig forbindelse, pr\u00F8v igen!";
25 | public static String unknownError = "Ukendt fejl, pr\u00F8v igen.";
26 | }
27 | public final static class test {
28 | public static String title = "NStack Demo";
29 | public static String message = "Bacon ipsum dolor amet magna meatball jerky in, shank sunt do burgdoggen spare ribs. Lorem boudin eiusmod short ribs pastrami. Sausage bresaola do turkey, dolor qui tail ground round culpa boudin nulla minim sunt beef ribs ham. Cillum in pastrami adipisicing swine lorem, velit sunt meatloaf bresaola short loin fugiat tri-tip boudin.";
30 | public static String subTitle = "Subtitle demo";
31 | public static String on = "on";
32 | public static String off = "off";
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/presentation/src/main/java/dk/nodes/template/presentation/ui/base/BaseActivity.kt:
--------------------------------------------------------------------------------
1 | package dk.nodes.template.presentation.ui.base
2 |
3 | import android.content.Context
4 | import android.os.Bundle
5 | import androidx.annotation.LayoutRes
6 | import androidx.appcompat.app.AppCompatActivity
7 | import androidx.lifecycle.ViewModel
8 | import androidx.lifecycle.ViewModelProvider
9 | import dagger.android.AndroidInjection
10 | import dagger.android.DispatchingAndroidInjector
11 | import dagger.android.HasAndroidInjector
12 | import dk.nodes.nstack.kotlin.inflater.NStackBaseContext
13 | import dk.nodes.template.presentation.extensions.getViewModel
14 | import dk.nodes.template.presentation.extensions.lifecycleAwareLazy
15 | import javax.inject.Inject
16 |
17 | abstract class BaseActivity : AppCompatActivity, HasAndroidInjector {
18 |
19 | constructor() : super()
20 | constructor(@LayoutRes resId: Int) : super(resId)
21 |
22 | @Inject
23 | lateinit var viewModelFactory: ViewModelProvider.Factory
24 |
25 | @Inject
26 | lateinit var androidInjector: DispatchingAndroidInjector
27 |
28 | override fun onCreate(savedInstanceState: Bundle?) {
29 | AndroidInjection.inject(this)
30 | super.onCreate(savedInstanceState)
31 | }
32 |
33 | override fun attachBaseContext(newBase: Context) {
34 | super.attachBaseContext(NStackBaseContext(newBase))
35 | }
36 |
37 | protected inline fun getViewModel(): VM =
38 | getViewModel(viewModelFactory)
39 |
40 | protected inline fun viewModel(): Lazy {
41 | return lifecycleAwareLazy(this) { getViewModel() }
42 | }
43 |
44 | override fun androidInjector() = androidInjector
45 | }
46 |
--------------------------------------------------------------------------------
/presentation/src/main/java/dk/nodes/template/presentation/ui/base/BaseFragment.kt:
--------------------------------------------------------------------------------
1 | package dk.nodes.template.presentation.ui.base
2 |
3 | import android.content.Context
4 | import androidx.annotation.LayoutRes
5 | import androidx.fragment.app.Fragment
6 | import androidx.lifecycle.ViewModel
7 | import androidx.lifecycle.ViewModelProvider
8 | import dagger.android.DispatchingAndroidInjector
9 | import dagger.android.HasAndroidInjector
10 | import dagger.android.support.AndroidSupportInjection
11 | import dk.nodes.template.presentation.extensions.getSharedViewModel
12 | import dk.nodes.template.presentation.extensions.getViewModel
13 | import dk.nodes.template.presentation.extensions.lifecycleAwareLazy
14 | import dk.nodes.template.presentation.util.ViewErrorController
15 | import javax.inject.Inject
16 |
17 | abstract class BaseFragment : Fragment, HasAndroidInjector {
18 |
19 | constructor()
20 | constructor(@LayoutRes resId: Int) : super(resId)
21 |
22 | @Inject
23 | lateinit var viewModelFactory: ViewModelProvider.Factory
24 |
25 | @Inject
26 | lateinit var defaultErrorController: dagger.Lazy
27 |
28 | @Inject
29 | lateinit var androidInjector: DispatchingAndroidInjector
30 |
31 | protected inline fun getViewModel(): VM =
32 | getViewModel(viewModelFactory)
33 |
34 | protected inline fun getSharedViewModel(): VM =
35 | getSharedViewModel(viewModelFactory)
36 |
37 | protected inline fun viewModel(): Lazy = lifecycleAwareLazy(this) {
38 | getViewModel()
39 | }
40 |
41 | protected inline fun sharedViewModel(): Lazy =
42 | lifecycleAwareLazy(this) {
43 | getSharedViewModel()
44 | }
45 |
46 | override fun onAttach(context: Context) {
47 | AndroidSupportInjection.inject(this)
48 | super.onAttach(context)
49 | }
50 |
51 | override fun androidInjector() = androidInjector
52 | }
--------------------------------------------------------------------------------
/presentation/src/main/java/dk/nodes/template/presentation/ui/base/BaseViewModel.kt:
--------------------------------------------------------------------------------
1 | package dk.nodes.template.presentation.ui.base
2 |
3 | import androidx.lifecycle.LiveData
4 | import androidx.lifecycle.MediatorLiveData
5 | import androidx.lifecycle.ViewModel
6 | import androidx.lifecycle.distinctUntilChanged
7 |
8 | abstract class BaseViewModel(initState: T) : ViewModel() {
9 |
10 | private val _viewState = MediatorLiveData().apply { value = initState }
11 | val viewState = _viewState.distinctUntilChanged()
12 | protected var state
13 | get() = _viewState.value!!
14 | set(value) {
15 | _viewState.value = value
16 | }
17 |
18 | protected var stateAsync: T = state
19 | set(value) {
20 | _viewState.postValue(value) // Sets the value asynchronously
21 | }
22 |
23 | protected fun addStateSource(source: LiveData, onChanged: (T) -> Unit) {
24 | _viewState.addSource(source, onChanged)
25 | }
26 | }
--------------------------------------------------------------------------------
/presentation/src/main/java/dk/nodes/template/presentation/ui/main/MainActivity+NStack.kt:
--------------------------------------------------------------------------------
1 | package dk.nodes.template.presentation.ui.main
2 |
3 | import android.content.Intent
4 | import android.net.Uri
5 | import androidx.appcompat.app.AlertDialog
6 | import androidx.lifecycle.lifecycleScope
7 | import dk.nodes.nstack.kotlin.NStack
8 | import dk.nodes.nstack.kotlin.models.AppUpdate
9 | import dk.nodes.nstack.kotlin.models.AppUpdateState
10 | import dk.nodes.nstack.kotlin.models.Message
11 | import dk.nodes.nstack.kotlin.models.RateReminder
12 | import dk.nodes.nstack.kotlin.models.Result
13 | import dk.nodes.nstack.kotlin.models.state
14 | import dk.nodes.nstack.kotlin.models.update
15 | import dk.nodes.template.presentation.nstack.Translation
16 | import kotlinx.coroutines.Dispatchers
17 | import kotlinx.coroutines.launch
18 | import kotlinx.coroutines.withContext
19 |
20 | fun MainActivity.setupNStack() {
21 | lifecycleScope.launch {
22 | when (val result = withContext(Dispatchers.IO) { NStack.appOpen() }) {
23 | is Result.Success -> {
24 | when (result.value.data.update.state) {
25 | AppUpdateState.NONE -> { /* Nothing to do */
26 | }
27 | AppUpdateState.UPDATE -> showUpdateDialog(result.value.data.update)
28 | AppUpdateState.FORCE -> showForceDialog(result.value.data.update)
29 | AppUpdateState.CHANGELOG -> showChangelogDialog(result.value.data.update)
30 | }
31 |
32 | result.value.data.message?.let { showMessageDialog(it) }
33 | result.value.data.rateReminder?.let { showRateReminderDialog(it) }
34 | }
35 | is Result.Error -> {
36 | }
37 | }
38 | }
39 | }
40 |
41 | fun MainActivity.showRateReminderDialog(rateReminder: RateReminder) {
42 | AlertDialog.Builder(this)
43 | .setMessage(rateReminder.body)
44 | .setTitle(rateReminder.title)
45 | .setCancelable(false)
46 | .setPositiveButton(rateReminder.yesButton) { dialog, _ ->
47 | NStack.onRateReminderAction(true)
48 | startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(rateReminder.link)))
49 | dialog.dismiss()
50 | }
51 | .setNegativeButton(rateReminder.noButton) { dialog, _ ->
52 | NStack.onRateReminderAction(false)
53 | dialog.dismiss()
54 | }
55 | .setNeutralButton(rateReminder.laterButton) { dialog, _ ->
56 | dialog.dismiss()
57 | }
58 | .show()
59 | }
60 |
61 | fun MainActivity.showMessageDialog(message: Message) {
62 | AlertDialog.Builder(this)
63 | .setMessage(message.message)
64 | .setCancelable(false)
65 | .setPositiveButton(Translation.defaultSection.ok) { dialog, _ ->
66 | NStack.messageSeen(message)
67 | dialog.dismiss()
68 | }
69 | .show()
70 | }
71 |
72 | fun MainActivity.showUpdateDialog(appUpdate: AppUpdate) {
73 | AlertDialog.Builder(this)
74 | .setTitle(appUpdate.update?.translate?.title ?: return)
75 | .setMessage(appUpdate.update?.translate?.message ?: return)
76 | .setPositiveButton(appUpdate.update?.translate?.positiveButton) { dialog, _ ->
77 | dialog.dismiss()
78 | }
79 | .show()
80 | }
81 |
82 | fun MainActivity.showChangelogDialog(appUpdate: AppUpdate) {
83 | AlertDialog.Builder(this)
84 | .setTitle(appUpdate.update?.translate?.title ?: return)
85 | .setMessage(appUpdate.update?.translate?.message ?: return)
86 | .setNegativeButton(appUpdate.update?.translate?.negativeButton ?: return) { dialog, _ ->
87 | dialog.dismiss()
88 | }
89 | .show()
90 | }
91 |
92 | fun MainActivity.startPlayStore() {
93 | try {
94 | startActivity(
95 | Intent(
96 | Intent.ACTION_VIEW,
97 | Uri.parse("market://details?id=$packageName")
98 | )
99 | )
100 | } catch (anfe: android.content.ActivityNotFoundException) {
101 | startActivity(
102 | Intent(
103 | Intent.ACTION_VIEW,
104 | Uri.parse("https://play.google.com/store/apps/details?id=$packageName")
105 | )
106 | )
107 | }
108 | }
109 |
110 | fun MainActivity.showForceDialog(appUpdate: AppUpdate) {
111 | val dialog = AlertDialog.Builder(this)
112 | .setTitle(appUpdate.update?.translate?.title ?: return)
113 | .setMessage(appUpdate.update?.translate?.message ?: return)
114 | .setCancelable(false)
115 | .setPositiveButton(appUpdate.update?.translate?.positiveButton, null)
116 | .create()
117 |
118 | dialog.setOnShowListener {
119 | val b = dialog.getButton(AlertDialog.BUTTON_POSITIVE)
120 | b.setOnClickListener {
121 | startPlayStore()
122 | }
123 | }
124 |
125 | dialog.show()
126 | }
127 |
--------------------------------------------------------------------------------
/presentation/src/main/java/dk/nodes/template/presentation/ui/main/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package dk.nodes.template.presentation.ui.main
2 |
3 | import android.content.Context
4 | import android.content.Intent
5 | import android.os.Bundle
6 | import dk.nodes.template.domain.extensions.guard
7 | import dk.nodes.template.presentation.R
8 | import dk.nodes.template.presentation.extensions.observeNonNull
9 | import dk.nodes.template.presentation.ui.base.BaseActivity
10 | import dk.nodes.template.presentation.util.consume
11 |
12 | class MainActivity : BaseActivity(R.layout.activity_main) {
13 |
14 | private val viewModel by viewModel()
15 |
16 | override fun onCreate(savedInstanceState: Bundle?) {
17 | super.onCreate(savedInstanceState)
18 | viewModel.viewState.observeNonNull(this) { state ->
19 | handleNStack(state)
20 | }
21 | savedInstanceState.guard { viewModel.checkNStack() }
22 | }
23 |
24 | private fun handleNStack(viewState: MainActivityViewState) {
25 | viewState.nstackMessage.consume { showMessageDialog(it) }
26 | viewState.nstackRateReminder.consume { showRateReminderDialog(it) }
27 | viewState.nstackUpdate.consume { showChangelogDialog(it) }
28 | }
29 |
30 | companion object {
31 | fun createIntent(context: Context) = Intent(context, MainActivity::class.java)
32 | .apply {
33 | flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
34 | }
35 | }
36 | }
--------------------------------------------------------------------------------
/presentation/src/main/java/dk/nodes/template/presentation/ui/main/MainActivityBuilder.kt:
--------------------------------------------------------------------------------
1 | package dk.nodes.template.presentation.ui.main
2 |
3 | import androidx.lifecycle.ViewModel
4 | import dagger.Binds
5 | import dagger.Module
6 | import dagger.android.ContributesAndroidInjector
7 | import dagger.multibindings.IntoMap
8 | import dk.nodes.template.presentation.injection.ViewModelKey
9 | import dk.nodes.template.presentation.ui.sample.SampleBuilder
10 |
11 | @Module
12 | internal abstract class MainActivityBuilder {
13 |
14 | @Binds
15 | @IntoMap
16 | @ViewModelKey(MainActivityViewModel::class)
17 | abstract fun bindMainActivityViewMode(viewModel: MainActivityViewModel): ViewModel
18 |
19 | @ContributesAndroidInjector(
20 | modules = [
21 | SampleBuilder::class
22 | ]
23 | )
24 | internal abstract fun mainActivity(): MainActivity
25 | }
--------------------------------------------------------------------------------
/presentation/src/main/java/dk/nodes/template/presentation/ui/main/MainActivityViewModel.kt:
--------------------------------------------------------------------------------
1 | package dk.nodes.template.presentation.ui.main
2 |
3 | import androidx.lifecycle.viewModelScope
4 | import dk.nodes.template.presentation.nstack.NStackPresenter
5 | import dk.nodes.template.presentation.ui.base.BaseViewModel
6 | import dk.nodes.template.presentation.util.SingleEvent
7 | import kotlinx.coroutines.Dispatchers
8 | import kotlinx.coroutines.delay
9 | import kotlinx.coroutines.launch
10 | import kotlinx.coroutines.withContext
11 | import javax.inject.Inject
12 |
13 | class MainActivityViewModel @Inject constructor(
14 | private val nStackPresenter: NStackPresenter
15 | ) : BaseViewModel(MainActivityViewState()) {
16 |
17 | fun checkNStack() = viewModelScope.launch {
18 | // Delay popup a bit so it's not super intrusive
19 | withContext(Dispatchers.IO) { delay(1000) }
20 | nStackPresenter
21 | .whenChangelog {
22 | state = state.copy(nstackUpdate = SingleEvent(it))
23 | }.whenMessage {
24 | state = state.copy(nstackMessage = SingleEvent(it))
25 | }.whenRateReminder {
26 | state = state.copy(nstackRateReminder = SingleEvent(it))
27 | }
28 | }
29 | }
--------------------------------------------------------------------------------
/presentation/src/main/java/dk/nodes/template/presentation/ui/main/MainActivityViewState.kt:
--------------------------------------------------------------------------------
1 | package dk.nodes.template.presentation.ui.main
2 |
3 | import dk.nodes.nstack.kotlin.models.AppUpdate
4 | import dk.nodes.nstack.kotlin.models.Message
5 | import dk.nodes.nstack.kotlin.models.RateReminder
6 | import dk.nodes.template.presentation.util.SingleEvent
7 |
8 | data class MainActivityViewState(
9 | val errorMessage: SingleEvent? = null,
10 | val isLoading: Boolean = false,
11 | val nstackMessage: SingleEvent? = null,
12 | val nstackRateReminder: SingleEvent? = null,
13 | val nstackUpdate: SingleEvent? = null
14 | )
--------------------------------------------------------------------------------
/presentation/src/main/java/dk/nodes/template/presentation/ui/sample/SampleAdapter.kt:
--------------------------------------------------------------------------------
1 | package dk.nodes.template.presentation.ui.sample
2 |
3 | import android.view.View
4 | import android.view.ViewGroup
5 | import androidx.recyclerview.widget.DiffUtil
6 | import androidx.recyclerview.widget.RecyclerView
7 | import dk.nodes.template.domain.entities.Post
8 | import dk.nodes.template.presentation.R
9 | import dk.nodes.utils.android.view.inflate
10 | import kotlinx.android.synthetic.main.row_sample.view.*
11 |
12 | class SampleAdapter : RecyclerView.Adapter() {
13 |
14 | private val list = mutableListOf()
15 |
16 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
17 | return ViewHolder(parent.inflate(R.layout.row_sample))
18 | }
19 |
20 | override fun getItemCount() = list.size
21 |
22 | override fun onBindViewHolder(holder: ViewHolder, position: Int) {
23 | holder.bind(list[position])
24 | }
25 |
26 | fun setData(list: List) {
27 | val diff = DiffUtil.calculateDiff(PostDiff(this.list, list))
28 | this.list.clear()
29 | this.list += list
30 | diff.dispatchUpdatesTo(this)
31 | }
32 |
33 | class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
34 |
35 | fun bind(post: Post) {
36 | itemView.run {
37 | titleTv.text = post.title
38 | bodyTv.text = post.body
39 | }
40 | }
41 | }
42 |
43 | private inner class PostDiff(
44 | private val oldList: List,
45 | private val newList: List
46 | ) : DiffUtil.Callback() {
47 | override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
48 | return newList[newItemPosition].id == oldList[oldItemPosition].id
49 | }
50 |
51 | override fun getOldListSize(): Int {
52 | return oldList.size
53 | }
54 |
55 | override fun getNewListSize(): Int {
56 | return newList.size
57 | }
58 |
59 | override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
60 | return newList[newItemPosition] == oldList[oldItemPosition]
61 | }
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/presentation/src/main/java/dk/nodes/template/presentation/ui/sample/SampleBuilder.kt:
--------------------------------------------------------------------------------
1 | package dk.nodes.template.presentation.ui.sample
2 |
3 | import androidx.lifecycle.ViewModel
4 | import dagger.Binds
5 | import dagger.Module
6 | import dagger.android.ContributesAndroidInjector
7 | import dagger.multibindings.IntoMap
8 | import dk.nodes.template.presentation.injection.ViewModelKey
9 |
10 | @Module
11 | abstract class SampleBuilder {
12 |
13 | @ContributesAndroidInjector
14 | abstract fun sampleFragment(): SampleFragment
15 |
16 | @Binds
17 | @IntoMap
18 | @ViewModelKey(SampleViewModel::class)
19 | internal abstract fun bindSampleViewModel(viewModel: SampleViewModel): ViewModel
20 | }
--------------------------------------------------------------------------------
/presentation/src/main/java/dk/nodes/template/presentation/ui/sample/SampleFragment.kt:
--------------------------------------------------------------------------------
1 | package dk.nodes.template.presentation.ui.sample
2 |
3 | import android.os.Bundle
4 | import android.view.View
5 | import androidx.core.view.isVisible
6 | import dk.nodes.template.presentation.R
7 | import dk.nodes.template.presentation.extensions.observeNonNull
8 | import dk.nodes.template.presentation.ui.base.BaseFragment
9 | import dk.nodes.template.presentation.util.consume
10 | import kotlinx.android.synthetic.main.fragment_sample.*
11 |
12 | class SampleFragment : BaseFragment(R.layout.fragment_sample) {
13 |
14 | private val viewModel by viewModel()
15 | private lateinit var adapter: SampleAdapter
16 |
17 | override fun onCreate(savedInstanceState: Bundle?) {
18 | super.onCreate(savedInstanceState)
19 | viewModel.fetchPosts()
20 | }
21 |
22 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
23 | super.onViewCreated(view, savedInstanceState)
24 | fragSampleFab.setOnClickListener { viewModel.switchTheme() }
25 | adapter = SampleAdapter().also(rv::setAdapter)
26 | }
27 |
28 | override fun onActivityCreated(savedInstanceState: Bundle?) {
29 | super.onActivityCreated(savedInstanceState)
30 | viewModel.viewState.observeNonNull(this) { state ->
31 | showLoading(state)
32 | showPosts(state)
33 | showErrorMessage(state)
34 | }
35 | }
36 |
37 | private fun showPosts(state: SampleViewState) {
38 | adapter.setData(state.posts)
39 | }
40 |
41 | private fun showLoading(state: SampleViewState) {
42 | postsProgressBar.isVisible = state.isLoading
43 | rv.isVisible = !state.isLoading
44 | }
45 |
46 | private fun showErrorMessage(state: SampleViewState) {
47 | state.viewError.consume { viewError ->
48 | defaultErrorController.get().showErrorSnackbar(requireView(), viewError) {
49 | viewModel.fetchPosts()
50 | }
51 | }
52 | }
53 | }
--------------------------------------------------------------------------------
/presentation/src/main/java/dk/nodes/template/presentation/ui/sample/SampleViewModel.kt:
--------------------------------------------------------------------------------
1 | package dk.nodes.template.presentation.ui.sample
2 |
3 | import androidx.lifecycle.viewModelScope
4 | import dk.nodes.template.domain.interactors.Fail
5 | import dk.nodes.template.domain.interactors.InteractorResult
6 | import dk.nodes.template.domain.interactors.Loading
7 | import dk.nodes.template.domain.interactors.PostFlowInteractor
8 | import dk.nodes.template.domain.interactors.Success
9 | import dk.nodes.template.domain.interactors.SwitchThemeInteractor
10 | import dk.nodes.template.domain.interactors.asResult
11 | import dk.nodes.template.domain.interactors.invoke
12 | import dk.nodes.template.domain.interactors.runInteractor
13 | import dk.nodes.template.presentation.ui.base.BaseViewModel
14 | import dk.nodes.template.presentation.util.SingleEvent
15 | import dk.nodes.template.presentation.util.ThemeHelper
16 | import dk.nodes.template.presentation.util.ViewErrorController
17 | import kotlinx.coroutines.Dispatchers
18 | import kotlinx.coroutines.flow.collect
19 | import kotlinx.coroutines.launch
20 | import javax.inject.Inject
21 |
22 | class SampleViewModel @Inject constructor(
23 | private val postsInteractor: PostFlowInteractor,
24 | private val switchThemeInteractor: SwitchThemeInteractor
25 | ) : BaseViewModel(SampleViewState()) {
26 |
27 | init {
28 | viewModelScope.launch {
29 | postsInteractor.flow().collect {
30 | state = state.copy(posts = it)
31 | }
32 | }
33 | }
34 |
35 | fun fetchPosts() {
36 | viewModelScope.launch(Dispatchers.Main) {
37 | state = mapResult(Loading())
38 | val result = runInteractor(postsInteractor.asResult())
39 | state = mapResult(result)
40 | }
41 | }
42 |
43 | fun switchTheme() {
44 | viewModelScope.launch {
45 | ThemeHelper.applyTheme(switchThemeInteractor())
46 | }
47 | }
48 |
49 | private fun mapResult(result: InteractorResult): SampleViewState {
50 | return when (result) {
51 | is Success -> state.copy(isLoading = false)
52 | is Loading -> state.copy(isLoading = true)
53 | is Fail -> state.copy(
54 | viewError = SingleEvent(ViewErrorController.mapThrowable(result.throwable)),
55 | isLoading = false
56 | )
57 | else -> SampleViewState()
58 | }
59 | }
60 | }
--------------------------------------------------------------------------------
/presentation/src/main/java/dk/nodes/template/presentation/ui/sample/SampleViewState.kt:
--------------------------------------------------------------------------------
1 | package dk.nodes.template.presentation.ui.sample
2 |
3 | import dk.nodes.template.domain.entities.Post
4 | import dk.nodes.template.presentation.util.SingleEvent
5 | import dk.nodes.template.presentation.util.ViewError
6 |
7 | data class SampleViewState(
8 | val posts: List = emptyList(),
9 | val viewError: SingleEvent? = null,
10 | val isLoading: Boolean = false
11 | )
--------------------------------------------------------------------------------
/presentation/src/main/java/dk/nodes/template/presentation/ui/splash/SplashActivity.kt:
--------------------------------------------------------------------------------
1 | package dk.nodes.template.presentation.ui.splash
2 |
3 | import dk.nodes.template.presentation.R
4 | import dk.nodes.template.presentation.ui.base.BaseActivity
5 |
6 | class SplashActivity : BaseActivity(R.layout.activity_splash)
--------------------------------------------------------------------------------
/presentation/src/main/java/dk/nodes/template/presentation/ui/splash/SplashBuilder.kt:
--------------------------------------------------------------------------------
1 | package dk.nodes.template.presentation.ui.splash
2 |
3 | import androidx.lifecycle.ViewModel
4 | import dagger.Binds
5 | import dagger.Module
6 | import dagger.android.ContributesAndroidInjector
7 | import dagger.multibindings.IntoMap
8 | import dk.nodes.template.presentation.injection.ViewModelKey
9 |
10 | @Module
11 | abstract class SplashBuilder {
12 |
13 | @Binds
14 | @IntoMap
15 | @ViewModelKey(SplashViewModel::class)
16 | internal abstract fun bindSplashViewModel(viewModel: SplashViewModel): ViewModel
17 |
18 | @ContributesAndroidInjector
19 | abstract fun splashActivity(): SplashActivity
20 |
21 | @ContributesAndroidInjector
22 | abstract fun splashFragment(): SplashFragment
23 | }
--------------------------------------------------------------------------------
/presentation/src/main/java/dk/nodes/template/presentation/ui/splash/SplashFragment.kt:
--------------------------------------------------------------------------------
1 | package dk.nodes.template.presentation.ui.splash
2 |
3 | import android.content.Intent
4 | import android.net.Uri
5 | import android.os.Bundle
6 | import android.view.View
7 | import androidx.appcompat.app.AlertDialog
8 | import dk.nodes.nstack.kotlin.models.AppUpdate
9 | import dk.nodes.nstack.kotlin.models.AppUpdateState
10 | import dk.nodes.nstack.kotlin.models.state
11 | import dk.nodes.nstack.kotlin.models.update
12 | import dk.nodes.template.presentation.R
13 | import dk.nodes.template.presentation.extensions.observeNonNull
14 | import dk.nodes.template.presentation.ui.base.BaseFragment
15 | import dk.nodes.template.presentation.ui.main.*
16 | import dk.nodes.template.presentation.util.consume
17 |
18 | class SplashFragment : BaseFragment(R.layout.fragment_splash) {
19 |
20 | private val viewModel by viewModel()
21 |
22 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
23 | super.onViewCreated(view, savedInstanceState)
24 | viewModel.viewState.observeNonNull(viewLifecycleOwner) { state ->
25 | handleNStack(state)
26 | handleNavigation(state)
27 | }
28 | viewModel.initAppState()
29 | }
30 |
31 | private fun handleNStack(state: SplashViewState) {
32 | state.nstackUpdateAvailable.consume { appUpdate ->
33 | when (appUpdate.state) {
34 | AppUpdateState.FORCE -> {
35 | showForceDialog(appUpdate)
36 | }
37 | // We handle the rest in MainActivity
38 | else -> {
39 | }
40 | }
41 | }
42 | }
43 |
44 | private fun handleNavigation(state: SplashViewState) {
45 | if (state.doneLoading && state.nstackUpdateAvailable?.peek()?.state != AppUpdateState.FORCE) {
46 | showApp()
47 | }
48 | }
49 |
50 | private fun showApp() {
51 | startActivity(MainActivity.createIntent(requireContext()))
52 | activity?.overridePendingTransition(0, 0)
53 | }
54 |
55 | private fun startPlayStore() {
56 | try {
57 | startActivity(
58 | Intent(
59 | Intent.ACTION_VIEW,
60 | Uri.parse("market://details?id=${context?.packageName}")
61 | )
62 | )
63 | } catch (anfe: android.content.ActivityNotFoundException) {
64 | startActivity(
65 | Intent(
66 | Intent.ACTION_VIEW,
67 | Uri.parse("https://play.google.com/store/apps/details?id=${context?.packageName}")
68 | )
69 | )
70 | }
71 | }
72 |
73 | private fun showForceDialog(appUpdate: AppUpdate) {
74 | val dialog = AlertDialog.Builder(context ?: return)
75 | .setTitle(appUpdate.update?.translate?.title ?: return)
76 | .setMessage(appUpdate.update?.translate?.message ?: return)
77 | .setCancelable(false)
78 | .setPositiveButton(appUpdate.update?.translate?.positiveButton, null)
79 | .create()
80 |
81 | dialog.setOnShowListener {
82 | val b = dialog.getButton(AlertDialog.BUTTON_POSITIVE)
83 | b.setOnClickListener {
84 | startPlayStore()
85 | }
86 | }
87 |
88 | dialog.show()
89 | }
90 | }
--------------------------------------------------------------------------------
/presentation/src/main/java/dk/nodes/template/presentation/ui/splash/SplashViewModel.kt:
--------------------------------------------------------------------------------
1 | package dk.nodes.template.presentation.ui.splash
2 |
3 | import androidx.lifecycle.viewModelScope
4 | import dk.nodes.nstack.kotlin.NStack
5 | import dk.nodes.nstack.kotlin.models.Result
6 | import dk.nodes.template.presentation.nstack.NStackPresenter
7 | import dk.nodes.template.presentation.ui.base.BaseViewModel
8 | import dk.nodes.template.presentation.util.SingleEvent
9 | import kotlinx.coroutines.Dispatchers
10 | import kotlinx.coroutines.async
11 | import kotlinx.coroutines.delay
12 | import kotlinx.coroutines.launch
13 | import timber.log.Timber
14 | import javax.inject.Inject
15 |
16 | class SplashViewModel @Inject constructor(
17 | private val nStackPresenter: NStackPresenter
18 | ) : BaseViewModel(
19 | SplashViewState(
20 | doneLoading = false,
21 | nstackUpdateAvailable = null
22 | )
23 | ) {
24 |
25 | fun initAppState() {
26 | viewModelScope.launch {
27 | Timber.d("initAppState() - start")
28 | val deferredAppOpen = async(Dispatchers.IO) { NStack.appOpen() }
29 | // Other API calls that might be needed
30 | // ...
31 | // Splash should be shown for min. x milliseconds
32 | val deferredMinDelay = async(Dispatchers.IO) { delay(2000) }
33 |
34 | // Parallel execution, wait on both to finish
35 | val appOpenResult = deferredAppOpen.await()
36 | deferredMinDelay.await()
37 |
38 | Timber.d("initAppState() - end")
39 | state = when (appOpenResult) {
40 | is Result.Success -> {
41 | nStackPresenter.saveAppState(appOpenResult.value.data)
42 | state.copy(
43 | doneLoading = true,
44 | nstackUpdateAvailable = SingleEvent(appOpenResult.value.data.update)
45 | )
46 | }
47 | else -> state.copy(doneLoading = true)
48 | }
49 | }
50 | }
51 | }
--------------------------------------------------------------------------------
/presentation/src/main/java/dk/nodes/template/presentation/ui/splash/SplashViewState.kt:
--------------------------------------------------------------------------------
1 | package dk.nodes.template.presentation.ui.splash
2 |
3 | import dk.nodes.nstack.kotlin.models.AppUpdate
4 | import dk.nodes.template.presentation.util.SingleEvent
5 |
6 | data class SplashViewState(
7 | val doneLoading: Boolean,
8 | val nstackUpdateAvailable: SingleEvent?
9 | )
--------------------------------------------------------------------------------
/presentation/src/main/java/dk/nodes/template/presentation/util/SharedElementHelper.kt:
--------------------------------------------------------------------------------
1 | package dk.nodes.template.presentation.util
2 |
3 | import android.app.Activity
4 | import android.os.Build
5 | import android.os.Bundle
6 | import android.view.View
7 | import android.widget.ImageView
8 | import androidx.annotation.RequiresApi
9 | import androidx.core.app.ActivityOptionsCompat
10 | import androidx.core.util.Pair
11 | import androidx.fragment.app.FragmentTransaction
12 | import java.lang.ref.WeakReference
13 |
14 | class SharedElementHelper {
15 | private val sharedElementViews = mutableMapOf, String?>()
16 | private val transitionData = mutableMapOf()
17 |
18 | fun addSharedElementTransitionData(key: String, data: String) {
19 | transitionData[key] = data
20 | }
21 |
22 | @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
23 | fun addSharedElement(view: View) {
24 | sharedElementViews[WeakReference(view)] = view.transitionName
25 | }
26 |
27 | fun addSharedElement(view: View, name: String) {
28 | sharedElementViews[WeakReference(view)] = name
29 | }
30 |
31 | fun applyToTransaction(tx: FragmentTransaction) {
32 | for ((viewRef, customTransitionName) in sharedElementViews) {
33 | viewRef.get()?.apply {
34 | tx.addSharedElement(this, customTransitionName!!)
35 | }
36 | }
37 | }
38 |
39 | fun applyToIntent(activity: Activity): Bundle? {
40 | return ActivityOptionsCompat.makeSceneTransitionAnimation(
41 | activity,
42 | *sharedElementViews.map { Pair(it.key.get(), it.value) }.toList().toTypedArray()
43 | ).toBundle()
44 | }
45 |
46 | fun isEmpty(): Boolean = sharedElementViews.isEmpty()
47 |
48 | fun hasExternalImageViews(): Boolean = sharedElementViews.any { it is ImageView }
49 | }
50 |
51 | fun sharedElements(vararg elements: kotlin.Pair): SharedElementHelper {
52 | return SharedElementHelper().apply {
53 | elements.forEach {
54 | addSharedElement(it.first, it.second)
55 | }
56 | }
57 | }
58 |
59 | @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
60 | fun sharedElements(vararg elements: View): SharedElementHelper {
61 | return SharedElementHelper().apply {
62 | elements.forEach {
63 | addSharedElement(it)
64 | }
65 | }
66 | }
67 |
68 | @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
69 | fun sharedElements(elements: List): SharedElementHelper {
70 | return sharedElements(*elements.toTypedArray())
71 | }
72 |
73 | fun SharedElementHelper.withTransitionData(vararg transitionDataPairs: kotlin.Pair): SharedElementHelper {
74 | transitionDataPairs.forEach {
75 | addSharedElementTransitionData(it.first, it.second)
76 | }
77 | return this
78 | }
79 |
80 | fun SharedElementHelper.withTransitionData(data: Map): SharedElementHelper {
81 | return withTransitionData(*(data.entries.map { kotlin.Pair(it.key, it.value) }.toTypedArray()))
82 | }
--------------------------------------------------------------------------------
/presentation/src/main/java/dk/nodes/template/presentation/util/SingleEvent.kt:
--------------------------------------------------------------------------------
1 | package dk.nodes.template.presentation.util
2 |
3 | import androidx.lifecycle.Observer
4 |
5 | /**
6 | * Used as a wrapper for data that is exposed via a LiveData that represents an event.
7 | *
8 | * [Read more about this.](https://medium.com/google-developers/livedata-with-snackbar-navigation-and-other-events-the-singleliveevent-case-ac2622673150)
9 | */
10 | open class SingleEvent(private val content: T) {
11 |
12 | var consumed = false
13 | private set // Allow external read but not write
14 |
15 | /**
16 | * Consumes the content if it's not been consumed yet.
17 | * @return The unconsumed content or `null` if it was consumed already.
18 | */
19 | fun consume(): T? {
20 | return if (consumed) {
21 | null
22 | } else {
23 | consumed = true
24 | content
25 | }
26 | }
27 |
28 | /**
29 | * @return The content whether it's been handled or not.
30 | */
31 | fun peek(): T = content
32 |
33 | override fun equals(other: Any?): Boolean {
34 | if (this === other) return true
35 | if (javaClass != other?.javaClass) return false
36 |
37 | other as SingleEvent<*>
38 |
39 | if (content != other.content) return false
40 | if (consumed != other.consumed) return false
41 |
42 | return true
43 | }
44 |
45 | override fun hashCode(): Int {
46 | var result = content?.hashCode() ?: 0
47 | result = 31 * result + consumed.hashCode()
48 | return result
49 | }
50 | }
51 |
52 | fun SingleEvent?.consume(block: (T) -> Unit) {
53 | this?.consume()?.let(block)
54 | }
55 |
56 | /**
57 | * An [Observer] for [SingleEvent]s, simplifying the pattern of checking if the [SingleEvent]'s content has
58 | * already been consumed.
59 | *
60 | * [onEventUnconsumedContent] is *only* called if the [SingleEvent]'s contents has not been consumed.
61 | */
62 |
63 | class EventObserver(private val onEventUnconsumedContent: (T) -> Unit) :
64 | Observer> {
65 | override fun onChanged(event: SingleEvent?) {
66 | event?.consume()?.run(onEventUnconsumedContent)
67 | }
68 | }
--------------------------------------------------------------------------------
/presentation/src/main/java/dk/nodes/template/presentation/util/ThemeHelper.kt:
--------------------------------------------------------------------------------
1 | package dk.nodes.template.presentation.util
2 |
3 | import android.os.Build
4 | import androidx.appcompat.app.AppCompatDelegate
5 | import dk.nodes.template.domain.entities.Theme
6 |
7 | object ThemeHelper {
8 |
9 | fun applyTheme(theme: Theme) {
10 | val systemTheme = when (theme) {
11 | Theme.LIGHT -> AppCompatDelegate.MODE_NIGHT_NO
12 | Theme.DARK -> AppCompatDelegate.MODE_NIGHT_YES
13 | else -> {
14 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
15 | AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
16 | } else {
17 | AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY
18 | }
19 | }
20 | }
21 | AppCompatDelegate.setDefaultNightMode(systemTheme)
22 | }
23 | }
--------------------------------------------------------------------------------
/presentation/src/main/java/dk/nodes/template/presentation/util/ViewError.kt:
--------------------------------------------------------------------------------
1 | package dk.nodes.template.presentation.util
2 |
3 | import java.io.Serializable
4 |
5 | data class ViewError(
6 | var title: String,
7 | var message: String,
8 | var code: Int = 400
9 | ) : Serializable
10 |
--------------------------------------------------------------------------------
/presentation/src/main/java/dk/nodes/template/presentation/util/ViewErrorController.kt:
--------------------------------------------------------------------------------
1 | package dk.nodes.template.presentation.util
2 |
3 | import android.content.Context
4 | import android.view.View
5 | import androidx.appcompat.app.AlertDialog
6 | import com.google.android.material.snackbar.Snackbar
7 | import dk.nodes.template.presentation.nstack.Translation
8 | import dk.nodes.template.domain.repositories.RepositoryException
9 | import javax.inject.Inject
10 |
11 | class ViewErrorController @Inject constructor(val context: Context) {
12 |
13 | fun showErrorDialog(error: ViewError, cancelable: Boolean = true, dismissAction: (() -> Unit)? = null) {
14 | val builder = AlertDialog.Builder(context)
15 | builder.setTitle(error.title)
16 | builder.setMessage(error.message)
17 | builder.setPositiveButton(Translation.defaultSection.ok) { _, _ ->
18 | isShowingError = false
19 | }
20 | builder.setOnDismissListener {
21 | isShowingError = false
22 | dismissAction?.invoke()
23 | }
24 | if (!isShowingError) {
25 | isShowingError = true
26 | val dialog = builder.show()
27 | dialog.setCancelable(cancelable)
28 | dialog.setCanceledOnTouchOutside(cancelable)
29 | }
30 | }
31 |
32 | fun showErrorSnackbar(view: View, error: ViewError, showAction: Boolean = false, dismissAction: (() -> Unit)? = null) {
33 | val showLength = if (showAction) Snackbar.LENGTH_INDEFINITE else Snackbar.LENGTH_LONG
34 | val snackbar = Snackbar.make(view, error.message ?: Translation.error.errorRandom, showLength)
35 | if (showAction) {
36 | snackbar.setAction(Translation.defaultSection.ok) {
37 | isShowingError = false
38 | dismissAction?.invoke()
39 | }
40 | }
41 | if (!isShowingError) {
42 | isShowingError = true
43 | snackbar.show()
44 | }
45 | }
46 |
47 | companion object {
48 | var isShowingError = false
49 |
50 | fun mapThrowable(throwable: Throwable): ViewError {
51 | return when (throwable) {
52 | is RepositoryException -> {
53 | when (throwable.code) {
54 | 401, 403 -> {
55 | ViewError(
56 | title = Translation.error.errorTitle,
57 | message = Translation.error.authenticationError,
58 | code = -1
59 | )
60 | }
61 | 402, in 404..500 -> {
62 | ViewError(
63 | title = Translation.error.errorTitle,
64 | message = Translation.error.unknownError,
65 | code = -1
66 | )
67 | }
68 | in 500..600 -> {
69 | ViewError(
70 | title = Translation.error.errorTitle,
71 | message = Translation.error.unknownError,
72 | code = -1
73 | )
74 | }
75 | else -> {
76 | ViewError(
77 | title = Translation.error.errorTitle,
78 | message = Translation.error.unknownError,
79 | code = -1
80 | )
81 | }
82 | }
83 | }
84 | else -> {
85 | ViewError(
86 | title = Translation.error.errorTitle,
87 | message = Translation.error.connectionError,
88 | code = -1
89 | )
90 | }
91 | }
92 | }
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/presentation/src/main/res/anim/slide_left_enter.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
15 |
--------------------------------------------------------------------------------
/presentation/src/main/res/anim/slide_left_exit.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
15 |
--------------------------------------------------------------------------------
/presentation/src/main/res/anim/slide_right_enter.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
15 |
--------------------------------------------------------------------------------
/presentation/src/main/res/anim/slide_right_exit.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
15 |
--------------------------------------------------------------------------------
/presentation/src/main/res/color/selector_primary.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/presentation/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
9 |
10 |
16 |
19 |
22 |
23 |
24 |
25 |
31 |
--------------------------------------------------------------------------------
/presentation/src/main/res/drawable/ic_half_moon_and_sun.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/presentation/src/main/res/font/qwigley.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ml-archive/kotlin-template/6399bf37288bdb294a47e21fc73a621f7f356a0b/presentation/src/main/res/font/qwigley.ttf
--------------------------------------------------------------------------------
/presentation/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
17 |
18 |
--------------------------------------------------------------------------------
/presentation/src/main/res/layout/activity_splash.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
14 |
15 |
--------------------------------------------------------------------------------
/presentation/src/main/res/layout/fragment_sample.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
18 |
19 |
27 |
28 |
40 |
41 |
49 |
50 |
--------------------------------------------------------------------------------
/presentation/src/main/res/layout/fragment_splash.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
11 |
12 |
--------------------------------------------------------------------------------
/presentation/src/main/res/layout/item_sample_list.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
12 |
13 |
--------------------------------------------------------------------------------
/presentation/src/main/res/layout/row_sample.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
14 |
15 |
21 |
22 |
--------------------------------------------------------------------------------
/presentation/src/main/res/menu/menu_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/presentation/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/presentation/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/presentation/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ml-archive/kotlin-template/6399bf37288bdb294a47e21fc73a621f7f356a0b/presentation/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/presentation/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ml-archive/kotlin-template/6399bf37288bdb294a47e21fc73a621f7f356a0b/presentation/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/presentation/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ml-archive/kotlin-template/6399bf37288bdb294a47e21fc73a621f7f356a0b/presentation/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/presentation/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ml-archive/kotlin-template/6399bf37288bdb294a47e21fc73a621f7f356a0b/presentation/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/presentation/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ml-archive/kotlin-template/6399bf37288bdb294a47e21fc73a621f7f356a0b/presentation/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/presentation/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ml-archive/kotlin-template/6399bf37288bdb294a47e21fc73a621f7f356a0b/presentation/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/presentation/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ml-archive/kotlin-template/6399bf37288bdb294a47e21fc73a621f7f356a0b/presentation/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/presentation/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ml-archive/kotlin-template/6399bf37288bdb294a47e21fc73a621f7f356a0b/presentation/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/presentation/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ml-archive/kotlin-template/6399bf37288bdb294a47e21fc73a621f7f356a0b/presentation/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/presentation/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ml-archive/kotlin-template/6399bf37288bdb294a47e21fc73a621f7f356a0b/presentation/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/presentation/src/main/res/navigation/main_nav_graph.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/presentation/src/main/res/navigation/splash_nav_graph.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/presentation/src/main/res/values-night-v27/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
10 |
11 |
--------------------------------------------------------------------------------
/presentation/src/main/res/values-night/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
19 |
35 |
--------------------------------------------------------------------------------
/presentation/src/main/res/values-v27/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
10 |
11 |
--------------------------------------------------------------------------------
/presentation/src/main/res/values/attrs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
--------------------------------------------------------------------------------
/presentation/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 | #bb86fc
46 | #6200ee
47 | #4b01d0
48 | #3700b3
49 |
50 | #03dac6
51 | #018786
52 |
53 | #cf6679
54 | #b00020
55 |
56 |
57 |
58 | #ffffff
59 |
60 | #121212
61 |
62 |
63 | #000000
64 |
65 |
--------------------------------------------------------------------------------
/presentation/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 4dp
5 | 8dp
6 | 12dp
7 | 16dp
8 |
9 | 4dp
10 | 8dp
11 | 12dp
12 | 16dp
13 |
14 |
--------------------------------------------------------------------------------
/presentation/src/main/res/values/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #3DDC84
4 |
--------------------------------------------------------------------------------
/presentation/src/main/res/values/nstack_keys.xml:
--------------------------------------------------------------------------------
1 |
2 | {default_hi}
3 | {default_cancel}
4 | {default_no}
5 | {default_yes}
6 | {default_edit}
7 | {default_next}
8 | {default_on}
9 | {default_off}
10 | {default_ok}
11 | {error_errorRandom}
12 | {error_errorTitle}
13 | {error_authenticationError}
14 | {error_connectionError}
15 | {error_unknownError}
16 | {test_title}
17 | {test_message}
18 | {test_subTitle}
19 | {test_on}
20 | {test_off}
21 |
--------------------------------------------------------------------------------
/presentation/src/main/res/values/shape.xml:
--------------------------------------------------------------------------------
1 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
40 |
41 |
42 |
45 |
46 |
47 |
50 |
51 |
--------------------------------------------------------------------------------
/presentation/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Kotlin Template
3 |
4 |
--------------------------------------------------------------------------------
/presentation/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
30 |
31 |
39 |
42 |
43 |
44 |
47 |
48 |
49 |
--------------------------------------------------------------------------------
/presentation/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
76 |
77 |
82 |
83 |
--------------------------------------------------------------------------------
/presentation/src/main/res/values/type.xml:
--------------------------------------------------------------------------------
1 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
43 |
44 |
47 |
48 |
51 |
52 |
55 |
56 |
59 |
60 |
63 |
64 |
67 |
68 |
71 |
72 |
75 |
76 |
79 |
80 |
83 |
84 |
87 |
88 |
91 |
92 |
--------------------------------------------------------------------------------
/presentation/src/test/java/dk/nodes/template/presentation/ExampleUnitTest.java:
--------------------------------------------------------------------------------
1 | package dk.nodes.template.presentation;
2 |
3 | import static org.junit.Assert.*;
4 |
5 | import org.junit.Test;
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * @see Testing documentation
11 | */
12 | public class ExampleUnitTest {
13 | @Test
14 | public void addition_isCorrect() {
15 | assertEquals(4, 2 + 2);
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app'
2 | include ':domain'
3 | include ':presentation'
4 | include ':data'
5 |
--------------------------------------------------------------------------------