From 1c26b94889bda25ba17717d72705113d80101612 Mon Sep 17 00:00:00 2001 From: Pell Date: Thu, 10 Apr 2025 09:35:52 +0200 Subject: [PATCH 01/17] Added api calls and retrofit instance --- .../com/example/core/api/di/DataModule.kt | 27 +++++++++++++++++++ .../com/example/core/api/model/MovieEntity.kt | 11 ++++++++ .../core/api/model/SearchResponseEntity.kt | 9 +++++++ .../example/core/api/service/ApiService.kt | 22 +++++++++++++++ 4 files changed, 69 insertions(+) create mode 100644 core/api/src/main/kotlin/com/example/core/api/di/DataModule.kt create mode 100644 core/api/src/main/kotlin/com/example/core/api/model/MovieEntity.kt create mode 100644 core/api/src/main/kotlin/com/example/core/api/model/SearchResponseEntity.kt create mode 100644 core/api/src/main/kotlin/com/example/core/api/service/ApiService.kt diff --git a/core/api/src/main/kotlin/com/example/core/api/di/DataModule.kt b/core/api/src/main/kotlin/com/example/core/api/di/DataModule.kt new file mode 100644 index 0000000..21befa6 --- /dev/null +++ b/core/api/src/main/kotlin/com/example/core/api/di/DataModule.kt @@ -0,0 +1,27 @@ +package com.example.core.api.di + +import com.example.core.api.service.ApiService +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 DataModule { + @Provides + @Singleton + fun provideRetrofit(): Retrofit = + Retrofit + .Builder() + .baseUrl(ApiService.BASE_URL) + .addConverterFactory(GsonConverterFactory.create()) + .build() + + @Provides + @Singleton + fun provideMovieApiService(retrofit: Retrofit): ApiService = retrofit.create(ApiService::class.java) +} diff --git a/core/api/src/main/kotlin/com/example/core/api/model/MovieEntity.kt b/core/api/src/main/kotlin/com/example/core/api/model/MovieEntity.kt new file mode 100644 index 0000000..566a8bb --- /dev/null +++ b/core/api/src/main/kotlin/com/example/core/api/model/MovieEntity.kt @@ -0,0 +1,11 @@ +package com.example.core.api.model + +import com.google.gson.annotations.SerializedName + +data class MovieEntity( + @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 +) \ No newline at end of file diff --git a/core/api/src/main/kotlin/com/example/core/api/model/SearchResponseEntity.kt b/core/api/src/main/kotlin/com/example/core/api/model/SearchResponseEntity.kt new file mode 100644 index 0000000..14e8ddf --- /dev/null +++ b/core/api/src/main/kotlin/com/example/core/api/model/SearchResponseEntity.kt @@ -0,0 +1,9 @@ +package com.example.core.api.model + +import com.google.gson.annotations.SerializedName + +data class SearchResponseEntity( + @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/ApiService.kt b/core/api/src/main/kotlin/com/example/core/api/service/ApiService.kt new file mode 100644 index 0000000..8a54aa0 --- /dev/null +++ b/core/api/src/main/kotlin/com/example/core/api/service/ApiService.kt @@ -0,0 +1,22 @@ +package com.example.core.api.service + +import com.example.core.api.model.SearchResponseEntity +import retrofit2.Response +import retrofit2.http.GET +import retrofit2.http.Query + +interface ApiService { + @GET + fun searchMovies( + @Query("s") query: String, + @Query("page") page: Int, + @Query("pageSize") pageSize: Int = PAGE_SIZE, + @Query("apikey") apikey: String = API_KEY, + ): Response + + companion object { + const val PAGE_SIZE = 10 + const val BASE_URL = "https://www.omdbapi.com/" + const val API_KEY = "1a64ba7b" + } +} From 829ff456a4939cb09144e58b6a143a2108d43d08 Mon Sep 17 00:00:00 2001 From: Pell Date: Thu, 10 Apr 2025 17:27:57 +0200 Subject: [PATCH 02/17] Added room database --- .../api/di/{DataModule.kt => ApiModule.kt} | 8 ++--- .../api/model/{MovieEntity.kt => MovieDto.kt} | 2 +- ...ResponseEntity.kt => SearchResponseDto.kt} | 4 +-- .../service/{ApiService.kt => MovieApi.kt} | 6 ++-- .../com/example/core/database/AppDatabase.kt | 16 ++++++++++ .../com/example/core/database/dao/MovieDao.kt | 16 ++++++++++ .../core/database/di/DatabaseModule.kt | 31 +++++++++++++++++++ .../core/database/model/DeletedMovieEntity.kt | 10 ++++++ 8 files changed, 83 insertions(+), 10 deletions(-) rename core/api/src/main/kotlin/com/example/core/api/di/{DataModule.kt => ApiModule.kt} (71%) rename core/api/src/main/kotlin/com/example/core/api/model/{MovieEntity.kt => MovieDto.kt} (93%) rename core/api/src/main/kotlin/com/example/core/api/model/{SearchResponseEntity.kt => SearchResponseDto.kt} (68%) rename core/api/src/main/kotlin/com/example/core/api/service/{ApiService.kt => MovieApi.kt} (80%) create mode 100644 core/database/src/main/kotlin/com/example/core/database/AppDatabase.kt create mode 100644 core/database/src/main/kotlin/com/example/core/database/dao/MovieDao.kt create mode 100644 core/database/src/main/kotlin/com/example/core/database/di/DatabaseModule.kt create mode 100644 core/database/src/main/kotlin/com/example/core/database/model/DeletedMovieEntity.kt diff --git a/core/api/src/main/kotlin/com/example/core/api/di/DataModule.kt b/core/api/src/main/kotlin/com/example/core/api/di/ApiModule.kt similarity index 71% rename from core/api/src/main/kotlin/com/example/core/api/di/DataModule.kt rename to core/api/src/main/kotlin/com/example/core/api/di/ApiModule.kt index 21befa6..254d207 100644 --- a/core/api/src/main/kotlin/com/example/core/api/di/DataModule.kt +++ b/core/api/src/main/kotlin/com/example/core/api/di/ApiModule.kt @@ -1,6 +1,6 @@ package com.example.core.api.di -import com.example.core.api.service.ApiService +import com.example.core.api.service.MovieApi import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -11,17 +11,17 @@ import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) -object DataModule { +object ApiModule { @Provides @Singleton fun provideRetrofit(): Retrofit = Retrofit .Builder() - .baseUrl(ApiService.BASE_URL) + .baseUrl(MovieApi.BASE_URL) .addConverterFactory(GsonConverterFactory.create()) .build() @Provides @Singleton - fun provideMovieApiService(retrofit: Retrofit): ApiService = retrofit.create(ApiService::class.java) + fun provideMovieApi(retrofit: Retrofit): MovieApi = retrofit.create(MovieApi::class.java) } diff --git a/core/api/src/main/kotlin/com/example/core/api/model/MovieEntity.kt b/core/api/src/main/kotlin/com/example/core/api/model/MovieDto.kt similarity index 93% rename from core/api/src/main/kotlin/com/example/core/api/model/MovieEntity.kt rename to core/api/src/main/kotlin/com/example/core/api/model/MovieDto.kt index 566a8bb..f9fda88 100644 --- a/core/api/src/main/kotlin/com/example/core/api/model/MovieEntity.kt +++ b/core/api/src/main/kotlin/com/example/core/api/model/MovieDto.kt @@ -2,7 +2,7 @@ package com.example.core.api.model import com.google.gson.annotations.SerializedName -data class MovieEntity( +data class MovieDto( @SerializedName("Title") val title: String, @SerializedName("Year") val year: String, @SerializedName("imdbID") val imdbId: String, diff --git a/core/api/src/main/kotlin/com/example/core/api/model/SearchResponseEntity.kt b/core/api/src/main/kotlin/com/example/core/api/model/SearchResponseDto.kt similarity index 68% rename from core/api/src/main/kotlin/com/example/core/api/model/SearchResponseEntity.kt rename to core/api/src/main/kotlin/com/example/core/api/model/SearchResponseDto.kt index 14e8ddf..f31bee1 100644 --- a/core/api/src/main/kotlin/com/example/core/api/model/SearchResponseEntity.kt +++ b/core/api/src/main/kotlin/com/example/core/api/model/SearchResponseDto.kt @@ -2,8 +2,8 @@ package com.example.core.api.model import com.google.gson.annotations.SerializedName -data class SearchResponseEntity( - @SerializedName("Search") val results: List, +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/ApiService.kt b/core/api/src/main/kotlin/com/example/core/api/service/MovieApi.kt similarity index 80% rename from core/api/src/main/kotlin/com/example/core/api/service/ApiService.kt rename to core/api/src/main/kotlin/com/example/core/api/service/MovieApi.kt index 8a54aa0..1048baa 100644 --- a/core/api/src/main/kotlin/com/example/core/api/service/ApiService.kt +++ b/core/api/src/main/kotlin/com/example/core/api/service/MovieApi.kt @@ -1,18 +1,18 @@ package com.example.core.api.service -import com.example.core.api.model.SearchResponseEntity +import com.example.core.api.model.SearchResponseDto import retrofit2.Response import retrofit2.http.GET import retrofit2.http.Query -interface ApiService { +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, - ): Response + ): Response companion object { const val PAGE_SIZE = 10 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..d54960a --- /dev/null +++ b/core/database/src/main/kotlin/com/example/core/database/AppDatabase.kt @@ -0,0 +1,16 @@ +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 + } +} \ No newline at end of file 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..16e197f --- /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) + fun insertDeletedMovie(movie: DeletedMovieEntity) + + @Query("SELECT * FROM deletedmovieentity") + 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..811db4e --- /dev/null +++ b/core/database/src/main/kotlin/com/example/core/database/di/DatabaseModule.kt @@ -0,0 +1,31 @@ +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 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) = 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, +) From 5851b80d7afcb041fd44b708feac0b78fa2442e2 Mon Sep 17 00:00:00 2001 From: Pell Date: Thu, 10 Apr 2025 18:19:43 +0200 Subject: [PATCH 03/17] Added datasources + formatting --- app/build.gradle.kts | 4 ++-- .../application/BaseProjectApplication.kt | 2 +- buildSrc/build.gradle.kts | 2 +- core/api/build.gradle.kts | 4 ++-- .../com/example/core/api/di/ApiModule.kt | 2 ++ .../com/example/core/api/model/MovieDto.kt | 4 ++-- .../com/example/core/api/service/MovieApi.kt | 4 ++-- .../com/example/core/database/AppDatabase.kt | 2 +- data/build.gradle.kts | 9 +++++--- .../data/datasource/MoviesApiDataSource.kt | 16 ++++++++++++++ .../datasource/MoviesDatabaseDataSource.kt | 22 +++++++++++++++++++ .../data/datasource/MoviesLocalDataSource.kt | 11 ++++++++++ .../data/datasource/MoviesRemoteDataSource.kt | 12 ++++++++++ .../com/example/data/errors/LocalErrors.kt | 5 +++++ .../com/example/data/errors/RemoteErrors.kt | 5 +++++ domain/build.gradle.kts | 4 ++-- gradle/libs.versions.toml | 2 ++ .../example/feature/components/Greeting.kt | 9 +++++--- 18 files changed, 100 insertions(+), 19 deletions(-) create mode 100644 data/src/main/kotlin/com/example/data/datasource/MoviesApiDataSource.kt create mode 100644 data/src/main/kotlin/com/example/data/datasource/MoviesDatabaseDataSource.kt create mode 100644 data/src/main/kotlin/com/example/data/datasource/MoviesLocalDataSource.kt create mode 100644 data/src/main/kotlin/com/example/data/datasource/MoviesRemoteDataSource.kt create mode 100644 data/src/main/kotlin/com/example/data/errors/LocalErrors.kt create mode 100644 data/src/main/kotlin/com/example/data/errors/RemoteErrors.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index df8309c..982336b 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", ) } } @@ -56,4 +56,4 @@ dependencies { implementation(project(":presentation:feature")) implementation(project(":core:ui")) -} \ 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/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..251bad4 100644 --- a/core/api/build.gradle.kts +++ b/core/api/build.gradle.kts @@ -17,7 +17,7 @@ android { isMinifyEnabled = false proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), - "proguard-rules.pro" + "proguard-rules.pro", ) } } @@ -37,4 +37,4 @@ dependencies { implementation(libs.bundles.layer.data) 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 index 254d207..1da210f 100644 --- 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 @@ -1,5 +1,6 @@ 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 @@ -19,6 +20,7 @@ object ApiModule { .Builder() .baseUrl(MovieApi.BASE_URL) .addConverterFactory(GsonConverterFactory.create()) + .addCallAdapterFactory(EitherCallAdapterFactory.create()) .build() @Provides 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 index f9fda88..f03626a 100644 --- 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 @@ -7,5 +7,5 @@ data class MovieDto( @SerializedName("Year") val year: String, @SerializedName("imdbID") val imdbId: String, @SerializedName("Type") val type: String, - @SerializedName("Poster") val poster: String -) \ No newline at end of file + @SerializedName("Poster") val poster: 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 index 1048baa..7174c48 100644 --- 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 @@ -1,7 +1,7 @@ package com.example.core.api.service +import arrow.core.Either import com.example.core.api.model.SearchResponseDto -import retrofit2.Response import retrofit2.http.GET import retrofit2.http.Query @@ -12,7 +12,7 @@ interface MovieApi { @Query("page") page: Int, @Query("pageSize") pageSize: Int = PAGE_SIZE, @Query("apikey") apikey: String = API_KEY, - ): Response + ): Either companion object { const val PAGE_SIZE = 10 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 index d54960a..57ad498 100644 --- a/core/database/src/main/kotlin/com/example/core/database/AppDatabase.kt +++ b/core/database/src/main/kotlin/com/example/core/database/AppDatabase.kt @@ -13,4 +13,4 @@ abstract class AppDatabase : RoomDatabase() { const val DB_NAME = "base_project_database" const val DB_VERSION = 1 } -} \ No newline at end of file +} diff --git a/data/build.gradle.kts b/data/build.gradle.kts index c099075..354fd91 100644 --- a/data/build.gradle.kts +++ b/data/build.gradle.kts @@ -17,7 +17,7 @@ android { isMinifyEnabled = false proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), - "proguard-rules.pro" + "proguard-rules.pro", ) } } @@ -34,7 +34,10 @@ dependencies { implementation(libs.androidx.core.ktx) implementation(libs.androidx.appcompat) implementation(libs.material) - implementation(libs.bundles.layer.domain) + implementation(libs.bundles.layer.data) + + 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..4308eee --- /dev/null +++ b/data/src/main/kotlin/com/example/data/datasource/MoviesApiDataSource.kt @@ -0,0 +1,16 @@ +package com.example.data.datasource + +import com.example.core.api.service.MovieApi +import com.example.data.errors.RemoteErrors + +internal class MoviesApiDataSource( + private val moviesApi: MovieApi, +) : MoviesRemoteDataSource { + override suspend fun searchMovies( + query: String, + size: Int, + ) = moviesApi + .searchMovies(query = query, page = size) + .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..c621b03 --- /dev/null +++ b/data/src/main/kotlin/com/example/data/datasource/MoviesDatabaseDataSource.kt @@ -0,0 +1,22 @@ +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 + +internal class MoviesDatabaseDataSource( + private val movieDao: MovieDao, +) : MoviesLocalDataSource { + override fun getDeletedMovies(): Either> = + Either + .catch { + movieDao.getAllDeletedMovies() + }.mapLeft { LocalErrors.DatabaseException } + + override 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..b9eb141 --- /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 + +internal interface MoviesLocalDataSource { + fun getDeletedMovies(): Either> + + 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..b3e5b7b --- /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 + +internal interface MoviesRemoteDataSource { + suspend fun searchMovies( + query: String, + size: Int, + ): Either> +} 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/domain/build.gradle.kts b/domain/build.gradle.kts index 8fb099c..e4b6c31 100644 --- a/domain/build.gradle.kts +++ b/domain/build.gradle.kts @@ -17,7 +17,7 @@ android { isMinifyEnabled = false proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), - "proguard-rules.pro" + "proguard-rules.pro", ) } } @@ -37,4 +37,4 @@ dependencies { implementation(libs.bundles.layer.data) testImplementation(libs.bundles.test.unit) -} \ No newline at end of file +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index aaa8cda..1528a18 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -45,6 +45,7 @@ com-google-dagger-hilt-compose = { module = "androidx.hilt:hilt-navigation-compo 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" } @@ -86,6 +87,7 @@ layer-data = [ "com-google-dagger-hilt-android", "arrow-core", "arrow-fx-coroutines", + "arrow-core-retrofit", "retrofit", "retrofit-gson", "okhttp", 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 index 99af2ea..e55b20b 100644 --- a/presentation/feature/src/main/java/com/example/feature/components/Greeting.kt +++ b/presentation/feature/src/main/java/com/example/feature/components/Greeting.kt @@ -5,9 +5,12 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @Composable -fun Greeting(name: String, modifier: Modifier = Modifier) { +fun Greeting( + name: String, + modifier: Modifier = Modifier, +) { Text( text = "Hello $name!", - modifier = modifier + modifier = modifier, ) -} \ No newline at end of file +} From d21a07fdcfa785495dc6b10ed0bedc03d14ec28a Mon Sep 17 00:00:00 2001 From: Pell Date: Fri, 11 Apr 2025 10:48:09 +0200 Subject: [PATCH 04/17] Added repository, models and mapper --- .editorconfig | 30 +++++++++++++++ .../com/example/core/api/service/MovieApi.kt | 3 +- .../com/example/core/database/AppDatabase.kt | 6 ++- .../core/database/di/DatabaseModule.kt | 3 +- data/build.gradle.kts | 1 + .../data/datasource/MoviesLocalDataSource.kt | 2 +- .../data/datasource/MoviesRemoteDataSource.kt | 2 +- .../kotlin/com/example/data/di/DataModule.kt | 37 +++++++++++++++++++ .../com/example/data/mapper/MoviesMapper.kt | 20 ++++++++++ .../data/repository/MoviesRepositoryImpl.kt | 24 ++++++++++++ .../kotlin/com/example/domain/model/Movie.kt | 9 +++++ .../domain/repository/MoviesRepository.kt | 15 ++++++++ 12 files changed, 147 insertions(+), 5 deletions(-) create mode 100644 .editorconfig create mode 100644 data/src/main/kotlin/com/example/data/di/DataModule.kt create mode 100644 data/src/main/kotlin/com/example/data/mapper/MoviesMapper.kt create mode 100644 data/src/main/kotlin/com/example/data/repository/MoviesRepositoryImpl.kt create mode 100644 domain/src/main/kotlin/com/example/domain/model/Movie.kt create mode 100644 domain/src/main/kotlin/com/example/domain/repository/MoviesRepository.kt diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..8d0265e --- /dev/null +++ b/.editorconfig @@ -0,0 +1,30 @@ +# 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 + +# 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/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 index 7174c48..5d9bd60 100644 --- 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 @@ -1,6 +1,7 @@ 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 @@ -12,7 +13,7 @@ interface MovieApi { @Query("page") page: Int, @Query("pageSize") pageSize: Int = PAGE_SIZE, @Query("apikey") apikey: String = API_KEY, - ): Either + ): Either companion object { const val PAGE_SIZE = 10 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 index 57ad498..7075e52 100644 --- a/core/database/src/main/kotlin/com/example/core/database/AppDatabase.kt +++ b/core/database/src/main/kotlin/com/example/core/database/AppDatabase.kt @@ -5,7 +5,11 @@ 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) +@Database( + entities = [DeletedMovieEntity::class], + version = AppDatabase.Companion.DB_VERSION, + exportSchema = false, +) abstract class AppDatabase : RoomDatabase() { abstract fun movieDao(): MovieDao 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 index 811db4e..7679ec6 100644 --- 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 @@ -4,6 +4,7 @@ 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 @@ -27,5 +28,5 @@ object DatabaseModule { @Provides @Singleton - fun provideMovieDao(db: AppDatabase) = db.movieDao() + fun provideMovieDao(db: AppDatabase): MovieDao = db.movieDao() } diff --git a/data/build.gradle.kts b/data/build.gradle.kts index 354fd91..ed1654d 100644 --- a/data/build.gradle.kts +++ b/data/build.gradle.kts @@ -36,6 +36,7 @@ dependencies { implementation(libs.material) implementation(libs.bundles.layer.data) + implementation(project(":domain")) implementation(project(":core:api")) implementation(project(":core:database")) diff --git a/data/src/main/kotlin/com/example/data/datasource/MoviesLocalDataSource.kt b/data/src/main/kotlin/com/example/data/datasource/MoviesLocalDataSource.kt index b9eb141..c380abc 100644 --- a/data/src/main/kotlin/com/example/data/datasource/MoviesLocalDataSource.kt +++ b/data/src/main/kotlin/com/example/data/datasource/MoviesLocalDataSource.kt @@ -4,7 +4,7 @@ import arrow.core.Either import com.example.core.database.model.DeletedMovieEntity import com.example.data.errors.LocalErrors -internal interface MoviesLocalDataSource { +interface MoviesLocalDataSource { fun getDeletedMovies(): Either> 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 index b3e5b7b..7b97f28 100644 --- a/data/src/main/kotlin/com/example/data/datasource/MoviesRemoteDataSource.kt +++ b/data/src/main/kotlin/com/example/data/datasource/MoviesRemoteDataSource.kt @@ -4,7 +4,7 @@ import arrow.core.Either import com.example.core.api.model.MovieDto import com.example.data.errors.RemoteErrors -internal interface MoviesRemoteDataSource { +interface MoviesRemoteDataSource { suspend fun searchMovies( query: String, size: Int, 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..c10ddd9 --- /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.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object DataModule { + @Binds + @Singleton + fun provideMoviesLocalDataSource(movieDao: MovieDao): MoviesLocalDataSource = MoviesDatabaseDataSource(movieDao) + + @Binds + @Singleton + fun provideMoviesRemoteDataSource(movieApi: MovieApi): MoviesRemoteDataSource = MoviesApiDataSource(movieApi) + + @Binds + fun provideMoviesRepository( + moviesRemoteDataSource: MoviesRemoteDataSource, + moviesLocalDataSource: MoviesLocalDataSource, + ): MoviesRepository = + MoviesRepositoryImpl( + moviesRemoteDataSource = moviesRemoteDataSource, + moviesLocalDataSource = moviesLocalDataSource, + ) +} diff --git a/data/src/main/kotlin/com/example/data/mapper/MoviesMapper.kt b/data/src/main/kotlin/com/example/data/mapper/MoviesMapper.kt new file mode 100644 index 0000000..b361591 --- /dev/null +++ b/data/src/main/kotlin/com/example/data/mapper/MoviesMapper.kt @@ -0,0 +1,20 @@ +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 + +fun List.toDomain() = map { it.toDomain() } + +fun MovieDto.toDomain() = + Movie( + id = imdbId, + title = title, + year = year, + type = type, + poster = poster, + ) + +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..8f39ead --- /dev/null +++ b/data/src/main/kotlin/com/example/data/repository/MoviesRepositoryImpl.kt @@ -0,0 +1,24 @@ +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.toDomain +import com.example.domain.repository.MoviesRepository +import javax.inject.Inject + +class MoviesRepositoryImpl + @Inject + constructor( + private val moviesRemoteDataSource: MoviesRemoteDataSource, + private val moviesLocalDataSource: MoviesLocalDataSource, + ) : MoviesRepository { + override suspend fun searchMovies( + query: String, + size: Int, + ) = moviesRemoteDataSource.searchMovies(query = query, size = size).map { list -> list.toDomain() } + + override fun getDeletedMovies() = moviesLocalDataSource.getDeletedMovies().map { list -> list.toDomain() } + + override fun deleteMovie(movieId: Int) = moviesLocalDataSource.deleteMovie(DeletedMovieEntity(id = movieId)) + } 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..1e29f57 --- /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: String, + 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..645e120 --- /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, + size: Int, + ): Either> + + fun getDeletedMovies(): Either> + + fun deleteMovie(movieId: Int): Either +} From 7ce528dda362f3984b67ba26c6755186f06607c8 Mon Sep 17 00:00:00 2001 From: Pell Date: Fri, 11 Apr 2025 14:43:00 +0200 Subject: [PATCH 05/17] Added usecases --- .../com/example/core/database/dao/MovieDao.kt | 4 +-- .../data/datasource/MoviesApiDataSource.kt | 4 +-- .../datasource/MoviesDatabaseDataSource.kt | 4 +-- .../data/datasource/MoviesLocalDataSource.kt | 4 +-- .../data/datasource/MoviesRemoteDataSource.kt | 2 +- .../data/repository/MoviesRepositoryImpl.kt | 12 ++++---- .../domain/repository/MoviesRepository.kt | 6 ++-- .../domain/usecase/DeleteMovieUseCase.kt | 14 ++++++++++ .../domain/usecase/SearchMoviesUseCase.kt | 28 +++++++++++++++++++ 9 files changed, 61 insertions(+), 17 deletions(-) create mode 100644 domain/src/main/kotlin/com/example/domain/usecase/DeleteMovieUseCase.kt create mode 100644 domain/src/main/kotlin/com/example/domain/usecase/SearchMoviesUseCase.kt 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 index 16e197f..4123cac 100644 --- 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 @@ -9,8 +9,8 @@ import com.example.core.database.model.DeletedMovieEntity @Dao interface MovieDao { @Insert(onConflict = OnConflictStrategy.IGNORE) - fun insertDeletedMovie(movie: DeletedMovieEntity) + suspend fun insertDeletedMovie(movie: DeletedMovieEntity) @Query("SELECT * FROM deletedmovieentity") - fun getAllDeletedMovies(): List + suspend fun getAllDeletedMovies(): List } diff --git a/data/src/main/kotlin/com/example/data/datasource/MoviesApiDataSource.kt b/data/src/main/kotlin/com/example/data/datasource/MoviesApiDataSource.kt index 4308eee..577bba0 100644 --- a/data/src/main/kotlin/com/example/data/datasource/MoviesApiDataSource.kt +++ b/data/src/main/kotlin/com/example/data/datasource/MoviesApiDataSource.kt @@ -8,9 +8,9 @@ internal class MoviesApiDataSource( ) : MoviesRemoteDataSource { override suspend fun searchMovies( query: String, - size: Int, + page: Int, ) = moviesApi - .searchMovies(query = query, page = size) + .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 index c621b03..ba0b4ba 100644 --- a/data/src/main/kotlin/com/example/data/datasource/MoviesDatabaseDataSource.kt +++ b/data/src/main/kotlin/com/example/data/datasource/MoviesDatabaseDataSource.kt @@ -8,13 +8,13 @@ import com.example.data.errors.LocalErrors internal class MoviesDatabaseDataSource( private val movieDao: MovieDao, ) : MoviesLocalDataSource { - override fun getDeletedMovies(): Either> = + override suspend fun getDeletedMovies(): Either> = Either .catch { movieDao.getAllDeletedMovies() }.mapLeft { LocalErrors.DatabaseException } - override fun deleteMovie(id: DeletedMovieEntity) = + override suspend fun deleteMovie(id: DeletedMovieEntity) = Either .catch { movieDao.insertDeletedMovie(id) diff --git a/data/src/main/kotlin/com/example/data/datasource/MoviesLocalDataSource.kt b/data/src/main/kotlin/com/example/data/datasource/MoviesLocalDataSource.kt index c380abc..fc31f89 100644 --- a/data/src/main/kotlin/com/example/data/datasource/MoviesLocalDataSource.kt +++ b/data/src/main/kotlin/com/example/data/datasource/MoviesLocalDataSource.kt @@ -5,7 +5,7 @@ import com.example.core.database.model.DeletedMovieEntity import com.example.data.errors.LocalErrors interface MoviesLocalDataSource { - fun getDeletedMovies(): Either> + suspend fun getDeletedMovies(): Either> - fun deleteMovie(id: DeletedMovieEntity): 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 index 7b97f28..5033b34 100644 --- a/data/src/main/kotlin/com/example/data/datasource/MoviesRemoteDataSource.kt +++ b/data/src/main/kotlin/com/example/data/datasource/MoviesRemoteDataSource.kt @@ -7,6 +7,6 @@ import com.example.data.errors.RemoteErrors interface MoviesRemoteDataSource { suspend fun searchMovies( query: String, - size: Int, + page: Int, ): Either> } diff --git a/data/src/main/kotlin/com/example/data/repository/MoviesRepositoryImpl.kt b/data/src/main/kotlin/com/example/data/repository/MoviesRepositoryImpl.kt index 8f39ead..c38fa73 100644 --- a/data/src/main/kotlin/com/example/data/repository/MoviesRepositoryImpl.kt +++ b/data/src/main/kotlin/com/example/data/repository/MoviesRepositoryImpl.kt @@ -7,7 +7,7 @@ import com.example.data.mapper.toDomain import com.example.domain.repository.MoviesRepository import javax.inject.Inject -class MoviesRepositoryImpl +internal class MoviesRepositoryImpl @Inject constructor( private val moviesRemoteDataSource: MoviesRemoteDataSource, @@ -15,10 +15,12 @@ class MoviesRepositoryImpl ) : MoviesRepository { override suspend fun searchMovies( query: String, - size: Int, - ) = moviesRemoteDataSource.searchMovies(query = query, size = size).map { list -> list.toDomain() } + page: Int, + ) = moviesRemoteDataSource.searchMovies(query = query, page = page).map { list -> list.toDomain() } - override fun getDeletedMovies() = moviesLocalDataSource.getDeletedMovies().map { list -> list.toDomain() } + override suspend fun getDeletedMovies() = + moviesLocalDataSource.getDeletedMovies().map { list -> list.toDomain() } - override fun deleteMovie(movieId: Int) = moviesLocalDataSource.deleteMovie(DeletedMovieEntity(id = movieId)) + override suspend fun deleteMovie(movieId: Int) = + moviesLocalDataSource.deleteMovie(DeletedMovieEntity(id = movieId)) } diff --git a/domain/src/main/kotlin/com/example/domain/repository/MoviesRepository.kt b/domain/src/main/kotlin/com/example/domain/repository/MoviesRepository.kt index 645e120..1129395 100644 --- a/domain/src/main/kotlin/com/example/domain/repository/MoviesRepository.kt +++ b/domain/src/main/kotlin/com/example/domain/repository/MoviesRepository.kt @@ -6,10 +6,10 @@ import com.example.domain.model.Movie interface MoviesRepository { suspend fun searchMovies( query: String, - size: Int, + page: Int, ): Either> - fun getDeletedMovies(): Either> + suspend fun getDeletedMovies(): Either> - fun deleteMovie(movieId: Int): 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() }) + }, + ) + } + } From c86a9ed54a9bec9e28dba1a34b794c1377409b71 Mon Sep 17 00:00:00 2001 From: Pell Date: Fri, 11 Apr 2025 15:09:17 +0200 Subject: [PATCH 06/17] First VM version --- presentation/feature/build.gradle.kts | 2 ++ .../feature/contract/event/FeatureEvent.kt | 11 ++++++ .../feature/contract/state/FeatureUiState.kt | 15 ++++++++ .../feature/contract/state/MovieUiState.kt | 18 ++++++++++ .../feature/viewmodel/FeatureViewModel.kt | 34 ++++++++++++++++++- 5 files changed, 79 insertions(+), 1 deletion(-) create mode 100644 presentation/feature/src/main/java/com/example/feature/contract/event/FeatureEvent.kt create mode 100644 presentation/feature/src/main/java/com/example/feature/contract/state/FeatureUiState.kt create mode 100644 presentation/feature/src/main/java/com/example/feature/contract/state/MovieUiState.kt diff --git a/presentation/feature/build.gradle.kts b/presentation/feature/build.gradle.kts index b0c552a..19a6f3b 100644 --- a/presentation/feature/build.gradle.kts +++ b/presentation/feature/build.gradle.kts @@ -46,6 +46,8 @@ dependencies { ksp(libs.com.google.dagger.hilt.android.compiler) + implementation(project(":domain")) + testImplementation(libs.bundles.test.unit) testImplementation(libs.bundles.test.compose) androidTestImplementation(libs.bundles.test.android) 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..147f261 --- /dev/null +++ b/presentation/feature/src/main/java/com/example/feature/contract/state/FeatureUiState.kt @@ -0,0 +1,15 @@ +package com.example.feature.contract.state + +import com.example.domain.model.Movie + +data class FeatureUiState( + val error: String? = null, + 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..fb80e28 --- /dev/null +++ b/presentation/feature/src/main/java/com/example/feature/contract/state/MovieUiState.kt @@ -0,0 +1,18 @@ +package com.example.feature.contract.state + +data class MovieUiState( + val id: String, + 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 class Deleting( + val movieId: Int, + ) : ContentState + } +} 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..a18aac1 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,42 @@ 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 dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel class FeatureViewModel @Inject - constructor() : ViewModel() + constructor( + private val deleteMovieUseCase: DeleteMovieUseCase, + private val searchMoviesUseCase: SearchMoviesUseCase, + ) : ViewModel() { + fun handleEvent(event: FeatureEvent) { + when (event) { + is FeatureEvent.OnSearchMovies -> { + searchMovies(text = event.text) + } + + is FeatureEvent.OnDeleteMovie -> { + deleteMovie(movieId = event.movieId) + } + } + } + + fun searchMovies(text: String) { + viewModelScope.launch { + searchMoviesUseCase(query = text, page = 0) + } + } + + fun deleteMovie(movieId: Int) { + viewModelScope.launch { + deleteMovieUseCase(movieId = movieId) + } + } + } From 57ff53b37e3d96205811fc29090e49a80727b861 Mon Sep 17 00:00:00 2001 From: Pell Date: Fri, 11 Apr 2025 15:27:05 +0200 Subject: [PATCH 07/17] Domain module to pure kotlin --- domain/build.gradle.kts | 36 ++--------- .../com/example/domain/di/DomainModule.kt | 18 ++++++ gradle/libs.versions.toml | 61 ++++++++++--------- 3 files changed, 55 insertions(+), 60 deletions(-) create mode 100644 domain/src/main/kotlin/com/example/domain/di/DomainModule.kt diff --git a/domain/build.gradle.kts b/domain/build.gradle.kts index e4b6c31..dae238b 100644 --- a/domain/build.gradle.kts +++ b/domain/build.gradle.kts @@ -1,40 +1,14 @@ 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 } 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) } 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..03d3d72 --- /dev/null +++ b/domain/src/main/kotlin/com/example/domain/di/DomainModule.kt @@ -0,0 +1,18 @@ +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 + +@Module +object DomainModule { + @Provides + fun provideSearchMoviesRoverUseCase(moviesRepository: MoviesRepository): SearchMoviesUseCase = + SearchMoviesUseCase(moviesRepository) + + @Provides + fun provideDeleteMovieUseCase(moviesRepository: MoviesRepository): DeleteMovieUseCase = + DeleteMovieUseCase(moviesRepository) +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1528a18..ade1beb 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -42,6 +42,8 @@ material = { group = "com.google.android.material", name = "material", version.r 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-compose = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "hiltCompose" } +com-google-dagger = { group = "com.google.dagger", name = "dagger", version.ref = "hilt" } +com-google-dagger-compiler = { group = "com.google.dagger", name = "dagger-compiler", version.ref = "hilt" } 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" } @@ -71,43 +73,44 @@ 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", ] layer-domain = [ - "com-google-dagger-hilt-android", - "arrow-core", - "arrow-fx-coroutines", + "com-google-dagger", + "com-google-dagger-compiler", + "arrow-core", + "arrow-fx-coroutines", ] layer-data = [ - "com-google-dagger-hilt-android", - "arrow-core", - "arrow-fx-coroutines", - "arrow-core-retrofit", - "retrofit", - "retrofit-gson", - "okhttp", - "room-ktx", + "com-google-dagger-hilt-android", + "arrow-core", + "arrow-fx-coroutines", + "arrow-core-retrofit", + "retrofit", + "retrofit-gson", + "okhttp", + "room-ktx", ] test-unit = [ - "junit", - "mockwebserver", - "io-mockk", - "kotlin-mockito-kotlin", - "app-cash-turbine", + "junit", + "mockwebserver", + "io-mockk", + "kotlin-mockito-kotlin", + "app-cash-turbine", ] test-android = [ - "androidx-junit" , - "androidx-espresso-core", - "androidx-ui-test-junit4", + "androidx-junit", + "androidx-espresso-core", + "androidx-ui-test-junit4", ] test-compose = [ - "roborazzi", - "roborazzi-compose", - "roborazzi-rule", -] \ No newline at end of file + "roborazzi", + "roborazzi-compose", + "roborazzi-rule", +] From 1727e23f06716076bb1d1d25a3826c8c03aca1d2 Mon Sep 17 00:00:00 2001 From: Pell Date: Fri, 11 Apr 2025 15:58:29 +0200 Subject: [PATCH 08/17] Added screen basic version --- gradle/libs.versions.toml | 2 + presentation/feature/build.gradle.kts | 2 + .../feature/components/FeatureContent.kt | 17 +++++++++ .../feature/contract/state/FeatureUiState.kt | 1 - .../example/feature/screen/FeatureScreen.kt | 37 ++++++++++++++----- .../feature/viewmodel/FeatureViewModel.kt | 6 +++ 6 files changed, 54 insertions(+), 11 deletions(-) create mode 100644 presentation/feature/src/main/java/com/example/feature/components/FeatureContent.kt diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ade1beb..1fb1a47 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -34,6 +34,7 @@ androidx-activity-compose = { group = "androidx.activity", name = "activity-comp 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 = { group = "androidx.compose.ui", name = "ui-tooling" } 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" } @@ -79,6 +80,7 @@ layer-presentation = [ "kotlinx-serialization-json", "arrow-core", "arrow-fx-coroutines", + "androidx-ui-tooling-preview", ] layer-domain = [ "com-google-dagger", diff --git a/presentation/feature/build.gradle.kts b/presentation/feature/build.gradle.kts index 19a6f3b..2cf9bc1 100644 --- a/presentation/feature/build.gradle.kts +++ b/presentation/feature/build.gradle.kts @@ -46,8 +46,10 @@ dependencies { ksp(libs.com.google.dagger.hilt.android.compiler) + implementation(project(":core:ui")) implementation(project(":domain")) + debugImplementation(libs.androidx.ui.tooling) testImplementation(libs.bundles.test.unit) testImplementation(libs.bundles.test.compose) androidTestImplementation(libs.bundles.test.android) diff --git a/presentation/feature/src/main/java/com/example/feature/components/FeatureContent.kt b/presentation/feature/src/main/java/com/example/feature/components/FeatureContent.kt new file mode 100644 index 0000000..3e71450 --- /dev/null +++ b/presentation/feature/src/main/java/com/example/feature/components/FeatureContent.kt @@ -0,0 +1,17 @@ +package com.example.feature.components + +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.example.feature.contract.state.FeatureUiState + +@Composable +internal fun FeatureContent( + state: FeatureUiState, + onDeleteMovie: (Int) -> Unit, + onSearchMovies: (String) -> Unit, +) { + Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> + } +} 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 index 147f261..dd4c899 100644 --- 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 @@ -3,7 +3,6 @@ package com.example.feature.contract.state import com.example.domain.model.Movie data class FeatureUiState( - val error: String? = null, val movies: List = emptyList(), val contentState: ContentState = ContentState.Idle, ) { 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..98096ba 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,35 @@ 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.compose.ui.tooling.preview.Preview +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.example.core.ui.theme.BaseProjectTheme +import com.example.feature.components.FeatureContent +import com.example.feature.contract.event.FeatureEvent +import com.example.feature.contract.state.FeatureUiState +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)) }, + ) +} + +@Preview +@Composable +private fun WakeupScreenUpdatingStatePreview() { + BaseProjectTheme { + FeatureContent( + state = FeatureUiState(), + onDeleteMovie = {}, + onSearchMovies = {}, ) } } 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 a18aac1..7a160ca 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 @@ -5,7 +5,10 @@ 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 dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch import javax.inject.Inject @@ -16,6 +19,9 @@ class FeatureViewModel 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 -> { From 08630bebed4cec71b3c8d9470ca62a19b5274fbf Mon Sep 17 00:00:00 2001 From: Pell Date: Sun, 13 Apr 2025 06:34:13 +0200 Subject: [PATCH 09/17] Added movie card --- .../{MoviesMapper.kt => MovieMapper.kt} | 0 .../feature/components/FeatureContent.kt | 17 --- .../example/feature/components/Greeting.kt | 16 -- .../example/feature/components/MovieCard.kt | 142 ++++++++++++++++++ .../feature/contract/state/FeatureUiState.kt | 4 +- .../feature/contract/state/MovieUiState.kt | 6 +- .../com/example/feature/mapper/MovieMapper.kt | 20 +++ .../example/feature/screen/FeatureContent.kt | 76 ++++++++++ .../example/feature/screen/FeatureScreen.kt | 16 -- .../feature/viewmodel/FeatureViewModel.kt | 64 +++++++- .../main/res/drawable/movie_placeholder.xml | 7 + 11 files changed, 308 insertions(+), 60 deletions(-) rename data/src/main/kotlin/com/example/data/mapper/{MoviesMapper.kt => MovieMapper.kt} (100%) delete mode 100644 presentation/feature/src/main/java/com/example/feature/components/FeatureContent.kt delete mode 100644 presentation/feature/src/main/java/com/example/feature/components/Greeting.kt create mode 100644 presentation/feature/src/main/java/com/example/feature/components/MovieCard.kt create mode 100644 presentation/feature/src/main/java/com/example/feature/mapper/MovieMapper.kt create mode 100644 presentation/feature/src/main/java/com/example/feature/screen/FeatureContent.kt create mode 100644 presentation/feature/src/main/res/drawable/movie_placeholder.xml diff --git a/data/src/main/kotlin/com/example/data/mapper/MoviesMapper.kt b/data/src/main/kotlin/com/example/data/mapper/MovieMapper.kt similarity index 100% rename from data/src/main/kotlin/com/example/data/mapper/MoviesMapper.kt rename to data/src/main/kotlin/com/example/data/mapper/MovieMapper.kt diff --git a/presentation/feature/src/main/java/com/example/feature/components/FeatureContent.kt b/presentation/feature/src/main/java/com/example/feature/components/FeatureContent.kt deleted file mode 100644 index 3e71450..0000000 --- a/presentation/feature/src/main/java/com/example/feature/components/FeatureContent.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.example.feature.components - -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material3.Scaffold -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import com.example.feature.contract.state.FeatureUiState - -@Composable -internal fun FeatureContent( - state: FeatureUiState, - onDeleteMovie: (Int) -> Unit, - onSearchMovies: (String) -> Unit, -) { - Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> - } -} 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 e55b20b..0000000 --- a/presentation/feature/src/main/java/com/example/feature/components/Greeting.kt +++ /dev/null @@ -1,16 +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, - ) -} 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..786fcda --- /dev/null +++ b/presentation/feature/src/main/java/com/example/feature/components/MovieCard.kt @@ -0,0 +1,142 @@ +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.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() { + MaterialTheme { + 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/state/FeatureUiState.kt b/presentation/feature/src/main/java/com/example/feature/contract/state/FeatureUiState.kt index dd4c899..124bda1 100644 --- 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 @@ -1,9 +1,7 @@ package com.example.feature.contract.state -import com.example.domain.model.Movie - data class FeatureUiState( - val movies: List = emptyList(), + val movies: List = emptyList(), val contentState: ContentState = ContentState.Idle, ) { sealed interface 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 index fb80e28..8e39eff 100644 --- 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 @@ -1,7 +1,7 @@ package com.example.feature.contract.state data class MovieUiState( - val id: String, + val id: Int, val title: String, val year: String, val type: String, @@ -11,8 +11,6 @@ data class MovieUiState( sealed interface ContentState { data object Idle : ContentState - data class Deleting( - val movieId: Int, - ) : 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..f6b6dba --- /dev/null +++ b/presentation/feature/src/main/java/com/example/feature/mapper/MovieMapper.kt @@ -0,0 +1,20 @@ +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 + +fun List.toDomain() = map { it.toDomain() } + +fun MovieDto.toDomain() = + Movie( + id = imdbId.toInt(), + title = title, + year = year, + type = type, + poster = poster, + ) + +fun List.toDomain() = map { it.toDomain() } + +fun DeletedMovieEntity.toDomain() = id 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..fa8332e --- /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.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 98096ba..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 @@ -2,13 +2,9 @@ package com.example.feature.screen import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.ui.tooling.preview.Preview import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.example.core.ui.theme.BaseProjectTheme -import com.example.feature.components.FeatureContent import com.example.feature.contract.event.FeatureEvent -import com.example.feature.contract.state.FeatureUiState import com.example.feature.viewmodel.FeatureViewModel @Composable @@ -21,15 +17,3 @@ fun FeatureScreen(viewModel: FeatureViewModel = hiltViewModel()) { onSearchMovies = { viewModel.handleEvent(FeatureEvent.OnSearchMovies(text = it)) }, ) } - -@Preview -@Composable -private fun WakeupScreenUpdatingStatePreview() { - BaseProjectTheme { - FeatureContent( - state = FeatureUiState(), - onDeleteMovie = {}, - onSearchMovies = {}, - ) - } -} 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 7a160ca..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 @@ -6,9 +6,12 @@ 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 @@ -34,15 +37,68 @@ class FeatureViewModel } } - fun searchMovies(text: String) { + private fun searchMovies(text: String) { viewModelScope.launch { - searchMoviesUseCase(query = text, page = 0) + 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(), + ) + } + }, + ) } } - fun deleteMovie(movieId: Int) { + 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) + 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 @@ + + + + + + + From f52467c5c20924e07b740ddf9f88fe9830c3d0a7 Mon Sep 17 00:00:00 2001 From: Pell Date: Sun, 13 Apr 2025 06:35:53 +0200 Subject: [PATCH 10/17] Dagger to hilt core --- app/build.gradle.kts | 6 +++- core/api/build.gradle.kts | 4 +++ core/database/build.gradle.kts | 6 ++-- core/ui/build.gradle.kts | 2 +- data/build.gradle.kts | 4 +++ .../data/datasource/MoviesApiDataSource.kt | 25 ++++++++------- .../datasource/MoviesDatabaseDataSource.kt | 31 ++++++++++--------- .../kotlin/com/example/data/di/DataModule.kt | 8 ++--- domain/build.gradle.kts | 3 ++ .../com/example/domain/di/DomainModule.kt | 3 ++ .../kotlin/com/example/domain/model/Movie.kt | 2 +- gradle/libs.versions.toml | 11 ++++--- presentation/feature/build.gradle.kts | 2 +- .../com/example/feature/mapper/MovieMapper.kt | 17 ++++------ 14 files changed, 73 insertions(+), 51 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 982336b..37a488c 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -52,8 +52,12 @@ dependencies { 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(":data")) + implementation(project(":domain")) implementation(project(":presentation:feature")) implementation(project(":core:ui")) + implementation(project(":core:api")) + implementation(project(":core:database")) } diff --git a/core/api/build.gradle.kts b/core/api/build.gradle.kts index 251bad4..8017813 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 { @@ -36,5 +38,7 @@ dependencies { implementation(libs.material) implementation(libs.bundles.layer.data) + ksp(libs.com.google.dagger.hilt.compiler) + testImplementation(libs.bundles.test.unit) } diff --git a/core/database/build.gradle.kts b/core/database/build.gradle.kts index fac0b40..4439f95 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", ) } } @@ -37,7 +38,8 @@ dependencies { implementation(libs.material) implementation(libs.bundles.layer.data) + 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/ui/build.gradle.kts b/core/ui/build.gradle.kts index 4bc7fec..a128468 100644 --- a/core/ui/build.gradle.kts +++ b/core/ui/build.gradle.kts @@ -44,7 +44,7 @@ dependencies { 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) diff --git a/data/build.gradle.kts b/data/build.gradle.kts index ed1654d..8240f36 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 { @@ -36,6 +38,8 @@ dependencies { implementation(libs.material) implementation(libs.bundles.layer.data) + ksp(libs.com.google.dagger.hilt.compiler) + implementation(project(":domain")) implementation(project(":core:api")) implementation(project(":core:database")) diff --git a/data/src/main/kotlin/com/example/data/datasource/MoviesApiDataSource.kt b/data/src/main/kotlin/com/example/data/datasource/MoviesApiDataSource.kt index 577bba0..0863e25 100644 --- a/data/src/main/kotlin/com/example/data/datasource/MoviesApiDataSource.kt +++ b/data/src/main/kotlin/com/example/data/datasource/MoviesApiDataSource.kt @@ -2,15 +2,18 @@ package com.example.data.datasource import com.example.core.api.service.MovieApi import com.example.data.errors.RemoteErrors +import javax.inject.Inject -internal class MoviesApiDataSource( - 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 } -} +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 index ba0b4ba..273251f 100644 --- a/data/src/main/kotlin/com/example/data/datasource/MoviesDatabaseDataSource.kt +++ b/data/src/main/kotlin/com/example/data/datasource/MoviesDatabaseDataSource.kt @@ -4,19 +4,22 @@ 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 -internal class MoviesDatabaseDataSource( - private val movieDao: MovieDao, -) : MoviesLocalDataSource { - override suspend fun getDeletedMovies(): Either> = - Either - .catch { - movieDao.getAllDeletedMovies() - }.mapLeft { LocalErrors.DatabaseException } +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 } -} + override suspend fun deleteMovie(id: DeletedMovieEntity) = + Either + .catch { + movieDao.insertDeletedMovie(id) + }.mapLeft { LocalErrors.DatabaseException } + } diff --git a/data/src/main/kotlin/com/example/data/di/DataModule.kt b/data/src/main/kotlin/com/example/data/di/DataModule.kt index c10ddd9..af60ae7 100644 --- a/data/src/main/kotlin/com/example/data/di/DataModule.kt +++ b/data/src/main/kotlin/com/example/data/di/DataModule.kt @@ -8,8 +8,8 @@ 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.Binds import dagger.Module +import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import javax.inject.Singleton @@ -17,15 +17,15 @@ import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) object DataModule { - @Binds + @Provides @Singleton fun provideMoviesLocalDataSource(movieDao: MovieDao): MoviesLocalDataSource = MoviesDatabaseDataSource(movieDao) - @Binds + @Provides @Singleton fun provideMoviesRemoteDataSource(movieApi: MovieApi): MoviesRemoteDataSource = MoviesApiDataSource(movieApi) - @Binds + @Provides fun provideMoviesRepository( moviesRemoteDataSource: MoviesRemoteDataSource, moviesLocalDataSource: MoviesLocalDataSource, diff --git a/domain/build.gradle.kts b/domain/build.gradle.kts index dae238b..6de8108 100644 --- a/domain/build.gradle.kts +++ b/domain/build.gradle.kts @@ -6,6 +6,9 @@ java { sourceCompatibility = AppVersions.javaVersion targetCompatibility = AppVersions.javaVersion } +kotlin { + jvmToolchain(AppVersions.JVM_TARGET.toInt()) +} dependencies { implementation(libs.bundles.layer.domain) diff --git a/domain/src/main/kotlin/com/example/domain/di/DomainModule.kt b/domain/src/main/kotlin/com/example/domain/di/DomainModule.kt index 03d3d72..0a610c3 100644 --- a/domain/src/main/kotlin/com/example/domain/di/DomainModule.kt +++ b/domain/src/main/kotlin/com/example/domain/di/DomainModule.kt @@ -5,8 +5,11 @@ 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 = diff --git a/domain/src/main/kotlin/com/example/domain/model/Movie.kt b/domain/src/main/kotlin/com/example/domain/model/Movie.kt index 1e29f57..c5c6495 100644 --- a/domain/src/main/kotlin/com/example/domain/model/Movie.kt +++ b/domain/src/main/kotlin/com/example/domain/model/Movie.kt @@ -1,7 +1,7 @@ package com.example.domain.model data class Movie( - val id: String, + val id: Int, val title: String, val year: String, val type: String, diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1fb1a47..281762d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -23,6 +23,7 @@ mockitoKotlin = "5.3.1" ioMockk = "1.13.17" turbine = "1.1.0" room = "2.6.1" +coil = "3.1.0" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } @@ -40,11 +41,10 @@ androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit 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" } -com-google-dagger = { group = "com.google.dagger", name = "dagger", version.ref = "hilt" } -com-google-dagger-compiler = { group = "com.google.dagger", name = "dagger-compiler", version.ref = "hilt" } 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" } @@ -62,6 +62,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" } @@ -81,10 +82,10 @@ layer-presentation = [ "arrow-core", "arrow-fx-coroutines", "androidx-ui-tooling-preview", + "coil-compose" ] layer-domain = [ - "com-google-dagger", - "com-google-dagger-compiler", + "com-google-dagger-hilt-core", "arrow-core", "arrow-fx-coroutines", ] diff --git a/presentation/feature/build.gradle.kts b/presentation/feature/build.gradle.kts index 2cf9bc1..e8e0149 100644 --- a/presentation/feature/build.gradle.kts +++ b/presentation/feature/build.gradle.kts @@ -44,7 +44,7 @@ dependencies { 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) implementation(project(":core:ui")) implementation(project(":domain")) 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 index f6b6dba..c08813c 100644 --- a/presentation/feature/src/main/java/com/example/feature/mapper/MovieMapper.kt +++ b/presentation/feature/src/main/java/com/example/feature/mapper/MovieMapper.kt @@ -1,20 +1,15 @@ -package com.example.data.mapper +package com.example.feature.mapper -import com.example.core.api.model.MovieDto -import com.example.core.database.model.DeletedMovieEntity import com.example.domain.model.Movie +import com.example.feature.contract.state.MovieUiState -fun List.toDomain() = map { it.toDomain() } +fun List.toUiState() = map { it.toUiState() } -fun MovieDto.toDomain() = - Movie( - id = imdbId.toInt(), +fun Movie.toUiState() = + MovieUiState( + id = id, title = title, year = year, type = type, poster = poster, ) - -fun List.toDomain() = map { it.toDomain() } - -fun DeletedMovieEntity.toDomain() = id From 9e04093d15c0e5f8715432e51090b38289769f25 Mon Sep 17 00:00:00 2001 From: Pell Date: Sun, 13 Apr 2025 06:44:40 +0200 Subject: [PATCH 11/17] Mappers to objects --- .../com/example/data/mapper/MovieMapper.kt | 26 +++++++++++-------- .../data/repository/MoviesRepositoryImpl.kt | 3 ++- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/data/src/main/kotlin/com/example/data/mapper/MovieMapper.kt b/data/src/main/kotlin/com/example/data/mapper/MovieMapper.kt index b361591..bbee52f 100644 --- a/data/src/main/kotlin/com/example/data/mapper/MovieMapper.kt +++ b/data/src/main/kotlin/com/example/data/mapper/MovieMapper.kt @@ -4,17 +4,21 @@ import com.example.core.api.model.MovieDto import com.example.core.database.model.DeletedMovieEntity import com.example.domain.model.Movie -fun List.toDomain() = map { it.toDomain() } +object MovieDtoMapper { + fun List.toDomain() = map { it.toDomain() } -fun MovieDto.toDomain() = - Movie( - id = imdbId, - title = title, - year = year, - type = type, - poster = poster, - ) + fun MovieDto.toDomain() = + Movie( + id = imdbId.toInt(), + title = title, + year = year, + type = type, + poster = poster, + ) +} -fun List.toDomain() = map { it.toDomain() } +object DeletedMovieMapper { + fun List.toDomain() = map { it.toDomain() } -fun DeletedMovieEntity.toDomain() = id + 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 index c38fa73..a4ca995 100644 --- a/data/src/main/kotlin/com/example/data/repository/MoviesRepositoryImpl.kt +++ b/data/src/main/kotlin/com/example/data/repository/MoviesRepositoryImpl.kt @@ -3,7 +3,8 @@ 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.toDomain +import com.example.data.mapper.DeletedMovieMapper.toDomain +import com.example.data.mapper.MovieDtoMapper.toDomain import com.example.domain.repository.MoviesRepository import javax.inject.Inject From 13cc77028ebb46456bf247503e5e0fd0bf382947 Mon Sep 17 00:00:00 2001 From: Pell Date: Sun, 13 Apr 2025 06:53:03 +0200 Subject: [PATCH 12/17] Bump versions --- gradle/libs.versions.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 281762d..8d8388c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,13 +1,13 @@ [versions] agp = "8.9.1" kotlin = "2.0.21" -coreKtx = "1.15.0" +coreKtx = "1.16.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" @@ -22,7 +22,7 @@ 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] From 25df2990d461a479f8a4d076ff55a4d4d0f1cf96 Mon Sep 17 00:00:00 2001 From: Pell Date: Sun, 13 Apr 2025 06:53:11 +0200 Subject: [PATCH 13/17] More colors --- .../java/com/example/core.ui/theme/Color.kt | 34 +++++++++-- .../java/com/example/core.ui/theme/Theme.kt | 61 ++++++++++++++----- .../example/feature/components/MovieCard.kt | 3 +- 3 files changed, 75 insertions(+), 23 deletions(-) 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 index 019381c..8eb50f6 100644 --- 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 @@ -2,10 +2,32 @@ 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 PrimaryBlue = Color(0xFF1A4B8C) +val PrimaryBlueLight = Color(0xFF466EAB) +val PrimaryBlueDark = Color(0xFF0D3060) -val Purple40 = Color(0xFF6650a4) -val PurpleGrey40 = Color(0xFF625b71) -val Pink40 = Color(0xFF7D5260) \ No newline at end of file +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/ui/src/main/java/com/example/core.ui/theme/Theme.kt b/core/ui/src/main/java/com/example/core.ui/theme/Theme.kt index f3526d6..78b430e 100644 --- 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 @@ -8,35 +8,64 @@ 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 = Purple80, - secondary = PurpleGrey80, - tertiary = Pink80, + 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 = 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), - */ + 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(), - // Dynamic color is available on Android 12+ dynamicColor: Boolean = true, content: @Composable () -> Unit, ) { 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 index 786fcda..547120f 100644 --- a/presentation/feature/src/main/java/com/example/feature/components/MovieCard.kt +++ b/presentation/feature/src/main/java/com/example/feature/components/MovieCard.kt @@ -31,6 +31,7 @@ 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.ui.theme.BaseProjectTheme import com.example.feature.contract.state.MovieUiState @Composable @@ -126,7 +127,7 @@ internal fun MovieCard( @PreviewLightDark @Composable private fun MovieCardPreview() { - MaterialTheme { + BaseProjectTheme { MovieCard( movie = MovieUiState( From 92b5414d283eb6393b21b8c0cb5e323d2805ea63 Mon Sep 17 00:00:00 2001 From: Pell Date: Sun, 13 Apr 2025 07:07:32 +0200 Subject: [PATCH 14/17] Added di module --- app/build.gradle.kts | 7 +--- .../BaseProjectApplicationNavHost.kt | 2 - di/.gitignore | 1 + di/build.gradle.kts | 38 +++++++++++++++++++ settings.gradle.kts | 7 ++-- 5 files changed, 45 insertions(+), 10 deletions(-) create mode 100644 di/.gitignore create mode 100644 di/build.gradle.kts diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 37a488c..dd5179d 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -54,10 +54,7 @@ dependencies { ksp(libs.com.google.dagger.hilt.compiler) - implementation(project(":data")) - implementation(project(":domain")) - implementation(project(":presentation:feature")) + implementation(project(":di")) implementation(project(":core:ui")) - implementation(project(":core:api")) - implementation(project(":core:database")) + implementation(project(":presentation:feature")) } 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..dcc7487 100644 --- a/app/src/main/java/com/example/baseproject/navigation/BaseProjectApplicationNavHost.kt +++ b/app/src/main/java/com/example/baseproject/navigation/BaseProjectApplicationNavHost.kt @@ -1,6 +1,5 @@ package com.example.baseproject.navigation -import android.annotation.SuppressLint import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.navigation.NavHostController @@ -9,7 +8,6 @@ import androidx.navigation.compose.composable import androidx.navigation.compose.navigation import com.example.feature.screen.FeatureScreen -@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") @Composable fun BaseProjectApplicationNavHost( modifier: Modifier = Modifier, diff --git a/di/.gitignore b/di/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/di/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/di/build.gradle.kts b/di/build.gradle.kts new file mode 100644 index 0000000..56280bf --- /dev/null +++ b/di/build.gradle.kts @@ -0,0 +1,38 @@ +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.android) +} + +android { + namespace = "${AppVersions.APPLICATION_ID}.di" + 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 + } +} + +dependencies { + implementation(project(":core:api")) + implementation(project(":core:database")) + implementation(project(":data")) + implementation(project(":domain")) +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 645c45f..c38e0b8 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -21,9 +21,10 @@ dependencyResolutionManagement { rootProject.name = "BaseProject" include(":app") -include(":presentation:feature") -include(":domain") -include(":data") +include(":di") include(":core:ui") include(":core:api") include(":core:database") +include(":data") +include(":domain") +include(":presentation:feature") From 6c1582f418c28d20edad7180ae8eb453f650ebf4 Mon Sep 17 00:00:00 2001 From: Pell Date: Mon, 14 Apr 2025 08:36:57 +0200 Subject: [PATCH 15/17] Added base navigation for feature modules and some refactors --- .editorconfig | 1 + app/build.gradle.kts | 2 +- app/src/main/AndroidManifest.xml | 46 +++++++++---------- ...rojectApp.kt => BaseProjectApplication.kt} | 2 +- .../BaseProjectApplicationNavHost.kt | 19 +++++--- .../navigation/NavigationRoutes.kt | 11 ----- .../navigation/di/NavigationModule.kt | 17 +++++++ .../viewmodel/NavigationViewModel.kt | 15 ++++++ .../{application => view}/MainActivity.kt | 8 ++-- core/{ui => presentation}/.gitignore | 0 core/{ui => presentation}/build.gradle.kts | 1 + core/{ui => presentation}/proguard-rules.pro | 0 .../feature/ExampleInstrumentedTest.kt | 0 .../navigation/BaseProjectNavRoutes.kt | 11 +++++ .../navigation/FeatureNavigation.kt | 11 +++++ .../core/presentation/ui}/theme/Color.kt | 2 +- .../core/presentation/ui}/theme/Theme.kt | 2 +- .../core/presentation/ui/theme/Type.kt | 36 +++++++++++++++ .../src/main/res/values/strings.xml | 0 .../src/main/res/values/themes.xml | 5 ++ .../com/example/feature/ExampleUnitTest.kt | 0 .../java/com/example/core.ui/theme/Type.kt | 34 -------------- di/build.gradle.kts | 6 +++ .../example/domain/ExampleInstrumentedTest.kt | 24 ---------- presentation/feature/build.gradle.kts | 3 +- .../example/feature/components/MovieCard.kt | 2 +- .../navigation/FeatureNavigationImpl.kt | 27 +++++++++++ .../feature/navigation/FeatureRoute.kt | 8 ++++ .../example/feature/screen/FeatureContent.kt | 2 +- settings.gradle.kts | 2 +- 30 files changed, 186 insertions(+), 111 deletions(-) rename app/src/main/java/com/example/baseproject/navigation/{BaseProjectApp.kt => BaseProjectApplication.kt} (90%) delete mode 100644 app/src/main/java/com/example/baseproject/navigation/NavigationRoutes.kt create mode 100644 app/src/main/java/com/example/baseproject/navigation/di/NavigationModule.kt create mode 100644 app/src/main/java/com/example/baseproject/navigation/viewmodel/NavigationViewModel.kt rename app/src/main/java/com/example/baseproject/{application => view}/MainActivity.kt (69%) rename core/{ui => presentation}/.gitignore (100%) rename core/{ui => presentation}/build.gradle.kts (97%) rename core/{ui => presentation}/proguard-rules.pro (100%) rename core/{ui => presentation}/src/androidTest/java/com/example/feature/ExampleInstrumentedTest.kt (100%) create mode 100644 core/presentation/src/main/java/com/example/core/presentation/navigation/BaseProjectNavRoutes.kt create mode 100644 core/presentation/src/main/java/com/example/core/presentation/navigation/FeatureNavigation.kt rename core/{ui/src/main/java/com/example/core.ui => presentation/src/main/java/com/example/core/presentation/ui}/theme/Color.kt (95%) rename core/{ui/src/main/java/com/example/core.ui => presentation/src/main/java/com/example/core/presentation/ui}/theme/Theme.kt (98%) create mode 100644 core/presentation/src/main/java/com/example/core/presentation/ui/theme/Type.kt rename core/{ui => presentation}/src/main/res/values/strings.xml (100%) create mode 100644 core/presentation/src/main/res/values/themes.xml rename core/{ui => presentation}/src/test/java/com/example/feature/ExampleUnitTest.kt (100%) delete mode 100644 core/ui/src/main/java/com/example/core.ui/theme/Type.kt delete mode 100644 domain/src/androidTest/java/com/example/domain/ExampleInstrumentedTest.kt create mode 100644 presentation/feature/src/main/java/com/example/feature/navigation/FeatureNavigationImpl.kt create mode 100644 presentation/feature/src/main/java/com/example/feature/navigation/FeatureRoute.kt diff --git a/.editorconfig b/.editorconfig index 8d0265e..00ae5f1 100644 --- a/.editorconfig +++ b/.editorconfig @@ -14,6 +14,7 @@ insert_final_newline = true 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 diff --git a/app/build.gradle.kts b/app/build.gradle.kts index dd5179d..9b143d0 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -55,6 +55,6 @@ dependencies { ksp(libs.com.google.dagger.hilt.compiler) implementation(project(":di")) - implementation(project(":core:ui")) + implementation(project(":core:presentation")) implementation(project(":presentation:feature")) } 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/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 dcc7487..b544cd1 100644 --- a/app/src/main/java/com/example/baseproject/navigation/BaseProjectApplicationNavHost.kt +++ b/app/src/main/java/com/example/baseproject/navigation/BaseProjectApplicationNavHost.kt @@ -2,27 +2,32 @@ package com.example.baseproject.navigation 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 @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..4bab530 --- /dev/null +++ b/app/src/main/java/com/example/baseproject/navigation/viewmodel/NavigationViewModel.kt @@ -0,0 +1,15 @@ +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 = 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/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 97% rename from core/ui/build.gradle.kts rename to core/presentation/build.gradle.kts index a128468..80f7c57 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) } 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/ui/src/main/java/com/example/core.ui/theme/Color.kt b/core/presentation/src/main/java/com/example/core/presentation/ui/theme/Color.kt similarity index 95% rename from core/ui/src/main/java/com/example/core.ui/theme/Color.kt rename to core/presentation/src/main/java/com/example/core/presentation/ui/theme/Color.kt index 8eb50f6..593708e 100644 --- a/core/ui/src/main/java/com/example/core.ui/theme/Color.kt +++ b/core/presentation/src/main/java/com/example/core/presentation/ui/theme/Color.kt @@ -1,4 +1,4 @@ -package com.example.core.ui.theme +package com.example.core.presentation.ui.theme import androidx.compose.ui.graphics.Color diff --git a/core/ui/src/main/java/com/example/core.ui/theme/Theme.kt b/core/presentation/src/main/java/com/example/core/presentation/ui/theme/Theme.kt similarity index 98% rename from core/ui/src/main/java/com/example/core.ui/theme/Theme.kt rename to core/presentation/src/main/java/com/example/core/presentation/ui/theme/Theme.kt index 78b430e..7a2a278 100644 --- a/core/ui/src/main/java/com/example/core.ui/theme/Theme.kt +++ b/core/presentation/src/main/java/com/example/core/presentation/ui/theme/Theme.kt @@ -1,4 +1,4 @@ -package com.example.core.ui.theme +package com.example.core.presentation.ui.theme import android.os.Build import androidx.compose.foundation.isSystemInDarkTheme 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 @@ + + + +