├── git ├── app ├── .gitignore ├── src │ ├── test │ │ ├── resources │ │ │ └── mockito-extensions │ │ │ │ └── org.mockito.plugins.MockMaker │ │ └── java │ │ │ └── ahmed │ │ │ └── atwa │ │ │ └── popularmovies │ │ │ ├── CoroutineTestingRule.kt │ │ │ └── movies │ │ │ ├── domain │ │ │ └── MovieMapperImpTest.kt │ │ │ └── presentation │ │ │ └── MoviesViewModelTest.kt │ ├── main │ │ ├── res │ │ │ ├── drawable │ │ │ │ ├── like.png │ │ │ │ ├── dislike.png │ │ │ │ ├── placeholder.png │ │ │ │ └── ic_launcher_background.xml │ │ │ ├── mipmap-hdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-mdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxxhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── values │ │ │ │ ├── dimens.xml │ │ │ │ ├── styles.xml │ │ │ │ ├── colors.xml │ │ │ │ └── strings.xml │ │ │ ├── mipmap-anydpi-v26 │ │ │ │ ├── ic_launcher.xml │ │ │ │ └── ic_launcher_round.xml │ │ │ ├── menu │ │ │ │ └── search_menu.xml │ │ │ ├── layout │ │ │ │ ├── progress_dialog.xml │ │ │ │ ├── activity_main.xml │ │ │ │ ├── item_movie_view.xml │ │ │ │ ├── load_state_layout.xml │ │ │ │ ├── fragment_movies.xml │ │ │ │ ├── item_trailer_view.xml │ │ │ │ └── fragment_detail.xml │ │ │ ├── navigation │ │ │ │ └── movies_graph.xml │ │ │ └── drawable-v24 │ │ │ │ └── ic_launcher_foreground.xml │ │ ├── java │ │ │ └── ahmed │ │ │ │ └── atwa │ │ │ │ └── popularmovies │ │ │ │ ├── utils │ │ │ │ ├── network │ │ │ │ │ ├── INetworkErrorHandler.kt │ │ │ │ │ ├── ResultType.kt │ │ │ │ │ ├── NoConnectionException.kt │ │ │ │ │ ├── UrlModule.kt │ │ │ │ │ ├── NetworkErrorHandler.kt │ │ │ │ │ ├── NetworkRouter.kt │ │ │ │ │ ├── NetworkConnectionInterceptor.kt │ │ │ │ │ └── NetworkModule.kt │ │ │ │ ├── commons │ │ │ │ │ ├── CoroutineDispatcher.kt │ │ │ │ │ ├── TestDispatcher.kt │ │ │ │ │ ├── AppConstants.kt │ │ │ │ │ ├── ViewModelProviderFactory.kt │ │ │ │ │ ├── AppUtils.kt │ │ │ │ │ ├── GridSpacingItemDecoration.kt │ │ │ │ │ └── LiveEvent.kt │ │ │ │ ├── database │ │ │ │ │ ├── MovieDao.kt │ │ │ │ │ ├── MovieDb.kt │ │ │ │ │ └── DbModule.kt │ │ │ │ └── di │ │ │ │ │ ├── module │ │ │ │ │ ├── AppModule.kt │ │ │ │ │ └── RepoModule.kt │ │ │ │ │ ├── builder │ │ │ │ │ └── ActivityBuilder.kt │ │ │ │ │ └── component │ │ │ │ │ └── AppComponent.kt │ │ │ │ ├── detail │ │ │ │ ├── data │ │ │ │ │ ├── TrailerResponse.kt │ │ │ │ │ ├── TrailerRemote.kt │ │ │ │ │ └── TrailerApi.kt │ │ │ │ ├── presentation │ │ │ │ │ ├── DetailViewState.kt │ │ │ │ │ ├── TrailerAdapter.kt │ │ │ │ │ └── DetailFragment.kt │ │ │ │ └── di │ │ │ │ │ ├── DetailFragmentProvider.kt │ │ │ │ │ └── DetailFragmentModule.kt │ │ │ │ ├── movies │ │ │ │ ├── presentation │ │ │ │ │ ├── MoviesViewState.kt │ │ │ │ │ ├── MovieStateAdapter.kt │ │ │ │ │ ├── MovieAdapter.kt │ │ │ │ │ ├── MoviesViewModel.kt │ │ │ │ │ └── MoviesFragment.kt │ │ │ │ ├── data │ │ │ │ │ ├── MovieResponse.kt │ │ │ │ │ ├── MovieRepo.kt │ │ │ │ │ ├── MovieApi.kt │ │ │ │ │ ├── MovieFilterSource.kt │ │ │ │ │ ├── MoviePagingSource.kt │ │ │ │ │ ├── Movie.kt │ │ │ │ │ └── MovieRepoImp.kt │ │ │ │ ├── di │ │ │ │ │ ├── MoviesFragmentProvider.kt │ │ │ │ │ ├── MovieSourceModule.kt │ │ │ │ │ └── MoviesFragmentModule.kt │ │ │ │ └── domain │ │ │ │ │ └── MovieSourceFactory.kt │ │ │ │ ├── base │ │ │ │ ├── BaseViewModel.kt │ │ │ │ ├── BaseFragment.kt │ │ │ │ └── BaseActivity.kt │ │ │ │ ├── PopMovApp.kt │ │ │ │ └── main │ │ │ │ ├── di │ │ │ │ └── MainActivityModule.kt │ │ │ │ └── presentation │ │ │ │ └── MainActivity.kt │ │ └── AndroidManifest.xml │ └── androidTest │ │ └── java │ │ └── ahmed │ │ └── atwa │ │ └── popularmovies │ │ ├── util │ │ ├── UiRunner.kt │ │ └── CustomMatcher.kt │ │ ├── utils │ │ └── di │ │ │ ├── MockDbModule.kt │ │ │ ├── MockUrlModule.kt │ │ │ └── TestAppComponent.kt │ │ ├── movie │ │ └── presentation │ │ │ └── MoviesFragmentTest.kt │ │ ├── main │ │ └── presentation │ │ │ └── MainActivityTest.kt │ │ └── detail │ │ └── presentation │ │ └── DetailFragmentTest.kt ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── .idea │ ├── modules.xml │ ├── misc.xml │ ├── runConfigurations.xml │ ├── gradle.xml │ ├── codeStyles │ │ └── Project.xml │ └── workspace.xml ├── local.properties ├── proguard-rules.pro ├── gradlew.bat ├── build.gradle └── gradlew ├── detail.png ├── movies.png ├── detail_liked.png ├── movies_search.png ├── movies_search_hint.png ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .idea ├── caches │ └── build_file_checksums.ser ├── encodings.xml ├── vcs.xml ├── compiler.xml ├── runConfigurations.xml ├── DtonatorPreferences.xml ├── gradle.xml ├── jarRepositories.xml ├── navEditor.xml ├── codeStyles │ └── Project.xml └── misc.xml ├── settings.gradle ├── .gitignore ├── gradle.properties ├── README.md ├── gradlew.bat └── gradlew /git: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /detail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Atwa/PopularMovies-MVVM-Sample/HEAD/detail.png -------------------------------------------------------------------------------- /movies.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Atwa/PopularMovies-MVVM-Sample/HEAD/movies.png -------------------------------------------------------------------------------- /app/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker: -------------------------------------------------------------------------------- 1 | mock-maker-inline 2 | -------------------------------------------------------------------------------- /detail_liked.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Atwa/PopularMovies-MVVM-Sample/HEAD/detail_liked.png -------------------------------------------------------------------------------- /movies_search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Atwa/PopularMovies-MVVM-Sample/HEAD/movies_search.png -------------------------------------------------------------------------------- /movies_search_hint.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Atwa/PopularMovies-MVVM-Sample/HEAD/movies_search_hint.png -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Atwa/PopularMovies-MVVM-Sample/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /app/src/main/res/drawable/like.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Atwa/PopularMovies-MVVM-Sample/HEAD/app/src/main/res/drawable/like.png -------------------------------------------------------------------------------- /.idea/caches/build_file_checksums.ser: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Atwa/PopularMovies-MVVM-Sample/HEAD/.idea/caches/build_file_checksums.ser -------------------------------------------------------------------------------- /app/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Atwa/PopularMovies-MVVM-Sample/HEAD/app/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /app/src/main/res/drawable/dislike.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Atwa/PopularMovies-MVVM-Sample/HEAD/app/src/main/res/drawable/dislike.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/placeholder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Atwa/PopularMovies-MVVM-Sample/HEAD/app/src/main/res/drawable/placeholder.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Atwa/PopularMovies-MVVM-Sample/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Atwa/PopularMovies-MVVM-Sample/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Atwa/PopularMovies-MVVM-Sample/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Atwa/PopularMovies-MVVM-Sample/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Atwa/PopularMovies-MVVM-Sample/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Atwa/PopularMovies-MVVM-Sample/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Atwa/PopularMovies-MVVM-Sample/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Atwa/PopularMovies-MVVM-Sample/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Atwa/PopularMovies-MVVM-Sample/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Atwa/PopularMovies-MVVM-Sample/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * * 3 | * * Created by Ahmed Atwa on 19/10/2018 4 | * * Copyright (c) 19/10/2018 . All rights reserved. 5 | * * 6 | */ 7 | 8 | include ':app' 9 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/libraries 5 | /.idea/modules.xml 6 | /.idea/workspace.xml 7 | .DS_Store 8 | /build 9 | /captures 10 | .externalNativeBuild 11 | 12 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/java/ahmed/atwa/popularmovies/utils/network/INetworkErrorHandler.kt: -------------------------------------------------------------------------------- 1 | package ahmed.atwa.popularmovies.utils.network 2 | 3 | import retrofit2.Response 4 | 5 | interface INetworkErrorHandler { 6 | fun resolveErrorMessage(response: Response): ResultType.Error 7 | } -------------------------------------------------------------------------------- /app/.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sun Jun 13 21:59:53 EET 2021 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.1-bin.zip 7 | -------------------------------------------------------------------------------- /app/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Mon Dec 23 16:04:48 EET 2019 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip 7 | -------------------------------------------------------------------------------- /app/.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | -------------------------------------------------------------------------------- /app/src/main/java/ahmed/atwa/popularmovies/utils/network/ResultType.kt: -------------------------------------------------------------------------------- 1 | package ahmed.atwa.popularmovies.utils.network 2 | 3 | /** 4 | * Created by Ahmed Atwa on 11/7/2019. 5 | */ 6 | sealed class ResultType { 7 | data class Success(val data: T) : ResultType() 8 | data class Error(val error: Exception) : ResultType() 9 | } -------------------------------------------------------------------------------- /app/src/main/java/ahmed/atwa/popularmovies/utils/commons/CoroutineDispatcher.kt: -------------------------------------------------------------------------------- 1 | package ahmed.atwa.popularmovies.utils.commons 2 | 3 | import kotlinx.coroutines.Dispatchers 4 | import kotlin.coroutines.CoroutineContext 5 | 6 | open class CoroutineDispatcher { 7 | open val Main: CoroutineContext by lazy { Dispatchers.Main } 8 | open val IO: CoroutineContext by lazy { Dispatchers.IO } 9 | } -------------------------------------------------------------------------------- /app/local.properties: -------------------------------------------------------------------------------- 1 | ## This file must *NOT* be checked into Version Control Systems, 2 | # as it contains information specific to your local configuration. 3 | # 4 | # Location of the SDK. This is only used by Gradle. 5 | # For customization when using a Version Control System, please read the 6 | # header note. 7 | #Mon Dec 23 16:04:24 EET 2019 8 | sdk.dir=C\:\\Users\\aatwa\\AppData\\Local\\Android\\Sdk 9 | -------------------------------------------------------------------------------- /app/src/main/java/ahmed/atwa/popularmovies/utils/commons/TestDispatcher.kt: -------------------------------------------------------------------------------- 1 | package ahmed.atwa.popularmovies.utils.commons 2 | 3 | import kotlinx.coroutines.Dispatchers 4 | import kotlin.coroutines.CoroutineContext 5 | 6 | class TestDispatcher : CoroutineDispatcher() { 7 | override val Main: CoroutineContext = Dispatchers.Unconfined 8 | override val IO: CoroutineContext = Dispatchers.Unconfined 9 | } -------------------------------------------------------------------------------- /app/src/main/java/ahmed/atwa/popularmovies/detail/data/TrailerResponse.kt: -------------------------------------------------------------------------------- 1 | package ahmed.atwa.popularmovies.detail.data 2 | 3 | import android.os.Parcelable 4 | import kotlinx.android.parcel.Parcelize 5 | 6 | /** 7 | * Created by Ahmed Atwa on 10/19/18. 8 | */ 9 | @Parcelize 10 | data class TrailerResponse( 11 | var id: Int, 12 | var results: List 13 | ) : Parcelable 14 | -------------------------------------------------------------------------------- /app/src/main/java/ahmed/atwa/popularmovies/utils/network/NoConnectionException.kt: -------------------------------------------------------------------------------- 1 | package ahmed.atwa.popularmovies.utils.network 2 | 3 | import java.io.IOException 4 | 5 | /** 6 | * Created by Ahmed Atwa on 11/7/2019. 7 | */ 8 | class NoConnectionException : IOException() { 9 | 10 | // You can send any message whatever you want from here. 11 | override val message: String 12 | get() = "No Internet Connection" 13 | } -------------------------------------------------------------------------------- /app/src/main/java/ahmed/atwa/popularmovies/movies/presentation/MoviesViewState.kt: -------------------------------------------------------------------------------- 1 | package ahmed.atwa.popularmovies.movies.presentation 2 | 3 | import ahmed.atwa.popularmovies.movies.data.Movie 4 | import androidx.paging.PagingData 5 | 6 | 7 | sealed class MoviesViewState { 8 | class FetchingMoviesError(val errorMessage: String?) : MoviesViewState() 9 | class FetchingMoviesSuccess(val movies: PagingData) : MoviesViewState() 10 | } 11 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16dp 4 | 8dp 5 | 150dp 6 | 250dp 7 | 30dp 8 | 60dp 9 | 56dp 10 | -------------------------------------------------------------------------------- /app/src/main/java/ahmed/atwa/popularmovies/movies/data/MovieResponse.kt: -------------------------------------------------------------------------------- 1 | package ahmed.atwa.popularmovies.movies.data 2 | 3 | 4 | import android.os.Parcelable 5 | import kotlinx.android.parcel.Parcelize 6 | 7 | /** 8 | * Created by Ahmed Atwa on 10/19/18. 9 | */ 10 | @Parcelize 11 | data class MovieResponse( 12 | var page: Int?, 13 | var total_results: Int?, 14 | var total_pages: Int, 15 | var results: ArrayList) : Parcelable 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/java/ahmed/atwa/popularmovies/utils/database/MovieDao.kt: -------------------------------------------------------------------------------- 1 | package ahmed.atwa.popularmovies.utils.database 2 | 3 | import ahmed.atwa.popularmovies.movies.data.Movie 4 | import androidx.room.* 5 | 6 | @Dao 7 | interface MovieDao { 8 | 9 | @Insert(onConflict = OnConflictStrategy.REPLACE) 10 | fun insertMovie(movie: Movie) 11 | 12 | @Query("SELECT id FROM movies") 13 | fun fetchFavouriteMovies(): List 14 | 15 | @Delete() 16 | fun removeMovie(movie: Movie) 17 | 18 | } -------------------------------------------------------------------------------- /app/src/main/java/ahmed/atwa/popularmovies/utils/database/MovieDb.kt: -------------------------------------------------------------------------------- 1 | package ahmed.atwa.popularmovies.utils.database 2 | 3 | import ahmed.atwa.popularmovies.movies.data.Movie 4 | import androidx.room.* 5 | import javax.inject.Singleton 6 | 7 | /** 8 | * Created by Ahmed Atwa on 10/19/18. 9 | */ 10 | 11 | @Singleton 12 | @Database(entities = [(Movie::class)], version = 1, exportSchema = false) 13 | abstract class MovieDb : RoomDatabase() { 14 | abstract fun movieDao(): MovieDao 15 | } 16 | 17 | 18 | -------------------------------------------------------------------------------- /app/src/main/res/menu/search_menu.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 10 | -------------------------------------------------------------------------------- /app/src/main/java/ahmed/atwa/popularmovies/detail/data/TrailerRemote.kt: -------------------------------------------------------------------------------- 1 | package ahmed.atwa.popularmovies.detail.data 2 | 3 | import android.os.Parcelable 4 | import kotlinx.android.parcel.Parcelize 5 | 6 | @Parcelize 7 | data class TrailerRemote( 8 | val id: String, 9 | val iso_639_1: String, 10 | val iso_3166_1: String, 11 | val key: String, 12 | val name: String, 13 | val site: String, 14 | var size: Int, 15 | val type: String 16 | ) : Parcelable -------------------------------------------------------------------------------- /app/src/main/java/ahmed/atwa/popularmovies/detail/presentation/DetailViewState.kt: -------------------------------------------------------------------------------- 1 | package ahmed.atwa.popularmovies.detail.presentation 2 | 3 | import ahmed.atwa.popularmovies.detail.data.TrailerRemote 4 | 5 | sealed class DetailViewState { 6 | object TrailersFetchedError : DetailViewState() 7 | class TrailersFetchedSuccess(val trailers: List) : DetailViewState() 8 | class LikeState(val isLiked: Boolean) : DetailViewState() 9 | class MessageRes(val resId: Int) : DetailViewState() 10 | } 11 | -------------------------------------------------------------------------------- /app/src/main/java/ahmed/atwa/popularmovies/utils/commons/AppConstants.kt: -------------------------------------------------------------------------------- 1 | package ahmed.atwa.popularmovies.utils.commons 2 | 3 | /** 4 | * Created by Ahmed Atwa on 10/19/18. 5 | */ 6 | 7 | class AppConstants { 8 | 9 | companion object { 10 | 11 | const val DB_NAME = "PopMov.db" 12 | const val DB_MOCK_NAME = "PopMovMock.db" 13 | const val API_KEY_QUERY: String = "api_key" 14 | const val BASE_URL_KEY: String = ("BASE_URL") 15 | const val DB_NAME_KEY: String = ("Database_name") 16 | } 17 | 18 | 19 | } -------------------------------------------------------------------------------- /app/src/main/java/ahmed/atwa/popularmovies/utils/network/UrlModule.kt: -------------------------------------------------------------------------------- 1 | package ahmed.atwa.popularmovies.utils.network 2 | 3 | import ahmed.atwa.popularmovies.BuildConfig 4 | import ahmed.atwa.popularmovies.utils.commons.AppConstants.Companion.BASE_URL_KEY 5 | import dagger.Module 6 | import dagger.Provides 7 | import javax.inject.Named 8 | import javax.inject.Singleton 9 | 10 | @Module 11 | class UrlModule { 12 | @Provides 13 | @Singleton 14 | @Named(BASE_URL_KEY) 15 | fun provideBaseUrl(): String { 16 | return BuildConfig.BASE_URL 17 | } 18 | } -------------------------------------------------------------------------------- /app/src/main/java/ahmed/atwa/popularmovies/utils/di/module/AppModule.kt: -------------------------------------------------------------------------------- 1 | 2 | 3 | package ahmed.atwa.popularmovies.utils.di.module 4 | 5 | import android.app.Application 6 | import android.content.Context 7 | import dagger.Module 8 | import dagger.Provides 9 | import javax.inject.Singleton 10 | 11 | 12 | /** 13 | * Created by Ahmed Atwa on 10/19/18. 14 | */ 15 | 16 | 17 | @Module 18 | class AppModule { 19 | 20 | 21 | @Provides 22 | @Singleton 23 | internal fun provideContext(application: Application): Context { 24 | return application 25 | } 26 | 27 | } -------------------------------------------------------------------------------- /app/src/main/java/ahmed/atwa/popularmovies/movies/data/MovieRepo.kt: -------------------------------------------------------------------------------- 1 | package ahmed.atwa.popularmovies.movies.data 2 | 3 | import ahmed.atwa.popularmovies.detail.data.TrailerResponse 4 | import ahmed.atwa.popularmovies.utils.network.ResultType 5 | 6 | interface MovieRepo { 7 | suspend fun getPopularMovies(page: Int): ResultType 8 | suspend fun getFilteredPopularMovies(filterText: String): MovieResponse? 9 | suspend fun fetchMovieTrailers(movieId: Int): ResultType? 10 | fun isMovieLiked(id: Int): Boolean 11 | fun changeLikeState(movie: Movie, newLikeState: Boolean) 12 | } -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | 11 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /app/.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | @color/blue_black_light 4 | @color/black 5 | #0d7cb3 6 | #000000 7 | #ffffff 8 | #2a2a2a 9 | #F06423 10 | #D85A1B 11 | #13171d 12 | #516179 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/java/ahmed/atwa/popularmovies/detail/di/DetailFragmentProvider.kt: -------------------------------------------------------------------------------- 1 | 2 | 3 | package ahmed.atwa.popularmovies.detail.di 4 | 5 | import ahmed.atwa.popularmovies.detail.di.DetailFragmentModule 6 | import ahmed.atwa.popularmovies.detail.presentation.DetailFragment 7 | import dagger.Module 8 | import dagger.android.ContributesAndroidInjector 9 | 10 | /** 11 | * Created by Ahmed Atwa on 10/19/18. 12 | */ 13 | 14 | @Module 15 | abstract class DetailFragmentProvider { 16 | 17 | @ContributesAndroidInjector(modules =[(DetailFragmentModule::class)]) 18 | internal abstract fun provideDetailFragmentFactory(): DetailFragment 19 | 20 | } -------------------------------------------------------------------------------- /app/src/main/res/layout/progress_dialog.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | 16 | -------------------------------------------------------------------------------- /app/src/main/java/ahmed/atwa/popularmovies/base/BaseViewModel.kt: -------------------------------------------------------------------------------- 1 | package ahmed.atwa.popularmovies.base 2 | 3 | import ahmed.atwa.popularmovies.utils.commons.toSingleEvent 4 | import androidx.lifecycle.LiveData 5 | import androidx.lifecycle.MutableLiveData 6 | import androidx.lifecycle.ViewModel 7 | 8 | /** 9 | * Created by Ahmed Atwa on 10/19/18. 10 | */ 11 | 12 | abstract class BaseViewModel() : ViewModel() { 13 | 14 | private val mUiState = MutableLiveData() 15 | open val uiState: LiveData = mUiState.toSingleEvent() 16 | 17 | fun updateViewState(result: T) { 18 | mUiState.value = result 19 | } 20 | 21 | } -------------------------------------------------------------------------------- /.idea/DtonatorPreferences.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/java/ahmed/atwa/popularmovies/utils/commons/ViewModelProviderFactory.kt: -------------------------------------------------------------------------------- 1 | 2 | 3 | package ahmed.atwa.popularmovies.utils.commons 4 | 5 | import androidx.lifecycle.ViewModel 6 | import androidx.lifecycle.ViewModelProvider 7 | 8 | /** 9 | * Created by Ahmed Atwa on 10/19/18. 10 | */ 11 | 12 | class ViewModelProviderFactory(private var viewModel: V) : ViewModelProvider.Factory { 13 | 14 | override fun create(modelClass: Class): T { 15 | if (modelClass.isAssignableFrom(viewModel.javaClass)) { 16 | return viewModel as T 17 | } 18 | throw IllegalArgumentException("Unknown class name") 19 | } 20 | } -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | PopularMovies 3 | Released in : %s 4 | Votes : %s 5 | Some error occurred ! 6 | Movie Removed from favourites 7 | Movie added to favourites 8 | No movies found 9 | No trailers found 10 | Retry 11 | Search movies 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/java/ahmed/atwa/popularmovies/detail/data/TrailerApi.kt: -------------------------------------------------------------------------------- 1 | package ahmed.atwa.popularmovies.detail.data 2 | 3 | import ahmed.atwa.popularmovies.utils.commons.AppConstants 4 | import retrofit2.Response 5 | import retrofit2.http.GET 6 | import retrofit2.http.Path 7 | import retrofit2.http.Query 8 | 9 | /** 10 | * Created by Ahmed Atwa on 10/17/2019. 11 | */ 12 | 13 | interface TrailerApi { 14 | 15 | 16 | companion object { 17 | 18 | const val GET_MOVIE_TRAILERS: String = ("movie/{movie_id}/videos") 19 | } 20 | 21 | @GET(GET_MOVIE_TRAILERS) 22 | suspend fun getMovieTrailer(@Path("movie_id") id: Int, @Query(AppConstants.API_KEY_QUERY) apiKey: String): Response 23 | } -------------------------------------------------------------------------------- /app/src/main/java/ahmed/atwa/popularmovies/movies/di/MoviesFragmentProvider.kt: -------------------------------------------------------------------------------- 1 | 2 | 3 | package ahmed.atwa.popularmovies.movies.di 4 | 5 | import ahmed.atwa.popularmovies.movies.di.MoviesFragmentModule 6 | import ahmed.atwa.popularmovies.movies.presentation.MoviesFragment 7 | import ahmed.atwa.popularmovies.utils.network.UrlModule 8 | import dagger.Module 9 | import dagger.android.ContributesAndroidInjector 10 | 11 | /** 12 | * Created by Ahmed Atwa on 10/19/18. 13 | */ 14 | 15 | @Module 16 | abstract class MoviesFragmentProvider { 17 | 18 | @ContributesAndroidInjector(modules =[(MoviesFragmentModule::class),(MovieSourceModule::class),]) 19 | internal abstract fun provideMainFragmentFactory(): MoviesFragment 20 | 21 | } -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /app/.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 19 | -------------------------------------------------------------------------------- /app/src/androidTest/java/ahmed/atwa/popularmovies/util/UiRunner.kt: -------------------------------------------------------------------------------- 1 | package ahmed.atwa.popularmovies.util 2 | 3 | import ahmed.atwa.popularmovies.PopMovApp 4 | import android.app.Application 5 | import android.content.Context 6 | import android.os.Bundle 7 | import android.os.StrictMode 8 | import androidx.test.runner.AndroidJUnitRunner 9 | 10 | open class UiRunner : AndroidJUnitRunner() { 11 | 12 | override fun onCreate(arguments: Bundle?) { 13 | StrictMode.setThreadPolicy(StrictMode.ThreadPolicy.Builder().permitAll().build()) 14 | super.onCreate(arguments) 15 | } 16 | 17 | override fun newApplication(cl: ClassLoader?, className: String?, context: Context?): Application { 18 | return super.newApplication(cl, PopMovApp::class.java.name, context) 19 | } 20 | } -------------------------------------------------------------------------------- /app/src/main/java/ahmed/atwa/popularmovies/detail/di/DetailFragmentModule.kt: -------------------------------------------------------------------------------- 1 | package ahmed.atwa.popularmovies.detail.di 2 | 3 | import ahmed.atwa.popularmovies.detail.presentation.DetailFragment 4 | import ahmed.atwa.popularmovies.detail.presentation.TrailerAdapter 5 | import androidx.recyclerview.widget.LinearLayoutManager 6 | import dagger.Module 7 | import dagger.Provides 8 | 9 | /** 10 | * Created by Ahmed Atwa on 10/19/18. 11 | */ 12 | 13 | @Module 14 | class DetailFragmentModule { 15 | 16 | @Provides 17 | internal fun provideLinearLayoutManager(fragment: DetailFragment): LinearLayoutManager { 18 | return LinearLayoutManager(fragment.activity) 19 | } 20 | 21 | @Provides 22 | internal fun provideTrailerAdapter(): TrailerAdapter { 23 | return TrailerAdapter(ArrayList()) 24 | } 25 | 26 | } -------------------------------------------------------------------------------- /app/src/main/java/ahmed/atwa/popularmovies/utils/di/builder/ActivityBuilder.kt: -------------------------------------------------------------------------------- 1 | 2 | 3 | package ahmed.atwa.popularmovies.utils.di.builder 4 | 5 | import ahmed.atwa.popularmovies.detail.di.DetailFragmentProvider 6 | import ahmed.atwa.popularmovies.main.di.MainActivityModule 7 | import ahmed.atwa.popularmovies.movies.di.MoviesFragmentProvider 8 | import ahmed.atwa.popularmovies.main.presentation.MainActivity 9 | import dagger.Module 10 | import dagger.android.ContributesAndroidInjector 11 | 12 | /** 13 | * Created by Ahmed Atwa on 10/19/18. 14 | */ 15 | 16 | @Module 17 | abstract class ActivityBuilder { 18 | 19 | 20 | @ContributesAndroidInjector(modules = [(MainActivityModule::class), (MoviesFragmentProvider::class), (DetailFragmentProvider::class)]) 21 | internal abstract fun bindMainActivity(): MainActivity 22 | 23 | } -------------------------------------------------------------------------------- /app/src/main/java/ahmed/atwa/popularmovies/movies/data/MovieApi.kt: -------------------------------------------------------------------------------- 1 | package ahmed.atwa.popularmovies.movies.data 2 | 3 | import ahmed.atwa.popularmovies.utils.commons.AppConstants.Companion.API_KEY_QUERY 4 | import retrofit2.Response 5 | import retrofit2.http.GET 6 | import retrofit2.http.Query 7 | import javax.inject.Singleton 8 | 9 | /** 10 | * Created by Ahmed Atwa on 10/19/18. 11 | */ 12 | 13 | @Singleton 14 | interface MovieApi { 15 | 16 | 17 | companion object { 18 | const val POPULAR_MOVIES_QUERY: String = ("discover/movie?sort_by=popularity.desc") 19 | const val PAGE_QUERY: String = ("page") 20 | } 21 | 22 | 23 | 24 | @GET(POPULAR_MOVIES_QUERY) 25 | suspend fun getMostPopular(@Query(API_KEY_QUERY) apiKey: String,@Query(PAGE_QUERY) page:Int): Response 26 | 27 | 28 | 29 | 30 | } -------------------------------------------------------------------------------- /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 22 | -------------------------------------------------------------------------------- /app/src/main/java/ahmed/atwa/popularmovies/utils/network/NetworkErrorHandler.kt: -------------------------------------------------------------------------------- 1 | package ahmed.atwa.popularmovies.utils.network 2 | 3 | import org.json.JSONObject 4 | import retrofit2.Response 5 | import java.io.IOException 6 | 7 | class NetworkErrorHandler : INetworkErrorHandler { 8 | 9 | override fun resolveErrorMessage(response: Response): ResultType.Error { 10 | val code = response.code().toString() 11 | val message = try { 12 | response.errorBody()?.string()?.let { 13 | val jObjError = JSONObject(it) 14 | jObjError.getJSONObject("error").getString("message") 15 | } 16 | } catch (e: Exception) { 17 | e.message 18 | } 19 | val errorMessage = if (message.isNullOrEmpty()) " error code = $code " 20 | else " error code = $code & error message = $message " 21 | return ResultType.Error(IOException(errorMessage)) 22 | } 23 | } -------------------------------------------------------------------------------- /app/src/main/res/layout/item_movie_view.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /app/src/main/java/ahmed/atwa/popularmovies/PopMovApp.kt: -------------------------------------------------------------------------------- 1 | package ahmed.atwa.popularmovies 2 | 3 | import ahmed.atwa.popularmovies.utils.di.component.DaggerAppComponent 4 | import android.app.Activity 5 | import android.app.Application 6 | import dagger.android.AndroidInjector 7 | import dagger.android.DispatchingAndroidInjector 8 | import dagger.android.HasActivityInjector 9 | import javax.inject.Inject 10 | 11 | /** 12 | * Created by Ahmed Atwa on 10/19/18. 13 | */ 14 | 15 | class PopMovApp : Application(), HasActivityInjector { 16 | 17 | @Inject 18 | lateinit var activityDispatchingAndroidInjector: DispatchingAndroidInjector 19 | 20 | 21 | 22 | 23 | override fun activityInjector(): AndroidInjector { 24 | return activityDispatchingAndroidInjector 25 | } 26 | 27 | override fun onCreate() { 28 | super.onCreate() 29 | DaggerAppComponent.builder().application(this).build().inject(this) 30 | } 31 | 32 | 33 | } -------------------------------------------------------------------------------- /app/src/main/java/ahmed/atwa/popularmovies/movies/domain/MovieSourceFactory.kt: -------------------------------------------------------------------------------- 1 | package ahmed.atwa.popularmovies.movies.domain 2 | 3 | import ahmed.atwa.popularmovies.movies.data.Movie 4 | import ahmed.atwa.popularmovies.movies.data.MovieFilterSource 5 | import ahmed.atwa.popularmovies.movies.data.MoviePagingSource 6 | import ahmed.atwa.popularmovies.movies.data.MovieRepo 7 | import ahmed.atwa.popularmovies.movies.presentation.MovieAdapter 8 | import androidx.paging.PagingSource 9 | import javax.inject.Inject 10 | 11 | class MovieSourceFactory @Inject constructor() { 12 | 13 | @Inject 14 | lateinit var movieFilterSource: MovieFilterSource 15 | 16 | @Inject 17 | lateinit var moviePagingSource: MoviePagingSource 18 | 19 | fun getSource( filterText: String): PagingSource { 20 | return if (filterText.isBlank() || filterText.isEmpty()) moviePagingSource 21 | else movieFilterSource.apply { this.filterText = filterText } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 21 | 22 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # 2 | # /** 3 | # * Created by Ahmed Atwa on 19/10/2018 4 | # * Copyright (c) 19/10/2018 . All rights reserved. 5 | # **/ 6 | # 7 | 8 | # Project-wide Gradle settings. 9 | # IDE (e.g. Android Studio) users: 10 | # Gradle settings configured through the IDE *will override* 11 | # any settings specified in this file. 12 | # For more details on how to configure your build environment visit 13 | # http://www.gradle.org/docs/current/userguide/build_environment.html 14 | # Specifies the JVM arguments used for the daemon process. 15 | # The setting is particularly useful for tweaking memory settings. 16 | android.enableJetifier=true 17 | android.useAndroidX=true 18 | org.gradle.jvmargs=-Xmx1536m 19 | # When configured, Gradle will run in incubating parallel mode. 20 | # This option should only be used with decoupled projects. More details, visit 21 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 22 | # org.gradle.parallel=true 23 | -------------------------------------------------------------------------------- /app/src/main/res/navigation/movies_graph.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 13 | 16 | 17 | 18 | 23 | 24 | -------------------------------------------------------------------------------- /app/src/test/java/ahmed/atwa/popularmovies/CoroutineTestingRule.kt: -------------------------------------------------------------------------------- 1 | package ahmed.atwa.popularmovies 2 | 3 | import kotlinx.coroutines.CoroutineDispatcher 4 | import kotlinx.coroutines.Dispatchers 5 | import kotlinx.coroutines.ExperimentalCoroutinesApi 6 | import kotlinx.coroutines.test.TestCoroutineScope 7 | import kotlinx.coroutines.test.resetMain 8 | import kotlinx.coroutines.test.setMain 9 | import org.junit.rules.TestWatcher 10 | import org.junit.runner.Description 11 | import kotlin.coroutines.ContinuationInterceptor 12 | 13 | @ExperimentalCoroutinesApi 14 | class CoroutineTestingRule : TestWatcher(), TestCoroutineScope by TestCoroutineScope() { 15 | 16 | override fun starting(description: Description?) { 17 | super.starting(description) 18 | Dispatchers.setMain(this.coroutineContext[ContinuationInterceptor] as CoroutineDispatcher) 19 | } 20 | 21 | override fun finished(description: Description?) { 22 | super.finished(description) 23 | Dispatchers.resetMain() 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/src/main/java/ahmed/atwa/popularmovies/utils/di/module/RepoModule.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * * 3 | * Created by Ahmed Atwa on 19/10/2018. 4 | * / 5 | */ 6 | 7 | package ahmed.atwa.popularmovies.utils.di.module 8 | 9 | import ahmed.atwa.popularmovies.detail.data.TrailerApi 10 | import ahmed.atwa.popularmovies.movies.data.MovieApi 11 | import ahmed.atwa.popularmovies.movies.data.MovieRepo 12 | import ahmed.atwa.popularmovies.movies.data.MovieRepoImp 13 | import ahmed.atwa.popularmovies.utils.database.MovieDao 14 | import dagger.Module 15 | import dagger.Provides 16 | import javax.inject.Singleton 17 | 18 | /** 19 | * Created by Ahmed Atwa on 10/17/2019. 20 | */ 21 | 22 | @Module 23 | class RepoModule { 24 | 25 | @Provides 26 | @Singleton 27 | internal fun provideMovieRepository(movieDao: MovieDao, 28 | movieApi: MovieApi, 29 | trailerApi: TrailerApi): MovieRepo { 30 | return MovieRepoImp(movieDao, movieApi, trailerApi) 31 | } 32 | 33 | } -------------------------------------------------------------------------------- /app/src/main/java/ahmed/atwa/popularmovies/main/di/MainActivityModule.kt: -------------------------------------------------------------------------------- 1 | package ahmed.atwa.popularmovies.main.di 2 | 3 | import ahmed.atwa.popularmovies.utils.commons.ViewModelProviderFactory 4 | import ahmed.atwa.popularmovies.movies.presentation.MoviesViewModel 5 | import ahmed.atwa.popularmovies.movies.data.MovieRepoImp 6 | import ahmed.atwa.popularmovies.movies.domain.MovieSourceFactory 7 | import androidx.lifecycle.ViewModelProvider 8 | import dagger.Module 9 | import dagger.Provides 10 | 11 | /** 12 | * Created by Ahmed Atwa on 10/19/18. 13 | */ 14 | 15 | @Module 16 | class MainActivityModule { 17 | 18 | @Provides 19 | internal fun provideMoviesViewModel(movieRepoImp: MovieRepoImp,sourceFactory:MovieSourceFactory): MoviesViewModel { 20 | return MoviesViewModel(movieRepoImp,sourceFactory) 21 | } 22 | 23 | @Provides 24 | internal fun provideMoviesViewModelFactory(moviesViewModel: MoviesViewModel): ViewModelProvider.Factory { 25 | return ViewModelProviderFactory(moviesViewModel) 26 | } 27 | 28 | } -------------------------------------------------------------------------------- /app/src/main/java/ahmed/atwa/popularmovies/utils/database/DbModule.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * * 3 | * Created by Ahmed Atwa on 19/10/2018. 4 | * / 5 | */ 6 | 7 | package ahmed.atwa.popularmovies.utils.database 8 | 9 | import ahmed.atwa.popularmovies.utils.commons.AppConstants 10 | import android.content.Context 11 | import androidx.room.Room 12 | import dagger.Module 13 | import dagger.Provides 14 | import javax.inject.Named 15 | import javax.inject.Singleton 16 | 17 | /** 18 | * Created by Ahmed Atwa on 11/8/2018. 19 | */ 20 | 21 | @Module 22 | class DbModule{ 23 | 24 | @Provides 25 | @Singleton 26 | @Named(AppConstants.DB_NAME_KEY) 27 | internal fun provideMovieDb(context: Context): MovieDb { 28 | return Room.databaseBuilder(context, MovieDb::class.java, AppConstants.DB_NAME).fallbackToDestructiveMigration() 29 | .build() 30 | } 31 | 32 | @Provides 33 | @Singleton 34 | internal fun provideMovieDao(context: Context): MovieDao { 35 | return provideMovieDb(context).movieDao() 36 | } 37 | } -------------------------------------------------------------------------------- /app/src/main/java/ahmed/atwa/popularmovies/movies/di/MovieSourceModule.kt: -------------------------------------------------------------------------------- 1 | package ahmed.atwa.popularmovies.movies.di 2 | 3 | import ahmed.atwa.popularmovies.movies.data.MovieFilterSource 4 | import ahmed.atwa.popularmovies.movies.data.MoviePagingSource 5 | import ahmed.atwa.popularmovies.movies.data.MovieRepoImp 6 | import ahmed.atwa.popularmovies.movies.domain.MovieSourceFactory 7 | import dagger.Module 8 | import dagger.Provides 9 | import javax.inject.Singleton 10 | 11 | @Module 12 | class MovieSourceModule { 13 | @Provides 14 | internal fun provideMoviesSource(): MovieSourceFactory { 15 | return MovieSourceFactory() 16 | } 17 | 18 | @Provides 19 | @Singleton 20 | internal fun provideMoviesPagingSource(movieRepoImp: MovieRepoImp): MoviePagingSource { 21 | return MoviePagingSource(movieRepoImp) 22 | } 23 | 24 | @Provides 25 | @Singleton 26 | internal fun provideMoviesFilterSource(movieRepoImp: MovieRepoImp): MovieFilterSource { 27 | return MovieFilterSource(movieRepoImp) 28 | } 29 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/ahmed/atwa/popularmovies/utils/di/MockDbModule.kt: -------------------------------------------------------------------------------- 1 | package ahmed.atwa.popularmovies.utils.di 2 | 3 | import ahmed.atwa.popularmovies.utils.commons.AppConstants 4 | import ahmed.atwa.popularmovies.utils.database.MovieDao 5 | import ahmed.atwa.popularmovies.utils.database.MovieDb 6 | import android.content.Context 7 | import androidx.room.Room 8 | import dagger.Module 9 | import dagger.Provides 10 | import javax.inject.Named 11 | import javax.inject.Singleton 12 | 13 | @Module 14 | class MockDbModule { 15 | @Provides 16 | @Singleton 17 | @Named(AppConstants.DB_NAME_KEY) 18 | internal fun provideMovieDb(context: Context): MovieDb { 19 | return Room.databaseBuilder(context, MovieDb::class.java, AppConstants.DB_MOCK_NAME).fallbackToDestructiveMigration() 20 | .build() 21 | } 22 | 23 | @Provides 24 | @Singleton 25 | internal fun provideMovieDao(context: Context): MovieDao { 26 | return provideMovieDb(context).movieDao() 27 | } 28 | } -------------------------------------------------------------------------------- /app/src/main/java/ahmed/atwa/popularmovies/movies/data/MovieFilterSource.kt: -------------------------------------------------------------------------------- 1 | package ahmed.atwa.popularmovies.movies.data 2 | 3 | import androidx.paging.PagingSource 4 | import androidx.paging.PagingState 5 | import javax.inject.Inject 6 | 7 | class MovieFilterSource @Inject constructor(private val repo: MovieRepo) : PagingSource() { 8 | 9 | var filterText:String? = null 10 | 11 | override suspend fun load(params: LoadParams): LoadResult { 12 | return if (filterText == null) LoadResult.Error(Exception("")) 13 | else try { 14 | val movieListResponse = repo.getFilteredPopularMovies(filterText!!)!! 15 | LoadResult.Page( 16 | data = movieListResponse.results, 17 | prevKey = null, 18 | nextKey = null 19 | ) 20 | } catch (e: Exception) { 21 | LoadResult.Error(e) 22 | } 23 | } 24 | 25 | override fun getRefreshKey(state: PagingState): Int? { 26 | return null 27 | } 28 | } -------------------------------------------------------------------------------- /app/src/main/java/ahmed/atwa/popularmovies/utils/network/NetworkRouter.kt: -------------------------------------------------------------------------------- 1 | package ahmed.atwa.popularmovies.utils.network 2 | 3 | import retrofit2.Response 4 | import java.io.IOException 5 | 6 | 7 | /** 8 | * Created by Ahmed Atwa on 11/7/2019. 9 | */ 10 | object NetworkRouter { 11 | 12 | private val networkErrorHandler: INetworkErrorHandler = NetworkErrorHandler() 13 | 14 | suspend fun invokeCall(call: suspend () -> Response): ResultType { 15 | return try { 16 | val response = call.invoke() 17 | if (isSuccessResponse(response)) ResultType.Success(response.body()!!) 18 | else networkErrorHandler.resolveErrorMessage(response) 19 | } catch (exception: IOException) { 20 | if (exception is NoConnectionException) ResultType.Error(exception) 21 | else ResultType.Error(exception) 22 | } 23 | } 24 | 25 | private fun isSuccessResponse(response: Response?): Boolean { 26 | return response != null && response.isSuccessful && response.body() != null 27 | } 28 | 29 | 30 | } -------------------------------------------------------------------------------- /app/src/main/java/ahmed/atwa/popularmovies/utils/network/NetworkConnectionInterceptor.kt: -------------------------------------------------------------------------------- 1 | package ahmed.atwa.popularmovies.utils.network 2 | 3 | import android.content.Context 4 | import android.net.ConnectivityManager 5 | import okhttp3.Interceptor 6 | import okhttp3.Response 7 | import java.io.IOException 8 | import javax.inject.Inject 9 | import javax.inject.Singleton 10 | 11 | 12 | /** 13 | * Created by Ahmed Atwa on 11/7/2019. 14 | */ 15 | @Singleton 16 | class NetworkConnectionInterceptor @Inject constructor(private val mContext: Context) : Interceptor { 17 | 18 | private val isConnected: Boolean 19 | get() { 20 | val connectivityManager = mContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager 21 | val netInfo = connectivityManager.activeNetworkInfo 22 | return netInfo != null && netInfo.isConnected 23 | } 24 | 25 | @Throws(IOException::class) 26 | override fun intercept(chain: Interceptor.Chain): Response { 27 | if (!isConnected) throw NoConnectionException() 28 | val builder = chain.request().newBuilder() 29 | return chain.proceed(builder.build()) 30 | } 31 | 32 | } -------------------------------------------------------------------------------- /app/src/main/java/ahmed/atwa/popularmovies/movies/di/MoviesFragmentModule.kt: -------------------------------------------------------------------------------- 1 | 2 | 3 | package ahmed.atwa.popularmovies.movies.di 4 | 5 | import ahmed.atwa.popularmovies.movies.data.MovieRepoImp 6 | import ahmed.atwa.popularmovies.movies.domain.MovieSourceFactory 7 | import ahmed.atwa.popularmovies.utils.commons.GridSpacingItemDecoration 8 | import ahmed.atwa.popularmovies.movies.presentation.MovieAdapter 9 | import ahmed.atwa.popularmovies.movies.presentation.MoviesFragment 10 | import androidx.recyclerview.widget.GridLayoutManager 11 | import dagger.Module 12 | import dagger.Provides 13 | 14 | /** 15 | * Created by Ahmed Atwa on 10/19/18. 16 | */ 17 | 18 | @Module 19 | class MoviesFragmentModule { 20 | 21 | @Provides 22 | internal fun provideGridLayoutManager(fragment: MoviesFragment): GridLayoutManager { 23 | return GridLayoutManager(fragment.requireContext(), 2) 24 | } 25 | 26 | @Provides 27 | internal fun provideGridSpacingItemDecoration(): GridSpacingItemDecoration { 28 | return GridSpacingItemDecoration(2, 5, true) 29 | } 30 | 31 | @Provides 32 | internal fun provideMovieAdapter(): MovieAdapter { 33 | return MovieAdapter() 34 | } 35 | 36 | } -------------------------------------------------------------------------------- /app/src/main/java/ahmed/atwa/popularmovies/movies/data/MoviePagingSource.kt: -------------------------------------------------------------------------------- 1 | package ahmed.atwa.popularmovies.movies.data 2 | 3 | import ahmed.atwa.popularmovies.utils.network.ResultType 4 | import androidx.paging.PagingSource 5 | import androidx.paging.PagingState 6 | import javax.inject.Inject 7 | 8 | class MoviePagingSource @Inject constructor(private val repo: MovieRepo) : PagingSource() { 9 | 10 | override suspend fun load(params: LoadParams): LoadResult { 11 | return try { 12 | val nextPage = params.key ?: 1 13 | val movieListResponse = (repo.getPopularMovies(nextPage) as ResultType.Success).data 14 | LoadResult.Page( 15 | data = movieListResponse.results, 16 | prevKey = if (nextPage == 1) null else nextPage - 1, 17 | nextKey = if (nextPage < movieListResponse.total_pages) 18 | movieListResponse.page?.plus(1) else null 19 | ) 20 | } catch (e: Exception) { 21 | LoadResult.Error(e) 22 | } 23 | } 24 | 25 | override fun getRefreshKey(state: PagingState): Int? { 26 | return null 27 | } 28 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/ahmed/atwa/popularmovies/utils/di/MockUrlModule.kt: -------------------------------------------------------------------------------- 1 | package ahmed.atwa.popularmovies.utils.di 2 | 3 | import ahmed.atwa.popularmovies.utils.commons.AppConstants.Companion.BASE_URL_KEY 4 | import dagger.Module 5 | import dagger.Provides 6 | import okhttp3.mockwebserver.MockWebServer 7 | import javax.inject.Named 8 | import javax.inject.Singleton 9 | 10 | @Module 11 | class MockUrlModule { 12 | 13 | @Provides 14 | @Singleton 15 | fun provideMockServer (): MockWebServer { 16 | 17 | var mockWebServer:MockWebServer? = null 18 | 19 | val thread = Thread(Runnable { 20 | mockWebServer = MockWebServer() 21 | mockWebServer?.start() 22 | }) 23 | 24 | thread.start() 25 | thread.join() 26 | 27 | return mockWebServer ?: throw NullPointerException() 28 | } 29 | 30 | 31 | @Provides 32 | @Singleton 33 | @Named(BASE_URL_KEY) 34 | fun provideBaseUrl (mockWebServer:MockWebServer): String { 35 | 36 | var url = "" 37 | 38 | val t = Thread(Runnable { 39 | url = mockWebServer.url("/").toString() 40 | }) 41 | t.start() 42 | t.join() 43 | 44 | return url 45 | } 46 | } -------------------------------------------------------------------------------- /.idea/jarRepositories.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 | 15 | 19 | 20 | 24 | 25 | 29 | 30 | -------------------------------------------------------------------------------- /app/src/main/java/ahmed/atwa/popularmovies/movies/data/Movie.kt: -------------------------------------------------------------------------------- 1 | package ahmed.atwa.popularmovies.movies.data 2 | 3 | import android.os.Parcelable 4 | import androidx.room.ColumnInfo 5 | import androidx.room.Entity 6 | import androidx.room.PrimaryKey 7 | import kotlinx.android.parcel.Parcelize 8 | 9 | @Parcelize 10 | @Entity(tableName = "movies") 11 | data class Movie( 12 | @PrimaryKey 13 | @ColumnInfo(name = "id") 14 | var id: Int, 15 | @ColumnInfo(name = "vote_count") 16 | var vote_count: Int, 17 | @ColumnInfo(name = "video") 18 | var video: Boolean, 19 | @ColumnInfo(name = "vote_average") 20 | val vote_average: Double, 21 | @ColumnInfo(name = "title") 22 | val title: String?, 23 | @ColumnInfo(name = "popularity") 24 | val popularity: Double, 25 | @ColumnInfo(name = "poster_path") 26 | val poster_path: String?, 27 | @ColumnInfo(name = "original_language") 28 | val original_language: String?, 29 | @ColumnInfo(name = "original_title") 30 | val original_title: String?, 31 | @ColumnInfo(name = "backdrop_path") 32 | val backdrop_path: String? = "null", 33 | @ColumnInfo(name = "adult") 34 | var adult: Boolean = false, 35 | @ColumnInfo(name = "overview") 36 | val overview: String?, 37 | @ColumnInfo(name = "release_date") 38 | val release_date: String? 39 | ) : Parcelable -------------------------------------------------------------------------------- /app/src/main/java/ahmed/atwa/popularmovies/utils/di/component/AppComponent.kt: -------------------------------------------------------------------------------- 1 | package ahmed.atwa.popularmovies.utils.di.component 2 | 3 | import ahmed.atwa.popularmovies.PopMovApp 4 | import ahmed.atwa.popularmovies.utils.database.DbModule 5 | import ahmed.atwa.popularmovies.utils.di.module.AppModule 6 | import ahmed.atwa.popularmovies.utils.di.module.RepoModule 7 | import ahmed.atwa.popularmovies.utils.network.UrlModule 8 | import ahmed.atwa.popularmovies.utils.network.NetworkModule 9 | import ahmed.atwa.popularmovies.utils.di.builder.ActivityBuilder 10 | import android.app.Application 11 | import dagger.BindsInstance 12 | import dagger.Component 13 | import dagger.android.AndroidInjectionModule 14 | import dagger.android.AndroidInjector 15 | import dagger.android.DaggerApplication 16 | import javax.inject.Singleton 17 | 18 | /** 19 | * Created by Ahmed Atwa on 10/19/18. 20 | */ 21 | 22 | @Singleton 23 | @Component(modules = [(AndroidInjectionModule::class), (AppModule::class), (DbModule::class), 24 | (NetworkModule::class), (UrlModule::class),(RepoModule::class), (ActivityBuilder::class)]) 25 | 26 | interface AppComponent : AndroidInjector { 27 | 28 | fun inject(app: PopMovApp) 29 | 30 | override fun inject(instance: DaggerApplication) 31 | 32 | @Component.Builder 33 | interface Builder { 34 | 35 | @BindsInstance 36 | fun application(application: Application): Builder 37 | 38 | fun build(): AppComponent 39 | } 40 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 10 | 11 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /app/src/main/java/ahmed/atwa/popularmovies/utils/commons/AppUtils.kt: -------------------------------------------------------------------------------- 1 | 2 | 3 | package ahmed.atwa.popularmovies.utils.commons 4 | 5 | import ahmed.atwa.popularmovies.R 6 | import android.app.ProgressDialog 7 | import android.content.Context 8 | import android.graphics.Color 9 | import android.graphics.drawable.ColorDrawable 10 | import android.net.ConnectivityManager 11 | 12 | /** 13 | * Created by Ahmed Atwa on 10/19/18. 14 | */ 15 | 16 | class AppUtils { 17 | 18 | companion object { 19 | fun showLoadingDialog(context: Context): ProgressDialog { 20 | val progressDialog = ProgressDialog(context) 21 | progressDialog.show() 22 | if (progressDialog.window != null) { 23 | progressDialog.window!!.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) 24 | } 25 | progressDialog.setContentView(R.layout.progress_dialog) 26 | progressDialog.isIndeterminate = true 27 | progressDialog.setCancelable(false) 28 | progressDialog.setCanceledOnTouchOutside(false) 29 | return progressDialog 30 | } 31 | 32 | fun isNetworkConnected(context: Context): Boolean { 33 | val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager? 34 | if (cm != null) { 35 | val activeNetwork = cm.activeNetworkInfo 36 | return activeNetwork != null && activeNetwork.isConnectedOrConnecting 37 | } 38 | return false 39 | } 40 | } 41 | 42 | } -------------------------------------------------------------------------------- /app/src/test/java/ahmed/atwa/popularmovies/movies/domain/MovieMapperImpTest.kt: -------------------------------------------------------------------------------- 1 | package ahmed.atwa.popularmovies.movies.domain 2 | 3 | import ahmed.atwa.popularmovies.movies.data.Movie 4 | import androidx.arch.core.executor.testing.InstantTaskExecutorRule 5 | import org.junit.Assert 6 | import org.junit.Before 7 | import org.junit.Rule 8 | import org.junit.Test 9 | import org.mockito.MockitoAnnotations 10 | 11 | class MovieMapperImpTest { 12 | 13 | @get:Rule 14 | var instantExecutorRule = InstantTaskExecutorRule() 15 | 16 | private lateinit var movieMapper: MovieMapper 17 | private val mockMovieEntity = MovieEntity(1, true, "", 3.4, 12, true, 2.3, "test", "", "", "") 18 | private val mockMovieLocal = MovieLocal(1, 1, "", 3.4, 12, true, 2.3, "test", "", "", "") 19 | private val mockMovieRemote = Movie(12, 1, true, 2.3, "test", 3.4, "", "", "", "", false, "", "") 20 | 21 | 22 | @Before 23 | @Throws(Exception::class) 24 | fun setup() { 25 | MockitoAnnotations.initMocks(this) 26 | movieMapper = MovieMapperImp() 27 | } 28 | 29 | 30 | @Test 31 | fun test_mapFromLocal(){ 32 | val expected = mockMovieEntity 33 | val actual = movieMapper.mapFromLocalToEntity(mockMovieLocal) 34 | Assert.assertEquals(expected, actual) 35 | } 36 | 37 | @Test 38 | fun test_mapFromRemoteToLocal(){ 39 | val expected = mockMovieLocal 40 | val actual = movieMapper.mapFromRemoteToLocal(mockMovieRemote,1) 41 | Assert.assertEquals(expected, actual) 42 | } 43 | 44 | 45 | } -------------------------------------------------------------------------------- /app/src/main/java/ahmed/atwa/popularmovies/utils/commons/GridSpacingItemDecoration.kt: -------------------------------------------------------------------------------- 1 | package ahmed.atwa.popularmovies.utils.commons 2 | 3 | import android.graphics.Rect 4 | import android.view.View 5 | import androidx.recyclerview.widget.RecyclerView 6 | 7 | /** 8 | * Created by Ahmed Atwa on 10/19/18. 9 | */ 10 | 11 | class GridSpacingItemDecoration(private val spanCount: Int, private val spacing: Int, private val includeEdge: Boolean) : RecyclerView.ItemDecoration() { 12 | 13 | 14 | override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) { 15 | val position = parent.getChildAdapterPosition(view) // item position 16 | val column = position % spanCount // item column 17 | 18 | if (includeEdge) { 19 | outRect.left = spacing - column * spacing / spanCount // spacing - column * ((1f / spanCount) * spacing) 20 | outRect.right = (column + 1) * spacing / spanCount // (column + 1) * ((1f / spanCount) * spacing) 21 | 22 | if (position < spanCount) { // top edge 23 | outRect.top = spacing 24 | } 25 | outRect.bottom = spacing // item bottom 26 | } else { 27 | outRect.left = column * spacing / spanCount // column * ((1f / spanCount) * spacing) 28 | outRect.right = spacing - (column + 1) * spacing / spanCount // spacing - (column + 1) * ((1f / spanCount) * spacing) 29 | if (position >= spanCount) { 30 | outRect.top = spacing // item top 31 | } 32 | } 33 | } 34 | 35 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/ahmed/atwa/popularmovies/utils/di/TestAppComponent.kt: -------------------------------------------------------------------------------- 1 | package ahmed.atwa.popularmovies.utils.di 2 | 3 | import ahmed.atwa.popularmovies.PopMovApp 4 | import ahmed.atwa.popularmovies.utils.di.builder.ActivityBuilder 5 | import ahmed.atwa.popularmovies.utils.di.component.AppComponent 6 | import ahmed.atwa.popularmovies.utils.di.module.AppModule 7 | import ahmed.atwa.popularmovies.utils.network.NetworkModule 8 | import ahmed.atwa.popularmovies.utils.di.module.RepoModule 9 | import android.app.Application 10 | import dagger.BindsInstance 11 | import dagger.Component 12 | import dagger.android.AndroidInjectionModule 13 | import dagger.android.DaggerApplication 14 | import okhttp3.mockwebserver.MockWebServer 15 | import javax.inject.Singleton 16 | 17 | 18 | @Singleton 19 | @Component(modules = [ 20 | AndroidInjectionModule::class, 21 | AppModule::class, 22 | MockDbModule::class, 23 | NetworkModule::class, 24 | RepoModule::class, 25 | ActivityBuilder::class, 26 | MockUrlModule::class 27 | ] 28 | ) 29 | interface TestAppComponent : AppComponent { 30 | 31 | override fun inject(app: PopMovApp) 32 | 33 | override fun inject(instance: DaggerApplication) 34 | 35 | fun getMockWebServer(): MockWebServer 36 | 37 | @Component.Builder 38 | interface Builder { 39 | 40 | /** 41 | * [BindsInstance] annotation is used for, if you want to bind particular object or instance 42 | * of an object through the time of component construction 43 | */ 44 | @BindsInstance 45 | fun application(application: Application): Builder 46 | 47 | fun build(): TestAppComponent 48 | } 49 | 50 | } -------------------------------------------------------------------------------- /app/src/main/res/layout/load_state_layout.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 19 | 20 | 27 | 28 |