diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index b2eec64..747ea1a 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -74,6 +74,8 @@ android {
dependencies {
+ implementation(project(":util"))
+
implementation(Libs.activity_compose)
implementation(Libs.navigation_compose)
implementation(Libs.core_ktx)
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 66ceb78..99f16f0 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -6,6 +6,7 @@
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
+ android:name=".app.App"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.AndroidStudyCase">
diff --git a/app/src/main/java/com/ericampire/android/androidstudycase/app/App.kt b/app/src/main/java/com/ericampire/android/androidstudycase/app/App.kt
new file mode 100644
index 0000000..643ba3e
--- /dev/null
+++ b/app/src/main/java/com/ericampire/android/androidstudycase/app/App.kt
@@ -0,0 +1,11 @@
+package com.ericampire.android.androidstudycase.app
+
+import android.app.Application
+import dagger.hilt.android.HiltAndroidApp
+
+@HiltAndroidApp
+class App : Application() {
+ override fun onCreate() {
+ super.onCreate()
+ }
+}
\ No newline at end of file
diff --git a/i18n/.gitignore b/i18n/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/i18n/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/i18n/build.gradle.kts b/i18n/build.gradle.kts
new file mode 100644
index 0000000..b4ff1ec
--- /dev/null
+++ b/i18n/build.gradle.kts
@@ -0,0 +1,11 @@
+plugins {
+ id("com.android.library")
+ id("kotlin-android")
+}
+
+android {
+ compileSdk = Apps.compileSdk
+ defaultConfig {
+ minSdk = Apps.minSdk
+ }
+}
\ No newline at end of file
diff --git a/i18n/consumer-rules.pro b/i18n/consumer-rules.pro
new file mode 100644
index 0000000..e69de29
diff --git a/i18n/proguard-rules.pro b/i18n/proguard-rules.pro
new file mode 100644
index 0000000..ff59496
--- /dev/null
+++ b/i18n/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.kts.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/i18n/src/main/AndroidManifest.xml b/i18n/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..557e3e4
--- /dev/null
+++ b/i18n/src/main/AndroidManifest.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/i18n/src/main/res/values/arrays.xml b/i18n/src/main/res/values/arrays.xml
new file mode 100644
index 0000000..7ce23a6
--- /dev/null
+++ b/i18n/src/main/res/values/arrays.xml
@@ -0,0 +1,37 @@
+
+
+
+ - Commandes
+ - Tout
+
+
+
+ - Nouveau fournisseur
+ - Promotions
+ - Commandes
+ - Nouvelle mise à jour
+ - Globale
+
+
+
+ - stores
+ - products
+ - orders
+ - global
+ - default
+
+
+
+ - Être notifié lorsqu\'un nouveau fournisseur est disponible sur Be Served.
+ - Être notifié lorsqu\'il y a de nouvelles promotions.
+ - Être notifié lorsque l\'état de votre commande change.
+ - Être notifié lorsqu\'une nouvelle version de l\'application est disponible.
+ - Autres notifications.
+
+
+
+ - En cours
+ - Refusée
+ - Historique
+
+
\ No newline at end of file
diff --git a/i18n/src/main/res/values/strings.xml b/i18n/src/main/res/values/strings.xml
new file mode 100644
index 0000000..6702bab
--- /dev/null
+++ b/i18n/src/main/res/values/strings.xml
@@ -0,0 +1,7 @@
+
+
+
+ Study case
+ Be Served Channel
+
+
\ No newline at end of file
diff --git a/settings.gradle.kts b/settings.gradle.kts
index edbbb7f..7156dd6 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -16,3 +16,5 @@ dependencyResolutionManagement {
}
rootProject.name = "android-study-case"
include(":app")
+include(":util")
+include(":i18n")
diff --git a/util/.gitignore b/util/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/util/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/util/build.gradle.kts b/util/build.gradle.kts
new file mode 100644
index 0000000..cbe6396
--- /dev/null
+++ b/util/build.gradle.kts
@@ -0,0 +1,48 @@
+plugins {
+ id("com.android.library")
+ id("kotlin-android")
+ kotlin("kapt")
+}
+
+android {
+ compileSdk = Apps.compileSdk
+ buildToolsVersion = Apps.buildToolsVersion
+
+ defaultConfig {
+ minSdk = Apps.minSdk
+ targetSdk = Apps.targetSdk
+
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ consumerProguardFiles("consumer-rules.pro")
+ }
+
+ buildTypes {
+ release {
+ isMinifyEnabled = false
+ proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
+ }
+ }
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_1_8
+ targetCompatibility = JavaVersion.VERSION_1_8
+ }
+}
+
+dependencies {
+
+ api(project(":i18n"))
+ implementation(Libs.core_ktx)
+
+ api(platform(Libs.kotlin_coroutine_bom))
+ api(Libs.kotlin_coroutine_core)
+
+ testImplementation(Libs.junit_jupiter_api)
+ testImplementation(Libs.junit_jupiter_engine)
+
+ testImplementation(Libs.mockk_core)
+
+ api(Libs.hilt_android)
+ kapt(Libs.hilt_android_compiler)
+
+ api(Libs.timber)
+}
\ No newline at end of file
diff --git a/util/consumer-rules.pro b/util/consumer-rules.pro
new file mode 100644
index 0000000..e69de29
diff --git a/util/proguard-rules.pro b/util/proguard-rules.pro
new file mode 100644
index 0000000..481bb43
--- /dev/null
+++ b/util/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/util/src/main/AndroidManifest.xml b/util/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..ebdb0bc
--- /dev/null
+++ b/util/src/main/AndroidManifest.xml
@@ -0,0 +1,5 @@
+
+
+
+
\ No newline at end of file
diff --git a/util/src/main/java/org/zxconnect/android/beserve/util/DefaultDispatcher.kt b/util/src/main/java/org/zxconnect/android/beserve/util/DefaultDispatcher.kt
new file mode 100644
index 0000000..8cab42a
--- /dev/null
+++ b/util/src/main/java/org/zxconnect/android/beserve/util/DefaultDispatcher.kt
@@ -0,0 +1,24 @@
+package org.zxconnect.android.beserve.util
+
+import javax.inject.Qualifier
+
+
+@Qualifier
+@Retention(AnnotationRetention.BINARY)
+annotation class DefaultDispatcher
+
+@Qualifier
+@Retention(AnnotationRetention.BINARY)
+annotation class IoDispatcher
+
+@Qualifier
+@Retention(AnnotationRetention.BINARY)
+annotation class MainDispatcher
+
+@Qualifier
+@Retention(AnnotationRetention.BINARY)
+annotation class MainImmediateDispatcher
+
+@Qualifier
+@Retention(AnnotationRetention.BINARY)
+annotation class ApplicationScope
\ No newline at end of file
diff --git a/util/src/main/java/org/zxconnect/android/beserve/util/Event.kt b/util/src/main/java/org/zxconnect/android/beserve/util/Event.kt
new file mode 100644
index 0000000..075be71
--- /dev/null
+++ b/util/src/main/java/org/zxconnect/android/beserve/util/Event.kt
@@ -0,0 +1,40 @@
+package org.zxconnect.android.beserve.util
+
+import androidx.lifecycle.Observer
+
+open class Event(private val content: T) {
+
+ var hasBeenHandled = false
+ private set // Allow external read but not write
+
+ /**
+ * Returns the content and prevents its use again.
+ */
+ fun getContentIfNotHandled(): T? {
+ return if (hasBeenHandled) {
+ null
+ } else {
+ hasBeenHandled = true
+ content
+ }
+ }
+
+ /**
+ * Returns the content, even if it's already been handled.
+ */
+ fun peekContent(): T = content
+}
+
+/**
+ * An [Observer] for [Event]s, simplifying the pattern of checking if the [Event]'s content has
+ * already been handled.
+ *
+ * [onEventUnhandledContent] is *only* called if the [Event]'s contents has not been handled.
+ */
+class EventObserver(private val onEventUnhandledContent: (T) -> Unit) : Observer> {
+ override fun onChanged(event: Event?) {
+ event?.getContentIfNotHandled()?.let { value ->
+ onEventUnhandledContent(value)
+ }
+ }
+}
\ No newline at end of file
diff --git a/util/src/main/java/org/zxconnect/android/beserve/util/Result.kt b/util/src/main/java/org/zxconnect/android/beserve/util/Result.kt
new file mode 100644
index 0000000..7614a19
--- /dev/null
+++ b/util/src/main/java/org/zxconnect/android/beserve/util/Result.kt
@@ -0,0 +1,40 @@
+package org.zxconnect.android.beserve.util
+
+import androidx.lifecycle.MutableLiveData
+
+sealed class Result {
+
+ data class Success(val data: T) : Result()
+ data class Error(val exception: Exception) : Result()
+ object Loading : Result()
+
+ override fun toString(): String {
+ return when (this) {
+ is Success<*> -> "Success[data=$data]"
+ is Error -> "Error[exception=$exception]"
+ Loading -> "Loading"
+ }
+ }
+}
+
+/**
+ * `true` if [Result] is of type [Success] & hoxlds non-null [Success.data].
+ */
+val Result<*>.succeeded
+ get() = this is Result.Success && data != null
+
+fun Result.successOr(fallback: T): T {
+ return (this as? Result.Success)?.data ?: fallback
+}
+
+val Result.data: T?
+ get() = (this as? Result.Success)?.data
+
+/**
+ * Updates value of [liveData] if [Result] is of type [Success]
+ */
+inline fun Result.updateOnSuccess(liveData: MutableLiveData) {
+ if (this is Result.Success) {
+ liveData.value = data
+ }
+}
\ No newline at end of file
diff --git a/util/src/main/java/org/zxconnect/android/beserve/util/usecase/CoroutineUseCase.kt b/util/src/main/java/org/zxconnect/android/beserve/util/usecase/CoroutineUseCase.kt
new file mode 100644
index 0000000..dd5ea05
--- /dev/null
+++ b/util/src/main/java/org/zxconnect/android/beserve/util/usecase/CoroutineUseCase.kt
@@ -0,0 +1,38 @@
+package org.zxconnect.android.beserve.util.usecase
+
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.withContext
+import org.zxconnect.android.beserve.util.Result
+import timber.log.Timber
+
+
+abstract class CoroutineUseCase(private val coroutineDispatcher: CoroutineDispatcher) {
+
+ /** Executes the use case asynchronously and returns a [Result].
+ *
+ * @return a [Result].
+ *
+ * @param parameters the input parameters to run the use case with
+ */
+ suspend operator fun invoke(parameters: P): Result {
+ return try {
+ // Moving all use case's executions to the injected dispatcher
+ // In production code, this is usually the Default dispatcher (background thread)
+ // In tests, this becomes a TestCoroutineDispatcher
+ withContext(coroutineDispatcher) {
+ execute(parameters).let {
+ Result.Success(it)
+ }
+ }
+ } catch (e: Exception) {
+ Timber.d(e)
+ Result.Error(e)
+ }
+ }
+
+ /**
+ * Override this to set the code to be executed.
+ */
+ @Throws(RuntimeException::class)
+ protected abstract suspend fun execute(parameters: P): R
+}
\ No newline at end of file
diff --git a/util/src/main/java/org/zxconnect/android/beserve/util/usecase/FlowUseCase.kt b/util/src/main/java/org/zxconnect/android/beserve/util/usecase/FlowUseCase.kt
new file mode 100644
index 0000000..6822028
--- /dev/null
+++ b/util/src/main/java/org/zxconnect/android/beserve/util/usecase/FlowUseCase.kt
@@ -0,0 +1,20 @@
+package org.zxconnect.android.beserve.util.usecase
+
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.catch
+import kotlinx.coroutines.flow.flowOn
+import org.zxconnect.android.beserve.util.Result
+
+/**
+ * Executes business logic in its execute method and keep posting updates to the result as
+ * [Result].
+ * Handling an exception (emit [Result.Error] to the result) is the subclasses's responsibility.
+ */
+abstract class FlowUseCase(private val coroutineDispatcher: CoroutineDispatcher) {
+ operator fun invoke(parameters: P): Flow> = execute(parameters)
+ .catch { e -> emit(Result.Error(Exception(e))) }
+ .flowOn(coroutineDispatcher)
+
+ protected abstract fun execute(parameters: P): Flow>
+}
diff --git a/util/src/test/java/org/zxconnect/android/beserve/util/ExampleUnitTest.kt b/util/src/test/java/org/zxconnect/android/beserve/util/ExampleUnitTest.kt
new file mode 100644
index 0000000..09669d1
--- /dev/null
+++ b/util/src/test/java/org/zxconnect/android/beserve/util/ExampleUnitTest.kt
@@ -0,0 +1,17 @@
+package org.zxconnect.android.beserve.util
+
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.Test
+
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+class ExampleUnitTest {
+ @Test
+ fun addition_isCorrect() {
+ assertEquals(4, 2 + 2)
+ }
+}
\ No newline at end of file