diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..00ae5f1
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,31 @@
+# https://editorconfig.org
+root = true
+
+[*]
+indent_style = space
+indent_size = 2
+max_line_length = 120
+end_of_line = lf
+charset = utf-8
+trim_trailing_whitespace = true
+insert_final_newline = true
+
+[*.{kt,kts}]
+indent_size = 4
+ij_kotlin_code_style_defaults = KOTLIN_OFFICIAL
+ij_kotlin_continuation_indent_in_if_conditions = false
+ktlint_function_naming_ignore_when_annotated_with = Composable
+
+# Don't allow any wildcard imports
+ij_kotlin_packages_to_use_import_on_demand = unset
+
+# Prevent wildcard imports
+ij_kotlin_name_count_to_use_star_import = 99
+ij_kotlin_name_count_to_use_star_import_for_members = 99
+
+[*.md]
+trim_trailing_whitespace = false
+max_line_length = unset
+
+[*.yml]
+ij_yaml_spaces_within_brackets = false
\ No newline at end of file
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index df8309c..ad19dfd 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -25,7 +25,7 @@ android {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
- "proguard-rules.pro"
+ "proguard-rules.pro",
)
}
}
@@ -42,18 +42,17 @@ android {
}
dependencies {
- implementation(libs.androidx.core.ktx)
- implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.activity.compose)
implementation(platform(libs.androidx.compose.bom))
- implementation(libs.androidx.ui)
- implementation(libs.androidx.ui.graphics)
- implementation(libs.androidx.ui.tooling.preview)
implementation(libs.androidx.material3)
implementation(libs.bundles.layer.presentation)
- ksp(libs.com.google.dagger.hilt.android.compiler)
+ ksp(libs.com.google.dagger.hilt.compiler)
+ implementation(project(":core:api"))
+ implementation(project(":core:database"))
+ implementation(project(":core:presentation"))
+ implementation(project(":data"))
+ implementation(project(":domain"))
implementation(project(":presentation:feature"))
- implementation(project(":core:ui"))
-}
\ No newline at end of file
+}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 2544bc7..8047c24 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -1,28 +1,28 @@
+ xmlns:tools="http://schemas.android.com/tools">
-
-
-
-
+
+
+
+
-
-
-
-
+
+
+
+
-
\ No newline at end of file
+
diff --git a/app/src/main/java/com/example/baseproject/application/BaseProjectApplication.kt b/app/src/main/java/com/example/baseproject/application/BaseProjectApplication.kt
index 0d15009..c09d425 100644
--- a/app/src/main/java/com/example/baseproject/application/BaseProjectApplication.kt
+++ b/app/src/main/java/com/example/baseproject/application/BaseProjectApplication.kt
@@ -4,4 +4,4 @@ import android.app.Application
import dagger.hilt.android.HiltAndroidApp
@HiltAndroidApp
-class BaseProjectApplication : Application()
\ No newline at end of file
+class BaseProjectApplication : Application()
diff --git a/app/src/main/java/com/example/baseproject/navigation/BaseProjectApp.kt b/app/src/main/java/com/example/baseproject/navigation/BaseProjectApplication.kt
similarity index 90%
rename from app/src/main/java/com/example/baseproject/navigation/BaseProjectApp.kt
rename to app/src/main/java/com/example/baseproject/navigation/BaseProjectApplication.kt
index dc6efe6..fae1690 100644
--- a/app/src/main/java/com/example/baseproject/navigation/BaseProjectApp.kt
+++ b/app/src/main/java/com/example/baseproject/navigation/BaseProjectApplication.kt
@@ -4,7 +4,7 @@ import androidx.compose.runtime.Composable
import androidx.navigation.compose.rememberNavController
@Composable
-fun BaseProjectApp() {
+fun BaseProjectApplication() {
val navController = rememberNavController()
BaseProjectApplicationNavHost(
diff --git a/app/src/main/java/com/example/baseproject/navigation/BaseProjectApplicationNavHost.kt b/app/src/main/java/com/example/baseproject/navigation/BaseProjectApplicationNavHost.kt
index 14f7789..b544cd1 100644
--- a/app/src/main/java/com/example/baseproject/navigation/BaseProjectApplicationNavHost.kt
+++ b/app/src/main/java/com/example/baseproject/navigation/BaseProjectApplicationNavHost.kt
@@ -1,30 +1,33 @@
package com.example.baseproject.navigation
-import android.annotation.SuppressLint
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
+import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
-import androidx.navigation.compose.composable
import androidx.navigation.compose.navigation
-import com.example.feature.screen.FeatureScreen
+import com.example.baseproject.navigation.viewmodel.NavigationViewModel
+import com.example.core.presentation.navigation.BaseProjectNavRoutes
-@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
@Composable
fun BaseProjectApplicationNavHost(
modifier: Modifier = Modifier,
navController: NavHostController,
+ viewModel: NavigationViewModel = hiltViewModel(),
) {
NavHost(
modifier = modifier,
navController = navController,
- startDestination = NavigationRoutes.MainGraph,
+ startDestination = BaseProjectNavRoutes.MainGraph,
) {
- navigation(
- startDestination = NavigationRoutes.Feature,
+ navigation(
+ startDestination = BaseProjectNavRoutes.Feature,
) {
- composable {
- FeatureScreen()
+ viewModel.subNavigation.forEach { subNavigation ->
+ subNavigation.registerNavGraph(
+ navGraphBuilder = this,
+ navController = navController,
+ )
}
}
}
diff --git a/app/src/main/java/com/example/baseproject/navigation/NavigationRoutes.kt b/app/src/main/java/com/example/baseproject/navigation/NavigationRoutes.kt
deleted file mode 100644
index b26532b..0000000
--- a/app/src/main/java/com/example/baseproject/navigation/NavigationRoutes.kt
+++ /dev/null
@@ -1,11 +0,0 @@
-package com.example.baseproject.navigation
-
-import kotlinx.serialization.Serializable
-
-sealed class NavigationRoutes {
- @Serializable
- data object MainGraph : NavigationRoutes()
-
- @Serializable
- data object Feature : NavigationRoutes()
-}
diff --git a/app/src/main/java/com/example/baseproject/navigation/di/NavigationModule.kt b/app/src/main/java/com/example/baseproject/navigation/di/NavigationModule.kt
new file mode 100644
index 0000000..7f23dba
--- /dev/null
+++ b/app/src/main/java/com/example/baseproject/navigation/di/NavigationModule.kt
@@ -0,0 +1,17 @@
+package com.example.baseproject.navigation.di
+
+import com.example.core.presentation.navigation.FeatureNavigation
+import com.example.feature.navigation.FeatureNavigationImpl
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+import dagger.multibindings.IntoSet
+
+@Module
+@InstallIn(SingletonComponent::class)
+object NavigationModule {
+ @Provides
+ @IntoSet
+ fun provideFeatureNavigation(): FeatureNavigation = FeatureNavigationImpl()
+}
diff --git a/app/src/main/java/com/example/baseproject/navigation/viewmodel/NavigationViewModel.kt b/app/src/main/java/com/example/baseproject/navigation/viewmodel/NavigationViewModel.kt
new file mode 100644
index 0000000..8e3d49b
--- /dev/null
+++ b/app/src/main/java/com/example/baseproject/navigation/viewmodel/NavigationViewModel.kt
@@ -0,0 +1,16 @@
+package com.example.baseproject.navigation.viewmodel
+
+import androidx.lifecycle.ViewModel
+import com.example.core.presentation.navigation.FeatureNavigation
+import dagger.hilt.android.lifecycle.HiltViewModel
+import javax.inject.Inject
+
+@HiltViewModel
+class NavigationViewModel
+ @Inject
+ constructor(
+ private val featureNavigation: Set<@JvmSuppressWildcards FeatureNavigation>,
+ ) : ViewModel() {
+ val subNavigation: Set
+ get() = featureNavigation
+ }
diff --git a/app/src/main/java/com/example/baseproject/application/MainActivity.kt b/app/src/main/java/com/example/baseproject/view/MainActivity.kt
similarity index 69%
rename from app/src/main/java/com/example/baseproject/application/MainActivity.kt
rename to app/src/main/java/com/example/baseproject/view/MainActivity.kt
index 57933fb..48a5c21 100644
--- a/app/src/main/java/com/example/baseproject/application/MainActivity.kt
+++ b/app/src/main/java/com/example/baseproject/view/MainActivity.kt
@@ -1,11 +1,11 @@
-package com.example.baseproject.application
+package com.example.baseproject.view
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
-import com.example.baseproject.navigation.BaseProjectApp
-import com.example.core.ui.theme.BaseProjectTheme
+import com.example.baseproject.navigation.BaseProjectApplication
+import com.example.core.presentation.ui.theme.BaseProjectTheme
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
@@ -15,7 +15,7 @@ class MainActivity : ComponentActivity() {
enableEdgeToEdge()
setContent {
BaseProjectTheme {
- BaseProjectApp()
+ BaseProjectApplication()
}
}
}
diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts
index 9666e7e..1c7c11b 100644
--- a/buildSrc/build.gradle.kts
+++ b/buildSrc/build.gradle.kts
@@ -11,4 +11,4 @@ repositories {
}
}
mavenCentral()
-}
\ No newline at end of file
+}
diff --git a/core/api/build.gradle.kts b/core/api/build.gradle.kts
index 36ce87e..358ed31 100644
--- a/core/api/build.gradle.kts
+++ b/core/api/build.gradle.kts
@@ -1,6 +1,8 @@
plugins {
alias(libs.plugins.android.library)
alias(libs.plugins.kotlin.android)
+ alias(libs.plugins.com.google.devtools.ksp)
+ alias(libs.plugins.com.google.dagger.hilt.android)
}
android {
@@ -17,7 +19,7 @@ android {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
- "proguard-rules.pro"
+ "proguard-rules.pro",
)
}
}
@@ -31,10 +33,13 @@ android {
}
dependencies {
- implementation(libs.androidx.core.ktx)
- implementation(libs.androidx.appcompat)
- implementation(libs.material)
implementation(libs.bundles.layer.data)
+ implementation(libs.arrow.core.retrofit)
+ implementation(libs.retrofit)
+ implementation(libs.retrofit.gson)
+ implementation(libs.okhttp)
+
+ ksp(libs.com.google.dagger.hilt.compiler)
testImplementation(libs.bundles.test.unit)
-}
\ No newline at end of file
+}
diff --git a/core/api/src/main/kotlin/com/example/core/api/di/ApiModule.kt b/core/api/src/main/kotlin/com/example/core/api/di/ApiModule.kt
new file mode 100644
index 0000000..1da210f
--- /dev/null
+++ b/core/api/src/main/kotlin/com/example/core/api/di/ApiModule.kt
@@ -0,0 +1,29 @@
+package com.example.core.api.di
+
+import arrow.retrofit.adapter.either.EitherCallAdapterFactory
+import com.example.core.api.service.MovieApi
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+import retrofit2.Retrofit
+import retrofit2.converter.gson.GsonConverterFactory
+import javax.inject.Singleton
+
+@Module
+@InstallIn(SingletonComponent::class)
+object ApiModule {
+ @Provides
+ @Singleton
+ fun provideRetrofit(): Retrofit =
+ Retrofit
+ .Builder()
+ .baseUrl(MovieApi.BASE_URL)
+ .addConverterFactory(GsonConverterFactory.create())
+ .addCallAdapterFactory(EitherCallAdapterFactory.create())
+ .build()
+
+ @Provides
+ @Singleton
+ fun provideMovieApi(retrofit: Retrofit): MovieApi = retrofit.create(MovieApi::class.java)
+}
diff --git a/core/api/src/main/kotlin/com/example/core/api/model/MovieDto.kt b/core/api/src/main/kotlin/com/example/core/api/model/MovieDto.kt
new file mode 100644
index 0000000..f03626a
--- /dev/null
+++ b/core/api/src/main/kotlin/com/example/core/api/model/MovieDto.kt
@@ -0,0 +1,11 @@
+package com.example.core.api.model
+
+import com.google.gson.annotations.SerializedName
+
+data class MovieDto(
+ @SerializedName("Title") val title: String,
+ @SerializedName("Year") val year: String,
+ @SerializedName("imdbID") val imdbId: String,
+ @SerializedName("Type") val type: String,
+ @SerializedName("Poster") val poster: String,
+)
diff --git a/core/api/src/main/kotlin/com/example/core/api/model/SearchResponseDto.kt b/core/api/src/main/kotlin/com/example/core/api/model/SearchResponseDto.kt
new file mode 100644
index 0000000..f31bee1
--- /dev/null
+++ b/core/api/src/main/kotlin/com/example/core/api/model/SearchResponseDto.kt
@@ -0,0 +1,9 @@
+package com.example.core.api.model
+
+import com.google.gson.annotations.SerializedName
+
+data class SearchResponseDto(
+ @SerializedName("Search") val results: List,
+ @SerializedName("totalResults") val totalResults: String,
+ @SerializedName("Response") val response: String,
+)
diff --git a/core/api/src/main/kotlin/com/example/core/api/service/MovieApi.kt b/core/api/src/main/kotlin/com/example/core/api/service/MovieApi.kt
new file mode 100644
index 0000000..5d9bd60
--- /dev/null
+++ b/core/api/src/main/kotlin/com/example/core/api/service/MovieApi.kt
@@ -0,0 +1,23 @@
+package com.example.core.api.service
+
+import arrow.core.Either
+import arrow.retrofit.adapter.either.networkhandling.CallError
+import com.example.core.api.model.SearchResponseDto
+import retrofit2.http.GET
+import retrofit2.http.Query
+
+interface MovieApi {
+ @GET
+ fun searchMovies(
+ @Query("s") query: String,
+ @Query("page") page: Int,
+ @Query("pageSize") pageSize: Int = PAGE_SIZE,
+ @Query("apikey") apikey: String = API_KEY,
+ ): Either
+
+ companion object {
+ const val PAGE_SIZE = 10
+ const val BASE_URL = "https://www.omdbapi.com/"
+ const val API_KEY = "1a64ba7b"
+ }
+}
diff --git a/core/database/build.gradle.kts b/core/database/build.gradle.kts
index fac0b40..05a3fff 100644
--- a/core/database/build.gradle.kts
+++ b/core/database/build.gradle.kts
@@ -2,6 +2,7 @@ plugins {
alias(libs.plugins.android.library)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.com.google.devtools.ksp)
+ alias(libs.plugins.com.google.dagger.hilt.android)
}
android {
@@ -18,7 +19,7 @@ android {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
- "proguard-rules.pro"
+ "proguard-rules.pro",
)
}
}
@@ -32,12 +33,11 @@ android {
}
dependencies {
- implementation(libs.androidx.core.ktx)
- implementation(libs.androidx.appcompat)
- implementation(libs.material)
implementation(libs.bundles.layer.data)
+ implementation(libs.room.ktx)
+ ksp(libs.com.google.dagger.hilt.compiler)
ksp(libs.room.compiler)
testImplementation(libs.bundles.test.unit)
-}
\ No newline at end of file
+}
diff --git a/core/database/src/main/kotlin/com/example/core/database/AppDatabase.kt b/core/database/src/main/kotlin/com/example/core/database/AppDatabase.kt
new file mode 100644
index 0000000..7075e52
--- /dev/null
+++ b/core/database/src/main/kotlin/com/example/core/database/AppDatabase.kt
@@ -0,0 +1,20 @@
+package com.example.core.database
+
+import androidx.room.Database
+import androidx.room.RoomDatabase
+import com.example.core.database.dao.MovieDao
+import com.example.core.database.model.DeletedMovieEntity
+
+@Database(
+ entities = [DeletedMovieEntity::class],
+ version = AppDatabase.Companion.DB_VERSION,
+ exportSchema = false,
+)
+abstract class AppDatabase : RoomDatabase() {
+ abstract fun movieDao(): MovieDao
+
+ companion object {
+ const val DB_NAME = "base_project_database"
+ const val DB_VERSION = 1
+ }
+}
diff --git a/core/database/src/main/kotlin/com/example/core/database/dao/MovieDao.kt b/core/database/src/main/kotlin/com/example/core/database/dao/MovieDao.kt
new file mode 100644
index 0000000..4123cac
--- /dev/null
+++ b/core/database/src/main/kotlin/com/example/core/database/dao/MovieDao.kt
@@ -0,0 +1,16 @@
+package com.example.core.database.dao
+
+import androidx.room.Dao
+import androidx.room.Insert
+import androidx.room.OnConflictStrategy
+import androidx.room.Query
+import com.example.core.database.model.DeletedMovieEntity
+
+@Dao
+interface MovieDao {
+ @Insert(onConflict = OnConflictStrategy.IGNORE)
+ suspend fun insertDeletedMovie(movie: DeletedMovieEntity)
+
+ @Query("SELECT * FROM deletedmovieentity")
+ suspend fun getAllDeletedMovies(): List
+}
diff --git a/core/database/src/main/kotlin/com/example/core/database/di/DatabaseModule.kt b/core/database/src/main/kotlin/com/example/core/database/di/DatabaseModule.kt
new file mode 100644
index 0000000..7679ec6
--- /dev/null
+++ b/core/database/src/main/kotlin/com/example/core/database/di/DatabaseModule.kt
@@ -0,0 +1,32 @@
+package com.example.core.database.di
+
+import android.content.Context
+import androidx.room.Room
+import com.example.core.database.AppDatabase
+import com.example.core.database.AppDatabase.Companion.DB_NAME
+import com.example.core.database.dao.MovieDao
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.android.qualifiers.ApplicationContext
+import dagger.hilt.components.SingletonComponent
+import javax.inject.Singleton
+
+@Module
+@InstallIn(SingletonComponent::class)
+object DatabaseModule {
+ @Provides
+ @Singleton
+ fun provideDatabase(
+ @ApplicationContext appContext: Context,
+ ) = Room
+ .databaseBuilder(
+ appContext,
+ AppDatabase::class.java,
+ DB_NAME,
+ ).build()
+
+ @Provides
+ @Singleton
+ fun provideMovieDao(db: AppDatabase): MovieDao = db.movieDao()
+}
diff --git a/core/database/src/main/kotlin/com/example/core/database/model/DeletedMovieEntity.kt b/core/database/src/main/kotlin/com/example/core/database/model/DeletedMovieEntity.kt
new file mode 100644
index 0000000..7d07d17
--- /dev/null
+++ b/core/database/src/main/kotlin/com/example/core/database/model/DeletedMovieEntity.kt
@@ -0,0 +1,10 @@
+package com.example.core.database.model
+
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+
+@Entity
+data class DeletedMovieEntity(
+ @PrimaryKey
+ var id: Int = 0,
+)
diff --git a/core/ui/.gitignore b/core/presentation/.gitignore
similarity index 100%
rename from core/ui/.gitignore
rename to core/presentation/.gitignore
diff --git a/core/ui/build.gradle.kts b/core/presentation/build.gradle.kts
similarity index 82%
rename from core/ui/build.gradle.kts
rename to core/presentation/build.gradle.kts
index 4bc7fec..4bf6c1c 100644
--- a/core/ui/build.gradle.kts
+++ b/core/presentation/build.gradle.kts
@@ -2,6 +2,7 @@ plugins {
alias(libs.plugins.android.library)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose)
+ alias(libs.plugins.kotlin.serialization)
alias(libs.plugins.com.google.devtools.ksp)
alias(libs.plugins.com.google.dagger.hilt.android)
}
@@ -37,18 +38,14 @@ android {
}
dependencies {
- implementation(libs.androidx.core.ktx)
implementation(libs.androidx.appcompat)
implementation(libs.androidx.material3)
implementation(libs.bundles.layer.presentation)
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.activity.compose)
- ksp(libs.com.google.dagger.hilt.android.compiler)
+ ksp(libs.com.google.dagger.hilt.compiler)
testImplementation(libs.bundles.test.unit)
testImplementation(libs.bundles.test.compose)
- androidTestImplementation(libs.bundles.test.android)
- androidTestImplementation(platform(libs.androidx.compose.bom))
- androidTestImplementation(libs.bundles.test.android)
}
diff --git a/core/ui/proguard-rules.pro b/core/presentation/proguard-rules.pro
similarity index 100%
rename from core/ui/proguard-rules.pro
rename to core/presentation/proguard-rules.pro
diff --git a/core/ui/src/androidTest/java/com/example/feature/ExampleInstrumentedTest.kt b/core/presentation/src/androidTest/java/com/example/feature/ExampleInstrumentedTest.kt
similarity index 100%
rename from core/ui/src/androidTest/java/com/example/feature/ExampleInstrumentedTest.kt
rename to core/presentation/src/androidTest/java/com/example/feature/ExampleInstrumentedTest.kt
diff --git a/core/presentation/src/main/java/com/example/core/presentation/navigation/BaseProjectNavRoutes.kt b/core/presentation/src/main/java/com/example/core/presentation/navigation/BaseProjectNavRoutes.kt
new file mode 100644
index 0000000..8f64290
--- /dev/null
+++ b/core/presentation/src/main/java/com/example/core/presentation/navigation/BaseProjectNavRoutes.kt
@@ -0,0 +1,11 @@
+package com.example.core.presentation.navigation
+
+import kotlinx.serialization.Serializable
+
+sealed class BaseProjectNavRoutes {
+ @Serializable
+ data object MainGraph : BaseProjectNavRoutes()
+
+ @Serializable
+ data object Feature : BaseProjectNavRoutes()
+}
diff --git a/core/presentation/src/main/java/com/example/core/presentation/navigation/FeatureNavigation.kt b/core/presentation/src/main/java/com/example/core/presentation/navigation/FeatureNavigation.kt
new file mode 100644
index 0000000..a3716e9
--- /dev/null
+++ b/core/presentation/src/main/java/com/example/core/presentation/navigation/FeatureNavigation.kt
@@ -0,0 +1,11 @@
+package com.example.core.presentation.navigation
+
+import androidx.navigation.NavGraphBuilder
+import androidx.navigation.NavHostController
+
+interface FeatureNavigation {
+ fun registerNavGraph(
+ navGraphBuilder: NavGraphBuilder,
+ navController: NavHostController,
+ )
+}
diff --git a/core/presentation/src/main/java/com/example/core/presentation/ui/theme/Color.kt b/core/presentation/src/main/java/com/example/core/presentation/ui/theme/Color.kt
new file mode 100644
index 0000000..593708e
--- /dev/null
+++ b/core/presentation/src/main/java/com/example/core/presentation/ui/theme/Color.kt
@@ -0,0 +1,33 @@
+package com.example.core.presentation.ui.theme
+
+import androidx.compose.ui.graphics.Color
+
+val PrimaryBlue = Color(0xFF1A4B8C)
+val PrimaryBlueLight = Color(0xFF466EAB)
+val PrimaryBlueDark = Color(0xFF0D3060)
+
+val SecondaryOrange = Color(0xFFFF7733)
+val SecondaryOrangeLight = Color(0xFFFF986B)
+val SecondaryOrangeDark = Color(0xFFD65D22)
+
+val AccentGreen = Color(0xFF1D9957)
+val AccentGreenLight = Color(0xFF4DBB7B)
+val AccentGreenDark = Color(0xFF0E7D3F)
+
+val Grey10 = Color(0xFFF5F7F9)
+val Grey20 = Color(0xFFE5E9F0)
+val Grey30 = Color(0xFFCDD3DE)
+val Grey40 = Color(0xFFADB6C3)
+val Grey50 = Color(0xFF8E99A8)
+val Grey60 = Color(0xFF707D8D)
+val Grey70 = Color(0xFF515F72)
+val Grey80 = Color(0xFF3B4857)
+val Grey90 = Color(0xFF242E3C)
+val Grey100 = Color(0xFF121A26)
+
+val SuccessColor = Color(0xFF28A745)
+val WarningColor = Color(0xFFFFC107)
+val ErrorColor = Color(0xFFDC3545)
+val ErrorDark = Color(0xFF93000A)
+val ErrorLight = Color(0xFFFFDAD6)
+val InfoColor = Color(0xFF17A2B8)
diff --git a/core/presentation/src/main/java/com/example/core/presentation/ui/theme/Theme.kt b/core/presentation/src/main/java/com/example/core/presentation/ui/theme/Theme.kt
new file mode 100644
index 0000000..7a2a278
--- /dev/null
+++ b/core/presentation/src/main/java/com/example/core/presentation/ui/theme/Theme.kt
@@ -0,0 +1,88 @@
+package com.example.core.presentation.ui.theme
+
+import android.os.Build
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.darkColorScheme
+import androidx.compose.material3.dynamicDarkColorScheme
+import androidx.compose.material3.dynamicLightColorScheme
+import androidx.compose.material3.lightColorScheme
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalContext
+
+private val DarkColorScheme =
+ darkColorScheme(
+ primary = SecondaryOrange,
+ onPrimary = Color.White,
+ primaryContainer = SecondaryOrangeDark,
+ onPrimaryContainer = Color.White,
+ secondary = PrimaryBlue,
+ onSecondary = Color.White,
+ secondaryContainer = PrimaryBlueDark,
+ onSecondaryContainer = Color.White,
+ tertiary = AccentGreen,
+ onTertiary = Color.White,
+ tertiaryContainer = AccentGreenDark,
+ onTertiaryContainer = Color.White,
+ background = Grey100,
+ onBackground = Grey20,
+ surface = Grey90,
+ onSurface = Grey20,
+ surfaceVariant = Grey80,
+ onSurfaceVariant = Grey30,
+ error = ErrorColor,
+ onError = Color.White,
+ errorContainer = ErrorDark,
+ onErrorContainer = ErrorLight,
+ )
+
+private val LightColorScheme =
+ lightColorScheme(
+ primary = PrimaryBlue,
+ onPrimary = Color.White,
+ primaryContainer = PrimaryBlueLight,
+ onPrimaryContainer = Grey100,
+ secondary = SecondaryOrange,
+ onSecondary = Color.White,
+ secondaryContainer = SecondaryOrangeLight,
+ onSecondaryContainer = Grey100,
+ tertiary = AccentGreen,
+ onTertiary = Color.White,
+ tertiaryContainer = AccentGreenLight,
+ onTertiaryContainer = Grey100,
+ background = Grey10,
+ onBackground = Grey100,
+ surface = Color.White,
+ onSurface = Grey100,
+ surfaceVariant = Grey20,
+ onSurfaceVariant = Grey90,
+ error = ErrorColor,
+ onError = Color.White,
+ errorContainer = ErrorLight,
+ onErrorContainer = ErrorDark,
+ )
+
+@Composable
+fun BaseProjectTheme(
+ darkTheme: Boolean = isSystemInDarkTheme(),
+ dynamicColor: Boolean = true,
+ content: @Composable () -> Unit,
+) {
+ val colorScheme =
+ when {
+ dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
+ val context = LocalContext.current
+ if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
+ }
+
+ darkTheme -> DarkColorScheme
+ else -> LightColorScheme
+ }
+
+ MaterialTheme(
+ colorScheme = colorScheme,
+ typography = Typography,
+ content = content,
+ )
+}
diff --git a/core/presentation/src/main/java/com/example/core/presentation/ui/theme/Type.kt b/core/presentation/src/main/java/com/example/core/presentation/ui/theme/Type.kt
new file mode 100644
index 0000000..ef4635f
--- /dev/null
+++ b/core/presentation/src/main/java/com/example/core/presentation/ui/theme/Type.kt
@@ -0,0 +1,36 @@
+package com.example.core.presentation.ui.theme
+
+import androidx.compose.material3.Typography
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.sp
+
+// Set of Material typography styles to start with
+val Typography =
+ Typography(
+ bodyLarge =
+ TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Normal,
+ fontSize = 16.sp,
+ lineHeight = 24.sp,
+ letterSpacing = 0.5.sp,
+ ),
+ /* Other default text styles to override
+ titleLarge = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Normal,
+ fontSize = 22.sp,
+ lineHeight = 28.sp,
+ letterSpacing = 0.sp
+ ),
+ labelSmall = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Medium,
+ fontSize = 11.sp,
+ lineHeight = 16.sp,
+ letterSpacing = 0.5.sp
+ )
+ */
+ )
diff --git a/core/ui/src/main/res/values/strings.xml b/core/presentation/src/main/res/values/strings.xml
similarity index 100%
rename from core/ui/src/main/res/values/strings.xml
rename to core/presentation/src/main/res/values/strings.xml
diff --git a/core/presentation/src/main/res/values/themes.xml b/core/presentation/src/main/res/values/themes.xml
new file mode 100644
index 0000000..c770e66
--- /dev/null
+++ b/core/presentation/src/main/res/values/themes.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/core/ui/src/test/java/com/example/feature/ExampleUnitTest.kt b/core/presentation/src/test/java/com/example/feature/ExampleUnitTest.kt
similarity index 100%
rename from core/ui/src/test/java/com/example/feature/ExampleUnitTest.kt
rename to core/presentation/src/test/java/com/example/feature/ExampleUnitTest.kt
diff --git a/core/ui/src/main/java/com/example/core.ui/theme/Color.kt b/core/ui/src/main/java/com/example/core.ui/theme/Color.kt
deleted file mode 100644
index 019381c..0000000
--- a/core/ui/src/main/java/com/example/core.ui/theme/Color.kt
+++ /dev/null
@@ -1,11 +0,0 @@
-package com.example.core.ui.theme
-
-import androidx.compose.ui.graphics.Color
-
-val Purple80 = Color(0xFFD0BCFF)
-val PurpleGrey80 = Color(0xFFCCC2DC)
-val Pink80 = Color(0xFFEFB8C8)
-
-val Purple40 = Color(0xFF6650a4)
-val PurpleGrey40 = Color(0xFF625b71)
-val Pink40 = Color(0xFF7D5260)
\ No newline at end of file
diff --git a/core/ui/src/main/java/com/example/core.ui/theme/Theme.kt b/core/ui/src/main/java/com/example/core.ui/theme/Theme.kt
deleted file mode 100644
index f3526d6..0000000
--- a/core/ui/src/main/java/com/example/core.ui/theme/Theme.kt
+++ /dev/null
@@ -1,59 +0,0 @@
-package com.example.core.ui.theme
-
-import android.os.Build
-import androidx.compose.foundation.isSystemInDarkTheme
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.darkColorScheme
-import androidx.compose.material3.dynamicDarkColorScheme
-import androidx.compose.material3.dynamicLightColorScheme
-import androidx.compose.material3.lightColorScheme
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.platform.LocalContext
-
-private val DarkColorScheme =
- darkColorScheme(
- primary = Purple80,
- secondary = PurpleGrey80,
- tertiary = Pink80,
- )
-
-private val LightColorScheme =
- lightColorScheme(
- primary = Purple40,
- secondary = PurpleGrey40,
- tertiary = Pink40,
- /* Other default colors to override
- background = Color(0xFFFFFBFE),
- surface = Color(0xFFFFFBFE),
- onPrimary = Color.White,
- onSecondary = Color.White,
- onTertiary = Color.White,
- onBackground = Color(0xFF1C1B1F),
- onSurface = Color(0xFF1C1B1F),
- */
- )
-
-@Composable
-fun BaseProjectTheme(
- darkTheme: Boolean = isSystemInDarkTheme(),
- // Dynamic color is available on Android 12+
- dynamicColor: Boolean = true,
- content: @Composable () -> Unit,
-) {
- val colorScheme =
- when {
- dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
- val context = LocalContext.current
- if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
- }
-
- darkTheme -> DarkColorScheme
- else -> LightColorScheme
- }
-
- MaterialTheme(
- colorScheme = colorScheme,
- typography = Typography,
- content = content,
- )
-}
diff --git a/core/ui/src/main/java/com/example/core.ui/theme/Type.kt b/core/ui/src/main/java/com/example/core.ui/theme/Type.kt
deleted file mode 100644
index 44694cb..0000000
--- a/core/ui/src/main/java/com/example/core.ui/theme/Type.kt
+++ /dev/null
@@ -1,34 +0,0 @@
-package com.example.core.ui.theme
-
-import androidx.compose.material3.Typography
-import androidx.compose.ui.text.TextStyle
-import androidx.compose.ui.text.font.FontFamily
-import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.unit.sp
-
-// Set of Material typography styles to start with
-val Typography = Typography(
- bodyLarge = TextStyle(
- fontFamily = FontFamily.Default,
- fontWeight = FontWeight.Normal,
- fontSize = 16.sp,
- lineHeight = 24.sp,
- letterSpacing = 0.5.sp
- )
- /* Other default text styles to override
- titleLarge = TextStyle(
- fontFamily = FontFamily.Default,
- fontWeight = FontWeight.Normal,
- fontSize = 22.sp,
- lineHeight = 28.sp,
- letterSpacing = 0.sp
- ),
- labelSmall = TextStyle(
- fontFamily = FontFamily.Default,
- fontWeight = FontWeight.Medium,
- fontSize = 11.sp,
- lineHeight = 16.sp,
- letterSpacing = 0.5.sp
- )
- */
-)
\ No newline at end of file
diff --git a/data/build.gradle.kts b/data/build.gradle.kts
index c099075..54daa0f 100644
--- a/data/build.gradle.kts
+++ b/data/build.gradle.kts
@@ -1,6 +1,8 @@
plugins {
alias(libs.plugins.android.library)
alias(libs.plugins.kotlin.android)
+ alias(libs.plugins.com.google.devtools.ksp)
+ alias(libs.plugins.com.google.dagger.hilt.android)
}
android {
@@ -17,7 +19,7 @@ android {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
- "proguard-rules.pro"
+ "proguard-rules.pro",
)
}
}
@@ -31,10 +33,14 @@ android {
}
dependencies {
- implementation(libs.androidx.core.ktx)
- implementation(libs.androidx.appcompat)
- implementation(libs.material)
- implementation(libs.bundles.layer.domain)
+ implementation(libs.bundles.layer.data)
+ implementation(libs.arrow.core.retrofit)
+
+ ksp(libs.com.google.dagger.hilt.compiler)
+
+ implementation(project(":domain"))
+ implementation(project(":core:api"))
+ implementation(project(":core:database"))
testImplementation(libs.bundles.test.unit)
-}
\ No newline at end of file
+}
diff --git a/data/src/main/kotlin/com/example/data/datasource/MoviesApiDataSource.kt b/data/src/main/kotlin/com/example/data/datasource/MoviesApiDataSource.kt
new file mode 100644
index 0000000..0863e25
--- /dev/null
+++ b/data/src/main/kotlin/com/example/data/datasource/MoviesApiDataSource.kt
@@ -0,0 +1,19 @@
+package com.example.data.datasource
+
+import com.example.core.api.service.MovieApi
+import com.example.data.errors.RemoteErrors
+import javax.inject.Inject
+
+class MoviesApiDataSource
+ @Inject
+ constructor(
+ private val moviesApi: MovieApi,
+ ) : MoviesRemoteDataSource {
+ override suspend fun searchMovies(
+ query: String,
+ page: Int,
+ ) = moviesApi
+ .searchMovies(query = query, page = page)
+ .map { it.results }
+ .mapLeft { RemoteErrors.NetworkException }
+ }
diff --git a/data/src/main/kotlin/com/example/data/datasource/MoviesDatabaseDataSource.kt b/data/src/main/kotlin/com/example/data/datasource/MoviesDatabaseDataSource.kt
new file mode 100644
index 0000000..273251f
--- /dev/null
+++ b/data/src/main/kotlin/com/example/data/datasource/MoviesDatabaseDataSource.kt
@@ -0,0 +1,25 @@
+package com.example.data.datasource
+
+import arrow.core.Either
+import com.example.core.database.dao.MovieDao
+import com.example.core.database.model.DeletedMovieEntity
+import com.example.data.errors.LocalErrors
+import javax.inject.Inject
+
+class MoviesDatabaseDataSource
+ @Inject
+ constructor(
+ private val movieDao: MovieDao,
+ ) : MoviesLocalDataSource {
+ override suspend fun getDeletedMovies(): Either> =
+ Either
+ .catch {
+ movieDao.getAllDeletedMovies()
+ }.mapLeft { LocalErrors.DatabaseException }
+
+ override suspend fun deleteMovie(id: DeletedMovieEntity) =
+ Either
+ .catch {
+ movieDao.insertDeletedMovie(id)
+ }.mapLeft { LocalErrors.DatabaseException }
+ }
diff --git a/data/src/main/kotlin/com/example/data/datasource/MoviesLocalDataSource.kt b/data/src/main/kotlin/com/example/data/datasource/MoviesLocalDataSource.kt
new file mode 100644
index 0000000..fc31f89
--- /dev/null
+++ b/data/src/main/kotlin/com/example/data/datasource/MoviesLocalDataSource.kt
@@ -0,0 +1,11 @@
+package com.example.data.datasource
+
+import arrow.core.Either
+import com.example.core.database.model.DeletedMovieEntity
+import com.example.data.errors.LocalErrors
+
+interface MoviesLocalDataSource {
+ suspend fun getDeletedMovies(): Either>
+
+ suspend fun deleteMovie(id: DeletedMovieEntity): Either
+}
diff --git a/data/src/main/kotlin/com/example/data/datasource/MoviesRemoteDataSource.kt b/data/src/main/kotlin/com/example/data/datasource/MoviesRemoteDataSource.kt
new file mode 100644
index 0000000..5033b34
--- /dev/null
+++ b/data/src/main/kotlin/com/example/data/datasource/MoviesRemoteDataSource.kt
@@ -0,0 +1,12 @@
+package com.example.data.datasource
+
+import arrow.core.Either
+import com.example.core.api.model.MovieDto
+import com.example.data.errors.RemoteErrors
+
+interface MoviesRemoteDataSource {
+ suspend fun searchMovies(
+ query: String,
+ page: Int,
+ ): Either>
+}
diff --git a/data/src/main/kotlin/com/example/data/di/DataModule.kt b/data/src/main/kotlin/com/example/data/di/DataModule.kt
new file mode 100644
index 0000000..af60ae7
--- /dev/null
+++ b/data/src/main/kotlin/com/example/data/di/DataModule.kt
@@ -0,0 +1,37 @@
+package com.example.data.di
+
+import com.example.core.api.service.MovieApi
+import com.example.core.database.dao.MovieDao
+import com.example.data.datasource.MoviesApiDataSource
+import com.example.data.datasource.MoviesDatabaseDataSource
+import com.example.data.datasource.MoviesLocalDataSource
+import com.example.data.datasource.MoviesRemoteDataSource
+import com.example.data.repository.MoviesRepositoryImpl
+import com.example.domain.repository.MoviesRepository
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+import javax.inject.Singleton
+
+@Module
+@InstallIn(SingletonComponent::class)
+object DataModule {
+ @Provides
+ @Singleton
+ fun provideMoviesLocalDataSource(movieDao: MovieDao): MoviesLocalDataSource = MoviesDatabaseDataSource(movieDao)
+
+ @Provides
+ @Singleton
+ fun provideMoviesRemoteDataSource(movieApi: MovieApi): MoviesRemoteDataSource = MoviesApiDataSource(movieApi)
+
+ @Provides
+ fun provideMoviesRepository(
+ moviesRemoteDataSource: MoviesRemoteDataSource,
+ moviesLocalDataSource: MoviesLocalDataSource,
+ ): MoviesRepository =
+ MoviesRepositoryImpl(
+ moviesRemoteDataSource = moviesRemoteDataSource,
+ moviesLocalDataSource = moviesLocalDataSource,
+ )
+}
diff --git a/data/src/main/kotlin/com/example/data/errors/LocalErrors.kt b/data/src/main/kotlin/com/example/data/errors/LocalErrors.kt
new file mode 100644
index 0000000..504ca51
--- /dev/null
+++ b/data/src/main/kotlin/com/example/data/errors/LocalErrors.kt
@@ -0,0 +1,5 @@
+package com.example.data.errors
+
+sealed class LocalErrors : Throwable() {
+ data object DatabaseException : LocalErrors()
+}
diff --git a/data/src/main/kotlin/com/example/data/errors/RemoteErrors.kt b/data/src/main/kotlin/com/example/data/errors/RemoteErrors.kt
new file mode 100644
index 0000000..3ded98c
--- /dev/null
+++ b/data/src/main/kotlin/com/example/data/errors/RemoteErrors.kt
@@ -0,0 +1,5 @@
+package com.example.data.errors
+
+sealed class RemoteErrors : Throwable() {
+ data object NetworkException : RemoteErrors()
+}
diff --git a/data/src/main/kotlin/com/example/data/mapper/MovieMapper.kt b/data/src/main/kotlin/com/example/data/mapper/MovieMapper.kt
new file mode 100644
index 0000000..bbee52f
--- /dev/null
+++ b/data/src/main/kotlin/com/example/data/mapper/MovieMapper.kt
@@ -0,0 +1,24 @@
+package com.example.data.mapper
+
+import com.example.core.api.model.MovieDto
+import com.example.core.database.model.DeletedMovieEntity
+import com.example.domain.model.Movie
+
+object MovieDtoMapper {
+ fun List.toDomain() = map { it.toDomain() }
+
+ fun MovieDto.toDomain() =
+ Movie(
+ id = imdbId.toInt(),
+ title = title,
+ year = year,
+ type = type,
+ poster = poster,
+ )
+}
+
+object DeletedMovieMapper {
+ fun List.toDomain() = map { it.toDomain() }
+
+ fun DeletedMovieEntity.toDomain() = id
+}
diff --git a/data/src/main/kotlin/com/example/data/repository/MoviesRepositoryImpl.kt b/data/src/main/kotlin/com/example/data/repository/MoviesRepositoryImpl.kt
new file mode 100644
index 0000000..a4ca995
--- /dev/null
+++ b/data/src/main/kotlin/com/example/data/repository/MoviesRepositoryImpl.kt
@@ -0,0 +1,27 @@
+package com.example.data.repository
+
+import com.example.core.database.model.DeletedMovieEntity
+import com.example.data.datasource.MoviesLocalDataSource
+import com.example.data.datasource.MoviesRemoteDataSource
+import com.example.data.mapper.DeletedMovieMapper.toDomain
+import com.example.data.mapper.MovieDtoMapper.toDomain
+import com.example.domain.repository.MoviesRepository
+import javax.inject.Inject
+
+internal class MoviesRepositoryImpl
+ @Inject
+ constructor(
+ private val moviesRemoteDataSource: MoviesRemoteDataSource,
+ private val moviesLocalDataSource: MoviesLocalDataSource,
+ ) : MoviesRepository {
+ override suspend fun searchMovies(
+ query: String,
+ page: Int,
+ ) = moviesRemoteDataSource.searchMovies(query = query, page = page).map { list -> list.toDomain() }
+
+ override suspend fun getDeletedMovies() =
+ moviesLocalDataSource.getDeletedMovies().map { list -> list.toDomain() }
+
+ override suspend fun deleteMovie(movieId: Int) =
+ moviesLocalDataSource.deleteMovie(DeletedMovieEntity(id = movieId))
+ }
diff --git a/domain/build.gradle.kts b/domain/build.gradle.kts
index 8fb099c..6de8108 100644
--- a/domain/build.gradle.kts
+++ b/domain/build.gradle.kts
@@ -1,40 +1,17 @@
plugins {
- alias(libs.plugins.android.library)
- alias(libs.plugins.kotlin.android)
+ kotlin("jvm")
}
-android {
- namespace = "${AppVersions.APPLICATION_ID}.domain"
- compileSdk = AppVersions.COMPILE_SDK
-
- defaultConfig {
- minSdk = AppVersions.MIN_SDK
- testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
- }
-
- buildTypes {
- release {
- isMinifyEnabled = false
- proguardFiles(
- getDefaultProguardFile("proguard-android-optimize.txt"),
- "proguard-rules.pro"
- )
- }
- }
- compileOptions {
- sourceCompatibility = AppVersions.javaVersion
- targetCompatibility = AppVersions.javaVersion
- }
- kotlinOptions {
- jvmTarget = AppVersions.JVM_TARGET
- }
+java {
+ sourceCompatibility = AppVersions.javaVersion
+ targetCompatibility = AppVersions.javaVersion
+}
+kotlin {
+ jvmToolchain(AppVersions.JVM_TARGET.toInt())
}
dependencies {
- implementation(libs.androidx.core.ktx)
- implementation(libs.androidx.appcompat)
- implementation(libs.material)
- implementation(libs.bundles.layer.data)
+ implementation(libs.bundles.layer.domain)
testImplementation(libs.bundles.test.unit)
-}
\ No newline at end of file
+}
diff --git a/domain/src/androidTest/java/com/example/domain/ExampleInstrumentedTest.kt b/domain/src/androidTest/java/com/example/domain/ExampleInstrumentedTest.kt
deleted file mode 100644
index 4d43b5b..0000000
--- a/domain/src/androidTest/java/com/example/domain/ExampleInstrumentedTest.kt
+++ /dev/null
@@ -1,24 +0,0 @@
-package com.example.domain
-
-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.example.domain", appContext.packageName)
- }
-}
\ No newline at end of file
diff --git a/domain/src/main/kotlin/com/example/domain/di/DomainModule.kt b/domain/src/main/kotlin/com/example/domain/di/DomainModule.kt
new file mode 100644
index 0000000..0a610c3
--- /dev/null
+++ b/domain/src/main/kotlin/com/example/domain/di/DomainModule.kt
@@ -0,0 +1,21 @@
+package com.example.domain.di
+
+import com.example.domain.repository.MoviesRepository
+import com.example.domain.usecase.DeleteMovieUseCase
+import com.example.domain.usecase.SearchMoviesUseCase
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+
+@Module
+@InstallIn(SingletonComponent::class)
+object DomainModule {
+ @Provides
+ fun provideSearchMoviesRoverUseCase(moviesRepository: MoviesRepository): SearchMoviesUseCase =
+ SearchMoviesUseCase(moviesRepository)
+
+ @Provides
+ fun provideDeleteMovieUseCase(moviesRepository: MoviesRepository): DeleteMovieUseCase =
+ DeleteMovieUseCase(moviesRepository)
+}
diff --git a/domain/src/main/kotlin/com/example/domain/model/Movie.kt b/domain/src/main/kotlin/com/example/domain/model/Movie.kt
new file mode 100644
index 0000000..c5c6495
--- /dev/null
+++ b/domain/src/main/kotlin/com/example/domain/model/Movie.kt
@@ -0,0 +1,9 @@
+package com.example.domain.model
+
+data class Movie(
+ val id: Int,
+ val title: String,
+ val year: String,
+ val type: String,
+ val poster: String,
+)
diff --git a/domain/src/main/kotlin/com/example/domain/repository/MoviesRepository.kt b/domain/src/main/kotlin/com/example/domain/repository/MoviesRepository.kt
new file mode 100644
index 0000000..1129395
--- /dev/null
+++ b/domain/src/main/kotlin/com/example/domain/repository/MoviesRepository.kt
@@ -0,0 +1,15 @@
+package com.example.domain.repository
+
+import arrow.core.Either
+import com.example.domain.model.Movie
+
+interface MoviesRepository {
+ suspend fun searchMovies(
+ query: String,
+ page: Int,
+ ): Either>
+
+ suspend fun getDeletedMovies(): Either>
+
+ suspend fun deleteMovie(movieId: Int): Either
+}
diff --git a/domain/src/main/kotlin/com/example/domain/usecase/DeleteMovieUseCase.kt b/domain/src/main/kotlin/com/example/domain/usecase/DeleteMovieUseCase.kt
new file mode 100644
index 0000000..0e95bb5
--- /dev/null
+++ b/domain/src/main/kotlin/com/example/domain/usecase/DeleteMovieUseCase.kt
@@ -0,0 +1,14 @@
+package com.example.domain.usecase
+
+import arrow.core.Either
+import com.example.domain.repository.MoviesRepository
+import javax.inject.Inject
+
+class DeleteMovieUseCase
+ @Inject
+ constructor(
+ private val moviesRepository: MoviesRepository,
+ ) {
+ suspend operator fun invoke(movieId: Int): Either =
+ moviesRepository.deleteMovie(movieId = movieId)
+ }
diff --git a/domain/src/main/kotlin/com/example/domain/usecase/SearchMoviesUseCase.kt b/domain/src/main/kotlin/com/example/domain/usecase/SearchMoviesUseCase.kt
new file mode 100644
index 0000000..7f99a15
--- /dev/null
+++ b/domain/src/main/kotlin/com/example/domain/usecase/SearchMoviesUseCase.kt
@@ -0,0 +1,28 @@
+package com.example.domain.usecase
+
+import arrow.core.Either
+import arrow.core.flatMap
+import com.example.domain.model.Movie
+import com.example.domain.repository.MoviesRepository
+import javax.inject.Inject
+
+class SearchMoviesUseCase
+ @Inject
+ constructor(
+ private val moviesRepository: MoviesRepository,
+ ) {
+ suspend operator fun invoke(
+ query: String,
+ page: Int,
+ ): Either> =
+ moviesRepository.searchMovies(query, page).flatMap { movies ->
+ moviesRepository.getDeletedMovies().fold(
+ ifLeft = {
+ Either.Right(movies)
+ },
+ ifRight = { deletedIds ->
+ Either.Right(movies.filterNot { it.id.toInt() in deletedIds.toSet() })
+ },
+ )
+ }
+ }
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index aaa8cda..a71b1a1 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,16 +1,11 @@
[versions]
agp = "8.9.1"
kotlin = "2.0.21"
-coreKtx = "1.15.0"
junit = "4.13.2"
-junitVersion = "1.2.1"
-espressoCore = "3.6.1"
-lifecycleRuntimeKtx = "2.8.7"
activityCompose = "1.10.1"
-composeBom = "2025.03.01"
+composeBom = "2025.04.00"
androidxNavigationCompose = "2.8.9"
appcompat = "1.7.0"
-material = "1.12.0"
hilt = "2.55"
hiltCompose = "1.2.0"
ksp = "2.0.21-1.0.27"
@@ -22,29 +17,24 @@ okhttp = "4.12.0"
mockitoKotlin = "5.3.1"
ioMockk = "1.13.17"
turbine = "1.1.0"
-room = "2.6.1"
+room = "2.7.0"
+coil = "3.1.0"
[libraries]
-androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
-androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
-androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
-androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
-androidx-ui = { group = "androidx.compose.ui", name = "ui" }
-androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
-androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
-material = { group = "com.google.android.material", name = "material", version.ref = "material" }
+com-google-dagger-hilt-core = { group = "com.google.dagger", name = "hilt-core", version.ref = "hilt" }
com-google-dagger-hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hilt" }
-com-google-dagger-hilt-android-compiler = { module = "com.google.dagger:hilt-android-compiler", version.ref = "hilt" }
+com-google-dagger-hilt-compiler = { module = "com.google.dagger:hilt-compiler", version.ref = "hilt" }
com-google-dagger-hilt-compose = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "hiltCompose" }
androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "androidxNavigationCompose" }
kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinxSerialization" }
arrow-core = { module = "io.arrow-kt:arrow-core", version.ref = "arrow" }
+arrow-core-retrofit = { module = "io.arrow-kt:arrow-core-retrofit", version.ref = "arrow" }
arrow-fx-coroutines = { module = "io.arrow-kt:arrow-fx-coroutines", version.ref = "arrow" }
roborazzi = { group = "io.github.takahirom.roborazzi", name = "roborazzi", version.ref = "roborazzi" }
roborazzi-compose = { group = "io.github.takahirom.roborazzi", name = "roborazzi-compose", version.ref = "roborazzi" }
@@ -58,6 +48,7 @@ kotlin-mockito-kotlin = { module = "org.mockito.kotlin:mockito-kotlin", version.
app-cash-turbine = { module = "app.cash.turbine:turbine", version.ref = "turbine" }
room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" }
room-ktx = { module = "androidx.room:room-ktx", version.ref = "room" }
+coil-compose = { group = "io.coil-kt.coil3", name = "coil-compose", version.ref = "coil" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
@@ -70,42 +61,34 @@ com-google-devtools-ksp = { id = "com.google.devtools.ksp", version.ref = "ksp"
[bundles]
layer-presentation = [
- "com-google-dagger-hilt-android",
- "com-google-dagger-hilt-compose",
- "androidx-navigation-compose",
- "kotlinx-serialization-json",
- "arrow-core",
- "arrow-fx-coroutines",
+ "com-google-dagger-hilt-android",
+ "com-google-dagger-hilt-compose",
+ "androidx-navigation-compose",
+ "kotlinx-serialization-json",
+ "arrow-core",
+ "arrow-fx-coroutines",
+ "androidx-ui-tooling-preview",
]
layer-domain = [
- "com-google-dagger-hilt-android",
- "arrow-core",
- "arrow-fx-coroutines",
+ "com-google-dagger-hilt-core",
+ "arrow-core",
+ "arrow-fx-coroutines",
]
layer-data = [
- "com-google-dagger-hilt-android",
- "arrow-core",
- "arrow-fx-coroutines",
- "retrofit",
- "retrofit-gson",
- "okhttp",
- "room-ktx",
+ "com-google-dagger-hilt-android",
+ "arrow-core",
+ "arrow-fx-coroutines",
]
test-unit = [
- "junit",
- "mockwebserver",
- "io-mockk",
- "kotlin-mockito-kotlin",
- "app-cash-turbine",
-]
-test-android = [
- "androidx-junit" ,
- "androidx-espresso-core",
- "androidx-ui-test-junit4",
+ "junit",
+ "mockwebserver",
+ "io-mockk",
+ "kotlin-mockito-kotlin",
+ "app-cash-turbine",
]
test-compose = [
- "roborazzi",
- "roborazzi-compose",
- "roborazzi-rule",
-]
\ No newline at end of file
+ "roborazzi",
+ "roborazzi-compose",
+ "roborazzi-rule",
+]
diff --git a/presentation/feature/build.gradle.kts b/presentation/feature/build.gradle.kts
index b0c552a..cf1a29c 100644
--- a/presentation/feature/build.gradle.kts
+++ b/presentation/feature/build.gradle.kts
@@ -2,6 +2,7 @@ plugins {
alias(libs.plugins.android.library)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose)
+ alias(libs.plugins.kotlin.serialization)
alias(libs.plugins.com.google.devtools.ksp)
alias(libs.plugins.com.google.dagger.hilt.android)
}
@@ -37,18 +38,17 @@ android {
}
dependencies {
- implementation(libs.androidx.core.ktx)
implementation(libs.androidx.appcompat)
implementation(libs.androidx.material3)
- implementation(libs.bundles.layer.presentation)
implementation(platform(libs.androidx.compose.bom))
- implementation(libs.androidx.activity.compose)
+ implementation(libs.bundles.layer.presentation)
+ implementation(libs.coil.compose)
+
+ ksp(libs.com.google.dagger.hilt.compiler)
- ksp(libs.com.google.dagger.hilt.android.compiler)
+ implementation(project(":core:presentation"))
+ implementation(project(":domain"))
testImplementation(libs.bundles.test.unit)
testImplementation(libs.bundles.test.compose)
- androidTestImplementation(libs.bundles.test.android)
- androidTestImplementation(platform(libs.androidx.compose.bom))
- androidTestImplementation(libs.bundles.test.android)
}
diff --git a/presentation/feature/src/main/java/com/example/feature/components/Greeting.kt b/presentation/feature/src/main/java/com/example/feature/components/Greeting.kt
deleted file mode 100644
index 99af2ea..0000000
--- a/presentation/feature/src/main/java/com/example/feature/components/Greeting.kt
+++ /dev/null
@@ -1,13 +0,0 @@
-package com.example.feature.components
-
-import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-
-@Composable
-fun Greeting(name: String, modifier: Modifier = Modifier) {
- Text(
- text = "Hello $name!",
- modifier = modifier
- )
-}
\ No newline at end of file
diff --git a/presentation/feature/src/main/java/com/example/feature/components/MovieCard.kt b/presentation/feature/src/main/java/com/example/feature/components/MovieCard.kt
new file mode 100644
index 0000000..61d276c
--- /dev/null
+++ b/presentation/feature/src/main/java/com/example/feature/components/MovieCard.kt
@@ -0,0 +1,143 @@
+package com.example.feature.components
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.IntrinsicSize
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Delete
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.tooling.preview.PreviewLightDark
+import androidx.compose.ui.unit.dp
+import coil3.compose.AsyncImage
+import com.example.baseproject.feature.R
+import com.example.core.presentation.ui.theme.BaseProjectTheme
+import com.example.feature.contract.state.MovieUiState
+
+@Composable
+internal fun MovieCard(
+ modifier: Modifier = Modifier,
+ movie: MovieUiState,
+ onDeleteMovie: (Int) -> Unit,
+) {
+ Card(
+ modifier =
+ modifier
+ .fillMaxWidth()
+ .padding(8.dp),
+ shape = RoundedCornerShape(8.dp),
+ elevation = CardDefaults.cardElevation(defaultElevation = 4.dp),
+ ) {
+ Row(
+ modifier =
+ Modifier
+ .fillMaxWidth()
+ .height(IntrinsicSize.Min),
+ ) {
+ AsyncImage(
+ model = movie.poster,
+ contentDescription = movie.title,
+ modifier =
+ Modifier
+ .width(100.dp)
+ .height(100.dp),
+ placeholder = painterResource(R.drawable.movie_placeholder),
+ contentScale = ContentScale.Crop,
+ )
+
+ Column(
+ modifier =
+ Modifier
+ .weight(1f)
+ .padding(horizontal = 12.dp, vertical = 8.dp),
+ ) {
+ Text(
+ text = movie.title,
+ style = MaterialTheme.typography.titleMedium,
+ fontWeight = FontWeight.Bold,
+ maxLines = 2,
+ overflow = TextOverflow.Ellipsis,
+ )
+
+ Spacer(modifier = Modifier.height(4.dp))
+
+ Text(
+ text = movie.year,
+ style = MaterialTheme.typography.bodyMedium,
+ )
+
+ Spacer(modifier = Modifier.height(4.dp))
+
+ Text(
+ text = movie.type,
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.secondary,
+ )
+ }
+
+ Box(
+ modifier =
+ Modifier
+ .align(Alignment.CenterVertically)
+ .padding(end = 8.dp),
+ ) {
+ when (movie.movieState) {
+ is MovieUiState.ContentState.Idle -> {
+ IconButton(onClick = { onDeleteMovie(movie.id) }) {
+ Icon(
+ imageVector = Icons.Default.Delete,
+ contentDescription = "Delete movie",
+ tint = MaterialTheme.colorScheme.error,
+ )
+ }
+ }
+
+ is MovieUiState.ContentState.Deleting -> {
+ CircularProgressIndicator(
+ modifier = Modifier.size(24.dp),
+ strokeWidth = 2.dp,
+ )
+ }
+ }
+ }
+ }
+ }
+}
+
+@PreviewLightDark
+@Composable
+private fun MovieCardPreview() {
+ BaseProjectTheme {
+ MovieCard(
+ movie =
+ MovieUiState(
+ id = 1,
+ title = "Inception",
+ year = "2010",
+ type = "Sci-Fi",
+ poster = "",
+ ),
+ onDeleteMovie = {},
+ )
+ }
+}
diff --git a/presentation/feature/src/main/java/com/example/feature/contract/event/FeatureEvent.kt b/presentation/feature/src/main/java/com/example/feature/contract/event/FeatureEvent.kt
new file mode 100644
index 0000000..90d4c88
--- /dev/null
+++ b/presentation/feature/src/main/java/com/example/feature/contract/event/FeatureEvent.kt
@@ -0,0 +1,11 @@
+package com.example.feature.contract.event
+
+sealed interface FeatureEvent {
+ data class OnSearchMovies(
+ val text: String,
+ ) : FeatureEvent
+
+ data class OnDeleteMovie(
+ val movieId: Int,
+ ) : FeatureEvent
+}
diff --git a/presentation/feature/src/main/java/com/example/feature/contract/state/FeatureUiState.kt b/presentation/feature/src/main/java/com/example/feature/contract/state/FeatureUiState.kt
new file mode 100644
index 0000000..124bda1
--- /dev/null
+++ b/presentation/feature/src/main/java/com/example/feature/contract/state/FeatureUiState.kt
@@ -0,0 +1,12 @@
+package com.example.feature.contract.state
+
+data class FeatureUiState(
+ val movies: List = emptyList(),
+ val contentState: ContentState = ContentState.Idle,
+) {
+ sealed interface ContentState {
+ data object Idle : ContentState
+
+ data object Loading : ContentState
+ }
+}
diff --git a/presentation/feature/src/main/java/com/example/feature/contract/state/MovieUiState.kt b/presentation/feature/src/main/java/com/example/feature/contract/state/MovieUiState.kt
new file mode 100644
index 0000000..8e39eff
--- /dev/null
+++ b/presentation/feature/src/main/java/com/example/feature/contract/state/MovieUiState.kt
@@ -0,0 +1,16 @@
+package com.example.feature.contract.state
+
+data class MovieUiState(
+ val id: Int,
+ val title: String,
+ val year: String,
+ val type: String,
+ val poster: String,
+ val movieState: ContentState = ContentState.Idle,
+) {
+ sealed interface ContentState {
+ data object Idle : ContentState
+
+ data object Deleting : ContentState
+ }
+}
diff --git a/presentation/feature/src/main/java/com/example/feature/mapper/MovieMapper.kt b/presentation/feature/src/main/java/com/example/feature/mapper/MovieMapper.kt
new file mode 100644
index 0000000..c08813c
--- /dev/null
+++ b/presentation/feature/src/main/java/com/example/feature/mapper/MovieMapper.kt
@@ -0,0 +1,15 @@
+package com.example.feature.mapper
+
+import com.example.domain.model.Movie
+import com.example.feature.contract.state.MovieUiState
+
+fun List.toUiState() = map { it.toUiState() }
+
+fun Movie.toUiState() =
+ MovieUiState(
+ id = id,
+ title = title,
+ year = year,
+ type = type,
+ poster = poster,
+ )
diff --git a/presentation/feature/src/main/java/com/example/feature/navigation/FeatureNavigationImpl.kt b/presentation/feature/src/main/java/com/example/feature/navigation/FeatureNavigationImpl.kt
new file mode 100644
index 0000000..17fb5d6
--- /dev/null
+++ b/presentation/feature/src/main/java/com/example/feature/navigation/FeatureNavigationImpl.kt
@@ -0,0 +1,27 @@
+package com.example.feature.navigation
+
+import androidx.navigation.NavGraphBuilder
+import androidx.navigation.NavHostController
+import androidx.navigation.compose.composable
+import androidx.navigation.navigation
+import com.example.core.presentation.navigation.BaseProjectNavRoutes
+import com.example.core.presentation.navigation.FeatureNavigation
+import com.example.feature.screen.FeatureScreen
+import javax.inject.Inject
+
+class FeatureNavigationImpl
+ @Inject
+ constructor() : FeatureNavigation {
+ override fun registerNavGraph(
+ navGraphBuilder: NavGraphBuilder,
+ navController: NavHostController,
+ ) {
+ navGraphBuilder.navigation(
+ startDestination = FeatureRoute.Home,
+ ) {
+ composable {
+ FeatureScreen()
+ }
+ }
+ }
+ }
diff --git a/presentation/feature/src/main/java/com/example/feature/navigation/FeatureRoute.kt b/presentation/feature/src/main/java/com/example/feature/navigation/FeatureRoute.kt
new file mode 100644
index 0000000..cefca9e
--- /dev/null
+++ b/presentation/feature/src/main/java/com/example/feature/navigation/FeatureRoute.kt
@@ -0,0 +1,8 @@
+package com.example.feature.navigation
+
+import kotlinx.serialization.Serializable
+
+sealed class FeatureRoute {
+ @Serializable
+ data object Home : FeatureRoute()
+}
diff --git a/presentation/feature/src/main/java/com/example/feature/screen/FeatureContent.kt b/presentation/feature/src/main/java/com/example/feature/screen/FeatureContent.kt
new file mode 100644
index 0000000..a04933f
--- /dev/null
+++ b/presentation/feature/src/main/java/com/example/feature/screen/FeatureContent.kt
@@ -0,0 +1,76 @@
+package com.example.feature.screen
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.Scaffold
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.example.core.presentation.ui.theme.BaseProjectTheme
+import com.example.feature.contract.state.FeatureUiState
+import com.example.feature.contract.state.MovieUiState
+
+@Composable
+internal fun FeatureContent(
+ state: FeatureUiState,
+ onDeleteMovie: (Int) -> Unit,
+ onSearchMovies: (String) -> Unit,
+) {
+ Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
+ when (state.contentState) {
+ is FeatureUiState.ContentState.Idle -> {
+ FeatureIdleContent(
+ modifier =
+ Modifier
+ .fillMaxSize()
+ .padding(innerPadding),
+ movies = state.movies,
+ onDeleteMovie = onDeleteMovie,
+ onSearchMovies = onSearchMovies,
+ )
+ }
+
+ is FeatureUiState.ContentState.Loading -> {
+ Box(
+ modifier =
+ Modifier
+ .fillMaxSize()
+ .padding(innerPadding),
+ contentAlignment = Alignment.Center,
+ ) {
+ CircularProgressIndicator(
+ strokeWidth = 6.dp,
+ )
+ }
+ }
+ }
+ }
+}
+
+@Composable
+private fun FeatureIdleContent(
+ modifier: Modifier,
+ movies: List,
+ onDeleteMovie: (Int) -> Unit,
+ onSearchMovies: (String) -> Unit,
+) {
+}
+
+@Preview
+@Composable
+private fun LoadingStatePreview() {
+ BaseProjectTheme {
+ FeatureContent(
+ state =
+ FeatureUiState(
+ contentState = FeatureUiState.ContentState.Loading,
+ ),
+ onDeleteMovie = {},
+ onSearchMovies = {},
+ )
+ }
+}
diff --git a/presentation/feature/src/main/java/com/example/feature/screen/FeatureScreen.kt b/presentation/feature/src/main/java/com/example/feature/screen/FeatureScreen.kt
index 8c6fa8b..4dea0aa 100644
--- a/presentation/feature/src/main/java/com/example/feature/screen/FeatureScreen.kt
+++ b/presentation/feature/src/main/java/com/example/feature/screen/FeatureScreen.kt
@@ -1,18 +1,19 @@
package com.example.feature.screen
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.padding
-import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import com.example.feature.components.Greeting
+import androidx.compose.runtime.getValue
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.example.feature.contract.event.FeatureEvent
+import com.example.feature.viewmodel.FeatureViewModel
@Composable
-fun FeatureScreen() {
- Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
- Greeting(
- name = "Android",
- modifier = Modifier.padding(innerPadding),
- )
- }
+fun FeatureScreen(viewModel: FeatureViewModel = hiltViewModel()) {
+ val state by viewModel.uiState.collectAsStateWithLifecycle()
+
+ FeatureContent(
+ state = state,
+ onDeleteMovie = { viewModel.handleEvent(FeatureEvent.OnDeleteMovie(movieId = it)) },
+ onSearchMovies = { viewModel.handleEvent(FeatureEvent.OnSearchMovies(text = it)) },
+ )
}
diff --git a/presentation/feature/src/main/java/com/example/feature/viewmodel/FeatureViewModel.kt b/presentation/feature/src/main/java/com/example/feature/viewmodel/FeatureViewModel.kt
index 5f8ff3f..ed8d1ca 100644
--- a/presentation/feature/src/main/java/com/example/feature/viewmodel/FeatureViewModel.kt
+++ b/presentation/feature/src/main/java/com/example/feature/viewmodel/FeatureViewModel.kt
@@ -1,10 +1,104 @@
package com.example.feature.viewmodel
import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.example.domain.usecase.DeleteMovieUseCase
+import com.example.domain.usecase.SearchMoviesUseCase
+import com.example.feature.contract.event.FeatureEvent
+import com.example.feature.contract.state.FeatureUiState
+import com.example.feature.contract.state.MovieUiState
+import com.example.feature.mapper.toUiState
import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class FeatureViewModel
@Inject
- constructor() : ViewModel()
+ constructor(
+ private val deleteMovieUseCase: DeleteMovieUseCase,
+ private val searchMoviesUseCase: SearchMoviesUseCase,
+ ) : ViewModel() {
+ private val _uiState: MutableStateFlow = MutableStateFlow(FeatureUiState())
+ val uiState: StateFlow = _uiState
+
+ fun handleEvent(event: FeatureEvent) {
+ when (event) {
+ is FeatureEvent.OnSearchMovies -> {
+ searchMovies(text = event.text)
+ }
+
+ is FeatureEvent.OnDeleteMovie -> {
+ deleteMovie(movieId = event.movieId)
+ }
+ }
+ }
+
+ private fun searchMovies(text: String) {
+ viewModelScope.launch {
+ searchMoviesUseCase(query = text, page = 0).fold(
+ ifLeft = {
+ _uiState.update { state ->
+ state.copy(
+ movies = emptyList(),
+ )
+ }
+ // TODO send error searching movies
+ },
+ ifRight = { movies ->
+ _uiState.update { state ->
+ state.copy(
+ movies = state.movies + movies.toUiState(),
+ )
+ }
+ },
+ )
+ }
+ }
+
+ private fun deleteMovie(movieId: Int) {
+ _uiState.update { state ->
+ state.copy(
+ movies =
+ state.movies.updateMovie(movieId) { movie ->
+ movie.copy(
+ movieState = MovieUiState.ContentState.Deleting,
+ )
+ },
+ )
+ }
+ viewModelScope.launch {
+ deleteMovieUseCase(movieId = movieId).fold(
+ ifLeft = {
+ _uiState.update { state ->
+ state.copy(
+ movies =
+ state.movies.updateMovie(movieId) { movie ->
+ movie.copy(movieState = MovieUiState.ContentState.Idle)
+ },
+ )
+ }
+ // TODO send error deleting movie
+ },
+ ifRight = { _ ->
+ _uiState.update { state ->
+ state.copy(
+ movies = state.movies.filterNot { it.id == movieId },
+ )
+ }
+ },
+ )
+ }
+ }
+ }
+
+private fun List.updateMovie(
+ id: Int,
+ updateMovie: (MovieUiState) -> MovieUiState,
+): List =
+ map { movie ->
+ if (movie.id == id) updateMovie(movie) else movie
+ }
diff --git a/presentation/feature/src/main/res/drawable/movie_placeholder.xml b/presentation/feature/src/main/res/drawable/movie_placeholder.xml
new file mode 100644
index 0000000..22b0585
--- /dev/null
+++ b/presentation/feature/src/main/res/drawable/movie_placeholder.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 645c45f..bbb7eff 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -21,9 +21,9 @@ dependencyResolutionManagement {
rootProject.name = "BaseProject"
include(":app")
-include(":presentation:feature")
-include(":domain")
-include(":data")
-include(":core:ui")
include(":core:api")
include(":core:database")
+include(":core:presentation")
+include(":data")
+include(":domain")
+include(":presentation:feature")