diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml
new file mode 100644
index 0000000..6f3d6b9
--- /dev/null
+++ b/.github/workflows/unit-test.yml
@@ -0,0 +1,47 @@
+name: Unit test on pull request
+
+on:
+ workflow_dispatch:
+ pull_request:
+ branches:
+ - 'develop'
+ - 'master'
+
+jobs:
+ test:
+ name: Run Unit Tests
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Cache
+ uses: actions/cache@v2
+ with:
+ path: |
+ ~/.gradle/caches
+ ~/.gradle/wrapper
+ key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties', '**/buildSrc/**/*.kt') }}
+ restore-keys: ${{ runner.os }}-gradle-
+
+ - name: Checkout
+ uses: actions/checkout@v2
+
+ - name: set up JDK 11
+ uses: actions/setup-java@v1
+ with:
+ java-version: 11
+
+ - name: Validate Gradle Wrapper
+ uses: gradle/wrapper-validation-action@v1
+
+ - name: Copy gradle properties file
+ run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties
+
+ - name: Unit tests
+ run: ./gradlew test --stacktrace
+
+ - name: Archive reports for failed build
+ if: ${{ failure() }}
+ uses: actions/upload-artifact@v2
+ with:
+ name: reports
+ path: '*/build/reports'
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..aa724b7
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,15 @@
+*.iml
+.gradle
+/local.properties
+/.idea/caches
+/.idea/libraries
+/.idea/modules.xml
+/.idea/workspace.xml
+/.idea/navEditor.xml
+/.idea/assetWizardSettings.xml
+.DS_Store
+/build
+/captures
+.externalNativeBuild
+.cxx
+local.properties
diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000..26d3352
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,3 @@
+# Default ignored files
+/shelf/
+/workspace.xml
diff --git a/.idea/compiler.xml b/.idea/compiler.xml
new file mode 100644
index 0000000..fb7f4a8
--- /dev/null
+++ b/.idea/compiler.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/deploymentTargetDropDown.xml b/.idea/deploymentTargetDropDown.xml
new file mode 100644
index 0000000..4e563bd
--- /dev/null
+++ b/.idea/deploymentTargetDropDown.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/gradle.xml b/.idea/gradle.xml
new file mode 100644
index 0000000..526b4c2
--- /dev/null
+++ b/.idea/gradle.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 0000000..1a7944f
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..301740a
--- /dev/null
+++ b/README.md
@@ -0,0 +1,70 @@
+# UnrdApp
+Protopyte project with intergration of API https://s3-eu-west-1.amazonaws.com
+
+## Best practices
+
+- Separation of concerns
+- KISS
+- DRY
+- SOLID
+- Commend Query
+- Clean Architecture
+
+## Architecture
+Logic in application can be seen as segregated into four abstract layers:
+
+**_User Interface layer:_**
+* renders ui model (View - part of UiComponent)
+* look&feel, animations, colors, styles, descriptions, layout
+* captures user interactions with UI elements and only routes them (NOT handling) into the system
+
+**_Application layer (aka "Glue layer"):_**
+* controls user flow inside app e.g navigation
+* passes ui model to UI layer
+* handles user interaction with UI
+* integrates standalone pieces of functionality from other layers
+
+**_Domain layer:_**
+* executes business domain flows aka "business logic
+
+**_Infrastructure layer:_**
+* provides general functionality not specific to business domain
+ * networking (Retrofit)
+ * reactive (RxJava)
+
+#### Remarks
+**UiModel** has only data to display, for a View. Colors, styles, descriptions etc. are View implementation details, they are not part of **UiModel**.
+
+### Benefits :
+Makes the code much more pleasant to work with!
+* No need to look for things
+* Readable and intuitive
+* Easy to reason about and modify
+* Productive and less stressful
+
+### Example Unit tests :
+* MainViewModelTest
+* ResultRepositoryTest
+* GetUiModelUseCaseTest
+
+### Example Unit tests :
+* MainViewModelTest
+* ResultRepositoryTest
+* GetUiModelUseCaseTest
+
+### My ideal CI World for Unrd :
+I will propose fastlane to sign and build application and GitHub action for run CI remote(cost $0.008 per minute)
+For this kind of project monthly cost should be on acceptable level
+Possible solution is set our own Linux machine and configure Jenkins/Bitrise or use CircleApp
+
+What should be cover by CI:
+* Run Unit test for each PR to develop and master
+* Run nightly daily internal release build
+* Internal release process(Firebase, Alpha, Beta)
+* Production release process
+
+Nice to have
+* Translation process
+* Run nightly UI test
+* Run code formatter for each PR
+* Security check for release file(MobSF)
\ No newline at end of file
diff --git a/app/.gitignore b/app/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/app/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/app/build.gradle b/app/build.gradle
new file mode 100644
index 0000000..77b33e1
--- /dev/null
+++ b/app/build.gradle
@@ -0,0 +1,141 @@
+plugins {
+ id 'com.android.application'
+ id 'kotlin-android'
+ id 'kotlinx-serialization'
+ id 'kotlin-kapt'
+ id 'dagger.hilt.android.plugin'
+ id 'androidx.navigation.safeargs'
+}
+
+android {
+ compileSdk 31
+
+ defaultConfig {
+ applicationId "com.example.unrd"
+ minSdk 21
+ targetSdk 31
+ versionCode 1
+ versionName "1.0"
+
+ testInstrumentationRunner "com.example.android.dagger.CustomTestRunner"
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled = true
+ shrinkResources = true
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ debug {
+ minifyEnabled = true
+ shrinkResources = true
+ }
+ applicationVariants.all {
+ variant ->
+ variant.outputs.each {
+ output ->
+ def name = "GIPHY_DEMO.apk"
+ output.outputFileName = name
+ }
+ }
+ }
+
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+ kotlinOptions {
+ jvmTarget = '1.8'
+ }
+
+ buildFeatures {
+ viewBinding true
+ dataBinding true
+ }
+}
+
+dependencies {
+
+ implementation 'androidx.core:core-ktx:1.7.0'
+ implementation 'androidx.appcompat:appcompat:1.4.1'
+ implementation 'com.google.android.material:material:1.5.0'
+ implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
+ implementation "androidx.recyclerview:recyclerview:1.2.1"
+ implementation "androidx.lifecycle:lifecycle-reactivestreams-ktx:2.4.1"
+ implementation 'androidx.browser:browser:1.4.0'
+ implementation 'androidx.fragment:fragment-ktx:1.4.1'
+ implementation 'androidx.activity:activity-ktx:1.4.0'
+
+ // ViewModel
+ implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.1'
+
+ // LiveData
+ implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.4.1'
+ implementation 'androidx.lifecycle:lifecycle-common-java8:2.4.1'
+
+ //Logger
+ api "com.orhanobut:logger:2.2.0"
+ implementation 'androidx.test.ext:junit-ktx:1.1.3'
+ implementation 'androidx.fragment:fragment-testing:1.4.1'
+ androidTestImplementation 'androidx.test:rules:1.4.0-alpha05'
+
+ //data binding
+ def data_binding_version = "7.1.1"
+ implementation "androidx.databinding:databinding-common:$data_binding_version"
+ implementation "androidx.databinding:databinding-runtime:$data_binding_version"
+
+ def nav_version = "2.4.1"
+ // Navigation
+ implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
+ implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
+
+ // Hilt
+ implementation "com.google.dagger:hilt-android:2.37"
+ kapt 'com.google.dagger:hilt-android-compiler:2.37'
+ implementation 'androidx.hilt:hilt-lifecycle-viewmodel:1.0.0-alpha03'
+ kapt 'androidx.hilt:hilt-compiler:1.0.0'
+ implementation 'androidx.hilt:hilt-navigation-fragment:1.0.0'
+ kaptTest 'com.google.dagger:hilt-android-compiler:2.37'
+ kaptAndroidTest 'com.google.dagger:hilt-android-compiler:2.37'
+ testImplementation 'com.google.dagger:hilt-android-testing:2.37'
+ androidTestImplementation 'com.google.dagger:hilt-android-testing:2.37'
+ androidTestAnnotationProcessor 'com.google.dagger:hilt-android-compiler:2.37'
+
+ // Retrofit
+ implementation 'com.squareup.retrofit2:retrofit:2.9.0'
+ implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
+ implementation "com.github.akarnokd:rxjava3-retrofit-adapter:3.0.0"
+
+ //RxJava
+ def rxJava_version = "3.0.0"
+ implementation "io.reactivex.rxjava3:rxandroid:$rxJava_version"
+ implementation "io.reactivex.rxjava3:rxjava:$rxJava_version"
+
+ // SquareUp tools - retrofit, okhttp
+ def retrofit2_version = "2.9.0"
+ def okhttp3_version = "4.9.1"
+ api "com.squareup.retrofit2:retrofit:$retrofit2_version"
+ api "com.squareup.retrofit2:converter-moshi:$retrofit2_version"
+ api "com.squareup.retrofit2:adapter-rxjava3:$retrofit2_version"
+ api "com.squareup.okhttp3:logging-interceptor:$okhttp3_version"
+
+ // moshi
+ def moshi_version = "1.13.0"
+ api "com.squareup.moshi:moshi-kotlin:$moshi_version"
+ kapt "com.squareup.moshi:moshi-kotlin-codegen:$moshi_version"
+
+ // Glide
+ def glide_version = "4.12.0"
+ implementation "com.github.bumptech.glide:glide:$glide_version"
+ annotationProcessor "com.github.bumptech.glide:compiler:$glide_version"
+
+ //mockk
+ testImplementation 'io.mockk:mockk:1.12.0'
+
+ testImplementation 'com.jraska.livedata:testing-ktx:1.2.0'
+ testImplementation "androidx.arch.core:core-testing:2.1.0"
+
+ testImplementation 'junit:junit:4.+'
+ androidTestImplementation 'androidx.test.ext:junit:1.1.3'
+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.0-alpha03'
+}
\ No newline at end of file
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
new file mode 100644
index 0000000..7bfd9c5
--- /dev/null
+++ b/app/proguard-rules.pro
@@ -0,0 +1,39 @@
+# Keep `Companion` object fields of serializable classes.
+# This avoids serializer lookup through `getDeclaredClasses` as done for named companion objects.
+-if @kotlinx.serialization.Serializable class **
+-keepclassmembers class <1> {
+ static <1>$Companion Companion;
+}
+
+# Keep `serializer()` on companion objects (both default and named) of serializable classes.
+-if @kotlinx.serialization.Serializable class ** {
+ static **$* *;
+}
+-keepclassmembers class <2>$<3> {
+ kotlinx.serialization.KSerializer serializer(...);
+}
+
+# Keep `INSTANCE.serializer()` of serializable objects.
+-if @kotlinx.serialization.Serializable class ** {
+ public static ** INSTANCE;
+}
+-keepclassmembers class <1> {
+ public static <1> INSTANCE;
+ kotlinx.serialization.KSerializer serializer(...);
+}
+
+# @Serializable and @Polymorphic are used at runtime for polymorphic serialization.
+-keepattributes RuntimeVisibleAnnotations,AnnotationDefault
+
+# Serializer for classes with named companion objects are retrieved using `getDeclaredClasses`.
+# If you have any, uncomment and replace classes with those containing named companion objects.
+#-keepattributes InnerClasses # Needed for `getDeclaredClasses`.
+#-if @kotlinx.serialization.Serializable class
+#com.example.myapplication.HasNamedCompanion, # <-- List serializable classes with named companions.
+#com.example.myapplication.HasNamedCompanion2
+#{
+# static **$* *;
+#}
+#-keepnames class <1>$$serializer { # -keepnames suffices; class is kept when serializer() is kept.
+# static <1>$$serializer INSTANCE;
+#}
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/example/unrd/CustomTestRunner.kt b/app/src/androidTest/java/com/example/unrd/CustomTestRunner.kt
new file mode 100644
index 0000000..2c3f328
--- /dev/null
+++ b/app/src/androidTest/java/com/example/unrd/CustomTestRunner.kt
@@ -0,0 +1,13 @@
+package com.example.unrd
+
+import android.app.Application
+import android.content.Context
+import androidx.test.runner.AndroidJUnitRunner
+import dagger.hilt.android.testing.HiltTestApplication
+
+class CustomTestRunner : AndroidJUnitRunner() {
+
+ override fun newApplication(cl: ClassLoader?, name: String?, context: Context?): Application {
+ return super.newApplication(cl, HiltTestApplication::class.java.name, context)
+ }
+}
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/example/unrd/DataBindingIdlingResource.kt b/app/src/androidTest/java/com/example/unrd/DataBindingIdlingResource.kt
new file mode 100644
index 0000000..566c289
--- /dev/null
+++ b/app/src/androidTest/java/com/example/unrd/DataBindingIdlingResource.kt
@@ -0,0 +1,94 @@
+package com.example.unrd
+
+import android.view.View
+import androidx.databinding.DataBindingUtil
+import androidx.databinding.ViewDataBinding
+import androidx.fragment.app.FragmentActivity
+import androidx.test.core.app.ActivityScenario
+import androidx.test.espresso.IdlingResource
+import com.orhanobut.logger.Logger
+import java.util.*
+
+/**
+ * An espresso idling resource implementation that reports idle status for all data binding
+ * layouts. Data Binding uses a mechanism to post messages which Espresso doesn't track yet.
+ *
+ * Since this application only uses fragments, the resource only checks the fragments and their
+ * children instead of the whole view tree.
+ *
+ * https://medium.com/androiddevelopers/android-testing-with-espressos-idling-resources-and-testing-fidelity-8b8647ed57f4
+ */
+class DataBindingIdlingResource : IdlingResource {
+ // list of registered callbacks
+ private val idlingCallbacks = mutableListOf()
+
+ // give it a unique id to workaround an espresso bug where you cannot register/unregister
+ // an idling resource w/ the same name.
+ private val id = UUID.randomUUID().toString()
+
+ // holds whether isIdle is called and the result was false. We track this to avoid calling
+ // onTransitionToIdle callbacks if Espresso never thought we were idle in the first place.
+ private var wasNotIdle = false
+
+ lateinit var activity: FragmentActivity
+
+ override fun getName() = "DataBinding $id"
+
+ override fun isIdleNow(): Boolean {
+ val idle = !getBindings().any { it.hasPendingBindings() }
+ @Suppress("LiftReturnOrAssignment")
+ if (idle) {
+ if (wasNotIdle) {
+ // notify observers to avoid espresso race detector
+ idlingCallbacks.forEach { it.onTransitionToIdle() }
+ }
+ wasNotIdle = false
+ } else {
+ wasNotIdle = true
+ // check next frame
+ activity.findViewById(android.R.id.content).postDelayed({
+ isIdleNow
+ }, 16)
+ }
+ return idle
+ }
+
+ override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback) {
+ idlingCallbacks.add(callback)
+ }
+
+ /**
+ * Find all binding classes in all currently available fragments.
+ */
+ private fun getBindings(): List {
+ return try {
+ val fragments = (activity as? FragmentActivity)
+ ?.supportFragmentManager
+ ?.fragments
+
+ val bindings =
+ fragments?.mapNotNull {
+ it.view?.getBinding()
+ } ?: emptyList()
+ val childrenBindings = fragments?.flatMap { it.childFragmentManager.fragments }
+ ?.mapNotNull { it.view?.getBinding() } ?: emptyList()
+ bindings + childrenBindings
+ } catch (e: IllegalStateException) {
+ Logger.i("obtain bindings with IllegalStateException - returned emptyList")
+ emptyList()
+ }
+ }
+}
+
+private fun View.getBinding(): ViewDataBinding? = DataBindingUtil.getBinding(this)
+
+/**
+ * Sets the activity from an [ActivityScenario] to be used from [DataBindingIdlingResource].
+ */
+fun DataBindingIdlingResource.monitorActivity(
+ activityScenario: ActivityScenario
+) {
+ activityScenario.onActivity {
+ this.activity = it
+ }
+}
diff --git a/app/src/androidTest/java/com/example/unrd/MainFlowTest.kt b/app/src/androidTest/java/com/example/unrd/MainFlowTest.kt
new file mode 100644
index 0000000..90d315b
--- /dev/null
+++ b/app/src/androidTest/java/com/example/unrd/MainFlowTest.kt
@@ -0,0 +1,119 @@
+package com.example.unrd
+
+import android.view.View
+import android.view.ViewGroup
+import androidx.fragment.app.testing.FragmentScenario
+import androidx.fragment.app.testing.launchFragmentInContainer
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.Espresso.pressBack
+import androidx.test.espresso.action.ViewActions.click
+import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
+import androidx.test.espresso.matcher.ViewMatchers.withId
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import com.example.unrd.fragment.MainFragment
+import dagger.hilt.android.testing.HiltAndroidRule
+import dagger.hilt.android.testing.HiltAndroidTest
+import org.hamcrest.Description
+import org.hamcrest.Matcher
+import org.hamcrest.Matchers.allOf
+import org.hamcrest.TypeSafeMatcher
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@LargeTest
+@HiltAndroidTest
+@RunWith(AndroidJUnit4::class)
+class MainFlowTest: TestWithBinding() {
+
+ @get:Rule
+ var hiltRule = HiltAndroidRule(this)
+
+ @Before
+ fun init() {
+ hiltRule.inject()
+ }
+
+ @Test
+ fun mainFlowTest() {
+
+ val scenario: FragmentScenario =
+ launchFragmentInContainer(null, R.style.AppTheme)
+
+ dataBindingIdlingResource.monitorFragment(scenario)
+
+ scenario.onFragment { fragment ->
+
+ val appCompatImageButton = onView(
+ allOf(
+ withId(R.id.intro_video),
+ childAtPosition(
+ childAtPosition(
+ withId(R.id.nav_host_fragment),
+ 0
+ ),
+ 5
+ ),
+ isDisplayed()
+ )
+ )
+ appCompatImageButton.perform(click())
+
+ pressBack()
+
+ val appCompatImageButton2 = onView(
+ allOf(
+ withId(R.id.video),
+ childAtPosition(
+ childAtPosition(
+ withId(R.id.nav_host_fragment),
+ 0
+ ),
+ 6
+ ),
+ isDisplayed()
+ )
+ )
+ appCompatImageButton2.perform(click())
+
+ pressBack()
+
+ val appCompatImageButton3 = onView(
+ allOf(
+ withId(R.id.image),
+ childAtPosition(
+ childAtPosition(
+ withId(R.id.nav_host_fragment),
+ 0
+ ),
+ 7
+ ),
+ isDisplayed()
+ )
+ )
+ appCompatImageButton3.perform(click())
+
+ pressBack()
+ }
+ }
+
+ private fun childAtPosition(
+ parentMatcher: Matcher, position: Int
+ ): Matcher {
+
+ return object : TypeSafeMatcher() {
+ override fun describeTo(description: Description) {
+ description.appendText("Child at position $position in parent ")
+ parentMatcher.describeTo(description)
+ }
+
+ public override fun matchesSafely(view: View): Boolean {
+ val parent = view.parent
+ return parent is ViewGroup && parentMatcher.matches(parent)
+ && view == parent.getChildAt(position)
+ }
+ }
+ }
+}
diff --git a/app/src/androidTest/java/com/example/unrd/TestWithBinding.kt b/app/src/androidTest/java/com/example/unrd/TestWithBinding.kt
new file mode 100644
index 0000000..dc9f885
--- /dev/null
+++ b/app/src/androidTest/java/com/example/unrd/TestWithBinding.kt
@@ -0,0 +1,40 @@
+package com.example.unrd
+
+import androidx.test.espresso.IdlingPolicies
+import androidx.test.espresso.IdlingRegistry
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import org.junit.After
+import org.junit.Before
+import org.junit.runner.RunWith
+import java.util.concurrent.TimeUnit
+
+@RunWith(AndroidJUnit4::class)
+@LargeTest
+abstract class TestWithBinding {
+
+ // An Idling Resource that waits for Data Binding to have no pending bindings
+ protected val dataBindingIdlingResource = DataBindingIdlingResource()
+
+ @Before
+ fun idlingResourceTimeout() {
+ IdlingPolicies.setIdlingResourceTimeout(4000, TimeUnit.MILLISECONDS)
+ }
+
+ /**
+ * Idling resources tell Espresso that the app is idle or busy. This is needed when operations
+ * are not scheduled in the main Looper (for example when executed on a different thread).
+ */
+
+ @Before
+ fun registerIdlingResource() {
+ IdlingRegistry.getInstance().register(dataBindingIdlingResource)
+ }
+
+ @After
+ fun unregisterIdlingResource() {
+ IdlingRegistry.getInstance().unregister(dataBindingIdlingResource)
+ }
+
+
+}
diff --git a/app/src/androidTest/java/com/example/unrd/_DataBindingResource.kt b/app/src/androidTest/java/com/example/unrd/_DataBindingResource.kt
new file mode 100644
index 0000000..61dd673
--- /dev/null
+++ b/app/src/androidTest/java/com/example/unrd/_DataBindingResource.kt
@@ -0,0 +1,11 @@
+package com.example.unrd
+
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.testing.FragmentScenario
+import androidx.fragment.app.testing.withFragment
+
+inline fun DataBindingIdlingResource.monitorFragment(fragmentScenario: FragmentScenario) {
+ fragmentScenario.withFragment {
+ this@monitorFragment.activity = this.requireActivity()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..b6f52d8
--- /dev/null
+++ b/app/src/main/AndroidManifest.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/java/com/example/unrd/MainActivity.kt b/app/src/main/java/com/example/unrd/MainActivity.kt
new file mode 100644
index 0000000..2048fb4
--- /dev/null
+++ b/app/src/main/java/com/example/unrd/MainActivity.kt
@@ -0,0 +1,18 @@
+package com.example.unrd
+
+import android.os.Bundle
+import androidx.activity.viewModels
+import androidx.appcompat.app.AppCompatActivity
+import dagger.hilt.android.AndroidEntryPoint
+
+@AndroidEntryPoint
+class MainActivity : AppCompatActivity() {
+
+ private val viewModel: MainActivityViewModel by viewModels()
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_main)
+ viewModel.refresh()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/unrd/MainActivityViewModel.kt b/app/src/main/java/com/example/unrd/MainActivityViewModel.kt
new file mode 100644
index 0000000..c96f0c1
--- /dev/null
+++ b/app/src/main/java/com/example/unrd/MainActivityViewModel.kt
@@ -0,0 +1,21 @@
+package com.example.unrd
+
+import com.example.unrd.core.common.BaseViewModel
+import com.example.unrd.core.repository.useCase.RefreshResultUseCase
+import com.orhanobut.logger.Logger
+import dagger.hilt.android.lifecycle.HiltViewModel
+import javax.inject.Inject
+
+@HiltViewModel
+class MainActivityViewModel @Inject constructor(
+ private val refreshResultUseCase: RefreshResultUseCase
+): BaseViewModel(){
+
+ fun refresh() {
+ refreshResultUseCase.refresh().subscribe({
+ Logger.d("Refresh data success")
+ }, {
+ Logger.d("Refresh data error ${it.localizedMessage}")
+ })
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/unrd/common/FragmentViewBindingDelegate.kt b/app/src/main/java/com/example/unrd/common/FragmentViewBindingDelegate.kt
new file mode 100644
index 0000000..6b74cc7
--- /dev/null
+++ b/app/src/main/java/com/example/unrd/common/FragmentViewBindingDelegate.kt
@@ -0,0 +1,62 @@
+package com.example.unrd.common
+
+import android.view.View
+import androidx.fragment.app.Fragment
+import androidx.lifecycle.DefaultLifecycleObserver
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.Observer
+import androidx.viewbinding.ViewBinding
+import kotlin.properties.ReadOnlyProperty
+import kotlin.reflect.KProperty
+
+class FragmentViewBindingDelegate(
+ val fragment: Fragment,
+ val viewBindingFactory: (View) -> T
+) : ReadOnlyProperty {
+ private var binding: T? = null
+
+ init {
+ fragment.lifecycle.addObserver(object : DefaultLifecycleObserver {
+ val viewLifecycleOwnerLiveDataObserver =
+ Observer {
+ val viewLifecycleOwner = it ?: return@Observer
+
+ viewLifecycleOwner.lifecycle.addObserver(object : DefaultLifecycleObserver {
+ override fun onDestroy(owner: LifecycleOwner) {
+ binding = null
+ }
+ })
+ }
+
+ override fun onCreate(owner: LifecycleOwner) {
+ fragment.viewLifecycleOwnerLiveData.observeForever(
+ viewLifecycleOwnerLiveDataObserver
+ )
+ }
+
+ override fun onDestroy(owner: LifecycleOwner) {
+ fragment.viewLifecycleOwnerLiveData.removeObserver(
+ viewLifecycleOwnerLiveDataObserver
+ )
+ }
+ })
+ }
+
+ override fun getValue(thisRef: Fragment, property: KProperty<*>): T {
+ val binding = binding
+ if (binding != null) {
+ return binding
+ }
+
+ val lifecycle = fragment.viewLifecycleOwner.lifecycle
+ if (!lifecycle.currentState.isAtLeast(Lifecycle.State.INITIALIZED)) {
+ throw IllegalStateException("Should not attempt to get bindings when Fragment views are destroyed.")
+ }
+
+ return viewBindingFactory(thisRef.requireView()).also { this.binding = it }
+ }
+}
+
+fun Fragment.viewBinding(viewBindingFactory: (View) -> T) =
+ FragmentViewBindingDelegate(this, viewBindingFactory)
\ No newline at end of file
diff --git a/app/src/main/java/com/example/unrd/common/LoadingDrawable.kt b/app/src/main/java/com/example/unrd/common/LoadingDrawable.kt
new file mode 100644
index 0000000..d554979
--- /dev/null
+++ b/app/src/main/java/com/example/unrd/common/LoadingDrawable.kt
@@ -0,0 +1,43 @@
+package com.example.unrd.common
+
+import android.animation.ValueAnimator
+import android.graphics.*
+import android.graphics.drawable.Drawable
+
+class LoadingDrawable : Drawable(), ValueAnimator.AnimatorUpdateListener {
+
+ private val paint = Paint().apply {
+ color = Color.MAGENTA
+ isAntiAlias = true
+ style = Paint.Style.FILL
+ }
+ private val animator: ValueAnimator = ValueAnimator.ofFloat(20.0f, 60f)
+ private var currentSize = 50f
+
+ init {
+ animator.addUpdateListener(this)
+ animator.duration = 500
+ animator.repeatMode = ValueAnimator.REVERSE
+ animator.repeatCount = ValueAnimator.INFINITE
+ animator.start()
+ }
+
+ override fun draw(canvas: Canvas) {
+ canvas.drawCircle(bounds.width() / 2f, bounds.height() / 2f, currentSize, paint)
+ }
+
+ override fun setAlpha(p0: Int) {}
+
+ override fun getOpacity(): Int {
+ return PixelFormat.TRANSPARENT
+ }
+
+ override fun setColorFilter(p0: ColorFilter?) {
+ paint.colorFilter = p0
+ }
+
+ override fun onAnimationUpdate(p0: ValueAnimator?) {
+ currentSize = p0?.animatedValue as Float
+ invalidateSelf()
+ }
+}
diff --git a/app/src/main/java/com/example/unrd/common/UnrdApplication.kt b/app/src/main/java/com/example/unrd/common/UnrdApplication.kt
new file mode 100644
index 0000000..07fee3b
--- /dev/null
+++ b/app/src/main/java/com/example/unrd/common/UnrdApplication.kt
@@ -0,0 +1,15 @@
+package com.example.unrd.common
+
+import android.app.Application
+import com.orhanobut.logger.AndroidLogAdapter
+import com.orhanobut.logger.Logger
+import dagger.hilt.android.HiltAndroidApp
+
+@HiltAndroidApp
+class UnrdApplication: Application(){
+
+ override fun onCreate() {
+ super.onCreate()
+ Logger.addLogAdapter(AndroidLogAdapter())
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/unrd/common/utils/Exhaustive.kt b/app/src/main/java/com/example/unrd/common/utils/Exhaustive.kt
new file mode 100644
index 0000000..99469a9
--- /dev/null
+++ b/app/src/main/java/com/example/unrd/common/utils/Exhaustive.kt
@@ -0,0 +1,4 @@
+package com.example.unrd.common.utils
+
+val T.exhaustive: T
+ get() = this
diff --git a/app/src/main/java/com/example/unrd/common/utils/Fragment.kt b/app/src/main/java/com/example/unrd/common/utils/Fragment.kt
new file mode 100644
index 0000000..05d30a8
--- /dev/null
+++ b/app/src/main/java/com/example/unrd/common/utils/Fragment.kt
@@ -0,0 +1,16 @@
+package com.example.unrd.common.utils
+
+import android.widget.Toast
+import androidx.fragment.app.Fragment
+
+fun Fragment.showToast(message: String, length: Int){
+ Toast.makeText(requireContext(), message, length).show()
+}
+
+fun Fragment.showToastShort(message: String){
+ showToast(message, Toast.LENGTH_SHORT)
+}
+
+ fun Fragment.showToastLong(message: String ){
+ showToast(message, Toast.LENGTH_LONG)
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/unrd/common/utils/IfNotNullAs.kt b/app/src/main/java/com/example/unrd/common/utils/IfNotNullAs.kt
new file mode 100644
index 0000000..d4a35a8
--- /dev/null
+++ b/app/src/main/java/com/example/unrd/common/utils/IfNotNullAs.kt
@@ -0,0 +1,26 @@
+package com.example.unrd.common.utils
+
+@JvmSynthetic
+inline fun Any?.ifNotNullAs(
+ ifNotNull: (R) -> Unit
+): Any? {
+
+ return ifNotNullAs(
+ ifNotNull = ifNotNull,
+ ifNull = { }
+ )
+}
+
+@JvmSynthetic
+inline fun Any?.ifNotNullAs(
+ ifNotNull: (R) -> Unit,
+ ifNull: () -> Unit
+): Any? {
+
+ if (this != null && this is R) {
+ ifNotNull(this as R)
+ return this
+ }
+ ifNull()
+ return this
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/unrd/core/common/BaseViewModel.kt b/app/src/main/java/com/example/unrd/core/common/BaseViewModel.kt
new file mode 100644
index 0000000..b50dc76
--- /dev/null
+++ b/app/src/main/java/com/example/unrd/core/common/BaseViewModel.kt
@@ -0,0 +1,33 @@
+package com.example.unrd.core.common
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+import io.reactivex.rxjava3.disposables.CompositeDisposable
+import io.reactivex.rxjava3.disposables.Disposable
+
+abstract class BaseViewModel: ViewModel(){
+
+ private val _loading: MutableLiveData = MutableLiveData(false)
+ val loading: LiveData
+ get() = _loading
+
+ private val _errorEvent: MutableLiveData = MutableLiveData()
+ val errorEvent: LiveData
+ get() = _errorEvent
+
+ private val compositeDisposable = CompositeDisposable()
+
+ override fun onCleared() {
+ compositeDisposable.clear()
+ super.onCleared()
+ }
+
+ fun Disposable?.remember(){
+ compositeDisposable.add(this)
+ }
+
+ fun setLoading(isLoading: Boolean) = _loading.postValue(isLoading)
+
+ fun triggerError(error: Throwable) = _errorEvent.postValue(error)
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/unrd/core/di/HttpModule.kt b/app/src/main/java/com/example/unrd/core/di/HttpModule.kt
new file mode 100644
index 0000000..c4f6349
--- /dev/null
+++ b/app/src/main/java/com/example/unrd/core/di/HttpModule.kt
@@ -0,0 +1,34 @@
+package com.example.unrd.core.di
+
+import com.example.unrd.core.service.BackendConfig
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+import okhttp3.OkHttpClient
+import okhttp3.logging.HttpLoggingInterceptor
+import java.util.concurrent.TimeUnit
+import javax.inject.Singleton
+
+@Module
+@InstallIn(SingletonComponent::class)
+class HttpModule {
+
+ @Provides
+ @Singleton
+ internal fun provideHttpClient(
+ httpLoggingInterceptor: HttpLoggingInterceptor,
+ config: BackendConfig
+ ): OkHttpClient = OkHttpClient().newBuilder()
+ .readTimeout(config.readTimeout, TimeUnit.SECONDS)
+ .connectTimeout(config.connectionTimeout, TimeUnit.SECONDS)
+ .addInterceptor(httpLoggingInterceptor)
+ .build()
+
+ @Provides
+ @Singleton
+ internal fun provideHttpLoggingInterceptor(): HttpLoggingInterceptor =
+ HttpLoggingInterceptor().apply {
+ level = HttpLoggingInterceptor.Level.BODY
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/unrd/core/di/NetworkModule.kt b/app/src/main/java/com/example/unrd/core/di/NetworkModule.kt
new file mode 100644
index 0000000..63b11bd
--- /dev/null
+++ b/app/src/main/java/com/example/unrd/core/di/NetworkModule.kt
@@ -0,0 +1,49 @@
+package com.example.unrd.core.di
+
+import com.example.unrd.core.service.BackendConfig
+import com.example.unrd.core.service.UnrdService
+import com.squareup.moshi.Moshi
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+import hu.akarnokd.rxjava3.retrofit.RxJava3CallAdapterFactory
+import okhttp3.OkHttpClient
+import retrofit2.Retrofit
+import retrofit2.converter.moshi.MoshiConverterFactory
+import javax.inject.Singleton
+
+@Module
+@InstallIn(SingletonComponent::class)
+class NetworkModule {
+
+ @Provides
+ @Singleton
+ internal fun provideMoshiConverterFactory(moshi: Moshi): MoshiConverterFactory =
+ MoshiConverterFactory.create(moshi)
+
+ @Provides
+ @Singleton
+ internal fun provideRxJava3CallAdapterFactory(): RxJava3CallAdapterFactory =
+ RxJava3CallAdapterFactory.create()
+
+ @Provides
+ @Singleton
+ internal fun provideConfig(): BackendConfig = BackendConfig()
+
+ @Provides
+ @Singleton
+ internal fun provideRetrofitBuilder(
+ backendConfig: BackendConfig,
+ httpClient: OkHttpClient,
+ moshiConverterFactory: MoshiConverterFactory,
+ rxJavaCallAdapterFactory: RxJava3CallAdapterFactory
+ ): UnrdService =
+ Retrofit.Builder()
+ .baseUrl(backendConfig.apiUrl)
+ .client(httpClient)
+ .addConverterFactory(moshiConverterFactory)
+ .addCallAdapterFactory(rxJavaCallAdapterFactory)
+ .build()
+ .create(UnrdService::class.java)
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/unrd/core/di/SchedulersModule.kt b/app/src/main/java/com/example/unrd/core/di/SchedulersModule.kt
new file mode 100644
index 0000000..1aa0f85
--- /dev/null
+++ b/app/src/main/java/com/example/unrd/core/di/SchedulersModule.kt
@@ -0,0 +1,59 @@
+package com.example.unrd.core.di
+
+import com.example.unrd.core.schedulers.*
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
+import io.reactivex.rxjava3.core.Scheduler
+import io.reactivex.rxjava3.schedulers.Schedulers
+import javax.inject.Singleton
+
+@Module
+@InstallIn(SingletonComponent::class)
+object SchedulersModule {
+
+ @Provides
+ @DbScheduler
+ internal fun provideDbScheduler(): Scheduler {
+ return Schedulers.single()
+ }
+
+ @Provides
+ @NetworkScheduler
+ internal fun provideNetworkScheduler(): Scheduler {
+ return Schedulers.io()
+ }
+
+ @Provides
+ @ComputationScheduler
+ internal fun provideComputationScheduler(): Scheduler {
+ return Schedulers.computation()
+ }
+
+ @Provides
+ @DiskIOScheduler
+ internal fun provideDiskIoScheduler(): Scheduler {
+ return Schedulers.io()
+ }
+
+ @Provides
+ @MainThreadScheduler
+ internal fun provideMainThreadScheduler(): Scheduler {
+ return AndroidSchedulers.mainThread()
+ }
+
+ @Provides
+ @Singleton
+ internal fun provideCoreSchedulers(
+ @ComputationScheduler computation: Scheduler,
+ @DbScheduler dbIO: Scheduler,
+ @DiskIOScheduler diskIO: Scheduler,
+ @NetworkScheduler networkIO: Scheduler,
+ @MainThreadScheduler mainThread: Scheduler
+ ): CoreSchedulers {
+ return CoreSchedulersImpl(computation, dbIO, diskIO, networkIO, mainThread)
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/unrd/core/model/Gif.kt b/app/src/main/java/com/example/unrd/core/model/Gif.kt
new file mode 100644
index 0000000..cfb83c1
--- /dev/null
+++ b/app/src/main/java/com/example/unrd/core/model/Gif.kt
@@ -0,0 +1,11 @@
+package com.example.unrd.core.model
+
+data class Gif(
+ val id: String,
+ val originalUrl: String,
+ val smallUrl: String,
+ val title: String,
+ val source: String,
+ val ratingCode: String,
+ val pageUrl: String
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/example/unrd/core/repository/ResultRepository.kt b/app/src/main/java/com/example/unrd/core/repository/ResultRepository.kt
new file mode 100644
index 0000000..7f489ac
--- /dev/null
+++ b/app/src/main/java/com/example/unrd/core/repository/ResultRepository.kt
@@ -0,0 +1,35 @@
+package com.example.unrd.core.repository
+
+import androidx.annotation.WorkerThread
+import com.example.unrd.core.schedulers.CoreSchedulers
+import com.example.unrd.core.service.UnrdService
+import com.example.unrd.core.service.model.ResultResponse
+import io.reactivex.rxjava3.core.BackpressureStrategy
+import io.reactivex.rxjava3.core.Completable
+import io.reactivex.rxjava3.core.Flowable
+import io.reactivex.rxjava3.subjects.BehaviorSubject
+import io.reactivex.rxjava3.subjects.Subject
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Singleton
+class ResultRepository @Inject constructor(
+ private val unrdService: UnrdService,
+ private val coreSchedulers: CoreSchedulers
+){
+
+ private val data: Subject =
+ BehaviorSubject.create().toSerialized()
+
+ @WorkerThread
+ fun refresh(): Completable = unrdService.getResult()
+ .doAfterSuccess(::updateResults)
+ .subscribeOn(coreSchedulers.networkIO)
+ .ignoreElement()
+
+ fun getData(): Flowable = data.toFlowable(BackpressureStrategy.LATEST)
+
+ private fun updateResults(updatedData: ResultResponse){
+ data.onNext(updatedData)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/unrd/core/repository/useCase/GetUiModelUseCase.kt b/app/src/main/java/com/example/unrd/core/repository/useCase/GetUiModelUseCase.kt
new file mode 100644
index 0000000..c3635bd
--- /dev/null
+++ b/app/src/main/java/com/example/unrd/core/repository/useCase/GetUiModelUseCase.kt
@@ -0,0 +1,17 @@
+package com.example.unrd.core.repository.useCase
+
+import com.example.unrd.core.repository.ResultRepository
+import com.example.unrd.fragment.MainFragmentUiModel
+import com.example.unrd.fragment.MainFragmentUiModelMapper
+import io.reactivex.rxjava3.core.Flowable
+import javax.inject.Inject
+
+class GetUiModelUseCase @Inject constructor(
+ private val resultRepository: ResultRepository,
+ private val mapper: MainFragmentUiModelMapper
+) {
+
+ fun get(): Flowable =
+ resultRepository.getData().map(mapper::map)
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/unrd/core/repository/useCase/RefreshResultUseCase.kt b/app/src/main/java/com/example/unrd/core/repository/useCase/RefreshResultUseCase.kt
new file mode 100644
index 0000000..8e25591
--- /dev/null
+++ b/app/src/main/java/com/example/unrd/core/repository/useCase/RefreshResultUseCase.kt
@@ -0,0 +1,11 @@
+package com.example.unrd.core.repository.useCase
+
+import com.example.unrd.core.repository.ResultRepository
+import javax.inject.Inject
+
+class RefreshResultUseCase @Inject constructor(
+ private val resultRepository: ResultRepository
+) {
+
+ fun refresh() = resultRepository.refresh()
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/unrd/core/schedulers/CoreSchedulers.kt b/app/src/main/java/com/example/unrd/core/schedulers/CoreSchedulers.kt
new file mode 100644
index 0000000..fe51b8b
--- /dev/null
+++ b/app/src/main/java/com/example/unrd/core/schedulers/CoreSchedulers.kt
@@ -0,0 +1,11 @@
+package com.example.unrd.core.schedulers
+
+import io.reactivex.rxjava3.core.Scheduler
+
+interface CoreSchedulers {
+ val computation: Scheduler
+ val dbIO: Scheduler
+ val diskIO: Scheduler
+ val networkIO: Scheduler
+ val mainThread: Scheduler
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/unrd/core/schedulers/CoreSchedulersImpl.kt b/app/src/main/java/com/example/unrd/core/schedulers/CoreSchedulersImpl.kt
new file mode 100644
index 0000000..b619361
--- /dev/null
+++ b/app/src/main/java/com/example/unrd/core/schedulers/CoreSchedulersImpl.kt
@@ -0,0 +1,37 @@
+package com.example.unrd.core.schedulers
+
+import io.reactivex.rxjava3.core.Scheduler
+import javax.inject.Inject
+import javax.inject.Qualifier
+import javax.inject.Singleton
+
+@Singleton
+open class CoreSchedulersImpl
+@Inject
+constructor(
+ @ComputationScheduler override val computation: Scheduler,
+ @DbScheduler override val dbIO: Scheduler,
+ @DiskIOScheduler override val diskIO: Scheduler,
+ @NetworkScheduler override val networkIO: Scheduler,
+ @MainThreadScheduler override val mainThread: Scheduler
+): CoreSchedulers
+
+@Qualifier
+@kotlin.annotation.Retention(AnnotationRetention.RUNTIME)
+annotation class ComputationScheduler
+
+@Qualifier
+@kotlin.annotation.Retention(AnnotationRetention.RUNTIME)
+annotation class DbScheduler
+
+@Qualifier
+@kotlin.annotation.Retention(AnnotationRetention.RUNTIME)
+annotation class DiskIOScheduler
+
+@Qualifier
+@kotlin.annotation.Retention(AnnotationRetention.RUNTIME)
+annotation class NetworkScheduler
+
+@Qualifier
+@kotlin.annotation.Retention(AnnotationRetention.RUNTIME)
+annotation class MainThreadScheduler
\ No newline at end of file
diff --git a/app/src/main/java/com/example/unrd/core/schedulers/TestSchedulers.kt b/app/src/main/java/com/example/unrd/core/schedulers/TestSchedulers.kt
new file mode 100644
index 0000000..d7b9db0
--- /dev/null
+++ b/app/src/main/java/com/example/unrd/core/schedulers/TestSchedulers.kt
@@ -0,0 +1,21 @@
+package com.example.unrd.core.schedulers
+
+import io.reactivex.rxjava3.core.Scheduler
+import io.reactivex.rxjava3.schedulers.Schedulers
+
+open class TestSchedulers : CoreSchedulers {
+ override val computation: Scheduler
+ get() = Schedulers.trampoline()
+
+ override val dbIO: Scheduler
+ get() = Schedulers.trampoline()
+
+ override val diskIO: Scheduler
+ get() = Schedulers.trampoline()
+
+ override val networkIO: Scheduler
+ get() = Schedulers.trampoline()
+
+ override val mainThread: Scheduler
+ get() = Schedulers.trampoline()
+}
diff --git a/app/src/main/java/com/example/unrd/core/serialization/JsonAdapter.kt b/app/src/main/java/com/example/unrd/core/serialization/JsonAdapter.kt
new file mode 100644
index 0000000..71d6809
--- /dev/null
+++ b/app/src/main/java/com/example/unrd/core/serialization/JsonAdapter.kt
@@ -0,0 +1,13 @@
+package com.example.unrd.core.serialization
+
+import java.lang.reflect.ParameterizedType
+
+interface JsonAdapter {
+ fun stringToJson(string: String?, clazz: Class): T?
+
+ fun stringToJson(string: String?, type: ParameterizedType): T?
+
+ fun jsonToString(json: T?, clazz: Class): String?
+
+ fun jsonToString(json: T?, type: ParameterizedType): String?
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/unrd/core/serialization/MoshiAdapter.kt b/app/src/main/java/com/example/unrd/core/serialization/MoshiAdapter.kt
new file mode 100644
index 0000000..50ad883
--- /dev/null
+++ b/app/src/main/java/com/example/unrd/core/serialization/MoshiAdapter.kt
@@ -0,0 +1,24 @@
+package com.example.unrd.core.serialization
+
+import com.squareup.moshi.Moshi
+import java.lang.reflect.ParameterizedType
+import javax.inject.Inject
+
+class MoshiAdapter @Inject constructor(private val moshi: Moshi) : JsonAdapter {
+
+ override fun stringToJson(string: String?, clazz: Class): T? {
+ return string?.let { moshi.adapter(clazz).fromJson(string) }
+ }
+
+ override fun stringToJson(string: String?, type: ParameterizedType): T? {
+ return string?.let { moshi.adapter(type).fromJson(string) }
+ }
+
+ override fun jsonToString(json: T?, clazz: Class): String? {
+ return json?.let { moshi.adapter(clazz).toJson(json) }
+ }
+
+ override fun jsonToString(json: T?, type: ParameterizedType): String? {
+ return json?.let { moshi.adapter(type).toJson(json) }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/unrd/core/serialization/di/SerializationModule.kt b/app/src/main/java/com/example/unrd/core/serialization/di/SerializationModule.kt
new file mode 100644
index 0000000..52014fe
--- /dev/null
+++ b/app/src/main/java/com/example/unrd/core/serialization/di/SerializationModule.kt
@@ -0,0 +1,24 @@
+package com.example.unrd.core.serialization.di
+
+import com.squareup.moshi.Moshi
+import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+import javax.inject.Singleton
+
+@Module
+@InstallIn(SingletonComponent::class)
+abstract class SerializationModule {
+
+ companion object{
+ @Provides
+ @Singleton
+ internal fun provideMoshi(): Moshi {
+ return Moshi.Builder()
+ .add(KotlinJsonAdapterFactory())
+ .build()
+ }
+ }
+}
diff --git a/app/src/main/java/com/example/unrd/core/service/BackendConfig.kt b/app/src/main/java/com/example/unrd/core/service/BackendConfig.kt
new file mode 100644
index 0000000..3d50b49
--- /dev/null
+++ b/app/src/main/java/com/example/unrd/core/service/BackendConfig.kt
@@ -0,0 +1,9 @@
+package com.example.unrd.core.service
+
+import javax.inject.Inject
+
+class BackendConfig @Inject constructor() {
+ val apiUrl = "https://s3-eu-west-1.amazonaws.com"
+ val readTimeout = 60L
+ val connectionTimeout = 60L
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/unrd/core/service/UnrdService.kt b/app/src/main/java/com/example/unrd/core/service/UnrdService.kt
new file mode 100644
index 0000000..bde0e81
--- /dev/null
+++ b/app/src/main/java/com/example/unrd/core/service/UnrdService.kt
@@ -0,0 +1,11 @@
+package com.example.unrd.core.service
+
+import com.example.unrd.core.service.model.ResultResponse
+import io.reactivex.rxjava3.core.Single
+import retrofit2.http.GET
+
+interface UnrdService {
+
+ @GET("/unrd-scratch/resp.json")
+ fun getResult(): Single
+}
diff --git a/app/src/main/java/com/example/unrd/core/service/model/GifsResponse.kt b/app/src/main/java/com/example/unrd/core/service/model/GifsResponse.kt
new file mode 100644
index 0000000..53bb405
--- /dev/null
+++ b/app/src/main/java/com/example/unrd/core/service/model/GifsResponse.kt
@@ -0,0 +1,56 @@
+package com.example.unrd.core.service.model
+
+import com.squareup.moshi.Json
+import com.squareup.moshi.JsonClass
+
+@JsonClass(generateAdapter = true)
+data class ResultResponse(
+ @Json(name = "result")
+ val data: ResultDataResponse
+)
+
+@JsonClass(generateAdapter = true)
+data class ResultDataResponse(
+ @Json(name = "story_id")
+ val storyId: Int,
+ @Json(name = "name")
+ val name: String,
+ @Json(name = "short_summary")
+ val shortSummary: String,
+ @Json(name = "full_summary")
+ val fullSummary: String,
+ @Json(name = "duration")
+ val duration: String,
+ @Json(name = "list_image")
+ val images: List,
+ @Json(name = "preview_media")
+ val previewsMedia: List,
+ @Json(name = "intro_video")
+ val introVideos: List,
+ @Json(name = "background_image")
+ val backgroundImages: List
+)
+
+@JsonClass(generateAdapter = true)
+data class Image(
+ @Json(name = "resource_uri")
+ val uri: String
+)
+
+@JsonClass(generateAdapter = true)
+data class PreviewMedia(
+ @Json(name = "resource_uri")
+ val uri: String
+)
+
+@JsonClass(generateAdapter = true)
+data class IntroVideo(
+ @Json(name = "resource_uri")
+ val uri: String
+)
+
+@JsonClass(generateAdapter = true)
+data class BackgroundImage(
+ @Json(name = "resource_uri")
+ val uri: String
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/example/unrd/fragment/MainFragment.kt b/app/src/main/java/com/example/unrd/fragment/MainFragment.kt
new file mode 100644
index 0000000..8df89f1
--- /dev/null
+++ b/app/src/main/java/com/example/unrd/fragment/MainFragment.kt
@@ -0,0 +1,75 @@
+package com.example.unrd.fragment
+
+import android.os.Bundle
+import android.view.View
+import android.widget.Toast
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.viewModels
+import androidx.navigation.fragment.findNavController
+import com.bumptech.glide.Glide
+import com.example.unrd.R
+import com.example.unrd.common.LoadingDrawable
+import com.example.unrd.common.viewBinding
+import com.example.unrd.databinding.MainFragmentBinding
+import dagger.hilt.android.AndroidEntryPoint
+
+@AndroidEntryPoint
+class MainFragment : Fragment(R.layout.main_fragment) {
+
+ private val viewModel: MainViewModel by viewModels()
+
+ private val binding: MainFragmentBinding by viewBinding(MainFragmentBinding::bind)
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ onBoundView()
+ }
+
+ private fun onBoundView() = with(binding) {
+ viewModel.uiModel.observe(viewLifecycleOwner) {
+ loadBackground(it.backgroundImage)
+ title.text = it.title
+ duration.text = it.duration
+ shortDescription.text = it.shortDescription
+ shortDescription.visibility = View.VISIBLE
+ longDescription.text = it.longDescription
+ longDescription.visibility = View.VISIBLE
+ setIntroClickListener(it.introUrl)
+ setImageClickListener(it.imageUrl)
+ setPreviewClickListener(it.previewUrl)
+ }
+ viewModel.errorEvent.observe(viewLifecycleOwner){
+ Toast.makeText(requireContext(), it.localizedMessage, Toast.LENGTH_SHORT).show()
+ }
+ }
+
+ private fun loadBackground(imageUrl: String){
+ Glide.with(requireContext())
+ .load(imageUrl)
+ .placeholder(LoadingDrawable())
+ .centerInside()
+ .into(binding.imageBackground)
+ }
+
+ private fun setIntroClickListener(imageUrl: String){
+ val direction = MainFragmentDirections.actionToVideoFragment(imageUrl)
+ binding.introVideo.setOnClickListener {
+ findNavController().navigate(direction)
+ }
+ }
+
+ private fun setPreviewClickListener(imageUrl: String){
+ val direction = MainFragmentDirections.actionToVideoFragment(imageUrl)
+ binding.video.setOnClickListener {
+ findNavController().navigate(direction)
+ }
+ }
+
+ private fun setImageClickListener(imageUrl: String){
+ val direction = MainFragmentDirections.actionToImageFragment(imageUrl)
+ binding.image.setOnClickListener {
+ findNavController().navigate(direction)
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/unrd/fragment/MainFragmentUiModel.kt b/app/src/main/java/com/example/unrd/fragment/MainFragmentUiModel.kt
new file mode 100644
index 0000000..dad4ac6
--- /dev/null
+++ b/app/src/main/java/com/example/unrd/fragment/MainFragmentUiModel.kt
@@ -0,0 +1,12 @@
+package com.example.unrd.fragment
+
+data class MainFragmentUiModel(
+ val title: String,
+ val duration: String,
+ val shortDescription: String,
+ val longDescription: String,
+ val backgroundImage: String,
+ val imageUrl: String,
+ val introUrl: String,
+ val previewUrl: String
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/example/unrd/fragment/MainFragmentUiModelMapper.kt b/app/src/main/java/com/example/unrd/fragment/MainFragmentUiModelMapper.kt
new file mode 100644
index 0000000..3a63d34
--- /dev/null
+++ b/app/src/main/java/com/example/unrd/fragment/MainFragmentUiModelMapper.kt
@@ -0,0 +1,20 @@
+package com.example.unrd.fragment
+
+import com.example.unrd.core.service.model.ResultResponse
+import javax.inject.Inject
+
+class MainFragmentUiModelMapper @Inject constructor() {
+
+ fun map(response: ResultResponse) = with(response.data){
+ MainFragmentUiModel(
+ title = name,
+ duration = "Duration: $duration",
+ shortDescription = shortSummary,
+ longDescription = fullSummary,
+ backgroundImage = backgroundImages.first().uri,
+ introUrl = introVideos.first().uri,
+ previewUrl = previewsMedia.first().uri,
+ imageUrl = images.first().uri
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/unrd/fragment/MainViewModel.kt b/app/src/main/java/com/example/unrd/fragment/MainViewModel.kt
new file mode 100644
index 0000000..2fe1597
--- /dev/null
+++ b/app/src/main/java/com/example/unrd/fragment/MainViewModel.kt
@@ -0,0 +1,19 @@
+package com.example.unrd.fragment
+
+import androidx.lifecycle.LiveDataReactiveStreams.fromPublisher
+import com.example.unrd.core.common.BaseViewModel
+import com.example.unrd.core.repository.useCase.GetUiModelUseCase
+import dagger.hilt.android.lifecycle.HiltViewModel
+import javax.inject.Inject
+
+@HiltViewModel
+class MainViewModel @Inject constructor(
+ private val getUiModelUseCase: GetUiModelUseCase
+): BaseViewModel(){
+
+ val uiModel
+ get() = fromPublisher(getUiModelUseCase.get()
+ .doOnError { triggerError(it) }
+ .onErrorComplete()
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/unrd/fragment/image/ImageFragment.kt b/app/src/main/java/com/example/unrd/fragment/image/ImageFragment.kt
new file mode 100644
index 0000000..bed6e17
--- /dev/null
+++ b/app/src/main/java/com/example/unrd/fragment/image/ImageFragment.kt
@@ -0,0 +1,33 @@
+package com.example.unrd.fragment.image
+
+import android.os.Bundle
+import android.view.View
+import androidx.fragment.app.Fragment
+import androidx.navigation.fragment.navArgs
+import com.bumptech.glide.Glide
+import com.example.unrd.R
+import com.example.unrd.common.LoadingDrawable
+import com.example.unrd.common.viewBinding
+import com.example.unrd.databinding.ImageFragmentBinding
+import dagger.hilt.android.AndroidEntryPoint
+
+@AndroidEntryPoint
+class ImageFragment : Fragment(R.layout.image_fragment) {
+
+ private val binding: ImageFragmentBinding by viewBinding(ImageFragmentBinding::bind)
+
+ private val args: ImageFragmentArgs by navArgs()
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ onBoundView()
+ }
+
+ private fun onBoundView() = with(binding) {
+ Glide.with(requireContext())
+ .load(args.url)
+ .placeholder(LoadingDrawable())
+ .centerInside()
+ .into(image)
+ }
+}
diff --git a/app/src/main/java/com/example/unrd/fragment/video/VideoFragment.kt b/app/src/main/java/com/example/unrd/fragment/video/VideoFragment.kt
new file mode 100644
index 0000000..12c4e2b
--- /dev/null
+++ b/app/src/main/java/com/example/unrd/fragment/video/VideoFragment.kt
@@ -0,0 +1,30 @@
+package com.example.unrd.fragment.video
+
+import android.os.Bundle
+import android.view.View
+import android.widget.MediaController
+import androidx.fragment.app.Fragment
+import androidx.navigation.fragment.navArgs
+import com.example.unrd.R
+import com.example.unrd.common.viewBinding
+import com.example.unrd.databinding.VideoFragmentBinding
+import dagger.hilt.android.AndroidEntryPoint
+
+@AndroidEntryPoint
+class VideoFragment : Fragment(R.layout.video_fragment) {
+
+ private val binding: VideoFragmentBinding by viewBinding(VideoFragmentBinding::bind)
+
+ private val args: VideoFragmentArgs by navArgs()
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ onBoundView()
+ }
+
+ private fun onBoundView() = with(binding) {
+ video.setMediaController(MediaController(requireContext()))
+ video.setVideoPath(args.url)
+ video.start()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
new file mode 100644
index 0000000..2b068d1
--- /dev/null
+++ b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/gradient.xml b/app/src/main/res/drawable/gradient.xml
new file mode 100644
index 0000000..1a05f80
--- /dev/null
+++ b/app/src/main/res/drawable/gradient.xml
@@ -0,0 +1,10 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_baseline_image_24.xml b/app/src/main/res/drawable/ic_baseline_image_24.xml
new file mode 100644
index 0000000..8232c4d
--- /dev/null
+++ b/app/src/main/res/drawable/ic_baseline_image_24.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_baseline_ondemand_video_24.xml b/app/src/main/res/drawable/ic_baseline_ondemand_video_24.xml
new file mode 100644
index 0000000..8cdf5de
--- /dev/null
+++ b/app/src/main/res/drawable/ic_baseline_ondemand_video_24.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_baseline_personal_video_24.xml b/app/src/main/res/drawable/ic_baseline_personal_video_24.xml
new file mode 100644
index 0000000..9951b48
--- /dev/null
+++ b/app/src/main/res/drawable/ic_baseline_personal_video_24.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 0000000..07d5da9
--- /dev/null
+++ b/app/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,170 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000..5badb61
--- /dev/null
+++ b/app/src/main/res/layout/activity_main.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/image_fragment.xml b/app/src/main/res/layout/image_fragment.xml
new file mode 100644
index 0000000..e0d1a09
--- /dev/null
+++ b/app/src/main/res/layout/image_fragment.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/main_fragment.xml b/app/src/main/res/layout/main_fragment.xml
new file mode 100644
index 0000000..f1299cf
--- /dev/null
+++ b/app/src/main/res/layout/main_fragment.xml
@@ -0,0 +1,98 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/video_fragment.xml b/app/src/main/res/layout/video_fragment.xml
new file mode 100644
index 0000000..f758ecf
--- /dev/null
+++ b/app/src/main/res/layout/video_fragment.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000..eca70cf
--- /dev/null
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 0000000..eca70cf
--- /dev/null
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp
new file mode 100644
index 0000000..c209e78
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..b2dfe3d
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp
new file mode 100644
index 0000000..4f0f1d6
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..62b611d
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
new file mode 100644
index 0000000..948a307
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..1b9a695
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
new file mode 100644
index 0000000..28d4b77
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..9287f50
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
new file mode 100644
index 0000000..aa7d642
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..9126ae3
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/navigation/nav_graph.xml b/app/src/main/res/navigation/nav_graph.xml
new file mode 100644
index 0000000..70087f2
--- /dev/null
+++ b/app/src/main/res/navigation/nav_graph.xml
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml
new file mode 100644
index 0000000..c188616
--- /dev/null
+++ b/app/src/main/res/values-night/themes.xml
@@ -0,0 +1,16 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
new file mode 100644
index 0000000..f8c6127
--- /dev/null
+++ b/app/src/main/res/values/colors.xml
@@ -0,0 +1,10 @@
+
+
+ #FFBB86FC
+ #FF6200EE
+ #FF3700B3
+ #FF03DAC5
+ #FF018786
+ #FF000000
+ #FFFFFFFF
+
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
new file mode 100644
index 0000000..fe94cfa
--- /dev/null
+++ b/app/src/main/res/values/strings.xml
@@ -0,0 +1,3 @@
+
+ Unrd
+
\ No newline at end of file
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
new file mode 100644
index 0000000..e86b936
--- /dev/null
+++ b/app/src/main/res/values/styles.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
new file mode 100644
index 0000000..a61b50e
--- /dev/null
+++ b/app/src/main/res/values/themes.xml
@@ -0,0 +1,16 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/test/java/com/example/unrd/ExampleUnitTest.kt b/app/src/test/java/com/example/unrd/ExampleUnitTest.kt
new file mode 100644
index 0000000..b839450
--- /dev/null
+++ b/app/src/test/java/com/example/unrd/ExampleUnitTest.kt
@@ -0,0 +1,17 @@
+package com.example.unrd
+
+import org.junit.Test
+
+import org.junit.Assert.*
+
+/**
+ * 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
diff --git a/app/src/test/java/com/example/unrd/core/repository/ResultRepositoryTest.kt b/app/src/test/java/com/example/unrd/core/repository/ResultRepositoryTest.kt
new file mode 100644
index 0000000..5b8172a
--- /dev/null
+++ b/app/src/test/java/com/example/unrd/core/repository/ResultRepositoryTest.kt
@@ -0,0 +1,43 @@
+package com.example.unrd.core.repository
+
+import com.example.unrd.core.schedulers.CoreSchedulers
+import com.example.unrd.core.schedulers.TestSchedulers
+import com.example.unrd.core.service.UnrdService
+import com.example.unrd.core.service.model.ResultResponse
+import io.mockk.every
+import io.mockk.mockk
+import io.reactivex.rxjava3.core.Single
+import org.junit.Test
+
+class ResultRepositoryTest {
+
+ private val service: UnrdService = mockk()
+ private val coreSchedulers: CoreSchedulers = TestSchedulers()
+
+ private val sut = ResultRepository(
+ unrdService = service,
+ coreSchedulers = coreSchedulers
+ )
+
+ @Test
+ fun refresh() {
+ val response: ResultResponse = mockk()
+ every { service.getResult() } returns Single.just(response)
+
+ sut.refresh().test()
+ .assertComplete()
+
+ sut.getData().test().assertValue(response)
+ }
+
+ @Test
+ fun `refreshAll when error`() {
+ val error = RuntimeException()
+ every { service.getResult() } returns Single.error(error)
+
+ sut.refresh().test()
+ .assertFailure(RuntimeException::class.java)
+
+ sut.getData().test().assertNoValues()
+ }
+}
\ No newline at end of file
diff --git a/app/src/test/java/com/example/unrd/core/repository/useCase/GetUiModelUseCaseTest.kt b/app/src/test/java/com/example/unrd/core/repository/useCase/GetUiModelUseCaseTest.kt
new file mode 100644
index 0000000..5de373d
--- /dev/null
+++ b/app/src/test/java/com/example/unrd/core/repository/useCase/GetUiModelUseCaseTest.kt
@@ -0,0 +1,29 @@
+package com.example.unrd.core.repository.useCase
+
+import com.example.unrd.core.repository.ResultRepository
+import com.example.unrd.core.service.model.ResultResponse
+import com.example.unrd.fragment.MainFragmentUiModel
+import com.example.unrd.fragment.MainFragmentUiModelMapper
+import io.mockk.every
+import io.mockk.mockk
+import io.reactivex.rxjava3.core.Flowable
+import org.junit.Test
+
+class GetUiModelUseCaseTest {
+
+ private val resultRepository: ResultRepository = mockk()
+ private val mapper: MainFragmentUiModelMapper = mockk()
+
+ private val sut = GetUiModelUseCase(resultRepository, mapper)
+
+ @Test
+ fun get() {
+ val resultResponse: ResultResponse = mockk()
+ val expectedResult: MainFragmentUiModel = mockk()
+
+ every { resultRepository.getData() } returns Flowable.just(resultResponse)
+ every { mapper.map(resultResponse) } returns expectedResult
+
+ sut.get().test().assertValue(expectedResult)
+ }
+}
\ No newline at end of file
diff --git a/app/src/test/java/com/example/unrd/fragment/MainViewModelTest.kt b/app/src/test/java/com/example/unrd/fragment/MainViewModelTest.kt
new file mode 100644
index 0000000..f85f473
--- /dev/null
+++ b/app/src/test/java/com/example/unrd/fragment/MainViewModelTest.kt
@@ -0,0 +1,39 @@
+package com.example.unrd.fragment
+
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule
+import com.example.unrd.core.repository.useCase.GetUiModelUseCase
+import com.jraska.livedata.test
+import io.mockk.every
+import io.mockk.mockk
+import io.reactivex.rxjava3.core.Flowable
+import org.junit.Rule
+import org.junit.Test
+import java.io.IOException
+
+class MainViewModelTest {
+
+ @get:Rule
+ var instantExecutorRule = InstantTaskExecutorRule()
+
+ private val getUiModelUseCase: GetUiModelUseCase = mockk()
+
+ private val sut = MainViewModel(getUiModelUseCase)
+
+ @Test
+ fun `get uiModel`() {
+ val uiModel: MainFragmentUiModel = mockk()
+ every { getUiModelUseCase.get() } returns Flowable.just(uiModel)
+
+ sut.uiModel.test().assertValue(uiModel)
+ sut.errorEvent.test().assertNoValue()
+ }
+
+ @Test
+ fun `get uiModel when failure`() {
+ val failure: IOException = mockk()
+ every { getUiModelUseCase.get() } returns Flowable.error(failure)
+
+ sut.uiModel.test().assertNoValue()
+ sut.errorEvent.test().assertValue(failure)
+ }
+}
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 0000000..a971b8a
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,21 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+buildscript {
+ repositories {
+ google()
+ mavenCentral()
+ }
+ dependencies {
+ classpath "com.android.tools.build:gradle:7.0.4"
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.10"
+ classpath 'com.google.dagger:hilt-android-gradle-plugin:2.37'
+ classpath 'androidx.navigation:navigation-safe-args-gradle-plugin:2.4.1'
+ classpath "org.jetbrains.kotlin:kotlin-serialization:1.6.10"
+
+ // NOTE: Do not place your application dependencies here; they belong
+ // in the individual module build.gradle files
+ }
+}
+
+task clean(type: Delete) {
+ delete rootProject.buildDir
+}
\ No newline at end of file
diff --git a/gradle.properties b/gradle.properties
new file mode 100644
index 0000000..336cd31
--- /dev/null
+++ b/gradle.properties
@@ -0,0 +1,22 @@
+# Project-wide Gradle settings.
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
+# AndroidX package structure to make it clearer which packages are bundled with the
+# Android operating system, and which are packaged with your app"s APK
+# https://developer.android.com/topic/libraries/support-library/androidx-rn
+android.useAndroidX=true
+# Automatically convert third-party libraries to use AndroidX
+android.enableJetifier=true
+# Kotlin code style for this project: "official" or "obsolete":
+kotlin.code.style=official
+android.jetifier.ignorelist=moshi-1.13.0
\ No newline at end of file
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..e708b1c
Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..fd94224
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Fri Feb 11 21:05:09 CET 2022
+distributionBase=GRADLE_USER_HOME
+distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-bin.zip
+distributionPath=wrapper/dists
+zipStorePath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
diff --git a/gradlew b/gradlew
new file mode 100755
index 0000000..4f906e0
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,185 @@
+#!/usr/bin/env sh
+
+#
+# Copyright 2015 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+ echo "$*"
+}
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=`expr $i + 1`
+ done
+ case $i in
+ 0) set -- ;;
+ 1) set -- "$args0" ;;
+ 2) set -- "$args0" "$args1" ;;
+ 3) set -- "$args0" "$args1" "$args2" ;;
+ 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Escape application args
+save () {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
+}
+APP_ARGS=`save "$@"`
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 0000000..107acd3
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,89 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/settings.gradle b/settings.gradle
new file mode 100644
index 0000000..b79b1bc
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1,10 @@
+dependencyResolutionManagement {
+ repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
+ repositories {
+ google()
+ mavenCentral()
+ jcenter() // Warning: this repository is going to shut down soon
+ }
+}
+rootProject.name = "Unrd"
+include ':app'