├── app ├── .gitignore ├── src │ ├── main │ │ ├── res │ │ │ ├── values │ │ │ │ ├── api_key.xml │ │ │ │ ├── attrs.xml │ │ │ │ ├── strings.xml │ │ │ │ ├── colors.xml │ │ │ │ └── themes.xml │ │ │ ├── mipmap-hdpi │ │ │ │ ├── ic_launcher.webp │ │ │ │ └── ic_launcher_round.webp │ │ │ ├── mipmap-mdpi │ │ │ │ ├── ic_launcher.webp │ │ │ │ └── ic_launcher_round.webp │ │ │ ├── mipmap-xhdpi │ │ │ │ ├── ic_launcher.webp │ │ │ │ └── ic_launcher_round.webp │ │ │ ├── mipmap-xxhdpi │ │ │ │ ├── ic_launcher.webp │ │ │ │ └── ic_launcher_round.webp │ │ │ ├── mipmap-xxxhdpi │ │ │ │ ├── ic_launcher.webp │ │ │ │ └── ic_launcher_round.webp │ │ │ ├── mipmap-anydpi-v26 │ │ │ │ ├── ic_launcher.xml │ │ │ │ └── ic_launcher_round.xml │ │ │ ├── drawable │ │ │ │ ├── ic_arrow_back.xml │ │ │ │ ├── ic_favorite_on.xml │ │ │ │ ├── ic_favorite_off.xml │ │ │ │ └── ic_launcher_background.xml │ │ │ ├── layout │ │ │ │ ├── activity_nav_host.xml │ │ │ │ ├── view_movie.xml │ │ │ │ ├── fragment_main.xml │ │ │ │ └── fragment_detail.xml │ │ │ ├── values-night │ │ │ │ └── themes.xml │ │ │ ├── navigation │ │ │ │ └── nav_graph.xml │ │ │ └── drawable-v24 │ │ │ │ └── ic_launcher_foreground.xml │ │ ├── java │ │ │ └── com │ │ │ │ └── devexperto │ │ │ │ └── architectcoders │ │ │ │ ├── di │ │ │ │ ├── ApiKey.kt │ │ │ │ ├── ApiUrl.kt │ │ │ │ ├── MovieId.kt │ │ │ │ └── AppModule.kt │ │ │ │ ├── App.kt │ │ │ │ ├── data │ │ │ │ ├── database │ │ │ │ │ ├── MovieDatabase.kt │ │ │ │ │ ├── Movie.kt │ │ │ │ │ ├── MovieDao.kt │ │ │ │ │ └── MovieRoomDataSource.kt │ │ │ │ ├── server │ │ │ │ │ ├── RemoteService.kt │ │ │ │ │ ├── RemoteResult.kt │ │ │ │ │ └── MovieServerDataSource.kt │ │ │ │ ├── extensions.kt │ │ │ │ ├── AndroidPermissionChecker.kt │ │ │ │ └── PlayServicesLocationDataSource.kt │ │ │ │ └── ui │ │ │ │ ├── detail │ │ │ │ ├── DetailBindingAdapters.kt │ │ │ │ ├── di.kt │ │ │ │ ├── MovieDetailInfoView.kt │ │ │ │ ├── DetailFragment.kt │ │ │ │ └── DetailViewModel.kt │ │ │ │ ├── main │ │ │ │ ├── MainBindingAdapters.kt │ │ │ │ ├── MoviesAdapter.kt │ │ │ │ ├── MainFragment.kt │ │ │ │ ├── MainViewModel.kt │ │ │ │ └── MainState.kt │ │ │ │ ├── common │ │ │ │ ├── BindingAdapters.kt │ │ │ │ ├── PermissionRequester.kt │ │ │ │ ├── AspectRatioImageView.kt │ │ │ │ └── extensions.kt │ │ │ │ └── NavHostActivity.kt │ │ └── AndroidManifest.xml │ ├── debug │ │ ├── res │ │ │ └── xml │ │ │ │ └── network_security_config.xml │ │ └── AndroidManifest.xml │ ├── androidTest │ │ ├── java │ │ │ └── com │ │ │ │ └── devexperto │ │ │ │ └── architectcoders │ │ │ │ ├── data │ │ │ │ └── server │ │ │ │ │ ├── MockWebServerRule.kt │ │ │ │ │ └── mockResponseExtensions.kt │ │ │ │ ├── di │ │ │ │ ├── HiltTestRunner.kt │ │ │ │ └── TestAppModule.kt │ │ │ │ └── ui │ │ │ │ ├── OkHttp3IdlingResource.kt │ │ │ │ └── MainInstrumentationTest.kt │ │ └── assets │ │ │ └── popular_movies.json │ └── test │ │ └── java │ │ └── com │ │ └── devexperto │ │ └── architectcoders │ │ ├── testrules │ │ └── CoroutinesTestRule.kt │ │ └── ui │ │ ├── detail │ │ ├── DetailViewModelTest.kt │ │ └── DetailIntegrationTests.kt │ │ └── main │ │ ├── MainViewModelTest.kt │ │ └── MainIntegrationTests.kt ├── proguard-rules.pro └── build.gradle ├── data ├── .gitignore ├── src │ ├── main │ │ └── java │ │ │ └── com │ │ │ └── devexperto │ │ │ └── architectcoders │ │ │ └── data │ │ │ ├── datasource │ │ │ ├── LocationDataSource.kt │ │ │ ├── MovieRemoteDataSource.kt │ │ │ └── MovieLocalDataSource.kt │ │ │ ├── PermissionChecker.kt │ │ │ ├── RegionRepository.kt │ │ │ └── MoviesRepository.kt │ └── test │ │ └── java │ │ └── com │ │ └── devexperto │ │ └── architectcoders │ │ └── data │ │ ├── RegionRepositoryTest.kt │ │ └── MoviesRepositoryTest.kt └── build.gradle ├── domain ├── .gitignore ├── build.gradle └── src │ └── main │ └── java │ └── com │ └── devexperto │ └── architectcoders │ └── domain │ ├── Error.kt │ └── Movie.kt ├── testShared ├── .gitignore ├── build.gradle └── src │ └── main │ └── java │ └── com │ └── devexperto │ └── architectcoders │ └── testshared │ └── samples.kt ├── usecases ├── .gitignore ├── src │ ├── main │ │ └── java │ │ │ └── com │ │ │ └── devexperto │ │ │ └── architectcoders │ │ │ └── usecases │ │ │ ├── GetPopularMoviesUseCase.kt │ │ │ ├── SwitchMovieFavoriteUseCase.kt │ │ │ ├── FindMovieUseCase.kt │ │ │ └── RequestPopularMoviesUseCase.kt │ └── test │ │ └── java │ │ └── com │ │ └── devexperto │ │ └── architectcoders │ │ └── usecases │ │ ├── RequestPopularMoviesUseCaseTest.kt │ │ ├── FindMovieUseCaseTest.kt │ │ ├── GetPopularMoviesUseCaseTest.kt │ │ └── SwitchMovieFavoriteUseCaseTest.kt └── build.gradle ├── appTestShared ├── .gitignore ├── consumer-rules.pro ├── src │ └── main │ │ ├── AndroidManifest.xml │ │ └── java │ │ └── com │ │ └── devexperto │ │ └── architectcoders │ │ └── appTestShared │ │ ├── helpers.kt │ │ └── fakes.kt ├── proguard-rules.pro └── build.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .gitignore ├── settings.gradle ├── gradle.properties ├── gradlew.bat └── gradlew /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /data/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /domain/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /testShared/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /usecases/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /appTestShared/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /appTestShared/consumer-rules.pro: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/src/main/res/values/api_key.xml: -------------------------------------------------------------------------------- 1 | 2 | d30e1f350220f9aad6c4110df385d380 3 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devexpert-io/architect-coders-v2/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devexpert-io/architect-coders-v2/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devexpert-io/architect-coders-v2/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devexpert-io/architect-coders-v2/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devexpert-io/architect-coders-v2/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devexpert-io/architect-coders-v2/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea 5 | .DS_Store 6 | /build 7 | /captures 8 | .externalNativeBuild 9 | .cxx 10 | local.properties 11 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devexpert-io/architect-coders-v2/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devexpert-io/architect-coders-v2/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devexpert-io/architect-coders-v2/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devexpert-io/architect-coders-v2/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devexpert-io/architect-coders-v2/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /appTestShared/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /domain/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java-library' 3 | id 'org.jetbrains.kotlin.jvm' 4 | } 5 | 6 | java { 7 | sourceCompatibility = JavaVersion.VERSION_17 8 | targetCompatibility = JavaVersion.VERSION_17 9 | } -------------------------------------------------------------------------------- /app/src/main/java/com/devexperto/architectcoders/di/ApiKey.kt: -------------------------------------------------------------------------------- 1 | package com.devexperto.architectcoders.di 2 | 3 | import javax.inject.Qualifier 4 | 5 | @Retention(AnnotationRetention.BINARY) 6 | @Qualifier 7 | annotation class ApiKey -------------------------------------------------------------------------------- /app/src/main/java/com/devexperto/architectcoders/di/ApiUrl.kt: -------------------------------------------------------------------------------- 1 | package com.devexperto.architectcoders.di 2 | 3 | import javax.inject.Qualifier 4 | 5 | @Retention(AnnotationRetention.BINARY) 6 | @Qualifier 7 | annotation class ApiUrl -------------------------------------------------------------------------------- /data/src/main/java/com/devexperto/architectcoders/data/datasource/LocationDataSource.kt: -------------------------------------------------------------------------------- 1 | package com.devexperto.architectcoders.data.datasource 2 | 3 | interface LocationDataSource { 4 | suspend fun findLastRegion(): String? 5 | } -------------------------------------------------------------------------------- /app/src/main/java/com/devexperto/architectcoders/App.kt: -------------------------------------------------------------------------------- 1 | package com.devexperto.architectcoders 2 | 3 | import android.app.Application 4 | import dagger.hilt.android.HiltAndroidApp 5 | 6 | @HiltAndroidApp 7 | class App : Application() -------------------------------------------------------------------------------- /app/src/main/java/com/devexperto/architectcoders/di/MovieId.kt: -------------------------------------------------------------------------------- 1 | package com.devexperto.architectcoders.di 2 | 3 | import javax.inject.Qualifier 4 | 5 | @Retention(AnnotationRetention.BINARY) 6 | @Qualifier 7 | annotation class MovieId -------------------------------------------------------------------------------- /app/src/main/res/values/attrs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /domain/src/main/java/com/devexperto/architectcoders/domain/Error.kt: -------------------------------------------------------------------------------- 1 | package com.devexperto.architectcoders.domain 2 | 3 | sealed interface Error { 4 | class Server(val code: Int) : Error 5 | object Connectivity : Error 6 | class Unknown(val message: String) : Error 7 | } -------------------------------------------------------------------------------- /data/src/main/java/com/devexperto/architectcoders/data/PermissionChecker.kt: -------------------------------------------------------------------------------- 1 | package com.devexperto.architectcoders.data 2 | 3 | interface PermissionChecker { 4 | 5 | enum class Permission { COARSE_LOCATION } 6 | 7 | fun check(permission: Permission): Boolean 8 | } 9 | 10 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Thu Jan 27 13:36:34 CET 2022 2 | distributionBase=GRADLE_USER_HOME 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip 4 | distributionPath=wrapper/dists 5 | zipStorePath=wrapper/dists 6 | zipStoreBase=GRADLE_USER_HOME 7 | -------------------------------------------------------------------------------- /testShared/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java-library' 3 | id 'org.jetbrains.kotlin.jvm' 4 | } 5 | 6 | dependencies { 7 | implementation project(":domain") 8 | } 9 | 10 | java { 11 | sourceCompatibility = JavaVersion.VERSION_17 12 | targetCompatibility = JavaVersion.VERSION_17 13 | } -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Architect Coders 3 | Connectivity Error 4 | \"Server Error: \" 5 | \"Unknown Error: \" 6 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/debug/res/xml/network_security_config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | localhost 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/java/com/devexperto/architectcoders/data/database/MovieDatabase.kt: -------------------------------------------------------------------------------- 1 | package com.devexperto.architectcoders.data.database 2 | 3 | import androidx.room.Database 4 | import androidx.room.RoomDatabase 5 | 6 | @Database(entities = [Movie::class], version = 1, exportSchema = false) 7 | abstract class MovieDatabase : RoomDatabase() { 8 | 9 | abstract fun movieDao(): MovieDao 10 | } -------------------------------------------------------------------------------- /testShared/src/main/java/com/devexperto/architectcoders/testshared/samples.kt: -------------------------------------------------------------------------------- 1 | package com.devexperto.architectcoders.testshared 2 | 3 | import com.devexperto.architectcoders.domain.Movie 4 | 5 | val sampleMovie = Movie( 6 | 0, 7 | "Title", 8 | "Overview", 9 | "01/01/2025", 10 | "", 11 | "", 12 | "EN", 13 | "Title", 14 | 5.0, 15 | 5.1, 16 | false 17 | ) -------------------------------------------------------------------------------- /usecases/src/main/java/com/devexperto/architectcoders/usecases/GetPopularMoviesUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.devexperto.architectcoders.usecases 2 | 3 | import com.devexperto.architectcoders.data.MoviesRepository 4 | import javax.inject.Inject 5 | 6 | class GetPopularMoviesUseCase @Inject constructor(private val repository: MoviesRepository) { 7 | 8 | operator fun invoke() = repository.popularMovies 9 | } -------------------------------------------------------------------------------- /app/src/main/java/com/devexperto/architectcoders/ui/detail/DetailBindingAdapters.kt: -------------------------------------------------------------------------------- 1 | package com.devexperto.architectcoders.ui.detail 2 | 3 | import androidx.databinding.BindingAdapter 4 | import com.devexperto.architectcoders.domain.Movie 5 | 6 | @BindingAdapter("movie") 7 | fun MovieDetailInfoView.updateMovieDetails(movie: Movie?) { 8 | if (movie != null) { 9 | setMovie(movie) 10 | } 11 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_arrow_back.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_favorite_on.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | -------------------------------------------------------------------------------- /data/src/main/java/com/devexperto/architectcoders/data/datasource/MovieRemoteDataSource.kt: -------------------------------------------------------------------------------- 1 | package com.devexperto.architectcoders.data.datasource 2 | 3 | import arrow.core.Either 4 | import com.devexperto.architectcoders.domain.Error 5 | import com.devexperto.architectcoders.domain.Movie 6 | 7 | interface MovieRemoteDataSource { 8 | suspend fun findPopularMovies(region: String): Either> 9 | } -------------------------------------------------------------------------------- /app/src/main/java/com/devexperto/architectcoders/data/server/RemoteService.kt: -------------------------------------------------------------------------------- 1 | package com.devexperto.architectcoders.data.server 2 | 3 | import retrofit2.http.GET 4 | import retrofit2.http.Query 5 | 6 | interface RemoteService { 7 | 8 | @GET("discover/movie?sort_by=popularity.desc") 9 | suspend fun listPopularMovies( 10 | @Query("api_key") apiKey: String, 11 | @Query("region") region: String 12 | ): RemoteResult 13 | 14 | } -------------------------------------------------------------------------------- /app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/java/com/devexperto/architectcoders/ui/main/MainBindingAdapters.kt: -------------------------------------------------------------------------------- 1 | package com.devexperto.architectcoders.ui.main 2 | 3 | import androidx.databinding.BindingAdapter 4 | import androidx.recyclerview.widget.RecyclerView 5 | import com.devexperto.architectcoders.domain.Movie 6 | 7 | @BindingAdapter("items") 8 | fun RecyclerView.setItems(movies: List?) { 9 | if (movies != null) { 10 | (adapter as? MoviesAdapter)?.submitList(movies) 11 | } 12 | } -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF6200EE 5 | #FF3700B3 6 | #FF03DAC5 7 | #FF018786 8 | #FF000000 9 | #FFFFFFFF 10 | #E0E0E0 11 | -------------------------------------------------------------------------------- /domain/src/main/java/com/devexperto/architectcoders/domain/Movie.kt: -------------------------------------------------------------------------------- 1 | package com.devexperto.architectcoders.domain 2 | 3 | data class Movie( 4 | val id: Int, 5 | val title: String, 6 | val overview: String, 7 | val releaseDate: String, 8 | val posterPath: String, 9 | val backdropPath: String, 10 | val originalLanguage: String, 11 | val originalTitle: String, 12 | val popularity: Double, 13 | val voteAverage: Double, 14 | val favorite: Boolean 15 | ) -------------------------------------------------------------------------------- /usecases/src/main/java/com/devexperto/architectcoders/usecases/SwitchMovieFavoriteUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.devexperto.architectcoders.usecases 2 | 3 | import com.devexperto.architectcoders.data.MoviesRepository 4 | import com.devexperto.architectcoders.domain.Movie 5 | import javax.inject.Inject 6 | 7 | class SwitchMovieFavoriteUseCase @Inject constructor(private val repository: MoviesRepository) { 8 | 9 | suspend operator fun invoke(movie: Movie) = repository.switchFavorite(movie) 10 | } -------------------------------------------------------------------------------- /usecases/src/main/java/com/devexperto/architectcoders/usecases/FindMovieUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.devexperto.architectcoders.usecases 2 | 3 | import com.devexperto.architectcoders.data.MoviesRepository 4 | import com.devexperto.architectcoders.domain.Movie 5 | import kotlinx.coroutines.flow.Flow 6 | import javax.inject.Inject 7 | 8 | class FindMovieUseCase @Inject constructor(private val repository: MoviesRepository) { 9 | 10 | operator fun invoke(id: Int): Flow = repository.findById(id) 11 | } -------------------------------------------------------------------------------- /app/src/main/java/com/devexperto/architectcoders/ui/common/BindingAdapters.kt: -------------------------------------------------------------------------------- 1 | package com.devexperto.architectcoders.ui.common 2 | 3 | import android.view.View 4 | import android.widget.ImageView 5 | import androidx.databinding.BindingAdapter 6 | 7 | @BindingAdapter("url") 8 | fun ImageView.bindUrl(url: String?) { 9 | if (url != null) loadUrl(url) 10 | } 11 | 12 | @BindingAdapter("visible") 13 | fun View.setVisible(visible: Boolean?) { 14 | visibility = if (visible == true) View.VISIBLE else View.GONE 15 | } -------------------------------------------------------------------------------- /data/src/main/java/com/devexperto/architectcoders/data/datasource/MovieLocalDataSource.kt: -------------------------------------------------------------------------------- 1 | package com.devexperto.architectcoders.data.datasource 2 | 3 | import com.devexperto.architectcoders.domain.Error 4 | import com.devexperto.architectcoders.domain.Movie 5 | import kotlinx.coroutines.flow.Flow 6 | 7 | interface MovieLocalDataSource { 8 | val movies: Flow> 9 | 10 | suspend fun isEmpty(): Boolean 11 | fun findById(id: Int): Flow 12 | suspend fun save(movies: List): Error? 13 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_favorite_off.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /usecases/src/main/java/com/devexperto/architectcoders/usecases/RequestPopularMoviesUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.devexperto.architectcoders.usecases 2 | 3 | import com.devexperto.architectcoders.data.MoviesRepository 4 | import com.devexperto.architectcoders.domain.Error 5 | import javax.inject.Inject 6 | 7 | class RequestPopularMoviesUseCase @Inject constructor(private val moviesRepository: MoviesRepository) { 8 | 9 | suspend operator fun invoke(): Error? { 10 | return moviesRepository.requestPopularMovies() 11 | } 12 | } -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_nav_host.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | gradlePluginPortal() 4 | google() 5 | mavenCentral() 6 | } 7 | } 8 | dependencyResolutionManagement { 9 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 10 | repositories { 11 | google() 12 | mavenCentral() 13 | } 14 | } 15 | rootProject.name = "Architect Coders" 16 | include ':app' 17 | include ':data' 18 | include ':domain' 19 | include ':usecases' 20 | include ':testShared' 21 | include ':appTestShared' 22 | -------------------------------------------------------------------------------- /app/src/main/java/com/devexperto/architectcoders/ui/NavHostActivity.kt: -------------------------------------------------------------------------------- 1 | package com.devexperto.architectcoders.ui 2 | 3 | import android.os.Bundle 4 | import androidx.appcompat.app.AppCompatActivity 5 | import com.devexperto.architectcoders.R 6 | import dagger.hilt.android.AndroidEntryPoint 7 | 8 | @AndroidEntryPoint 9 | class NavHostActivity : AppCompatActivity() { 10 | override fun onCreate(savedInstanceState: Bundle?) { 11 | super.onCreate(savedInstanceState) 12 | setContentView(R.layout.activity_nav_host) 13 | } 14 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/com/devexperto/architectcoders/data/server/MockWebServerRule.kt: -------------------------------------------------------------------------------- 1 | package com.devexperto.architectcoders.data.server 2 | 3 | import okhttp3.mockwebserver.MockWebServer 4 | import org.junit.rules.TestWatcher 5 | import org.junit.runner.Description 6 | 7 | class MockWebServerRule : TestWatcher() { 8 | 9 | lateinit var server: MockWebServer 10 | 11 | override fun starting(description: Description) { 12 | server = MockWebServer() 13 | server.start(8080) 14 | } 15 | 16 | override fun finished(description: Description) { 17 | server.shutdown() 18 | } 19 | } -------------------------------------------------------------------------------- /app/src/main/java/com/devexperto/architectcoders/data/database/Movie.kt: -------------------------------------------------------------------------------- 1 | package com.devexperto.architectcoders.data.database 2 | 3 | import androidx.room.Entity 4 | import androidx.room.PrimaryKey 5 | 6 | @Entity 7 | data class Movie( 8 | @PrimaryKey(autoGenerate = true) val id: Int, 9 | val title: String, 10 | val overview: String, 11 | val releaseDate: String, 12 | val posterPath: String, 13 | val backdropPath: String, 14 | val originalLanguage: String, 15 | val originalTitle: String, 16 | val popularity: Double, 17 | val voteAverage: Double, 18 | val favorite: Boolean 19 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/devexperto/architectcoders/data/database/MovieDao.kt: -------------------------------------------------------------------------------- 1 | package com.devexperto.architectcoders.data.database 2 | 3 | import androidx.room.* 4 | import kotlinx.coroutines.flow.Flow 5 | 6 | @Dao 7 | interface MovieDao { 8 | 9 | @Query("SELECT * FROM Movie") 10 | fun getAll(): Flow> 11 | 12 | @Query("SELECT * FROM Movie WHERE id = :id") 13 | fun findById(id: Int): Flow 14 | 15 | @Query("SELECT COUNT(id) FROM Movie") 16 | suspend fun movieCount(): Int 17 | 18 | @Insert(onConflict = OnConflictStrategy.REPLACE) 19 | suspend fun insertMovies(movies: List) 20 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/com/devexperto/architectcoders/di/HiltTestRunner.kt: -------------------------------------------------------------------------------- 1 | package com.devexperto.architectcoders.di 2 | 3 | import android.app.Application 4 | import android.content.Context 5 | import androidx.test.runner.AndroidJUnitRunner 6 | import dagger.hilt.android.testing.HiltTestApplication 7 | 8 | // A custom runner to set up the instrumented application class for tests. 9 | @Suppress("unused") 10 | class HiltTestRunner : AndroidJUnitRunner() { 11 | 12 | override fun newApplication(cl: ClassLoader?, name: String?, context: Context?): Application { 13 | return super.newApplication(cl, HiltTestApplication::class.java.name, context) 14 | } 15 | } -------------------------------------------------------------------------------- /app/src/main/java/com/devexperto/architectcoders/data/extensions.kt: -------------------------------------------------------------------------------- 1 | package com.devexperto.architectcoders.data 2 | 3 | import arrow.core.Either 4 | import arrow.core.left 5 | import arrow.core.right 6 | import com.devexperto.architectcoders.domain.Error 7 | import retrofit2.HttpException 8 | import java.io.IOException 9 | 10 | fun Throwable.toError(): Error = when (this) { 11 | is IOException -> Error.Connectivity 12 | is HttpException -> Error.Server(code()) 13 | else -> Error.Unknown(message ?: "") 14 | } 15 | 16 | suspend fun tryCall(action: suspend () -> T): Either = try { 17 | action().right() 18 | } catch (e: Exception) { 19 | e.toError().left() 20 | } -------------------------------------------------------------------------------- /app/src/main/java/com/devexperto/architectcoders/ui/detail/di.kt: -------------------------------------------------------------------------------- 1 | package com.devexperto.architectcoders.ui.detail 2 | 3 | import androidx.lifecycle.SavedStateHandle 4 | import com.devexperto.architectcoders.di.MovieId 5 | import dagger.Module 6 | import dagger.Provides 7 | import dagger.hilt.InstallIn 8 | import dagger.hilt.android.components.ViewModelComponent 9 | import dagger.hilt.android.scopes.ViewModelScoped 10 | 11 | @Module 12 | @InstallIn(ViewModelComponent::class) 13 | class DetailViewModelModule { 14 | 15 | @Provides 16 | @ViewModelScoped 17 | @MovieId 18 | fun provideMovieId(savedStateHandle: SavedStateHandle) = 19 | DetailFragmentArgs.fromSavedStateHandle(savedStateHandle).movieId 20 | 21 | } -------------------------------------------------------------------------------- /data/build.gradle: -------------------------------------------------------------------------------- 1 | import com.devexperto.architectcoders.buildsrc.Libs 2 | 3 | plugins { 4 | id 'java-library' 5 | id 'org.jetbrains.kotlin.jvm' 6 | id 'org.jetbrains.kotlin.kapt' 7 | } 8 | 9 | dependencies { 10 | implementation project(":domain") 11 | 12 | implementation Libs.Kotlin.Coroutines.core 13 | implementation Libs.JavaX.inject 14 | implementation Libs.Arrow.core 15 | 16 | testImplementation project(":testShared") 17 | testImplementation Libs.JUnit.junit 18 | testImplementation Libs.Mockito.kotlin 19 | testImplementation Libs.Mockito.inline 20 | } 21 | 22 | java { 23 | sourceCompatibility = JavaVersion.VERSION_17 24 | targetCompatibility = JavaVersion.VERSION_17 25 | } -------------------------------------------------------------------------------- /usecases/build.gradle: -------------------------------------------------------------------------------- 1 | import com.devexperto.architectcoders.buildsrc.Libs 2 | 3 | plugins { 4 | id 'java-library' 5 | id 'org.jetbrains.kotlin.jvm' 6 | id 'org.jetbrains.kotlin.kapt' 7 | } 8 | 9 | dependencies { 10 | implementation project(":domain") 11 | implementation project(":data") 12 | 13 | implementation Libs.Kotlin.Coroutines.core 14 | implementation Libs.JavaX.inject 15 | 16 | testImplementation project(":testShared") 17 | testImplementation Libs.JUnit.junit 18 | testImplementation Libs.Mockito.kotlin 19 | testImplementation Libs.Mockito.inline 20 | } 21 | 22 | java { 23 | sourceCompatibility = JavaVersion.VERSION_17 24 | targetCompatibility = JavaVersion.VERSION_17 25 | } -------------------------------------------------------------------------------- /usecases/src/test/java/com/devexperto/architectcoders/usecases/RequestPopularMoviesUseCaseTest.kt: -------------------------------------------------------------------------------- 1 | package com.devexperto.architectcoders.usecases 2 | 3 | import com.devexperto.architectcoders.data.MoviesRepository 4 | import kotlinx.coroutines.runBlocking 5 | import org.junit.Test 6 | import org.mockito.kotlin.mock 7 | import org.mockito.kotlin.verify 8 | 9 | class RequestPopularMoviesUseCaseTest { 10 | 11 | @Test 12 | fun `Invoke calls movies repository`(): Unit = runBlocking { 13 | val moviesRepository = mock() 14 | val requestPopularMoviesUseCase = RequestPopularMoviesUseCase(moviesRepository) 15 | 16 | requestPopularMoviesUseCase() 17 | 18 | verify(moviesRepository).requestPopularMovies() 19 | } 20 | } -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /usecases/src/test/java/com/devexperto/architectcoders/usecases/FindMovieUseCaseTest.kt: -------------------------------------------------------------------------------- 1 | package com.devexperto.architectcoders.usecases 2 | 3 | import com.devexperto.architectcoders.testshared.sampleMovie 4 | import kotlinx.coroutines.flow.flowOf 5 | import kotlinx.coroutines.runBlocking 6 | import org.junit.Assert.assertEquals 7 | import org.junit.Test 8 | import org.mockito.kotlin.doReturn 9 | import org.mockito.kotlin.mock 10 | 11 | class FindMovieUseCaseTest { 12 | 13 | @Test 14 | fun `Invoke calls movies repository`(): Unit = runBlocking { 15 | val movie = flowOf(sampleMovie.copy(id = 1)) 16 | val findMovieUseCase = FindMovieUseCase(mock() { 17 | on { findById(1) } doReturn (movie) 18 | }) 19 | 20 | val result = findMovieUseCase(1) 21 | 22 | assertEquals(movie, result) 23 | } 24 | } -------------------------------------------------------------------------------- /appTestShared/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /data/src/main/java/com/devexperto/architectcoders/data/RegionRepository.kt: -------------------------------------------------------------------------------- 1 | package com.devexperto.architectcoders.data 2 | 3 | import com.devexperto.architectcoders.data.PermissionChecker.Permission.COARSE_LOCATION 4 | import com.devexperto.architectcoders.data.datasource.LocationDataSource 5 | import javax.inject.Inject 6 | 7 | class RegionRepository @Inject constructor( 8 | private val locationDataSource: LocationDataSource, 9 | private val permissionChecker: PermissionChecker 10 | ) { 11 | 12 | companion object { 13 | const val DEFAULT_REGION = "US" 14 | } 15 | 16 | suspend fun findLastRegion(): String { 17 | return if (permissionChecker.check(COARSE_LOCATION)) { 18 | locationDataSource.findLastRegion() ?: DEFAULT_REGION 19 | } else { 20 | DEFAULT_REGION 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /app/src/main/java/com/devexperto/architectcoders/data/AndroidPermissionChecker.kt: -------------------------------------------------------------------------------- 1 | package com.devexperto.architectcoders.data 2 | 3 | import android.Manifest 4 | import android.app.Application 5 | import android.content.pm.PackageManager 6 | import androidx.core.content.ContextCompat 7 | import javax.inject.Inject 8 | 9 | class AndroidPermissionChecker @Inject constructor(private val application: Application) : 10 | PermissionChecker { 11 | 12 | override fun check(permission: PermissionChecker.Permission): Boolean = 13 | ContextCompat.checkSelfPermission( 14 | application, 15 | permission.toAndroidId() 16 | ) == PackageManager.PERMISSION_GRANTED 17 | } 18 | 19 | private fun PermissionChecker.Permission.toAndroidId() = when (this) { 20 | PermissionChecker.Permission.COARSE_LOCATION -> Manifest.permission.ACCESS_COARSE_LOCATION 21 | } -------------------------------------------------------------------------------- /usecases/src/test/java/com/devexperto/architectcoders/usecases/GetPopularMoviesUseCaseTest.kt: -------------------------------------------------------------------------------- 1 | package com.devexperto.architectcoders.usecases 2 | 3 | import com.devexperto.architectcoders.testshared.sampleMovie 4 | import kotlinx.coroutines.flow.flowOf 5 | import kotlinx.coroutines.runBlocking 6 | import org.junit.Assert 7 | import org.junit.Test 8 | import org.mockito.kotlin.doReturn 9 | import org.mockito.kotlin.mock 10 | 11 | class GetPopularMoviesUseCaseTest { 12 | 13 | @Test 14 | fun `Invoke calls movies repository`(): Unit = runBlocking { 15 | val movies = flowOf(listOf(sampleMovie.copy(id = 1))) 16 | val getPopularMoviesUseCase = GetPopularMoviesUseCase(mock { 17 | on { popularMovies } doReturn movies 18 | }) 19 | 20 | val result = getPopularMoviesUseCase() 21 | 22 | Assert.assertEquals(movies, result) 23 | } 24 | } -------------------------------------------------------------------------------- /usecases/src/test/java/com/devexperto/architectcoders/usecases/SwitchMovieFavoriteUseCaseTest.kt: -------------------------------------------------------------------------------- 1 | package com.devexperto.architectcoders.usecases 2 | 3 | import com.devexperto.architectcoders.data.MoviesRepository 4 | import com.devexperto.architectcoders.testshared.sampleMovie 5 | import kotlinx.coroutines.runBlocking 6 | import org.junit.Test 7 | import org.mockito.kotlin.mock 8 | import org.mockito.kotlin.verify 9 | 10 | class SwitchMovieFavoriteUseCaseTest { 11 | 12 | @Test 13 | fun `Invoke calls movies repository`(): Unit = runBlocking { 14 | val movie = sampleMovie.copy(id = 1) 15 | val moviesRepository = mock() 16 | val switchMovieFavoriteUseCase = SwitchMovieFavoriteUseCase(moviesRepository) 17 | 18 | switchMovieFavoriteUseCase(movie) 19 | 20 | verify(moviesRepository).switchFavorite(movie) 21 | } 22 | } -------------------------------------------------------------------------------- /app/src/test/java/com/devexperto/architectcoders/testrules/CoroutinesTestRule.kt: -------------------------------------------------------------------------------- 1 | package com.devexperto.architectcoders.testrules 2 | 3 | import kotlinx.coroutines.Dispatchers 4 | import kotlinx.coroutines.ExperimentalCoroutinesApi 5 | import kotlinx.coroutines.test.StandardTestDispatcher 6 | import kotlinx.coroutines.test.resetMain 7 | import kotlinx.coroutines.test.setMain 8 | import org.junit.rules.TestWatcher 9 | import org.junit.runner.Description 10 | 11 | @ExperimentalCoroutinesApi 12 | class CoroutinesTestRule : TestWatcher() { 13 | 14 | val testDispatcher = StandardTestDispatcher() 15 | 16 | override fun starting(description: Description) { 17 | super.starting(description) 18 | Dispatchers.setMain(testDispatcher) 19 | } 20 | 21 | override fun finished(description: Description) { 22 | super.finished(description) 23 | Dispatchers.resetMain() 24 | } 25 | } -------------------------------------------------------------------------------- /appTestShared/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.library' 3 | id 'org.jetbrains.kotlin.android' 4 | } 5 | 6 | android { 7 | namespace 'com.devexperto.architectcoders.appTestShared' 8 | compileSdk 31 9 | 10 | defaultConfig { 11 | minSdk 23 12 | targetSdk 31 13 | consumerProguardFiles "consumer-rules.pro" 14 | } 15 | 16 | buildTypes { 17 | release { 18 | minifyEnabled false 19 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 20 | } 21 | } 22 | compileOptions { 23 | sourceCompatibility JavaVersion.VERSION_1_8 24 | targetCompatibility JavaVersion.VERSION_1_8 25 | } 26 | kotlinOptions { 27 | jvmTarget = '1.8' 28 | } 29 | } 30 | 31 | dependencies { 32 | implementation project(":app") 33 | implementation project(":data") 34 | } -------------------------------------------------------------------------------- /app/src/main/res/values-night/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | -------------------------------------------------------------------------------- /app/src/main/java/com/devexperto/architectcoders/ui/common/PermissionRequester.kt: -------------------------------------------------------------------------------- 1 | package com.devexperto.architectcoders.ui.common 2 | 3 | import androidx.activity.result.contract.ActivityResultContracts 4 | import androidx.fragment.app.Fragment 5 | import kotlinx.coroutines.suspendCancellableCoroutine 6 | import kotlin.coroutines.resume 7 | 8 | class PermissionRequester(fragment: Fragment, private val permission: String) { 9 | 10 | private var onRequest: (Boolean) -> Unit = {} 11 | private val launcher = 12 | fragment.registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted -> 13 | onRequest(isGranted) 14 | } 15 | 16 | suspend fun request(): Boolean = 17 | suspendCancellableCoroutine { continuation -> 18 | onRequest = { 19 | continuation.resume(it) 20 | } 21 | launcher.launch(permission) 22 | } 23 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 15 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/java/com/devexperto/architectcoders/data/server/RemoteResult.kt: -------------------------------------------------------------------------------- 1 | package com.devexperto.architectcoders.data.server 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | data class RemoteResult( 6 | val page: Int, 7 | val results: List, 8 | @SerializedName("total_pages") val totalPages: Int, 9 | @SerializedName("total_results") val totalResults: Int 10 | ) 11 | 12 | data class RemoteMovie( 13 | val adult: Boolean, 14 | @SerializedName("backdrop_path") val backdropPath: String?, 15 | @SerializedName("genre_ids") val genreIds: List, 16 | val id: Int, 17 | @SerializedName("original_language") val originalLanguage: String, 18 | @SerializedName("original_title") val originalTitle: String, 19 | val overview: String, 20 | val popularity: Double, 21 | @SerializedName("poster_path") val posterPath: String, 22 | @SerializedName("release_date") val releaseDate: String, 23 | val title: String, 24 | val video: Boolean, 25 | @SerializedName("vote_average") val voteAverage: Double, 26 | @SerializedName("vote_count") val voteCount: Int 27 | ) -------------------------------------------------------------------------------- /app/src/androidTest/java/com/devexperto/architectcoders/data/server/mockResponseExtensions.kt: -------------------------------------------------------------------------------- 1 | package com.devexperto.architectcoders 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import okhttp3.mockwebserver.MockResponse 5 | import java.io.BufferedReader 6 | import java.io.InputStreamReader 7 | import java.nio.charset.StandardCharsets 8 | 9 | fun MockResponse.fromJson(jsonFile: String): MockResponse = 10 | setBody(readJsonFile(jsonFile)) 11 | 12 | private fun readJsonFile(jsonFilePath: String): String { 13 | val context = InstrumentationRegistry.getInstrumentation().context 14 | 15 | var br: BufferedReader? = null 16 | 17 | try { 18 | br = BufferedReader( 19 | InputStreamReader( 20 | context.assets.open( 21 | jsonFilePath 22 | ), StandardCharsets.UTF_8 23 | ) 24 | ) 25 | var line: String? 26 | val text = StringBuilder() 27 | 28 | do { 29 | line = br.readLine() 30 | line?.let { text.append(line) } 31 | } while (line != null) 32 | br.close() 33 | return text.toString() 34 | } finally { 35 | br?.close() 36 | } 37 | } -------------------------------------------------------------------------------- /app/src/main/java/com/devexperto/architectcoders/ui/detail/MovieDetailInfoView.kt: -------------------------------------------------------------------------------- 1 | package com.devexperto.architectcoders.ui.detail 2 | 3 | import android.content.Context 4 | import android.util.AttributeSet 5 | import androidx.appcompat.widget.AppCompatTextView 6 | import androidx.core.text.bold 7 | import androidx.core.text.buildSpannedString 8 | import com.devexperto.architectcoders.domain.Movie 9 | 10 | class MovieDetailInfoView @JvmOverloads constructor( 11 | context: Context, 12 | attrs: AttributeSet? = null, 13 | defStyleAttr: Int = 0 14 | ) : AppCompatTextView(context, attrs, defStyleAttr) { 15 | 16 | fun setMovie(movie: Movie) = movie.apply { 17 | text = buildSpannedString { 18 | 19 | bold { append("Original language: ") } 20 | appendLine(originalLanguage) 21 | 22 | bold { append("Original title: ") } 23 | appendLine(originalTitle) 24 | 25 | bold { append("Release date: ") } 26 | appendLine(releaseDate) 27 | 28 | bold { append("Popularity: ") } 29 | appendLine(popularity.toString()) 30 | 31 | bold { append("Vote Average: ") } 32 | append(voteAverage.toString()) 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 17 | 18 | 23 | 24 |