diff --git a/.gitignore b/.gitignore
new file mode 100644
index 00000000..d4c3a57e
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,16 @@
+*.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
+/.idea/
diff --git a/app/.gitignore b/app/.gitignore
new file mode 100644
index 00000000..42afabfd
--- /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 00000000..9040808d
--- /dev/null
+++ b/app/build.gradle
@@ -0,0 +1,129 @@
+plugins {
+ id 'com.android.application'
+ id 'org.jetbrains.kotlin.android'
+ id 'kotlin-kapt'
+ id 'androidx.navigation.safeargs'
+}
+
+android {
+ compileSdk 32
+
+ defaultConfig {
+ applicationId "com.github.hramogin.movieapp"
+ minSdk 21
+ targetSdk 32
+ versionCode 1
+ versionName "1.0"
+
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ }
+
+ buildTypes {
+ debug {
+ applicationIdSuffix ".debug"
+ minifyEnabled false
+ debuggable true
+ manifestPlaceholders = [crashlyticsCollectionEnabled: "false"]
+
+ buildConfigField "String", "REST_BASE_URL", "\"https://api.themoviedb.org/3/\""
+ buildConfigField "String", "API_TOKEN", "\"499865c4f3d665373c5f8c17c40d55e3\""
+ buildConfigField "String", "BASE_IMAGE_URL", "\"https://image.tmdb.org/t/p/original\""
+ }
+ stage {
+ applicationIdSuffix ".stage"
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ manifestPlaceholders = [crashlyticsCollectionEnabled: "true"]
+
+ buildConfigField "String", "REST_BASE_URL", "\"https://api.themoviedb.org/3/\""
+ buildConfigField "String", "API_TOKEN", "\"499865c4f3d665373c5f8c17c40d55e3\""
+ buildConfigField "String", "BASE_IMAGE_URL", "\"https://image.tmdb.org/t/p/original\""
+ }
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ manifestPlaceholders = [crashlyticsCollectionEnabled: "true"]
+
+ buildConfigField "String", "REST_BASE_URL", "\"https://api.themoviedb.org/3/\""
+ buildConfigField "String", "API_TOKEN", "\"499865c4f3d665373c5f8c17c40d55e3\""
+ buildConfigField "String", "BASE_IMAGE_URL", "\"https://image.tmdb.org/t/p/original\""
+ }
+ }
+
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+ kotlinOptions {
+ jvmTarget = '1.8'
+ }
+ viewBinding {
+ enabled = true
+ }
+}
+
+dependencies {
+
+ implementation fileTree(dir: 'libs', include: ['*.jar'])
+ implementation libraries.kotlinStdLib
+ implementation libraries.kotlinStdLib
+ implementation libraries.appCompat
+ implementation libraries.constraintLayout
+
+ //AndroidX
+ implementation libraries.coreKtx
+ implementation libraries.coreX
+ implementation libraries.legacySupport
+ testImplementation libraries.coreTestX
+
+ //junit
+ testImplementation libraries.junit
+ androidTestImplementation libraries.junitExtention
+
+ //espresso
+ androidTestImplementation libraries.espresso
+
+ //Material components
+ implementation libraries.material
+
+ //leakcanary
+ implementation libraries.leakcanary
+
+ //lifecycle
+ implementation libraries.lifecycleViewModelKtx
+ implementation libraries.lifecycleLiveDataKtx
+
+ //Navigation
+ implementation libraries.navigation
+ implementation libraries.navigationKtx
+
+ //Timber
+ implementation libraries.timber
+
+ //Retrofit
+ implementation libraries.retrofit
+ implementation libraries.retrofitConverterGson
+ implementation libraries.okhttp
+ implementation libraries.okhttpLoggingInterceptor
+
+ //koin
+ implementation libraries.koinCore
+ implementation libraries.koinAndroid
+
+ //firebase
+ implementation libraries.firebaseCrashlytics
+ implementation libraries.firebaseCloudMessaging
+
+ //Glide
+ implementation libraries.glide
+ kapt libraries.glideCompiler
+
+ //room
+ implementation libraries.room
+ implementation libraries.roomKtx
+ kapt libraries.annotationProcessorRoom
+
+ testImplementation libraries.mockotlin
+ testImplementation libraries.coroutinesTest
+ testImplementation libraries.androidXCoreTesting
+}
\ No newline at end of file
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
new file mode 100644
index 00000000..481bb434
--- /dev/null
+++ b/app/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/app/src/androidTest/java/com/github/hramogin/movieapp/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/github/hramogin/movieapp/ExampleInstrumentedTest.kt
new file mode 100644
index 00000000..0ee38b3a
--- /dev/null
+++ b/app/src/androidTest/java/com/github/hramogin/movieapp/ExampleInstrumentedTest.kt
@@ -0,0 +1,24 @@
+package com.github.hramogin.movieapp
+
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.ext.junit.runners.AndroidJUnit4
+
+import org.junit.Test
+import org.junit.runner.RunWith
+
+import org.junit.Assert.*
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+@RunWith(AndroidJUnit4::class)
+class ExampleInstrumentedTest {
+ @Test
+ fun useAppContext() {
+ // Context of the app under test.
+ val appContext = InstrumentationRegistry.getInstrumentation().targetContext
+ assertEquals("com.github.hramogin.movieapp", appContext.packageName)
+ }
+}
\ 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 00000000..480f1129
--- /dev/null
+++ b/app/src/main/AndroidManifest.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/java/com/github/hramogin/movieapp/App.kt b/app/src/main/java/com/github/hramogin/movieapp/App.kt
new file mode 100644
index 00000000..14b623ef
--- /dev/null
+++ b/app/src/main/java/com/github/hramogin/movieapp/App.kt
@@ -0,0 +1,39 @@
+package com.github.hramogin.movieapp
+
+import android.app.Application
+import com.github.hramogin.movieapp.di.modules.applicationModule
+import com.github.hramogin.movieapp.di.modules.databaseModule
+import com.github.hramogin.movieapp.di.modules.mapperModule
+import com.github.hramogin.movieapp.di.modules.networkModule
+import com.github.hramogin.movieapp.di.modules.networkServiceModule
+import com.github.hramogin.movieapp.di.modules.repositoryModule
+import com.github.hramogin.movieapp.di.modules.useCaseModule
+import com.github.hramogin.movieapp.di.modules.viewModelModule
+import org.koin.android.ext.koin.androidContext
+import org.koin.core.context.GlobalContext.startKoin
+
+class App : Application() {
+
+ override fun onCreate() {
+ super.onCreate()
+ setupKoin()
+ }
+
+ private fun setupKoin() {
+ startKoin {
+ androidContext(this@App)
+ modules(
+ listOf(
+ applicationModule,
+ viewModelModule,
+ repositoryModule,
+ useCaseModule,
+ networkServiceModule,
+ networkModule,
+ databaseModule,
+ mapperModule,
+ )
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/hramogin/movieapp/AppActivity.kt b/app/src/main/java/com/github/hramogin/movieapp/AppActivity.kt
new file mode 100644
index 00000000..10608dc8
--- /dev/null
+++ b/app/src/main/java/com/github/hramogin/movieapp/AppActivity.kt
@@ -0,0 +1,11 @@
+package com.github.hramogin.movieapp
+
+import androidx.appcompat.app.AppCompatActivity
+import android.os.Bundle
+
+class AppActivity : AppCompatActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_main)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/hramogin/movieapp/data/database/AppDatabase.kt b/app/src/main/java/com/github/hramogin/movieapp/data/database/AppDatabase.kt
new file mode 100644
index 00000000..886f0647
--- /dev/null
+++ b/app/src/main/java/com/github/hramogin/movieapp/data/database/AppDatabase.kt
@@ -0,0 +1,18 @@
+package com.github.hramogin.movieapp.data.database
+
+import androidx.room.Database
+import androidx.room.RoomDatabase
+import com.github.hramogin.movieapp.data.database.model.MovieDb
+import com.github.hramogin.movieapp.data.database.model.MovieDetailsDb
+import com.github.hramogin.movieapp.data.database.model.MovieReviewsDb
+import com.github.hramogin.movieapp.data.repository.movie.DaoMovie
+
+@Database(
+ entities = [MovieDb::class, MovieDetailsDb::class, MovieReviewsDb::class],
+ version = 1,
+ exportSchema = false
+)
+abstract class AppDatabase : RoomDatabase() {
+
+ abstract fun moviesDao(): DaoMovie
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/hramogin/movieapp/data/database/model/MovieDb.kt b/app/src/main/java/com/github/hramogin/movieapp/data/database/model/MovieDb.kt
new file mode 100644
index 00000000..62d253b5
--- /dev/null
+++ b/app/src/main/java/com/github/hramogin/movieapp/data/database/model/MovieDb.kt
@@ -0,0 +1,18 @@
+package com.github.hramogin.movieapp.data.database.model
+
+import androidx.room.ColumnInfo
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+
+@Entity(tableName = "Movie")
+class MovieDb(
+ @PrimaryKey
+ @ColumnInfo
+ val id: String,
+
+ @ColumnInfo
+ val posterPath: String,
+
+ @ColumnInfo(name = "title")
+ val title: String,
+)
diff --git a/app/src/main/java/com/github/hramogin/movieapp/data/database/model/MovieDetailsDb.kt b/app/src/main/java/com/github/hramogin/movieapp/data/database/model/MovieDetailsDb.kt
new file mode 100644
index 00000000..29da3425
--- /dev/null
+++ b/app/src/main/java/com/github/hramogin/movieapp/data/database/model/MovieDetailsDb.kt
@@ -0,0 +1,20 @@
+package com.github.hramogin.movieapp.data.database.model
+
+import androidx.room.ColumnInfo
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+
+@Entity(tableName = "MovieDetails")
+class MovieDetailsDb(
+ @PrimaryKey
+ @ColumnInfo
+ val id: String,
+ @ColumnInfo
+ val posterPath: String,
+ @ColumnInfo(name = "original_title")
+ val title: String,
+ @ColumnInfo(name = "tagline")
+ val tagline: String,
+ @ColumnInfo(name = "overview")
+ val overview: String,
+)
diff --git a/app/src/main/java/com/github/hramogin/movieapp/data/database/model/MovieReviewsDb.kt b/app/src/main/java/com/github/hramogin/movieapp/data/database/model/MovieReviewsDb.kt
new file mode 100644
index 00000000..43dadc03
--- /dev/null
+++ b/app/src/main/java/com/github/hramogin/movieapp/data/database/model/MovieReviewsDb.kt
@@ -0,0 +1,18 @@
+package com.github.hramogin.movieapp.data.database.model
+
+import androidx.room.ColumnInfo
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+
+@Entity(tableName = "MovieReview")
+class MovieReviewsDb(
+ @PrimaryKey
+ @ColumnInfo
+ val id: String,
+ @ColumnInfo(name = "content")
+ val content: String,
+ @ColumnInfo(name = "created_at")
+ val createdAt: String,
+ @ColumnInfo(name = "author")
+ val author: String,
+)
diff --git a/app/src/main/java/com/github/hramogin/movieapp/data/model/ListResponse.kt b/app/src/main/java/com/github/hramogin/movieapp/data/model/ListResponse.kt
new file mode 100644
index 00000000..26e97f52
--- /dev/null
+++ b/app/src/main/java/com/github/hramogin/movieapp/data/model/ListResponse.kt
@@ -0,0 +1,10 @@
+package com.github.hramogin.movieapp.data.model
+
+import com.google.gson.annotations.SerializedName
+
+class ListResponse(
+ @SerializedName("page")
+ val page: String,
+ @SerializedName("results")
+ val results: List,
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/github/hramogin/movieapp/data/model/MovieApi.kt b/app/src/main/java/com/github/hramogin/movieapp/data/model/MovieApi.kt
new file mode 100644
index 00000000..dc74f7e1
--- /dev/null
+++ b/app/src/main/java/com/github/hramogin/movieapp/data/model/MovieApi.kt
@@ -0,0 +1,12 @@
+package com.github.hramogin.movieapp.data.model
+
+import com.google.gson.annotations.SerializedName
+
+class MovieApi(
+ @SerializedName("id")
+ val id: String,
+ @SerializedName("poster_path")
+ val posterPath: String,
+ @SerializedName("title")
+ val title: String,
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/github/hramogin/movieapp/data/model/MovieDetailsApi.kt b/app/src/main/java/com/github/hramogin/movieapp/data/model/MovieDetailsApi.kt
new file mode 100644
index 00000000..aeb8c73f
--- /dev/null
+++ b/app/src/main/java/com/github/hramogin/movieapp/data/model/MovieDetailsApi.kt
@@ -0,0 +1,16 @@
+package com.github.hramogin.movieapp.data.model
+
+import com.google.gson.annotations.SerializedName
+
+class MovieDetailsApi(
+ @SerializedName("id")
+ val id: String,
+ @SerializedName("poster_path")
+ val posterPath: String,
+ @SerializedName("original_title")
+ val title: String,
+ @SerializedName("tagline")
+ val tagline: String,
+ @SerializedName("overview")
+ val overview: String,
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/github/hramogin/movieapp/data/model/ReviewApi.kt b/app/src/main/java/com/github/hramogin/movieapp/data/model/ReviewApi.kt
new file mode 100644
index 00000000..789bc9ac
--- /dev/null
+++ b/app/src/main/java/com/github/hramogin/movieapp/data/model/ReviewApi.kt
@@ -0,0 +1,14 @@
+package com.github.hramogin.movieapp.data.model
+
+import com.google.gson.annotations.SerializedName
+
+class ReviewApi(
+ @SerializedName("id")
+ val id: String,
+ @SerializedName("content")
+ val content: String,
+ @SerializedName("created_at")
+ val createdAt: String,
+ @SerializedName("author")
+ val author: String,
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/github/hramogin/movieapp/data/network/MovieAppGlideModule.kt b/app/src/main/java/com/github/hramogin/movieapp/data/network/MovieAppGlideModule.kt
new file mode 100644
index 00000000..c981814d
--- /dev/null
+++ b/app/src/main/java/com/github/hramogin/movieapp/data/network/MovieAppGlideModule.kt
@@ -0,0 +1,34 @@
+package com.github.hramogin.movieapp.data.network
+
+import android.content.Context
+import android.graphics.drawable.Drawable
+import android.util.Log
+import com.bumptech.glide.GlideBuilder
+import com.bumptech.glide.annotation.GlideModule
+import com.bumptech.glide.load.engine.cache.LruResourceCache
+import com.bumptech.glide.load.engine.cache.MemorySizeCalculator
+import com.bumptech.glide.load.resource.bitmap.CenterCrop
+import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions
+import com.bumptech.glide.module.AppGlideModule
+import com.bumptech.glide.request.RequestOptions
+import com.github.hramogin.movieapp.BuildConfig
+
+@GlideModule
+class MovieAppGlideModule : AppGlideModule() {
+
+ override fun applyOptions(context: Context, builder: GlideBuilder) {
+ super.applyOptions(context, builder)
+ val memorySize = MemorySizeCalculator.Builder(context)
+ .setMemoryCacheScreens(5f)
+ .build()
+ builder.setMemoryCache(LruResourceCache(memorySize.memoryCacheSize.toLong()))
+ builder.setLogLevel(if (BuildConfig.DEBUG) Log.DEBUG else Log.ERROR)
+ builder.setDefaultTransitionOptions(
+ Drawable::class.java,
+ DrawableTransitionOptions.withCrossFade()
+ )
+ builder.setDefaultRequestOptions(
+ RequestOptions().transform(CenterCrop())
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/hramogin/movieapp/data/network/TokenInterceptor.kt b/app/src/main/java/com/github/hramogin/movieapp/data/network/TokenInterceptor.kt
new file mode 100644
index 00000000..554a4f6d
--- /dev/null
+++ b/app/src/main/java/com/github/hramogin/movieapp/data/network/TokenInterceptor.kt
@@ -0,0 +1,33 @@
+package com.github.hramogin.movieapp.data.network
+
+import com.github.hramogin.movieapp.BuildConfig
+import kotlinx.coroutines.runBlocking
+import okhttp3.Interceptor
+import okhttp3.Response
+import timber.log.Timber
+
+class TokenInterceptor : Interceptor {
+
+ override fun intercept(chain: Interceptor.Chain): Response {
+ var original = chain.request()
+ val token = getToken()
+ val url = original.url.newBuilder().addQueryParameter(API_KEY, token).build()
+ original = original.newBuilder().url(url).build()
+ return chain.proceed(original)
+ }
+
+ private fun getToken(): String {
+ return runBlocking {
+ try {
+ BuildConfig.API_TOKEN
+ } catch (exception: Exception) {
+ Timber.e(exception)
+ ""
+ }
+ }
+ }
+
+ private companion object {
+ const val API_KEY = "api_key"
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/hramogin/movieapp/data/repository/movie/DaoMovie.kt b/app/src/main/java/com/github/hramogin/movieapp/data/repository/movie/DaoMovie.kt
new file mode 100644
index 00000000..cb05cf10
--- /dev/null
+++ b/app/src/main/java/com/github/hramogin/movieapp/data/repository/movie/DaoMovie.kt
@@ -0,0 +1,34 @@
+package com.github.hramogin.movieapp.data.repository.movie
+
+import androidx.room.Dao
+import androidx.room.Insert
+import androidx.room.OnConflictStrategy
+import androidx.room.Query
+import com.github.hramogin.movieapp.data.database.model.MovieDb
+import com.github.hramogin.movieapp.data.database.model.MovieDetailsDb
+import com.github.hramogin.movieapp.data.database.model.MovieReviewsDb
+
+@Dao
+interface DaoMovie {
+
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ suspend fun insertMovie(movie: MovieDb)
+
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ suspend fun insertAllMovies(list: List)
+
+ @Query("SELECT * FROM Movie")
+ suspend fun getAllMovies(): List
+
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ suspend fun insertMovieDetails(movie: MovieDetailsDb)
+
+ @Query("SELECT * FROM MovieDetails WHERE MovieDetails.id == :id")
+ suspend fun getMovieDetails(id: String): MovieDetailsDb
+
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ suspend fun insertAllMovieReviews(list: List)
+
+ @Query("SELECT * FROM MovieReview")
+ suspend fun getAllMovieReviews(): List
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/hramogin/movieapp/data/repository/movie/MovieRepository.kt b/app/src/main/java/com/github/hramogin/movieapp/data/repository/movie/MovieRepository.kt
new file mode 100644
index 00000000..2311a1f0
--- /dev/null
+++ b/app/src/main/java/com/github/hramogin/movieapp/data/repository/movie/MovieRepository.kt
@@ -0,0 +1,30 @@
+package com.github.hramogin.movieapp.data.repository.movie
+
+import com.github.hramogin.movieapp.data.database.model.MovieDb
+import com.github.hramogin.movieapp.data.database.model.MovieDetailsDb
+import com.github.hramogin.movieapp.data.database.model.MovieReviewsDb
+import com.github.hramogin.movieapp.data.model.MovieApi
+import com.github.hramogin.movieapp.data.model.MovieDetailsApi
+import com.github.hramogin.movieapp.data.model.ReviewApi
+
+interface MovieRepository {
+
+ suspend fun getFilmsFromServer(): List
+
+ suspend fun getFilmsFromDb(): List
+
+ suspend fun setFilmsToDb(films: List)
+
+ suspend fun getFilmDetailsFromServer(filId: String): MovieDetailsApi
+
+ suspend fun getFilmDetailsFromDb(filmId: String): MovieDetailsDb
+
+ suspend fun setFilmDetailsToDb(movieDetailsDb: MovieDetailsDb)
+
+ suspend fun getFilmReviewsFromServer(filId: String): List
+
+ suspend fun getFilmReviewsFromDb(filId: String): List
+
+ suspend fun setFilmReviewsToDb(reviews: List)
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/hramogin/movieapp/data/repository/movie/MovieRepositoryImpl.kt b/app/src/main/java/com/github/hramogin/movieapp/data/repository/movie/MovieRepositoryImpl.kt
new file mode 100644
index 00000000..fcd93c66
--- /dev/null
+++ b/app/src/main/java/com/github/hramogin/movieapp/data/repository/movie/MovieRepositoryImpl.kt
@@ -0,0 +1,47 @@
+package com.github.hramogin.movieapp.data.repository.movie
+
+import com.github.hramogin.movieapp.data.database.model.MovieDb
+import com.github.hramogin.movieapp.data.database.model.MovieDetailsDb
+import com.github.hramogin.movieapp.data.database.model.MovieReviewsDb
+import com.github.hramogin.movieapp.data.model.MovieApi
+import com.github.hramogin.movieapp.data.model.MovieDetailsApi
+import com.github.hramogin.movieapp.data.model.ReviewApi
+
+class MovieRepositoryImpl(private val movieService: MovieService, private val movieDao: DaoMovie) :
+ MovieRepository {
+ override suspend fun getFilmsFromServer(): List {
+ return movieService.getFilms().results
+ }
+
+ override suspend fun getFilmDetailsFromServer(filId: String): MovieDetailsApi {
+ return movieService.getFilmDetails(filId)
+ }
+
+ override suspend fun getFilmReviewsFromServer(filId: String): List {
+ return movieService.getFilmReviews(filId).results
+ }
+
+ override suspend fun getFilmsFromDb(): List {
+ return movieDao.getAllMovies()
+ }
+
+ override suspend fun setFilmsToDb(films: List) {
+ movieDao.insertAllMovies(films)
+ }
+
+ override suspend fun getFilmDetailsFromDb(filmId: String): MovieDetailsDb {
+ return movieDao.getMovieDetails(filmId)
+ }
+
+ override suspend fun setFilmDetailsToDb(movieDetailsDb: MovieDetailsDb) {
+ movieDao.insertMovieDetails(movieDetailsDb)
+ }
+
+ override suspend fun getFilmReviewsFromDb(filId: String): List {
+ return movieDao.getAllMovieReviews()
+ }
+
+ override suspend fun setFilmReviewsToDb(reviews: List) {
+ movieDao.insertAllMovieReviews(reviews)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/hramogin/movieapp/data/repository/movie/MovieService.kt b/app/src/main/java/com/github/hramogin/movieapp/data/repository/movie/MovieService.kt
new file mode 100644
index 00000000..a98cc0d3
--- /dev/null
+++ b/app/src/main/java/com/github/hramogin/movieapp/data/repository/movie/MovieService.kt
@@ -0,0 +1,20 @@
+package com.github.hramogin.movieapp.data.repository.movie
+
+import com.github.hramogin.movieapp.data.model.ListResponse
+import com.github.hramogin.movieapp.data.model.MovieApi
+import com.github.hramogin.movieapp.data.model.MovieDetailsApi
+import com.github.hramogin.movieapp.data.model.ReviewApi
+import retrofit2.http.GET
+import retrofit2.http.Path
+
+interface MovieService {
+
+ @GET("movie/top_rated")
+ suspend fun getFilms(): ListResponse
+
+ @GET("movie/{movie_id}")
+ suspend fun getFilmDetails(@Path("movie_id") movieId: String): MovieDetailsApi
+
+ @GET("movie/{movie_id}/reviews")
+ suspend fun getFilmReviews(@Path("movie_id") movieId: String): ListResponse
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/hramogin/movieapp/di/modules/ApplicationModule.kt b/app/src/main/java/com/github/hramogin/movieapp/di/modules/ApplicationModule.kt
new file mode 100644
index 00000000..0e3c513a
--- /dev/null
+++ b/app/src/main/java/com/github/hramogin/movieapp/di/modules/ApplicationModule.kt
@@ -0,0 +1,10 @@
+package com.github.hramogin.movieapp.di.modules
+
+import com.github.hramogin.movieapp.presentation.base.ResourceProvider
+import com.github.hramogin.movieapp.presentation.base.ResourceProviderImpl
+import org.koin.dsl.module
+
+val applicationModule = module {
+
+ single { ResourceProviderImpl(get()) }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/hramogin/movieapp/di/modules/DataBaseModule.kt b/app/src/main/java/com/github/hramogin/movieapp/di/modules/DataBaseModule.kt
new file mode 100644
index 00000000..2fc4800e
--- /dev/null
+++ b/app/src/main/java/com/github/hramogin/movieapp/di/modules/DataBaseModule.kt
@@ -0,0 +1,15 @@
+package com.github.hramogin.movieapp.di.modules
+
+import android.content.Context
+import androidx.room.Room
+import com.github.hramogin.movieapp.data.database.AppDatabase
+import org.koin.dsl.module
+
+const val DATABASE_NAME = "APP_DATABASE"
+
+val databaseModule = module {
+ single { Room.databaseBuilder(get(), AppDatabase::class.java, DATABASE_NAME).build() }
+ single { get().cacheDir }
+
+ single { get().moviesDao() }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/hramogin/movieapp/di/modules/MapperModule.kt b/app/src/main/java/com/github/hramogin/movieapp/di/modules/MapperModule.kt
new file mode 100644
index 00000000..4263224b
--- /dev/null
+++ b/app/src/main/java/com/github/hramogin/movieapp/di/modules/MapperModule.kt
@@ -0,0 +1,9 @@
+package com.github.hramogin.movieapp.di.modules
+
+import com.github.hramogin.movieapp.presentation.screens.details.MovieDetailsMapper
+import org.koin.dsl.module
+
+val mapperModule = module {
+
+ single { MovieDetailsMapper(get())}
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/hramogin/movieapp/di/modules/NetworkModule.kt b/app/src/main/java/com/github/hramogin/movieapp/di/modules/NetworkModule.kt
new file mode 100644
index 00000000..5ba3a732
--- /dev/null
+++ b/app/src/main/java/com/github/hramogin/movieapp/di/modules/NetworkModule.kt
@@ -0,0 +1,64 @@
+package com.github.hramogin.movieapp.di.modules
+
+import com.github.hramogin.movieapp.BuildConfig
+import com.github.hramogin.movieapp.data.network.TokenInterceptor
+import com.google.gson.Gson
+import com.google.gson.GsonBuilder
+import okhttp3.Cache
+import okhttp3.Interceptor
+import okhttp3.OkHttpClient
+import okhttp3.logging.HttpLoggingInterceptor
+import org.koin.android.ext.koin.androidApplication
+import org.koin.core.qualifier.named
+import org.koin.dsl.bind
+import org.koin.dsl.module
+import retrofit2.Converter
+import retrofit2.Retrofit
+import retrofit2.converter.gson.GsonConverterFactory
+import java.util.concurrent.TimeUnit
+
+const val REST_API_URL_TAG = "REST_API_URL_TAG"
+const val GENERAL_OK_HTTP_TAG = "GENERAL_OK_HTTP_TAG"
+const val OPEN_OK_HTTP_TAG = "OPEN_OK_HTTP_TAG"
+private const val TIME_OUT = 30L
+private const val CACHE_TAG = "CACHE_TAG"
+
+const val GENERAL_BACKEND = "GENERAL_BACKEND"
+
+val networkModule = module {
+
+ single(named(REST_API_URL_TAG)) { BuildConfig.REST_BASE_URL }
+
+ single(named(CACHE_TAG)) { androidApplication().cacheDir }
+ single() { TokenInterceptor() }
+
+ single { getGson() }
+
+ single { Cache(get(named(CACHE_TAG)), 1 * 1024 * 1024) }
+
+ single(named(OPEN_OK_HTTP_TAG)) {
+ OkHttpClient.Builder()
+ .readTimeout(TIME_OUT, TimeUnit.SECONDS)
+ .writeTimeout(TIME_OUT, TimeUnit.SECONDS)
+ .connectTimeout(TIME_OUT, TimeUnit.SECONDS)
+ .addInterceptor(HttpLoggingInterceptor().apply {
+ level =
+ if (BuildConfig.DEBUG) HttpLoggingInterceptor.Level.BODY else HttpLoggingInterceptor.Level.NONE
+ })
+ .addInterceptor(get())
+ .build()
+ } bind OkHttpClient::class
+
+ single { GsonConverterFactory.create(get()) } bind Converter.Factory::class
+
+ single {
+ Retrofit.Builder()
+ .baseUrl(get(named(REST_API_URL_TAG)) as String)
+ .client(get(named(OPEN_OK_HTTP_TAG)))
+ .addConverterFactory(get())
+ .build()
+ }
+}
+
+fun getGson(): Gson = GsonBuilder()
+ .create()
\ No newline at end of file
diff --git a/app/src/main/java/com/github/hramogin/movieapp/di/modules/NetworkServiceModule.kt b/app/src/main/java/com/github/hramogin/movieapp/di/modules/NetworkServiceModule.kt
new file mode 100644
index 00000000..bc51cc67
--- /dev/null
+++ b/app/src/main/java/com/github/hramogin/movieapp/di/modules/NetworkServiceModule.kt
@@ -0,0 +1,9 @@
+package com.github.hramogin.movieapp.di.modules
+
+import com.github.hramogin.movieapp.data.repository.movie.MovieService
+import org.koin.dsl.module
+import retrofit2.Retrofit
+
+val networkServiceModule = module {
+ single { get().create(MovieService::class.java) }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/hramogin/movieapp/di/modules/RepositoryModule.kt b/app/src/main/java/com/github/hramogin/movieapp/di/modules/RepositoryModule.kt
new file mode 100644
index 00000000..d16f1a13
--- /dev/null
+++ b/app/src/main/java/com/github/hramogin/movieapp/di/modules/RepositoryModule.kt
@@ -0,0 +1,9 @@
+package com.github.hramogin.movieapp.di.modules
+
+import com.github.hramogin.movieapp.data.repository.movie.MovieRepository
+import com.github.hramogin.movieapp.data.repository.movie.MovieRepositoryImpl
+import org.koin.dsl.module
+
+val repositoryModule = module {
+ single { MovieRepositoryImpl(get(), get()) }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/hramogin/movieapp/di/modules/UseCaseModule.kt b/app/src/main/java/com/github/hramogin/movieapp/di/modules/UseCaseModule.kt
new file mode 100644
index 00000000..9ec72fdc
--- /dev/null
+++ b/app/src/main/java/com/github/hramogin/movieapp/di/modules/UseCaseModule.kt
@@ -0,0 +1,12 @@
+package com.github.hramogin.movieapp.di.modules
+
+import com.github.hramogin.movieapp.domain.useCases.movie.GetFilmDetailsUseCase
+import com.github.hramogin.movieapp.domain.useCases.movie.GetFilmReviewsUseCase
+import com.github.hramogin.movieapp.domain.useCases.movie.GetMoviesUseCase
+import org.koin.dsl.module
+
+val useCaseModule = module {
+ single { GetMoviesUseCase(get()) }
+ single { GetFilmDetailsUseCase(get()) }
+ single { GetFilmReviewsUseCase(get()) }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/hramogin/movieapp/di/modules/ViewModelVodule.kt b/app/src/main/java/com/github/hramogin/movieapp/di/modules/ViewModelVodule.kt
new file mode 100644
index 00000000..486ebb15
--- /dev/null
+++ b/app/src/main/java/com/github/hramogin/movieapp/di/modules/ViewModelVodule.kt
@@ -0,0 +1,13 @@
+package com.github.hramogin.movieapp.di.modules
+
+import com.github.hramogin.movieapp.presentation.screens.details.DetailsViewModel
+import com.github.hramogin.movieapp.presentation.screens.moviesList.MoviesListViewModel
+import com.github.hramogin.movieapp.presentation.screens.splash.SplashViewModel
+import org.koin.androidx.viewmodel.dsl.viewModel
+import org.koin.dsl.module
+
+val viewModelModule = module {
+ viewModel { SplashViewModel() }
+ viewModel { MoviesListViewModel(get()) }
+ viewModel { (id: String) -> DetailsViewModel(id, get(), get()) }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/hramogin/movieapp/domain/mappers/MovieMappers.kt b/app/src/main/java/com/github/hramogin/movieapp/domain/mappers/MovieMappers.kt
new file mode 100644
index 00000000..ffe98d13
--- /dev/null
+++ b/app/src/main/java/com/github/hramogin/movieapp/domain/mappers/MovieMappers.kt
@@ -0,0 +1,94 @@
+package com.github.hramogin.movieapp.domain.mappers
+
+import com.github.hramogin.movieapp.data.database.model.MovieDb
+import com.github.hramogin.movieapp.data.database.model.MovieDetailsDb
+import com.github.hramogin.movieapp.data.database.model.MovieReviewsDb
+import com.github.hramogin.movieapp.data.model.MovieApi
+import com.github.hramogin.movieapp.data.model.MovieDetailsApi
+import com.github.hramogin.movieapp.data.model.ReviewApi
+import com.github.hramogin.movieapp.domain.model.Movie
+import com.github.hramogin.movieapp.domain.model.MovieDetails
+import com.github.hramogin.movieapp.domain.model.Review
+
+fun MovieApi.toFilm(): Movie {
+ return Movie(
+ id = id,
+ posterPath = posterPath,
+ title = title,
+ )
+}
+
+fun Movie.toMovieDb(): MovieDb {
+ return MovieDb(
+ id = id,
+ posterPath = posterPath,
+ title = title,
+ )
+}
+
+fun MovieDb.toFilm(): Movie {
+ return Movie(
+ id = id,
+ posterPath = posterPath,
+ title = title,
+ )
+}
+
+fun ReviewApi.toReview(): Review {
+ return Review(
+ id = id,
+ name = author,
+ content = content,
+ date = createdAt,
+ )
+}
+
+fun Review.toMovieReviewDb(): MovieReviewsDb {
+ return MovieReviewsDb(
+ id = id,
+ content = content,
+ createdAt = date,
+ author = name,
+ )
+}
+
+fun MovieReviewsDb.toFilmReview(): Review {
+ return Review(
+ id = id,
+ content = content,
+ date = createdAt,
+ name = author
+ )
+}
+
+fun MovieDetailsDb.toFilmDetails(): MovieDetails {
+ return MovieDetails(
+ id = id,
+ reviews = emptyList(),
+ posterPath = posterPath,
+ title = title,
+ tagline = tagline,
+ description = overview,
+ )
+}
+
+fun MovieDetailsApi.toDetails(): MovieDetails {
+ return MovieDetails(
+ id = id,
+ reviews = emptyList(),
+ posterPath = posterPath,
+ title = title,
+ tagline = tagline,
+ description = overview,
+ )
+}
+
+fun MovieDetails.toMovieDetailsDb(): MovieDetailsDb {
+ return MovieDetailsDb(
+ id = id,
+ posterPath = posterPath,
+ title = title,
+ overview = description,
+ tagline = tagline,
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/hramogin/movieapp/domain/model/Movie.kt b/app/src/main/java/com/github/hramogin/movieapp/domain/model/Movie.kt
new file mode 100644
index 00000000..36ca7276
--- /dev/null
+++ b/app/src/main/java/com/github/hramogin/movieapp/domain/model/Movie.kt
@@ -0,0 +1,7 @@
+package com.github.hramogin.movieapp.domain.model
+
+class Movie(
+ val id: String,
+ val posterPath: String,
+ val title: String,
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/github/hramogin/movieapp/domain/model/MovieDetails.kt b/app/src/main/java/com/github/hramogin/movieapp/domain/model/MovieDetails.kt
new file mode 100644
index 00000000..687aff13
--- /dev/null
+++ b/app/src/main/java/com/github/hramogin/movieapp/domain/model/MovieDetails.kt
@@ -0,0 +1,10 @@
+package com.github.hramogin.movieapp.domain.model
+
+data class MovieDetails(
+ val id: String,
+ val reviews: List,
+ val posterPath: String,
+ val description: String,
+ val title: String,
+ val tagline: String,
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/github/hramogin/movieapp/domain/model/Review.kt b/app/src/main/java/com/github/hramogin/movieapp/domain/model/Review.kt
new file mode 100644
index 00000000..df2e140d
--- /dev/null
+++ b/app/src/main/java/com/github/hramogin/movieapp/domain/model/Review.kt
@@ -0,0 +1,8 @@
+package com.github.hramogin.movieapp.domain.model
+
+class Review(
+ val id: String,
+ val content: String,
+ val name: String,
+ val date: String,
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/github/hramogin/movieapp/domain/useCases/base/ApiError.kt b/app/src/main/java/com/github/hramogin/movieapp/domain/useCases/base/ApiError.kt
new file mode 100644
index 00000000..a735fcf4
--- /dev/null
+++ b/app/src/main/java/com/github/hramogin/movieapp/domain/useCases/base/ApiError.kt
@@ -0,0 +1,17 @@
+package com.github.hramogin.movieapp.domain.useCases.base
+
+import com.github.hramogin.movieapp.R
+
+
+sealed class ApiError(
+ val type: String,
+ val stringResId: Int = R.string.unknown_error,
+ val errorCode: Int = 0
+) {
+
+ object UnknownApiError : ApiError(UNKNOWN, R.string.unknown_error, 100)
+
+ companion object {
+ const val UNKNOWN = "Unknown error"
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/hramogin/movieapp/domain/useCases/base/BaseUseCase.kt b/app/src/main/java/com/github/hramogin/movieapp/domain/useCases/base/BaseUseCase.kt
new file mode 100644
index 00000000..c3baa7fc
--- /dev/null
+++ b/app/src/main/java/com/github/hramogin/movieapp/domain/useCases/base/BaseUseCase.kt
@@ -0,0 +1,42 @@
+package com.github.hramogin.movieapp.domain.useCases.base
+
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.async
+import kotlinx.coroutines.launch
+import java.net.ConnectException
+import java.net.UnknownHostException
+
+abstract class BaseUseCase where Type : Any {
+
+ abstract suspend fun run(param: Param): Either
+
+ open val dispatcher: CoroutineDispatcher = Dispatchers.Default
+
+ open operator fun invoke(
+ scope: CoroutineScope,
+ param: Param,
+ result: (Either) -> Unit = {}
+ ): Job {
+ val backgroundJob = scope.async(dispatcher) { run(param) }
+ return scope.launch(Dispatchers.Main) { result.invoke(backgroundJob.await()) }
+ }
+
+ protected fun onWrapException(exception: Exception): Failure {
+ return try {
+ when (exception) {
+ is UnknownHostException -> Failure.NetworkFailureL(
+ exception
+ )
+ is ConnectException -> Failure.NetworkFailureL(
+ exception
+ )
+ else -> Failure.UnknownFailure(exception)
+ }
+ } catch (exception: Exception) {
+ Failure.UnknownFailure(exception)
+ }
+ }
+}
diff --git a/app/src/main/java/com/github/hramogin/movieapp/domain/useCases/base/Either.kt b/app/src/main/java/com/github/hramogin/movieapp/domain/useCases/base/Either.kt
new file mode 100644
index 00000000..f88ff6b2
--- /dev/null
+++ b/app/src/main/java/com/github/hramogin/movieapp/domain/useCases/base/Either.kt
@@ -0,0 +1,44 @@
+package com.github.hramogin.movieapp.domain.useCases.base
+
+/**
+ * Represents a value of one of two possible types (a disjoint union).
+ * Instances of [Either] are either an instance of [Left] or [Right].
+ * FP Convention dictates that [Left] is used for "failure"
+ * and [Right] is used for "success".
+ *
+ * @see Left
+ * @see Right
+ */
+sealed class Either {
+ /** * Represents the left side of [Either] class which by convention is a "Failure". */
+ data class Left(val left: L) : Either()
+
+ /** * Represents the right side of [Either] class which by convention is a "Success". */
+ data class Right(val right: R) : Either()
+
+ val isRight get() = this is Right
+ val isLeft get() = this is Left
+
+ fun left(left: L) = Left(left)
+ fun right(right: R) = Right(right)
+
+ fun either(fnL: (L) -> Any, fnR: (R) -> Any): Any =
+ when (this) {
+ is Left -> fnL(left)
+ is Right -> fnR(right)
+ }
+}
+
+fun ((A) -> B).c(f: (B) -> C): (A) -> C = {
+ f(this(it))
+}
+
+fun Either.flatMap(fn: (R) -> Either): Either =
+ when (this) {
+ is Either.Left -> Either.Left(
+ left
+ )
+ is Either.Right -> fn(right)
+ }
+
+fun Either.map(fn: (R) -> (T)): Either = this.flatMap(fn.c(::right))
\ No newline at end of file
diff --git a/app/src/main/java/com/github/hramogin/movieapp/domain/useCases/base/Failure.kt b/app/src/main/java/com/github/hramogin/movieapp/domain/useCases/base/Failure.kt
new file mode 100644
index 00000000..974d3352
--- /dev/null
+++ b/app/src/main/java/com/github/hramogin/movieapp/domain/useCases/base/Failure.kt
@@ -0,0 +1,10 @@
+package com.github.hramogin.movieapp.domain.useCases.base
+
+sealed class Failure(val exception: Exception) {
+ open class FeatureFailure(exception: Exception) : Failure(exception)
+ open class UnknownFailure(exception: Exception) : Failure(exception)
+ open class NetworkFailureL(exception: Exception) : Failure(exception)
+ open class ApiFailure(val apiError: ApiError) : Failure(Exception())
+
+ class NoDataFailure : FeatureFailure(java.lang.Exception("No data"))
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/hramogin/movieapp/domain/useCases/movie/GetFilmDetailsUseCase.kt b/app/src/main/java/com/github/hramogin/movieapp/domain/useCases/movie/GetFilmDetailsUseCase.kt
new file mode 100644
index 00000000..7b203223
--- /dev/null
+++ b/app/src/main/java/com/github/hramogin/movieapp/domain/useCases/movie/GetFilmDetailsUseCase.kt
@@ -0,0 +1,38 @@
+package com.github.hramogin.movieapp.domain.useCases.movie
+
+import com.github.hramogin.movieapp.data.repository.movie.MovieRepository
+import com.github.hramogin.movieapp.domain.mappers.toDetails
+import com.github.hramogin.movieapp.domain.mappers.toFilmDetails
+import com.github.hramogin.movieapp.domain.mappers.toMovieDetailsDb
+import com.github.hramogin.movieapp.domain.mappers.toMovieReviewDb
+import com.github.hramogin.movieapp.domain.mappers.toReview
+import com.github.hramogin.movieapp.domain.model.MovieDetails
+import com.github.hramogin.movieapp.domain.useCases.base.BaseUseCase
+import com.github.hramogin.movieapp.domain.useCases.base.Either
+import com.github.hramogin.movieapp.domain.useCases.base.Failure
+import java.net.UnknownHostException
+
+class GetFilmDetailsUseCase(private val movieRepository: MovieRepository) :
+ BaseUseCase() {
+
+ override suspend fun run(param: Param): Either {
+ return try {
+ val filmDetails = movieRepository.getFilmDetailsFromServer(param.movieId).toDetails()
+ val reviews =
+ movieRepository.getFilmReviewsFromServer(param.movieId).map { it.toReview() }
+ movieRepository.setFilmDetailsToDb(filmDetails.toMovieDetailsDb())
+ movieRepository.setFilmReviewsToDb(reviews.map { it.toMovieReviewDb() })
+ Either.Right(filmDetails.copy(reviews = reviews))
+ } catch (exception: Exception) {
+ if (exception is UnknownHostException) {
+ Either.Right(
+ movieRepository.getFilmDetailsFromDb(param.movieId).toFilmDetails()
+ )
+ } else {
+ Either.Left(onWrapException(exception))
+ }
+ }
+ }
+
+ class Param(val movieId: String)
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/hramogin/movieapp/domain/useCases/movie/GetFilmReviewsUseCase.kt b/app/src/main/java/com/github/hramogin/movieapp/domain/useCases/movie/GetFilmReviewsUseCase.kt
new file mode 100644
index 00000000..f131abb2
--- /dev/null
+++ b/app/src/main/java/com/github/hramogin/movieapp/domain/useCases/movie/GetFilmReviewsUseCase.kt
@@ -0,0 +1,33 @@
+package com.github.hramogin.movieapp.domain.useCases.movie
+
+import com.github.hramogin.movieapp.data.repository.movie.MovieRepository
+import com.github.hramogin.movieapp.domain.mappers.toFilmReview
+import com.github.hramogin.movieapp.domain.mappers.toMovieReviewDb
+import com.github.hramogin.movieapp.domain.mappers.toReview
+import com.github.hramogin.movieapp.domain.model.Review
+import com.github.hramogin.movieapp.domain.useCases.base.BaseUseCase
+import com.github.hramogin.movieapp.domain.useCases.base.Either
+import com.github.hramogin.movieapp.domain.useCases.base.Failure
+import java.net.UnknownHostException
+
+class GetFilmReviewsUseCase(private val movieRepository: MovieRepository) :
+ BaseUseCase>() {
+
+ override suspend fun run(param: Param): Either> {
+ return try {
+ val reviews =
+ movieRepository.getFilmReviewsFromServer(param.filmId).map { it.toReview() }
+ movieRepository.setFilmReviewsToDb(reviews.map { it.toMovieReviewDb() })
+ Either.Right(reviews)
+ } catch (exception: Exception) {
+ if (exception is UnknownHostException) {
+ Either.Right(
+ movieRepository.getFilmReviewsFromDb(param.filmId).map { it.toFilmReview() })
+ } else {
+ Either.Left(onWrapException(exception))
+ }
+ }
+ }
+
+ class Param(val filmId: String)
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/hramogin/movieapp/domain/useCases/movie/GetMoviesUseCase.kt b/app/src/main/java/com/github/hramogin/movieapp/domain/useCases/movie/GetMoviesUseCase.kt
new file mode 100644
index 00000000..3f9c7aca
--- /dev/null
+++ b/app/src/main/java/com/github/hramogin/movieapp/domain/useCases/movie/GetMoviesUseCase.kt
@@ -0,0 +1,30 @@
+package com.github.hramogin.movieapp.domain.useCases.movie
+
+import com.github.hramogin.movieapp.data.repository.movie.MovieRepository
+import com.github.hramogin.movieapp.domain.mappers.toFilm
+import com.github.hramogin.movieapp.domain.mappers.toMovieDb
+import com.github.hramogin.movieapp.domain.model.Movie
+import com.github.hramogin.movieapp.domain.useCases.base.BaseUseCase
+import com.github.hramogin.movieapp.domain.useCases.base.Either
+import com.github.hramogin.movieapp.domain.useCases.base.Failure
+import java.net.UnknownHostException
+
+open class GetMoviesUseCase(private val moviesRepository: MovieRepository) :
+ BaseUseCase>() {
+
+ override suspend fun run(param: Params): Either> {
+ return try {
+ val movies = moviesRepository.getFilmsFromServer().map { it.toFilm() }
+ moviesRepository.setFilmsToDb(movies.map { it.toMovieDb() })
+ return Either.Right(movies)
+ } catch (exception: Exception) {
+ if (exception is UnknownHostException) {
+ Either.Right(moviesRepository.getFilmsFromDb().map { it.toFilm() })
+ } else {
+ Either.Left(onWrapException(exception))
+ }
+ }
+ }
+
+ open class Params
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/hramogin/movieapp/navigation/AppNavigator.kt b/app/src/main/java/com/github/hramogin/movieapp/navigation/AppNavigator.kt
new file mode 100644
index 00000000..320a1a4b
--- /dev/null
+++ b/app/src/main/java/com/github/hramogin/movieapp/navigation/AppNavigator.kt
@@ -0,0 +1,47 @@
+package com.github.hramogin.movieapp.navigation
+
+import android.os.Bundle
+import android.view.View
+import androidx.fragment.app.Fragment
+import androidx.navigation.NavDirections
+import androidx.navigation.fragment.FragmentNavigatorExtras
+import androidx.navigation.fragment.NavHostFragment.findNavController
+import com.github.hramogin.movieapp.R
+import com.github.hramogin.movieapp.presentation.screens.moviesList.MoviesListFragmentDirections
+import com.github.hramogin.movieapp.presentation.screens.splash.SplashFragment
+
+object AppNavigator : Navigator {
+
+ override fun toMainScreen(component: Fragment) {
+ when (component) {
+ is SplashFragment -> navigateTo(
+ component,
+ R.id.action_splashFragment_to_mainFragment
+ )
+ }
+ }
+
+ override fun toDetailsScreen(component: Fragment, id: String, view: View) {
+ val action = MoviesListFragmentDirections.actionMoviesListFragmentToDetailsFragment(id)
+ val extras = FragmentNavigatorExtras()
+ navigateTo(
+ component = component,
+ action = action,
+ )
+ }
+
+ override fun onHandleBack(component: Fragment): Boolean {
+ val controller = findNavController(component)
+ return controller.popBackStack()
+ }
+
+
+ private fun navigateTo(component: Fragment, action: Int, bundle: Bundle? = null) {
+ val controller = findNavController(component)
+ controller.navigate(action, bundle)
+ }
+
+ private fun navigateTo(component: Fragment, action: NavDirections) {
+ findNavController(component).navigate(action)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/hramogin/movieapp/navigation/Navigator.kt b/app/src/main/java/com/github/hramogin/movieapp/navigation/Navigator.kt
new file mode 100644
index 00000000..606e0d50
--- /dev/null
+++ b/app/src/main/java/com/github/hramogin/movieapp/navigation/Navigator.kt
@@ -0,0 +1,13 @@
+package com.github.hramogin.movieapp.navigation
+
+import android.view.View
+import androidx.fragment.app.Fragment
+
+interface Navigator {
+
+ fun toMainScreen(component: Fragment)
+
+ fun toDetailsScreen(component: Fragment, id: String, view: View)
+
+ fun onHandleBack(component: Fragment): Boolean
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/hramogin/movieapp/presentation/base/BaseFragment.kt b/app/src/main/java/com/github/hramogin/movieapp/presentation/base/BaseFragment.kt
new file mode 100644
index 00000000..1591f6b7
--- /dev/null
+++ b/app/src/main/java/com/github/hramogin/movieapp/presentation/base/BaseFragment.kt
@@ -0,0 +1,40 @@
+package com.github.hramogin.movieapp.presentation.base
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.fragment.app.Fragment
+import androidx.viewbinding.ViewBinding
+import com.github.hramogin.movieapp.presentation.base.viewModel.BaseViewModel
+
+abstract class BaseFragment : Fragment() {
+
+ abstract val viewModel: VM
+ abstract fun getLayoutId(): Int
+ abstract val bindingInflater: (LayoutInflater, ViewGroup?, Boolean) -> VB
+
+ private var _binding: ViewBinding? = null
+
+ @Suppress("UNCHECKED_CAST")
+ protected val binding: VB
+ get() = _binding as VB
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ viewModel.onCreate()
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater, container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
+ _binding = bindingInflater.invoke(inflater, container, false)
+ return requireNotNull(_binding).root
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ viewModel.onViewCreated()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/hramogin/movieapp/presentation/base/IntExtensions.kt b/app/src/main/java/com/github/hramogin/movieapp/presentation/base/IntExtensions.kt
new file mode 100644
index 00000000..e001331b
--- /dev/null
+++ b/app/src/main/java/com/github/hramogin/movieapp/presentation/base/IntExtensions.kt
@@ -0,0 +1,10 @@
+package com.github.hramogin.movieapp.presentation.base
+
+import android.content.res.Resources
+
+val Float.toPx: Int
+ get() = (this * Resources.getSystem().displayMetrics.density).toInt()
+
+val Int.toDp: Int
+ get() = (this / Resources.getSystem().displayMetrics.density).toInt()
+
diff --git a/app/src/main/java/com/github/hramogin/movieapp/presentation/base/ResourceProvider.kt b/app/src/main/java/com/github/hramogin/movieapp/presentation/base/ResourceProvider.kt
new file mode 100644
index 00000000..34be4fcc
--- /dev/null
+++ b/app/src/main/java/com/github/hramogin/movieapp/presentation/base/ResourceProvider.kt
@@ -0,0 +1,20 @@
+package com.github.hramogin.movieapp.presentation.base
+
+import android.content.Context
+import android.content.res.Resources
+import androidx.annotation.StringRes
+import javax.inject.Inject
+
+interface ResourceProvider {
+
+ fun getString(@StringRes id: Int): String
+}
+
+class ResourceProviderImpl @Inject constructor(context: Context): ResourceProvider {
+
+ private val resources: Resources = context.resources
+
+ override fun getString(id: Int): String {
+ return resources.getString(id)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/hramogin/movieapp/presentation/base/adapter/BaseAdapter.kt b/app/src/main/java/com/github/hramogin/movieapp/presentation/base/adapter/BaseAdapter.kt
new file mode 100644
index 00000000..cb7ff7ce
--- /dev/null
+++ b/app/src/main/java/com/github/hramogin/movieapp/presentation/base/adapter/BaseAdapter.kt
@@ -0,0 +1,35 @@
+package com.github.hramogin.movieapp.presentation.base.adapter
+
+import androidx.recyclerview.widget.RecyclerView
+import androidx.viewbinding.ViewBinding
+
+abstract class BaseAdapter(
+ protected var items: List = emptyList(),
+ protected val listener: (item: T) -> Unit = {}
+) :
+ RecyclerView.Adapter>() {
+
+ override fun onBindViewHolder(holder: BaseViewHolder, position: Int) {
+ if (items.isEmpty()) {
+ holder.onBind(null)
+ } else {
+ holder.onBind(items[position])
+ }
+ }
+
+ override fun getItemCount(): Int = if (items.isEmpty()) 1 else items.size
+
+ override fun getItemViewType(position: Int): Int {
+ return if (items.isEmpty()) EMPTY_TYPE else GENERAL_TYPE
+ }
+
+ open fun setNewData(newItems: List) {
+ items = newItems
+ notifyDataSetChanged()
+ }
+
+ companion object {
+ const val EMPTY_TYPE = 0
+ const val GENERAL_TYPE = 1
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/hramogin/movieapp/presentation/base/adapter/BaseViewHolder.kt b/app/src/main/java/com/github/hramogin/movieapp/presentation/base/adapter/BaseViewHolder.kt
new file mode 100644
index 00000000..044776c5
--- /dev/null
+++ b/app/src/main/java/com/github/hramogin/movieapp/presentation/base/adapter/BaseViewHolder.kt
@@ -0,0 +1,15 @@
+package com.github.hramogin.movieapp.presentation.base.adapter
+
+import androidx.recyclerview.widget.RecyclerView
+import androidx.viewbinding.ViewBinding
+
+abstract class BaseViewHolder(binding: VB, protected val listener: (item: T) -> Unit = {}) :
+ RecyclerView.ViewHolder(binding.root){
+
+ open fun onBind(item: T?) {
+ val data = item ?: return
+ itemView.setOnClickListener {
+ listener.invoke(data)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/hramogin/movieapp/presentation/base/extensions/LiveDataExtensions.kt b/app/src/main/java/com/github/hramogin/movieapp/presentation/base/extensions/LiveDataExtensions.kt
new file mode 100644
index 00000000..37aa95e0
--- /dev/null
+++ b/app/src/main/java/com/github/hramogin/movieapp/presentation/base/extensions/LiveDataExtensions.kt
@@ -0,0 +1,13 @@
+package com.github.hramogin.movieapp.presentation.base.extensions
+
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.Observer
+import com.github.hramogin.movieapp.presentation.base.viewModel.Event
+
+inline fun LiveData>.observeEvent(
+ owner: LifecycleOwner,
+ crossinline onHandleEvent: (T) -> Unit
+) {
+ observe(owner, Observer { it?.updateIfNeed()?.let(onHandleEvent) })
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/hramogin/movieapp/presentation/base/viewModel/ActionLiveData.kt b/app/src/main/java/com/github/hramogin/movieapp/presentation/base/viewModel/ActionLiveData.kt
new file mode 100644
index 00000000..cea0ee2e
--- /dev/null
+++ b/app/src/main/java/com/github/hramogin/movieapp/presentation/base/viewModel/ActionLiveData.kt
@@ -0,0 +1,32 @@
+package com.github.hramogin.movieapp.presentation.base.viewModel
+
+import androidx.annotation.MainThread
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.Observer
+import timber.log.Timber
+import java.util.concurrent.atomic.AtomicBoolean
+
+class ActionLiveData : MutableLiveData() {
+
+ private var mPending: AtomicBoolean = AtomicBoolean(false)
+
+ override fun observe(owner: LifecycleOwner, observer: Observer) {
+
+ if (hasActiveObservers()) {
+ Timber.w("Has multiple observers")
+ }
+
+ super.observe(owner, Observer {
+ if (mPending.compareAndSet(true, false)) {
+ observer.onChanged(it)
+ }
+ })
+ }
+
+ @MainThread
+ override fun setValue(value: T?) {
+ mPending.set(true)
+ super.setValue(value)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/hramogin/movieapp/presentation/base/viewModel/BaseViewModel.kt b/app/src/main/java/com/github/hramogin/movieapp/presentation/base/viewModel/BaseViewModel.kt
new file mode 100644
index 00000000..f7b525fd
--- /dev/null
+++ b/app/src/main/java/com/github/hramogin/movieapp/presentation/base/viewModel/BaseViewModel.kt
@@ -0,0 +1,23 @@
+package com.github.hramogin.movieapp.presentation.base.viewModel
+
+import androidx.lifecycle.ViewModel
+import com.github.hramogin.movieapp.domain.useCases.base.Failure
+import timber.log.Timber
+
+abstract class BaseViewModel: ViewModel() {
+
+ private val _backAction: ActionLiveData> = ActionLiveData()
+
+ open fun onCreate() {}
+
+ open fun onViewCreated() {}
+
+ open fun onBackClicked() {
+ _backAction.value = Event(Any())
+ }
+
+ open fun onHandleError(failure: Failure) {
+ Timber.e(failure.exception)
+ //Todo implement logic for display error on UI
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/hramogin/movieapp/presentation/base/viewModel/Event.kt b/app/src/main/java/com/github/hramogin/movieapp/presentation/base/viewModel/Event.kt
new file mode 100644
index 00000000..b4f7ea4b
--- /dev/null
+++ b/app/src/main/java/com/github/hramogin/movieapp/presentation/base/viewModel/Event.kt
@@ -0,0 +1,16 @@
+package com.github.hramogin.movieapp.presentation.base.viewModel
+
+class Event(private val data: T) {
+
+ private var hasBeenHandled = false
+ private set
+
+ fun updateIfNeed(): T?{
+ return if(hasBeenHandled){
+ null
+ }else{
+ hasBeenHandled = true
+ data
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/hramogin/movieapp/presentation/screens/details/BaseDetailItem.kt b/app/src/main/java/com/github/hramogin/movieapp/presentation/screens/details/BaseDetailItem.kt
new file mode 100644
index 00000000..e8bba23b
--- /dev/null
+++ b/app/src/main/java/com/github/hramogin/movieapp/presentation/screens/details/BaseDetailItem.kt
@@ -0,0 +1,22 @@
+package com.github.hramogin.movieapp.presentation.screens.details
+
+import com.github.hramogin.movieapp.domain.model.Review
+
+abstract class BaseDetailItem(
+ open val viewType: Int
+)
+
+class TitleItem(
+ override val viewType: Int = 0,
+ val title: String,
+) : BaseDetailItem(viewType)
+
+class DescriptionItem(
+ override val viewType: Int = 1,
+ val description: String
+) : BaseDetailItem(viewType)
+
+class ReviewItem(
+ override val viewType: Int = 2,
+ val review: Review,
+) : BaseDetailItem(viewType)
\ No newline at end of file
diff --git a/app/src/main/java/com/github/hramogin/movieapp/presentation/screens/details/DetailsAdapter.kt b/app/src/main/java/com/github/hramogin/movieapp/presentation/screens/details/DetailsAdapter.kt
new file mode 100644
index 00000000..d3f42478
--- /dev/null
+++ b/app/src/main/java/com/github/hramogin/movieapp/presentation/screens/details/DetailsAdapter.kt
@@ -0,0 +1,106 @@
+package com.github.hramogin.movieapp.presentation.screens.details
+
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.recyclerview.widget.RecyclerView
+import com.github.hramogin.movieapp.databinding.DescriptionItemBinding
+import com.github.hramogin.movieapp.databinding.ReviewItemBinding
+import com.github.hramogin.movieapp.databinding.TitleItemBinding
+
+class DetailsAdapter() : RecyclerView.Adapter() {
+
+ private val items: MutableList = mutableListOf()
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseDetailViewHolder {
+ return when (viewType) {
+ TitleViewHolder.TYPE -> {
+ val binding =
+ TitleItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
+ TitleViewHolder(binding)
+ }
+ DescriptionViewHolder.TYPE -> {
+ val binding = DescriptionItemBinding.inflate(
+ LayoutInflater.from(parent.context),
+ parent,
+ false
+ )
+ DescriptionViewHolder(binding)
+ }
+ ReviewViewHolder.TYPE -> {
+ val binding =
+ ReviewItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
+ ReviewViewHolder(binding)
+ }
+ else -> throw IllegalStateException()
+ }
+ }
+
+ override fun onBindViewHolder(holder: BaseDetailViewHolder, position: Int) {
+ holder.onBind(items[position])
+ }
+
+ override fun getItemCount(): Int {
+ return items.size
+ }
+
+ override fun getItemViewType(position: Int): Int {
+ return items[position].viewType
+ }
+
+ fun setItems(data: List) {
+ items.clear()
+ items.addAll(data)
+ notifyDataSetChanged()
+ }
+}
+
+class TitleViewHolder(private val binding: TitleItemBinding) : BaseDetailViewHolder(binding.root) {
+
+ override fun onBind(data: BaseDetailItem) {
+ if (data is TitleItem) {
+ binding.tvTitle.text = data.title
+ }
+ }
+
+ companion object {
+ const val TYPE = 0
+ }
+}
+
+class DescriptionViewHolder(private val binding: DescriptionItemBinding) :
+ BaseDetailViewHolder(binding.root) {
+
+
+ override fun onBind(data: BaseDetailItem) {
+ if (data is DescriptionItem) {
+ binding.tvDescription.text = data.description
+ }
+ }
+
+ companion object {
+ const val TYPE = 1
+ }
+}
+
+class ReviewViewHolder(private val binding: ReviewItemBinding) :
+ BaseDetailViewHolder(binding.root) {
+
+ override fun onBind(data: BaseDetailItem) {
+ if (data is ReviewItem) {
+ binding.tvReviewContent.text = data.review.content
+ binding.tvReviewerName.text = data.review.name
+ binding.tvReviewDate.text = data.review.date
+ }
+ }
+
+ companion object {
+ const val TYPE = 2
+ }
+}
+
+abstract class BaseDetailViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
+
+
+ abstract fun onBind(data: BaseDetailItem)
+}
diff --git a/app/src/main/java/com/github/hramogin/movieapp/presentation/screens/details/DetailsFragment.kt b/app/src/main/java/com/github/hramogin/movieapp/presentation/screens/details/DetailsFragment.kt
new file mode 100644
index 00000000..d089d3d7
--- /dev/null
+++ b/app/src/main/java/com/github/hramogin/movieapp/presentation/screens/details/DetailsFragment.kt
@@ -0,0 +1,56 @@
+package com.github.hramogin.movieapp.presentation.screens.details
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.navigation.fragment.navArgs
+import com.github.hramogin.movieapp.BuildConfig
+import com.github.hramogin.movieapp.R
+import com.github.hramogin.movieapp.data.network.GlideApp
+import com.github.hramogin.movieapp.databinding.FragmentDetailsBinding
+import com.github.hramogin.movieapp.navigation.AppNavigator.onHandleBack
+import com.github.hramogin.movieapp.presentation.base.BaseFragment
+import org.koin.android.ext.android.inject
+import org.koin.core.parameter.parametersOf
+
+class DetailsFragment : BaseFragment() {
+
+ override val viewModel: DetailsViewModel by inject { parametersOf(param.filmId) }
+
+ override fun getLayoutId(): Int = R.layout.fragment_details
+
+ private val param: DetailsFragmentArgs by navArgs()
+
+ private lateinit var adapter: DetailsAdapter
+
+ override val bindingInflater: (LayoutInflater, ViewGroup?, Boolean) -> FragmentDetailsBinding =
+ { inflater, container, attached ->
+ FragmentDetailsBinding.inflate(inflater, container, attached)
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ binding.ivMovieImage.transitionName = param.filmId
+ binding.toolbar.setNavigationOnClickListener {
+ onHandleBack(this)
+ }
+ initRecycler()
+
+ viewModel.movieDetails.observe(viewLifecycleOwner) {
+ binding.collapsingToolbarLayout.title = it.title
+ GlideApp.with(requireContext())
+ .load(BuildConfig.BASE_IMAGE_URL + it.posterPath)
+ .dontTransform()
+ .into(binding.ivMovieImage)
+ }
+ viewModel.detailsItem.observe(viewLifecycleOwner) {
+ adapter.setItems(it)
+ }
+ }
+
+ private fun initRecycler() {
+ adapter = DetailsAdapter()
+ binding.rvMovieDetailsContent.adapter = adapter
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/hramogin/movieapp/presentation/screens/details/DetailsViewModel.kt b/app/src/main/java/com/github/hramogin/movieapp/presentation/screens/details/DetailsViewModel.kt
new file mode 100644
index 00000000..d18658f6
--- /dev/null
+++ b/app/src/main/java/com/github/hramogin/movieapp/presentation/screens/details/DetailsViewModel.kt
@@ -0,0 +1,40 @@
+package com.github.hramogin.movieapp.presentation.screens.details
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.viewModelScope
+import com.github.hramogin.movieapp.domain.model.MovieDetails
+import com.github.hramogin.movieapp.domain.useCases.movie.GetFilmDetailsUseCase
+import com.github.hramogin.movieapp.presentation.base.viewModel.BaseViewModel
+
+class DetailsViewModel(
+ private val id: String,
+ private val getFilmDetailsUseCase: GetFilmDetailsUseCase,
+ private val movieDetailsMapper: MovieDetailsMapper,
+) : BaseViewModel() {
+
+ private val _filmId: MutableLiveData = MutableLiveData()
+ private val _movieDetails: MutableLiveData = MutableLiveData()
+ private val _detailsItem: MutableLiveData> = MutableLiveData()
+
+ val movieDetails: LiveData = _movieDetails
+ val detailsItem: LiveData> = _detailsItem
+
+ override fun onCreate() {
+ super.onCreate()
+ loadFilmDetails()
+ }
+
+ private fun loadFilmDetails() {
+ getFilmDetailsUseCase.invoke(viewModelScope, GetFilmDetailsUseCase.Param(id)) {
+ it.either(::onHandleError, ::onFilmDetailsLoaded)
+ }
+ }
+
+ private fun onFilmDetailsLoaded(movieDetails: MovieDetails) {
+ _filmId.value = movieDetails.id
+ _movieDetails.value = movieDetails
+ _detailsItem.value = movieDetailsMapper.map(movieDetails)
+ return
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/hramogin/movieapp/presentation/screens/details/MovieDetailsMapper.kt b/app/src/main/java/com/github/hramogin/movieapp/presentation/screens/details/MovieDetailsMapper.kt
new file mode 100644
index 00000000..e6a826d5
--- /dev/null
+++ b/app/src/main/java/com/github/hramogin/movieapp/presentation/screens/details/MovieDetailsMapper.kt
@@ -0,0 +1,25 @@
+package com.github.hramogin.movieapp.presentation.screens.details
+
+import com.github.hramogin.movieapp.R
+import com.github.hramogin.movieapp.domain.model.MovieDetails
+import com.github.hramogin.movieapp.presentation.base.ResourceProvider
+import javax.inject.Inject
+
+class MovieDetailsMapper @Inject constructor(private val resourceProvider: ResourceProvider) {
+
+ fun map(details: MovieDetails): List {
+ val items: MutableList = mutableListOf()
+
+ if (details.description.isNotEmpty()) {
+ items.add(TitleItem(title = resourceProvider.getString(R.string.movie_details_description_title)))
+ items.add(DescriptionItem(description = details.description))
+ }
+
+ if (details.reviews.isNotEmpty()) {
+ items.add(TitleItem(title = resourceProvider.getString(R.string.movie_details_reviews_title)))
+ items.addAll(details.reviews.map { ReviewItem(review = it) })
+ }
+
+ return items
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/hramogin/movieapp/presentation/screens/moviesList/MoviesListFragment.kt b/app/src/main/java/com/github/hramogin/movieapp/presentation/screens/moviesList/MoviesListFragment.kt
new file mode 100644
index 00000000..4471bcd8
--- /dev/null
+++ b/app/src/main/java/com/github/hramogin/movieapp/presentation/screens/moviesList/MoviesListFragment.kt
@@ -0,0 +1,56 @@
+package com.github.hramogin.movieapp.presentation.screens.moviesList
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import com.github.hramogin.movieapp.R
+import com.github.hramogin.movieapp.databinding.FragmentMoviesListBinding
+import com.github.hramogin.movieapp.navigation.AppNavigator
+import com.github.hramogin.movieapp.presentation.base.BaseFragment
+import com.github.hramogin.movieapp.presentation.base.extensions.observeEvent
+import com.github.hramogin.movieapp.presentation.screens.moviesList.adapter.MovieItemDecorator
+import com.github.hramogin.movieapp.presentation.screens.moviesList.adapter.MoviesListAdapter
+import org.koin.androidx.viewmodel.ext.android.viewModel
+
+class MoviesListFragment : BaseFragment() {
+
+ override val viewModel: MoviesListViewModel by viewModel()
+
+ override fun getLayoutId(): Int = R.layout.fragment_movies_list
+
+ private lateinit var adapter: MoviesListAdapter
+
+ override val bindingInflater: (LayoutInflater, ViewGroup?, Boolean) -> FragmentMoviesListBinding =
+ { inflater, container, attached ->
+ FragmentMoviesListBinding.inflate(inflater, container, attached)
+ }
+
+ private fun initViews() {
+ adapter = MoviesListAdapter { movie, view ->
+ AppNavigator.toDetailsScreen(this, movie.id, view)
+ }
+ binding.rwMoviesList.adapter = adapter
+ binding.rwMoviesList.addItemDecoration(MovieItemDecorator())
+ binding.rwMoviesList.apply {
+ postponeEnterTransition()
+ viewTreeObserver
+ .addOnPreDrawListener {
+ startPostponedEnterTransition()
+ true
+ }
+ }
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ initViews()
+ viewModel.openDetailsScreenAction.observeEvent(viewLifecycleOwner) {
+ AppNavigator.toDetailsScreen(this, it, binding.rwMoviesList)
+ }
+
+ viewModel.moviesLiveData.observe(viewLifecycleOwner) {
+ adapter.setNewData(it)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/hramogin/movieapp/presentation/screens/moviesList/MoviesListViewModel.kt b/app/src/main/java/com/github/hramogin/movieapp/presentation/screens/moviesList/MoviesListViewModel.kt
new file mode 100644
index 00000000..977922f3
--- /dev/null
+++ b/app/src/main/java/com/github/hramogin/movieapp/presentation/screens/moviesList/MoviesListViewModel.kt
@@ -0,0 +1,34 @@
+package com.github.hramogin.movieapp.presentation.screens.moviesList
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.viewModelScope
+import com.github.hramogin.movieapp.domain.model.Movie
+import com.github.hramogin.movieapp.domain.useCases.movie.GetMoviesUseCase
+import com.github.hramogin.movieapp.presentation.base.viewModel.ActionLiveData
+import com.github.hramogin.movieapp.presentation.base.viewModel.BaseViewModel
+import com.github.hramogin.movieapp.presentation.base.viewModel.Event
+
+class MoviesListViewModel(private val getMoviesUseCase: GetMoviesUseCase) : BaseViewModel() {
+
+ private val _openDetailsAction: ActionLiveData> = ActionLiveData()
+ private val _moviesLiveData: MutableLiveData> = MutableLiveData()
+
+ val openDetailsScreenAction: LiveData> = _openDetailsAction
+ val moviesLiveData: LiveData> = _moviesLiveData
+
+ override fun onCreate() {
+ super.onCreate()
+ loadMovies()
+ }
+
+ private fun loadMovies() {
+ getMoviesUseCase.invoke(viewModelScope, GetMoviesUseCase.Params()) {
+ it.either(::onHandleError, ::onFilmsLoaded)
+ }
+ }
+
+ private fun onFilmsLoaded(movies: List) {
+ _moviesLiveData.value = movies
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/hramogin/movieapp/presentation/screens/moviesList/adapter/MovieItemDecorator.kt b/app/src/main/java/com/github/hramogin/movieapp/presentation/screens/moviesList/adapter/MovieItemDecorator.kt
new file mode 100644
index 00000000..a669b753
--- /dev/null
+++ b/app/src/main/java/com/github/hramogin/movieapp/presentation/screens/moviesList/adapter/MovieItemDecorator.kt
@@ -0,0 +1,40 @@
+package com.github.hramogin.movieapp.presentation.screens.moviesList.adapter
+
+import android.graphics.Rect
+import android.view.View
+import androidx.recyclerview.widget.RecyclerView
+import com.github.hramogin.movieapp.R
+
+class MovieItemDecorator() : RecyclerView.ItemDecoration() {
+
+ override fun getItemOffsets(
+ outRect: Rect,
+ view: View,
+ parent: RecyclerView,
+ state: RecyclerView.State
+ ) {
+ val position = parent.getChildAdapterPosition(view)
+
+ if (position == RecyclerView.NO_POSITION) {
+ return
+ }
+
+ val sideMargin =
+ view.context.resources.getDimension(R.dimen.base_margi).toInt()
+ val innerMargin = view.context.resources.getDimension(R.dimen.base_margi).toInt() / 2
+
+ if (position % 2 == 0) {
+ outRect.set(sideMargin, innerMargin, innerMargin, innerMargin)
+ } else {
+ outRect.set(innerMargin, innerMargin, sideMargin, innerMargin)
+ }
+
+ val adapter = parent.adapter ?: return
+
+ if (position == adapter.itemCount - 1 ||
+ position == adapter.itemCount - 2
+ ) {
+ outRect.bottom = sideMargin * 2
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/hramogin/movieapp/presentation/screens/moviesList/adapter/MovieViewHolder.kt b/app/src/main/java/com/github/hramogin/movieapp/presentation/screens/moviesList/adapter/MovieViewHolder.kt
new file mode 100644
index 00000000..5478ebd5
--- /dev/null
+++ b/app/src/main/java/com/github/hramogin/movieapp/presentation/screens/moviesList/adapter/MovieViewHolder.kt
@@ -0,0 +1,65 @@
+package com.github.hramogin.movieapp.presentation.screens.moviesList.adapter
+
+import android.graphics.drawable.Drawable
+import android.view.View
+import com.bumptech.glide.load.DataSource
+import com.bumptech.glide.load.engine.GlideException
+import com.bumptech.glide.load.resource.bitmap.CenterCrop
+import com.bumptech.glide.load.resource.bitmap.RoundedCorners
+import com.bumptech.glide.request.RequestListener
+import com.bumptech.glide.request.RequestOptions
+import com.bumptech.glide.request.target.Target
+import com.github.hramogin.movieapp.BuildConfig
+import com.github.hramogin.movieapp.R
+import com.github.hramogin.movieapp.data.network.GlideApp
+import com.github.hramogin.movieapp.databinding.LayoutMovieItemBinding
+import com.github.hramogin.movieapp.domain.model.Movie
+import com.github.hramogin.movieapp.presentation.base.adapter.BaseViewHolder
+import com.github.hramogin.movieapp.presentation.base.toPx
+
+class MovieViewHolder(
+ val binding: LayoutMovieItemBinding,
+) : BaseViewHolder(binding) {
+
+ override fun onBind(item: Movie?) {
+ super.onBind(item)
+ item ?: return
+
+ binding.tvMovieTitle.text = item.title
+ val requestOptions = RequestOptions().transforms(
+ CenterCrop(), RoundedCorners(
+ itemView.context.resources.getDimension(
+ R.dimen.base_round_corner
+ ).toPx
+ )
+ )
+ GlideApp.with(itemView.context)
+ .load(BuildConfig.BASE_IMAGE_URL + item.posterPath)
+ .apply(requestOptions)
+ .listener(object : RequestListener {
+ override fun onResourceReady(
+ resource: Drawable?,
+ model: Any?,
+ target: Target?,
+ dataSource: DataSource?,
+ isFirstResource: Boolean
+ ): Boolean {
+ binding.tvMovieTitle.text = item.title
+ binding.tvMovieTitle.visibility = View.VISIBLE
+ binding.vTitleContainer.visibility = View.VISIBLE
+
+ return false
+ }
+
+ override fun onLoadFailed(
+ e: GlideException?,
+ model: Any?,
+ target: Target?,
+ isFirstResource: Boolean
+ ): Boolean {
+ return false
+ }
+ })
+ .into(binding.ivMovieImage)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/hramogin/movieapp/presentation/screens/moviesList/adapter/MoviesListAdapter.kt b/app/src/main/java/com/github/hramogin/movieapp/presentation/screens/moviesList/adapter/MoviesListAdapter.kt
new file mode 100644
index 00000000..7e6a51c5
--- /dev/null
+++ b/app/src/main/java/com/github/hramogin/movieapp/presentation/screens/moviesList/adapter/MoviesListAdapter.kt
@@ -0,0 +1,33 @@
+package com.github.hramogin.movieapp.presentation.screens.moviesList.adapter
+
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import com.github.hramogin.movieapp.databinding.LayoutMovieItemBinding
+import com.github.hramogin.movieapp.domain.model.Movie
+import com.github.hramogin.movieapp.presentation.base.adapter.BaseAdapter
+import com.github.hramogin.movieapp.presentation.base.adapter.BaseViewHolder
+
+class MoviesListAdapter(private val movieClickListener: (Movie, View) -> Unit) : BaseAdapter() {
+
+ override fun onCreateViewHolder(
+ parent: ViewGroup,
+ viewType: Int
+ ): BaseViewHolder {
+ val binding =
+ LayoutMovieItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
+ return MovieViewHolder(binding)
+ }
+
+ override fun onBindViewHolder(
+ holder: BaseViewHolder,
+ position: Int
+ ) {
+ super.onBindViewHolder(holder, position)
+ if(holder is MovieViewHolder) {
+ holder.itemView.setOnClickListener {
+ movieClickListener.invoke(items[position], holder.binding.ivMovieImage)
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/hramogin/movieapp/presentation/screens/splash/SplashFragment.kt b/app/src/main/java/com/github/hramogin/movieapp/presentation/screens/splash/SplashFragment.kt
new file mode 100644
index 00000000..ec876b3c
--- /dev/null
+++ b/app/src/main/java/com/github/hramogin/movieapp/presentation/screens/splash/SplashFragment.kt
@@ -0,0 +1,36 @@
+package com.github.hramogin.movieapp.presentation.screens.splash
+
+import android.os.Bundle
+import android.view.ContextThemeWrapper
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import com.github.hramogin.movieapp.R
+import com.github.hramogin.movieapp.databinding.FragmentSplashBinding
+import com.github.hramogin.movieapp.navigation.AppNavigator
+import com.github.hramogin.movieapp.presentation.base.BaseFragment
+import com.github.hramogin.movieapp.presentation.base.extensions.observeEvent
+import org.koin.androidx.viewmodel.ext.android.viewModel
+
+class SplashFragment : BaseFragment() {
+
+ override val viewModel: SplashViewModel by viewModel()
+
+ override fun getLayoutId(): Int = R.layout.fragment_splash
+
+ override val bindingInflater: (LayoutInflater, ViewGroup?, Boolean) -> FragmentSplashBinding =
+ { inflater, container, attached ->
+ val contextThemeWrapper =
+ ContextThemeWrapper(activity, R.style.AppTheme_MovieApp_Splash)
+ val localInflater = inflater.cloneInContext(contextThemeWrapper)
+ FragmentSplashBinding.inflate(localInflater, container, attached)
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ viewModel.openMainScreenAction.observeEvent(viewLifecycleOwner) {
+ AppNavigator.toMainScreen(this)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/github/hramogin/movieapp/presentation/screens/splash/SplashViewModel.kt b/app/src/main/java/com/github/hramogin/movieapp/presentation/screens/splash/SplashViewModel.kt
new file mode 100644
index 00000000..2a95439a
--- /dev/null
+++ b/app/src/main/java/com/github/hramogin/movieapp/presentation/screens/splash/SplashViewModel.kt
@@ -0,0 +1,32 @@
+package com.github.hramogin.movieapp.presentation.screens.splash
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.viewModelScope
+import com.github.hramogin.movieapp.presentation.base.viewModel.ActionLiveData
+import com.github.hramogin.movieapp.presentation.base.viewModel.BaseViewModel
+import com.github.hramogin.movieapp.presentation.base.viewModel.Event
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+
+class SplashViewModel : BaseViewModel() {
+
+ private val _openMainAction: ActionLiveData> = ActionLiveData()
+
+ val openMainScreenAction: LiveData> = _openMainAction
+
+ override fun onViewCreated() {
+ super.onViewCreated()
+ openRequiredScreen()
+ }
+
+ private fun openRequiredScreen() {
+ viewModelScope.launch {
+ delay(SPLASH_DURATION)
+ _openMainAction.value = Event(Any())
+ }
+ }
+
+ private companion object {
+ const val SPLASH_DURATION = 3000L
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/res/anim/fade_in.xml b/app/src/main/res/anim/fade_in.xml
new file mode 100644
index 00000000..a8b83f8e
--- /dev/null
+++ b/app/src/main/res/anim/fade_in.xml
@@ -0,0 +1,7 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/anim/fade_out.xml b/app/src/main/res/anim/fade_out.xml
new file mode 100644
index 00000000..116e700b
--- /dev/null
+++ b/app/src/main/res/anim/fade_out.xml
@@ -0,0 +1,7 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/anim/slide_in_left.xml b/app/src/main/res/anim/slide_in_left.xml
new file mode 100644
index 00000000..148de843
--- /dev/null
+++ b/app/src/main/res/anim/slide_in_left.xml
@@ -0,0 +1,10 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/anim/slide_in_right.xml b/app/src/main/res/anim/slide_in_right.xml
new file mode 100644
index 00000000..cc5a30a3
--- /dev/null
+++ b/app/src/main/res/anim/slide_in_right.xml
@@ -0,0 +1,10 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/anim/slide_out_left.xml b/app/src/main/res/anim/slide_out_left.xml
new file mode 100644
index 00000000..575237e5
--- /dev/null
+++ b/app/src/main/res/anim/slide_out_left.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/anim/slide_out_right.xml b/app/src/main/res/anim/slide_out_right.xml
new file mode 100644
index 00000000..0e5f9e61
--- /dev/null
+++ b/app/src/main/res/anim/slide_out_right.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
\ 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 00000000..2b068d11
--- /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/bg_gray_rounded.xml b/app/src/main/res/drawable/bg_gray_rounded.xml
new file mode 100644
index 00000000..0126e317
--- /dev/null
+++ b/app/src/main/res/drawable/bg_gray_rounded.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/bg_gray_rounded_bottom.xml b/app/src/main/res/drawable/bg_gray_rounded_bottom.xml
new file mode 100644
index 00000000..d1b865bb
--- /dev/null
+++ b/app/src/main/res/drawable/bg_gray_rounded_bottom.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
\ No newline at end of file
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 00000000..07d5da9c
--- /dev/null
+++ b/app/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,170 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_left_arrow.xml b/app/src/main/res/drawable/ic_left_arrow.xml
new file mode 100644
index 00000000..cee03e16
--- /dev/null
+++ b/app/src/main/res/drawable/ic_left_arrow.xml
@@ -0,0 +1,16 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/logo.PNG b/app/src/main/res/drawable/logo.PNG
new file mode 100644
index 00000000..969e6213
Binary files /dev/null and b/app/src/main/res/drawable/logo.PNG differ
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 00000000..847d4696
--- /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/description_item.xml b/app/src/main/res/layout/description_item.xml
new file mode 100644
index 00000000..846b935a
--- /dev/null
+++ b/app/src/main/res/layout/description_item.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_details.xml b/app/src/main/res/layout/fragment_details.xml
new file mode 100644
index 00000000..61335ddb
--- /dev/null
+++ b/app/src/main/res/layout/fragment_details.xml
@@ -0,0 +1,66 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_movies_list.xml b/app/src/main/res/layout/fragment_movies_list.xml
new file mode 100644
index 00000000..1ab7bc5c
--- /dev/null
+++ b/app/src/main/res/layout/fragment_movies_list.xml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_splash.xml b/app/src/main/res/layout/fragment_splash.xml
new file mode 100644
index 00000000..968fdbe2
--- /dev/null
+++ b/app/src/main/res/layout/fragment_splash.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/layout_movie_item.xml b/app/src/main/res/layout/layout_movie_item.xml
new file mode 100644
index 00000000..8cf55332
--- /dev/null
+++ b/app/src/main/res/layout/layout_movie_item.xml
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/review_item.xml b/app/src/main/res/layout/review_item.xml
new file mode 100644
index 00000000..a9ce0060
--- /dev/null
+++ b/app/src/main/res/layout/review_item.xml
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/title_item.xml b/app/src/main/res/layout/title_item.xml
new file mode 100644
index 00000000..5830e3f5
--- /dev/null
+++ b/app/src/main/res/layout/title_item.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
\ 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 00000000..eca70cfe
--- /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 00000000..eca70cfe
--- /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 00000000..c209e78e
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 00000000..b2dfe3d1
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 00000000..4f0f1d64
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 00000000..62b611da
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 00000000..948a3070
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 00000000..1b9a6956
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 00000000..28d4b77f
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 00000000..9287f508
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 00000000..aa7d6427
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 00000000..9126ae37
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/general_navigation.xml b/app/src/main/res/navigation/general_navigation.xml
new file mode 100644
index 00000000..457fd682
--- /dev/null
+++ b/app/src/main/res/navigation/general_navigation.xml
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ 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 00000000..6319378c
--- /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 00000000..b490282c
--- /dev/null
+++ b/app/src/main/res/values/colors.xml
@@ -0,0 +1,15 @@
+
+
+ #FFBB86FC
+ #FF6200EE
+ #FF3700B3
+ #FF03DAC5
+ #FF018786
+ #FF000000
+ #FFFFFFFF
+
+ #BFBFBF
+ #F2BFBFBF
+ #d59944
+ #588693
+
\ No newline at end of file
diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml
new file mode 100644
index 00000000..6f1c49eb
--- /dev/null
+++ b/app/src/main/res/values/dimens.xml
@@ -0,0 +1,16 @@
+
+
+ 8dp
+ 4dp
+ 10dp
+ 16dp
+ 16sp
+ 400dp
+ 336dp
+ 300dp
+ 50dp
+ 12sp
+ 20dp
+ 40dp
+ 36sp
+
\ 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 00000000..bba793c9
--- /dev/null
+++ b/app/src/main/res/values/strings.xml
@@ -0,0 +1,8 @@
+
+ MovieApp
+ Try later
+
+ More about movie
+ Reviews
+ Best movies
+
\ 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 00000000..89e0df42
--- /dev/null
+++ b/app/src/main/res/values/styles.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
\ 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 00000000..a7cf6acd
--- /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/main/res/xml/backup_rules.xml b/app/src/main/res/xml/backup_rules.xml
new file mode 100644
index 00000000..fa0f996d
--- /dev/null
+++ b/app/src/main/res/xml/backup_rules.xml
@@ -0,0 +1,13 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/xml/data_extraction_rules.xml b/app/src/main/res/xml/data_extraction_rules.xml
new file mode 100644
index 00000000..9ee9997b
--- /dev/null
+++ b/app/src/main/res/xml/data_extraction_rules.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/test/java/com/github/hramogin/movieapp/ExampleUnitTest.kt b/app/src/test/java/com/github/hramogin/movieapp/ExampleUnitTest.kt
new file mode 100644
index 00000000..18f29d83
--- /dev/null
+++ b/app/src/test/java/com/github/hramogin/movieapp/ExampleUnitTest.kt
@@ -0,0 +1,17 @@
+package com.github.hramogin.movieapp
+
+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/github/hramogin/movieapp/domain/useCases/movie/GetMoviesUseCaseTest.kt b/app/src/test/java/com/github/hramogin/movieapp/domain/useCases/movie/GetMoviesUseCaseTest.kt
new file mode 100644
index 00000000..17e4636c
--- /dev/null
+++ b/app/src/test/java/com/github/hramogin/movieapp/domain/useCases/movie/GetMoviesUseCaseTest.kt
@@ -0,0 +1,66 @@
+package com.github.hramogin.movieapp.domain.useCases.movie
+
+import com.github.hramogin.movieapp.data.repository.movie.MovieRepository
+import com.nhaarman.mockitokotlin2.any
+import com.nhaarman.mockitokotlin2.doReturn
+import com.nhaarman.mockitokotlin2.mock
+import com.nhaarman.mockitokotlin2.never
+import com.nhaarman.mockitokotlin2.verifyBlocking
+import kotlinx.coroutines.DelicateCoroutinesApi
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.newSingleThreadContext
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.resetMain
+import kotlinx.coroutines.test.setMain
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+
+@DelicateCoroutinesApi
+@ExperimentalCoroutinesApi
+internal class GetMoviesUseCaseTest {
+
+
+ private val mainThreadSurrogate = newSingleThreadContext("UI thread")
+
+ @Before
+ fun setUp() {
+ Dispatchers.setMain(mainThreadSurrogate)
+ }
+
+ @After
+ fun tearDown() {
+ Dispatchers.resetMain()
+ mainThreadSurrogate.close()
+ }
+
+
+ @Test
+ fun `check fetch data from server and store in db`() = runBlocking {
+ with(createEnvironment()) {
+ getMoviesUseCase.invoke(this@runBlocking, mock(), mock())
+
+ verifyBlocking(repository) { getFilmsFromServer() }
+ verifyBlocking(repository) { setFilmsToDb(any()) }
+ verifyBlocking(repository, never()) { getFilmsFromDb() }
+ }
+ }
+
+ private class Environment(
+ val getMoviesUseCase: GetMoviesUseCase,
+ val repository: MovieRepository,
+ )
+
+ private fun createEnvironment(): Environment {
+ val repository: MovieRepository = mock {
+ onBlocking { getFilmsFromServer() } doReturn emptyList()
+ onBlocking { setFilmsToDb(any()) } doReturn Unit
+ onBlocking { getFilmsFromDb() } doReturn emptyList()
+ }
+ return Environment(
+ getMoviesUseCase = GetMoviesUseCase(repository),
+ repository = repository,
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/test/java/com/github/hramogin/movieapp/presentation/screens/moviesList/MoviesListViewModelTest.kt b/app/src/test/java/com/github/hramogin/movieapp/presentation/screens/moviesList/MoviesListViewModelTest.kt
new file mode 100644
index 00000000..fc7b7607
--- /dev/null
+++ b/app/src/test/java/com/github/hramogin/movieapp/presentation/screens/moviesList/MoviesListViewModelTest.kt
@@ -0,0 +1,37 @@
+package com.github.hramogin.movieapp.presentation.screens.moviesList
+
+import com.github.hramogin.movieapp.domain.useCases.movie.GetMoviesUseCase
+import com.nhaarman.mockitokotlin2.any
+import com.nhaarman.mockitokotlin2.doReturn
+import com.nhaarman.mockitokotlin2.mock
+import com.nhaarman.mockitokotlin2.verifyBlocking
+import kotlinx.coroutines.runBlocking
+import org.junit.Test
+import org.mockito.Mockito
+
+internal class MoviesListViewModelTest {
+
+ @Test
+ fun `check view model call getMoviesUseCase during create`() = runBlocking {
+ with(createEnvironment()) {
+ viewModel.onCreate()
+
+ verifyBlocking(getMoviesUseCase) { invoke(any(), any(), any()) }
+ }
+ }
+
+ private class Environment(
+ val viewModel: MoviesListViewModel,
+ val getMoviesUseCase: GetMoviesUseCase
+ )
+
+ private fun createEnvironment(): Environment {
+ val getMoviesUseCase: GetMoviesUseCase = mock(defaultAnswer = Mockito.RETURNS_DEEP_STUBS) {
+ on { invoke(any(), any(), any()) } doReturn mock()
+ }
+ return Environment(
+ MoviesListViewModel(getMoviesUseCase),
+ getMoviesUseCase = getMoviesUseCase,
+ )
+ }
+}
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 00000000..d9f8af26
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,19 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+
+buildscript {
+ ext.nav_version = "2.3.0-beta01"
+ dependencies {
+ classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version"
+ }
+}
+plugins {
+ id 'com.android.application' version '7.2.2' apply false
+ id 'com.android.library' version '7.2.2' apply false
+ id 'org.jetbrains.kotlin.android' version '1.6.10' apply false
+}
+
+apply from: 'dependencies.gradle'
+
+task clean(type: Delete) {
+ delete rootProject.buildDir
+}
\ No newline at end of file
diff --git a/dependencies.gradle b/dependencies.gradle
new file mode 100644
index 00000000..4445be12
--- /dev/null
+++ b/dependencies.gradle
@@ -0,0 +1,172 @@
+ext.androidConfig = [
+ compileSdkVersion : 30,
+ buildToolsVersion : '30.0.2',
+ minSdkVersion : 21,
+ targetSdkVersion : 30,
+]
+
+ext.versions = [
+ kotlinVersion : '1.7.0',
+
+ legacySupport : '1.0.0',
+
+ // Coroutines
+ coroutines : '1.6.4',
+
+ // Design
+ appCompat : '1.1.0',
+ constraintLayout : '1.1.3',
+ recyclerView : '1.0.0',
+ cardView : '1.0.0',
+ material : '1.2.1',
+
+ // ViewModel
+ lifecycleKtx : '2.2.0-beta01',
+
+ lifecycle : '2.1.0',
+
+ glide : '4.10.0',
+ glideTransformations : '4.0.0',
+
+ timber : '4.7.1',
+
+ // Common
+ coreKtx : '1.2.0',
+ coreX : '1.2.0',
+
+ koin : '3.2.2',
+
+ crashlytics : '2.10.1@aar',
+
+ // Retrofit
+ retrofit : '2.6.2',
+ okhttp : '4.2.2',
+ gson : '2.8.6',
+
+ // Test
+ junit : '4.12',
+ junitExtention : '1.1.1',
+ runner : '1.2.0',
+ espresso : '3.2.0',
+
+ swipeLayout : '1.2.0@aar',
+
+ socketIO : '1.0.0',
+
+ chromeTabs : '2.0.3',
+ browser : '1.3.0',
+
+ firebaseAnalytics : '18.0.0',
+ firebaseCloudMessaging: '20.1.0',
+ firebaseCrashlytics : '17.3.0',
+
+ stetho : '1.5.1',
+ leakcanary : '2.2',
+
+ room : '2.4.2',
+
+ navigation : '2.3.0-alpha05',
+
+ imageCrop : '2.8.0',
+ paging : '2.1.0-alpha01',
+
+ jsoup : '1.11.2',
+
+ apollo : '2.5.2',
+
+ lottie : '3.6.0',
+
+ worldCountryData : '1.5.2',
+
+ mockotlin : '2.2.0',
+ coreTestX : '2.1.0',
+]
+
+ext.libraries = [
+ kotlinStdLib : "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$versions.kotlinVersion",
+ legacySupport : "androidx.legacy:legacy-support-v4:$versions.legacySupport",
+
+ coroutinesCore : "org.jetbrains.kotlinx:kotlinx-coroutines-core:$versions.coroutines",
+ coroutinesAndroid : "org.jetbrains.kotlinx:kotlinx-coroutines-android:$versions.coroutines",
+ coroutinesTest : "org.jetbrains.kotlinx:kotlinx-coroutines-test:$versions.coroutines",
+ androidXCoreTesting : "androidx.arch.core:core-testing:$versions.coreTestX",
+
+ appCompat : "androidx.appcompat:appcompat:$versions.appCompat",
+ constraintLayout : "androidx.constraintlayout:constraintlayout:$versions.constraintLayout",
+ recyclerView : "androidx.recyclerview:recyclerview:$versions.recyclerView",
+ cardView : "androidx.cardview:cardview:$versions.cardView",
+ material : "com.google.android.material:material:$versions.material",
+
+ lifecycleViewModelKtx : "androidx.lifecycle:lifecycle-viewmodel-ktx:$versions.lifecycleKtx",
+ lifecycleLiveDataKtx : "androidx.lifecycle:lifecycle-livedata-ktx:$versions.lifecycleKtx",
+
+ lifecycleRuntime : "androidx.lifecycle:lifecycle-runtime:$versions.lifecycle",
+ lifecycleCompiler : "androidx.lifecycle:lifecycle-compiler:$versions.lifecycle",
+
+ coreKtx : "androidx.core:core-ktx:$versions.coreKtx",
+ coreX : "androidx.core:core:$versions.coreX",
+ coreTestX : "androidx.test:core:$versions.coreX",
+
+ glide : "com.github.bumptech.glide:glide:$versions.glide",
+ glideCompiler : "com.github.bumptech.glide:compiler:$versions.glide",
+ glideTransformations : "jp.wasabeef:glide-transformations:$versions.glideTransformations",
+
+ timber : "com.jakewharton.timber:timber:$versions.timber",
+
+ koinCore : "io.insert-koin:koin-core:$versions.koin",
+ koinAndroid : "io.insert-koin:koin-android:$versions.koin",
+ koinViewModel : "io.insert-koin:koin-androidx-viewmodel:$versions.koin",
+ koinTest : "io.insert-koiio.insert-koin:koin-androidx-navigationn:koin-test:$versions.koin",
+ koinNavigation : "io.insert-koin:koin-androidx-navigation:$versions.koin",
+
+ retrofit : "com.squareup.retrofit2:retrofit:$versions.retrofit",
+ retrofitConverterGson : "com.squareup.retrofit2:converter-gson:$versions.retrofit",
+ okhttp : "com.squareup.okhttp3:okhttp:$versions.okhttp",
+ okhttpLoggingInterceptor: "com.squareup.okhttp3:logging-interceptor:$versions.okhttp",
+
+ gson : "com.google.code.gson:gson:$versions.gson",
+
+ crashlytics : "com.crashlytics.sdk.android:crashlytics:$versions.crashlytics",
+
+ junit : "junit:junit:$versions.junit",
+ junitExtention : "androidx.test.ext:junit:$versions.junitExtention",
+ runner : "androidx.test:runner:$versions.runner",
+ espresso : "androidx.test.espresso:espresso-core:$versions.espresso",
+
+ swipeLayout : "com.daimajia.swipelayout:library:$versions.swipeLayout",
+
+ socketIO : "io.socket:socket.io-client:$versions.socketIO",
+
+ chromeTabs : "saschpe.android:customtabs:$versions.chromeTabs",
+ browser : "androidx.browser:browser:$versions.browser",
+
+ firebaseAnalytics : "com.google.firebase:firebase-analytics:$versions.firebaseAnalytics",
+ firebaseCloudMessaging : "com.google.firebase:firebase-messaging:$versions.firebaseCloudMessaging",
+ firebaseCrashlytics : "com.google.firebase:firebase-crashlytics:$versions.firebaseCrashlytics",
+
+ stetho : "com.facebook.stetho:stetho-okhttp3:$versions.stetho",
+
+ leakcanary : "com.squareup.leakcanary:leakcanary-android:$versions.leakcanary",
+
+ room : "androidx.room:room-runtime:$versions.room",
+ annotationProcessorRoom : "androidx.room:room-compiler:$versions.room",
+ roomKtx : "androidx.room:room-ktx:$versions.room",
+ roomTesting : "androidx.room:room-testing:$versions.room",
+
+ navigation : "androidx.navigation:navigation-fragment-ktx:$versions.navigation",
+ navigationKtx : "androidx.navigation:navigation-ui-ktx:$versions.navigation",
+
+ imageCrop : "com.theartofdev.edmodo:android-image-cropper:$versions.imageCrop",
+ paging : "androidx.paging:paging-runtime:$versions.paging",
+
+ jsoup : "org.jsoup:jsoup:$versions.jsoup",
+
+ apolloRuntime : "com.apollographql.apollo:apollo-runtime:$versions.apollo",
+ apolloSupport : "com.apollographql.apollo:apollo-android-support:$versions.apollo",
+ apolloCoroutine : "com.apollographql.apollo:apollo-coroutines-support:$versions.apollo",
+
+ lottie : "com.airbnb.android:lottie:$versions.lottie",
+
+ worldCountryData : "com.github.blongho:worldCountryData:$versions.worldCountryData",
+ mockotlin : "com.nhaarman.mockitokotlin2:mockito-kotlin:$versions.mockotlin",
+]
\ No newline at end of file
diff --git a/gradle.properties b/gradle.properties
new file mode 100644
index 00000000..cd0519bb
--- /dev/null
+++ b/gradle.properties
@@ -0,0 +1,23 @@
+# 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
+# Kotlin code style for this project: "official" or "obsolete":
+kotlin.code.style=official
+# Enables namespacing of each library's R class so that its R class includes only the
+# resources declared in the library itself and none from the library's dependencies,
+# thereby reducing the size of the R class for that library
+android.nonTransitiveRClass=true
\ 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 00000000..e708b1c0
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 00000000..6974408a
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Fri Oct 14 22:01:09 MSK 2022
+distributionBase=GRADLE_USER_HOME
+distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip
+distributionPath=wrapper/dists
+zipStorePath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
diff --git a/gradlew b/gradlew
new file mode 100755
index 00000000..4f906e0c
--- /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 00000000..107acd32
--- /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 00000000..37f18458
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1,16 @@
+pluginManagement {
+ repositories {
+ gradlePluginPortal()
+ google()
+ mavenCentral()
+ }
+}
+dependencyResolutionManagement {
+ repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
+ repositories {
+ google()
+ mavenCentral()
+ }
+}
+rootProject.name = "MovieApp"
+include ':app'