├── .idea ├── .name ├── codeStyles │ ├── codeStyleConfig.xml │ └── Project.xml ├── vcs.xml ├── kotlinScripting.xml ├── kotlinc.xml ├── artifacts │ ├── shared_android.xml │ └── shared_jvm.xml ├── inspectionProfiles │ └── Project_Default.xml ├── runConfigurations.xml ├── compiler.xml ├── gradle.xml ├── misc.xml └── jarRepositories.xml ├── android ├── .gitignore ├── src │ └── main │ │ ├── res │ │ ├── values │ │ │ ├── strings.xml │ │ │ ├── colors.xml │ │ │ └── styles.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 │ │ ├── mipmap-anydpi-v26 │ │ │ ├── ic_launcher.xml │ │ │ └── ic_launcher_round.xml │ │ ├── drawable │ │ │ ├── ic_cloud_download.xml │ │ │ └── ic_launcher_background.xml │ │ ├── layout │ │ │ ├── activity_main.xml │ │ │ └── item_movie.xml │ │ └── drawable-v24 │ │ │ └── ic_launcher_foreground.xml │ │ ├── kotlin │ │ └── app │ │ │ └── web │ │ │ └── drjackycv │ │ │ └── tmdbmultiplatform │ │ │ └── presentation │ │ │ ├── base │ │ │ └── util │ │ │ │ └── MyAppGlideModule.kt │ │ │ ├── extension │ │ │ ├── Extensions.kt │ │ │ └── ViewExtensions.kt │ │ │ ├── entity │ │ │ └── MovieUI.kt │ │ │ ├── mapper │ │ │ └── MoviesUIMapper.kt │ │ │ ├── adapter │ │ │ └── MoviesAdapter.kt │ │ │ └── ui │ │ │ └── MainActivity.kt │ │ └── AndroidManifest.xml ├── proguard-rules.pro └── build.gradle.kts ├── .github ├── FUNDING.yml └── ISSUE_TEMPLATE │ └── bug_report.md ├── list.png ├── cover.jpg ├── shared ├── src │ ├── androidMain │ │ └── AndroidManifest.xml │ └── commonMain │ │ └── kotlin │ │ ├── data │ │ ├── base │ │ │ ├── mapper │ │ │ │ └── Mapper.kt │ │ │ └── remote │ │ │ │ └── Api.kt │ │ ├── repository │ │ │ └── MoviesRepositoryImpl.kt │ │ ├── entity │ │ │ ├── Movie.kt │ │ │ └── MoviesResponse.kt │ │ ├── mapper │ │ │ └── MoviesMapper.kt │ │ └── remote │ │ │ └── MoviesApiImpl.kt │ │ ├── domain │ │ ├── base │ │ │ ├── usecase │ │ │ │ └── UseCase.kt │ │ │ └── repository │ │ │ │ └── Repository.kt │ │ └── usecase │ │ │ └── GetMoviesUseCaseImpl.kt │ │ ├── presentation │ │ ├── base │ │ │ ├── ListViewModel.kt │ │ │ └── ViewModelBinding.kt │ │ └── movies │ │ │ └── MoviesListViewModel.kt │ │ └── di │ │ └── DI.kt └── build.gradle.kts ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── ios ├── ios │ ├── Assets.xcassets │ │ ├── Contents.json │ │ └── AppIcon.appiconset │ │ │ └── Contents.json │ ├── AppDelegate.swift │ ├── Base.lproj │ │ ├── Main.storyboard │ │ └── LaunchScreen.storyboard │ ├── MovieItem.swift │ ├── Info.plist │ ├── SceneDelegate.swift │ └── ViewController.swift ├── ios.xcodeproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ ├── xcuserdata │ │ │ └── drjacky.xcuserdatad │ │ │ │ ├── UserInterfaceState.xcuserstate │ │ │ │ ├── IDEFindNavigatorScopes.plist │ │ │ │ ├── WorkspaceSettings.xcsettings │ │ │ │ └── xcdebugger │ │ │ │ └── Expressions.xcexplist │ │ └── xcshareddata │ │ │ ├── WorkspaceSettings.xcsettings │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── swiftpm │ │ │ └── Package.resolved │ ├── xcuserdata │ │ └── drjacky.xcuserdatad │ │ │ ├── xcschemes │ │ │ └── xcschememanagement.plist │ │ │ └── xcdebugger │ │ │ └── Breakpoints_v2.xcbkptlist │ └── project.pbxproj ├── iosTests │ ├── Info.plist │ └── iosTests.swift └── iosUITests │ ├── Info.plist │ └── iosUITests.swift ├── CONTRIBUTING.md ├── settings.gradle.kts ├── gradle.properties ├── LICENSE ├── README.md ├── gradlew.bat ├── .gitignore ├── CODE_OF_CONDUCT.md └── gradlew /.idea/.name: -------------------------------------------------------------------------------- 1 | TMDbMultiplatform -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: Drjacky -------------------------------------------------------------------------------- /list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Drjacky/TMDbMultiplatform/HEAD/list.png -------------------------------------------------------------------------------- /cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Drjacky/TMDbMultiplatform/HEAD/cover.jpg -------------------------------------------------------------------------------- /shared/src/androidMain/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Drjacky/TMDbMultiplatform/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /ios/ios/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /android/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | TMDbMultiplatform 3 | 4 | -------------------------------------------------------------------------------- /android/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Drjacky/TMDbMultiplatform/HEAD/android/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Drjacky/TMDbMultiplatform/HEAD/android/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Drjacky/TMDbMultiplatform/HEAD/android/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Drjacky/TMDbMultiplatform/HEAD/android/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Drjacky/TMDbMultiplatform/HEAD/android/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Drjacky/TMDbMultiplatform/HEAD/android/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Drjacky/TMDbMultiplatform/HEAD/android/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Drjacky/TMDbMultiplatform/HEAD/android/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Drjacky/TMDbMultiplatform/HEAD/android/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/data/base/mapper/Mapper.kt: -------------------------------------------------------------------------------- 1 | package data.base.mapper 2 | 3 | interface Mapper { 4 | 5 | fun mapTo(response: T): E 6 | 7 | } -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/data/base/remote/Api.kt: -------------------------------------------------------------------------------- 1 | package data.base.remote 2 | 3 | interface Api { 4 | 5 | suspend fun execute(request: R?): T 6 | 7 | } -------------------------------------------------------------------------------- /android/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Drjacky/TMDbMultiplatform/HEAD/android/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/domain/base/usecase/UseCase.kt: -------------------------------------------------------------------------------- 1 | package domain.base.usecase 2 | 3 | interface UseCase { 4 | 5 | suspend fun execute(request: R?): T 6 | 7 | } -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/domain/base/repository/Repository.kt: -------------------------------------------------------------------------------- 1 | package domain.base.repository 2 | 3 | interface Repository { 4 | 5 | suspend fun get(request: R?): T 6 | 7 | } -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /ios/ios.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/kotlinScripting.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /ios/ios.xcodeproj/project.xcworkspace/xcuserdata/drjacky.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Drjacky/TMDbMultiplatform/HEAD/ios/ios.xcodeproj/project.xcworkspace/xcuserdata/drjacky.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /.idea/kotlinc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | -------------------------------------------------------------------------------- /android/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #008577 4 | #00574B 5 | #D81B60 6 | 7 | -------------------------------------------------------------------------------- /.idea/artifacts/shared_android.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | $PROJECT_DIR$/shared/build/libs 4 | 5 | 6 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sun Nov 01 11:48:21 GMT 2020 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-7.0-all.zip 7 | -------------------------------------------------------------------------------- /ios/ios.xcodeproj/project.xcworkspace/xcuserdata/drjacky.xcuserdatad/IDEFindNavigatorScopes.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /ios/ios.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /android/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /android/src/main/kotlin/app/web/drjackycv/tmdbmultiplatform/presentation/base/util/MyAppGlideModule.kt: -------------------------------------------------------------------------------- 1 | package app.web.drjackycv.tmdbmultiplatform.presentation.base.util 2 | 3 | import com.bumptech.glide.annotation.GlideModule 4 | import com.bumptech.glide.module.AppGlideModule 5 | 6 | @GlideModule 7 | class MyAppGlideModule : AppGlideModule() -------------------------------------------------------------------------------- /android/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /ios/ios.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/artifacts/shared_jvm.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | $PROJECT_DIR$/shared/build/libs 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/data/repository/MoviesRepositoryImpl.kt: -------------------------------------------------------------------------------- 1 | package data.repository 2 | 3 | import data.base.remote.Api 4 | import data.entity.Movie 5 | import domain.base.repository.Repository 6 | 7 | class MoviesRepositoryImpl( 8 | private val api: Api> 9 | ) : Repository> { 10 | 11 | override suspend fun get(request: R?): List = api.execute(request) 12 | 13 | } -------------------------------------------------------------------------------- /ios/ios.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "Nuke", 6 | "repositoryURL": "https://github.com/kean/Nuke.git", 7 | "state": { 8 | "branch": "master", 9 | "revision": "5558b60564bc25887b4ac1e7189442e8740521ce", 10 | "version": null 11 | } 12 | } 13 | ] 14 | }, 15 | "version": 1 16 | } 17 | -------------------------------------------------------------------------------- /android/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /ios/ios.xcodeproj/xcuserdata/drjacky.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | ios.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/domain/usecase/GetMoviesUseCaseImpl.kt: -------------------------------------------------------------------------------- 1 | package domain.usecase 2 | 3 | import data.entity.Movie 4 | import domain.base.repository.Repository 5 | import domain.base.usecase.UseCase 6 | 7 | class GetMoviesUseCaseImpl( 8 | private val repository: Repository> 9 | ) : UseCase> { 10 | 11 | override suspend fun execute(request: R?): List { 12 | return repository.get(request) 13 | } 14 | 15 | } -------------------------------------------------------------------------------- /android/src/main/res/drawable/ic_cloud_download.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/presentation/base/ListViewModel.kt: -------------------------------------------------------------------------------- 1 | package presentation.base 2 | 3 | import com.badoo.reaktive.observable.Observable 4 | 5 | interface ListViewModelInput { 6 | 7 | fun get(request: R) 8 | fun loadMore(request: R) 9 | 10 | } 11 | 12 | interface ListViewModelOutput { 13 | 14 | val loading: Observable 15 | val result: Observable> 16 | 17 | } 18 | 19 | interface ListViewModel { 20 | 21 | val inputs: ListViewModelInput 22 | val outputs: ListViewModelOutput 23 | 24 | } -------------------------------------------------------------------------------- /android/src/main/kotlin/app/web/drjackycv/tmdbmultiplatform/presentation/extension/Extensions.kt: -------------------------------------------------------------------------------- 1 | package app.web.drjackycv.tmdbmultiplatform.presentation.extension 2 | 3 | import androidx.fragment.app.Fragment 4 | import androidx.lifecycle.* 5 | 6 | inline fun Fragment.viewModel( 7 | factory: ViewModelProvider.Factory, 8 | body: T.() -> Unit = {} 9 | ): T { 10 | val vm = ViewModelProvider(this, factory).get(T::class.java) 11 | vm.body() 12 | 13 | return vm 14 | } 15 | 16 | fun LifecycleOwner.observe(liveData: LiveData, action: (t: T) -> Unit) { 17 | liveData.observe(this, Observer { it?.let { t -> action(t) } }) 18 | } -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/data/entity/Movie.kt: -------------------------------------------------------------------------------- 1 | package data.entity 2 | 3 | private const val POSTER_PATH_BASE_URL = "https://image.tmdb.org/t/p/original" 4 | 5 | data class Movie( 6 | val popularity: Double, 7 | val voteCount: Int, 8 | val video: Boolean, 9 | val posterPath: String, 10 | val id: Int, 11 | val adult: Boolean, 12 | val backdropPath: String, 13 | val originalLanguage: String, 14 | val originalTitle: String, 15 | val genreIds: List, 16 | val title: String, 17 | val voteAverage: Double, 18 | val overview: String, 19 | val releaseDate: String, 20 | val fullPosterPath: String = POSTER_PATH_BASE_URL + posterPath 21 | ) -------------------------------------------------------------------------------- /ios/ios.xcodeproj/project.xcworkspace/xcuserdata/drjacky.xcuserdatad/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | BuildLocationStyle 6 | UseAppPreferences 7 | CustomBuildLocationType 8 | RelativeToDerivedData 9 | DerivedDataLocationStyle 10 | Default 11 | IssueFilterStyle 12 | ShowActiveSchemeOnly 13 | LiveSourceIssuesEnabled 14 | 15 | ShowSharedSchemesAutomaticallyEnabled 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /android/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 14 | 15 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 12 | 13 | -------------------------------------------------------------------------------- /android/src/main/kotlin/app/web/drjackycv/tmdbmultiplatform/presentation/entity/MovieUI.kt: -------------------------------------------------------------------------------- 1 | package app.web.drjackycv.tmdbmultiplatform.presentation.entity 2 | 3 | import android.os.Parcelable 4 | import kotlinx.android.parcel.Parcelize 5 | 6 | @Parcelize 7 | data class MovieUI( 8 | val popularity: Double, 9 | val voteCount: Int, 10 | val video: Boolean, 11 | val posterPath: String, 12 | val id: Int, 13 | val adult: Boolean, 14 | val backdropPath: String, 15 | val originalLanguage: String, 16 | val originalTitle: String, 17 | val genreIds: List, 18 | val title: String, 19 | val voteAverage: Double, 20 | val overview: String, 21 | val releaseDate: String, 22 | val fullPosterPath: String 23 | ) : Parcelable -------------------------------------------------------------------------------- /ios/iosTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /ios/iosUITests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /android/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 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | When contributing to this repository, please first discuss the change you wish to make via issue, email, or any other method with the owners of this repository before making a change. 3 | 4 | Please note we have a code of conduct, please follow it in all your interactions with the project. 5 | 6 | ## Pull Request Process 7 | 1. Ensure you had a clean install and check the result before making the pull request. 8 | 2. Update the README.md with details of changes to the interface, this includes new environment variables, and container parameters. 9 | 3. Increase the version numbers in any examples files and the README.md to the new version that this Pull Request would represent. 10 | 4. You may merge the Pull Request in once you have the sign-off of one developer, or if you do not have permission to do that, you may request the reviewer to merge it for you. 11 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | resolutionStrategy { 3 | eachPlugin { 4 | when (requested.id.id) { 5 | "kotlinx-serialization" -> useModule("org.jetbrains.kotlin:kotlin-serialization:1.5.0") 6 | //"kotlinx-serialization" -> useModule("org.jetbrains.kotlin:kotlin-serialization:${requested.version}") 7 | "kotlin-multiplatform" -> useModule("org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.0") 8 | //"kotlin-multiplatform" -> useModule("org.jetbrains.kotlin:kotlin-gradle-plugin:${requested.version}") 9 | //"kotlin-platform-js" -> useModule("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion") 10 | } 11 | } 12 | } 13 | } 14 | 15 | //enableFeaturePreview("GRADLE_METADATA") 16 | 17 | 18 | include(":android", ":shared") 19 | rootProject.name = "TMDbMultiplatform" 20 | -------------------------------------------------------------------------------- /android/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG]: Title goes here" 5 | labels: bug 6 | assignees: Drjacky 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. macOS] 28 | - Version [e.g. 11] 29 | 30 | **Smartphone (please complete the following information):** 31 | - Device: [e.g. iPhone6] 32 | - OS: [e.g. iOS8.1] 33 | - Version [e.g. 13] 34 | 35 | **Additional context** 36 | Add any other context about the problem here. 37 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/data/mapper/MoviesMapper.kt: -------------------------------------------------------------------------------- 1 | package data.mapper 2 | 3 | import data.base.mapper.Mapper 4 | import data.entity.Movie 5 | import data.entity.MoviesResponse 6 | 7 | class MoviesMapper : Mapper> { 8 | 9 | override fun mapTo(response: MoviesResponse): List = response.results.map { 10 | Movie( 11 | popularity = it.popularity, 12 | voteCount = it.voteCount, 13 | video = it.video, 14 | posterPath = it.posterPath ?: "", 15 | id = it.id, 16 | adult = it.adult, 17 | backdropPath = it.backdropPath ?: "", 18 | originalLanguage = it.originalLanguage, 19 | originalTitle = it.originalTitle, 20 | genreIds = it.genreIds, 21 | title = it.title, 22 | voteAverage = it.voteAverage, 23 | overview = it.overview, 24 | releaseDate = it.releaseDate 25 | ) 26 | } 27 | 28 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # gradle 2 | org.gradle.jvmargs=-Xmx1536m 3 | org.gradle.parallel=true 4 | # kotlin 5 | kotlin.code.style=official 6 | kotlinVersion=1.5.0 7 | # 1.3.72 8 | kotlin.incremental.multiplatform=true 9 | kotlin.parallel.tasks.in.project=true 10 | ktorVersion=1.5.4 11 | # 1.3.2 12 | kotlinSerializationVersion=1.0.1 13 | # 0.20.0 14 | reactiveVersion=1.1.22 15 | reactiveNmtcVersion=1.1.22-nmtc 16 | coroutinesVersion=1.4.3-native-mt 17 | # 1.5.0-RC-native-mt 18 | kodeinVersion=7.5.0 19 | # 7.0.0 20 | # android 21 | android.useAndroidX=true 22 | android.enableJetifier=true 23 | systemProp.optimize.conservatively=true 24 | buildToolsVersion=30.0.3 25 | gradleAndroidVersion=4.2.0 26 | androidxBaseVersion=1.1.0 27 | androidxUiVersion=2.2.0 28 | androidxTestVersion=1.2.0 29 | androidxEspressoVersion=3.2.0 30 | androidMaterialVersion=1.3.0 31 | androidxSwipeRefreshLayoutVersion=1.1.0 32 | androidxRecyclerviewVersion=1.2.0 33 | androidConstraintLayoutVersion=1.1.3 34 | androidMultidexVersion=2.0.1 35 | glideVersion=4.12.0 -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 22 | 23 | -------------------------------------------------------------------------------- /ios/iosTests/iosTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // iosTests.swift 3 | // iosTests 4 | // 5 | // Created by Hossein Abbasi on 6/4/20. 6 | // Copyright © 2020 Hossein Abbasi. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import ios 11 | 12 | class iosTests: XCTestCase { 13 | 14 | override func setUpWithError() throws { 15 | // Put setup code here. This method is called before the invocation of each test method in the class. 16 | } 17 | 18 | override func tearDownWithError() throws { 19 | // Put teardown code here. This method is called after the invocation of each test method in the class. 20 | } 21 | 22 | func testExample() throws { 23 | // This is an example of a functional test case. 24 | // Use XCTAssert and related functions to verify your tests produce the correct results. 25 | } 26 | 27 | func testPerformanceExample() throws { 28 | // This is an example of a performance test case. 29 | self.measure { 30 | // Put the code you want to measure the time of here. 31 | } 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Drjacky 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /android/src/main/kotlin/app/web/drjackycv/tmdbmultiplatform/presentation/mapper/MoviesUIMapper.kt: -------------------------------------------------------------------------------- 1 | package app.web.drjackycv.tmdbmultiplatform.presentation.mapper 2 | 3 | import app.web.drjackycv.tmdbmultiplatform.presentation.entity.MovieUI 4 | import data.base.mapper.Mapper 5 | import data.entity.Movie 6 | 7 | class MoviesUIMapper : Mapper, List> { 8 | 9 | override fun mapTo(response: List): List = response.map { 10 | MovieUI( 11 | popularity = it.popularity, 12 | voteCount = it.voteCount, 13 | video = it.video, 14 | posterPath = it.posterPath, 15 | id = it.id, 16 | adult = it.adult, 17 | backdropPath = it.backdropPath, 18 | originalLanguage = it.originalLanguage, 19 | originalTitle = it.originalTitle, 20 | genreIds = it.genreIds, 21 | title = it.title, 22 | voteAverage = it.voteAverage, 23 | overview = it.overview, 24 | releaseDate = it.releaseDate, 25 | fullPosterPath = it.fullPosterPath 26 | ) 27 | } 28 | 29 | } -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/data/entity/MoviesResponse.kt: -------------------------------------------------------------------------------- 1 | package data.entity 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | data class MoviesResponse( 8 | @SerialName("page") val page: Int, 9 | @SerialName("total_results") val totalResults: Int, 10 | @SerialName("total_pages") val totalPages: Int, 11 | @SerialName("results") val results: List 12 | ) 13 | 14 | @Serializable 15 | data class MovieResponse( 16 | @SerialName("popularity") val popularity: Double, 17 | @SerialName("vote_count") val voteCount: Int, 18 | @SerialName("video") val video: Boolean, 19 | @SerialName("poster_path") val posterPath: String?, 20 | @SerialName("id") val id: Int, 21 | @SerialName("adult") val adult: Boolean, 22 | @SerialName("backdrop_path") val backdropPath: String?, 23 | @SerialName("original_language") val originalLanguage: String, 24 | @SerialName("original_title") val originalTitle: String, 25 | @SerialName("genre_ids") val genreIds: List, 26 | @SerialName("title") val title: String, 27 | @SerialName("vote_average") val voteAverage: Double, 28 | @SerialName("overview") val overview: String, 29 | @SerialName("release_date") val releaseDate: String 30 | ) -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/presentation/base/ViewModelBinding.kt: -------------------------------------------------------------------------------- 1 | package presentation.base 2 | 3 | import com.badoo.reaktive.annotations.ExperimentalReaktiveApi 4 | import com.badoo.reaktive.disposable.Disposable 5 | import com.badoo.reaktive.disposable.scope.DisposableScope 6 | import com.badoo.reaktive.observable.Observable 7 | 8 | @OptIn(ExperimentalReaktiveApi::class) 9 | class ViewModelBinding : DisposableScope by DisposableScope() { 10 | 11 | fun subscribe( 12 | observable: Observable, 13 | isThreadLocal: Boolean = true, 14 | onSubscribe: ((Disposable) -> Unit)? = null, 15 | onError: ((Throwable) -> Unit)? = null, 16 | onComplete: (() -> Unit)? = null, 17 | onNext: ((T) -> Unit)? = null 18 | ) { 19 | observable.subscribeScoped( 20 | isThreadLocal = isThreadLocal, 21 | onSubscribe = onSubscribe, 22 | onError = onError, 23 | onComplete = onComplete, 24 | onNext = onNext 25 | ) 26 | } 27 | 28 | fun subscribe( 29 | observable: Observable, 30 | onError: ((Throwable) -> Unit)? = null, 31 | onNext: ((T) -> Unit)? = null 32 | ) { 33 | observable.subscribeScoped( 34 | isThreadLocal = true, 35 | onError = onError, 36 | onNext = onNext 37 | ) 38 | } 39 | 40 | fun subscribe(observable: Observable, onNext: ((T) -> Unit)? = null) { 41 | observable.subscribeScoped(isThreadLocal = true, onNext = onNext) 42 | } 43 | 44 | } -------------------------------------------------------------------------------- /ios/ios/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // ios 4 | // 5 | // Created by Hossein Abbasi on 6/4/20. 6 | // Copyright © 2020 Hossein Abbasi. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | 15 | 16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 17 | // Override point for customization after application launch. 18 | return true 19 | } 20 | 21 | // MARK: UISceneSession Lifecycle 22 | 23 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { 24 | // Called when a new scene session is being created. 25 | // Use this method to select a configuration to create the new scene with. 26 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 27 | } 28 | 29 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { 30 | // Called when the user discards a scene session. 31 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. 32 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return. 33 | } 34 | 35 | 36 | } 37 | 38 | -------------------------------------------------------------------------------- /android/src/main/kotlin/app/web/drjackycv/tmdbmultiplatform/presentation/extension/ViewExtensions.kt: -------------------------------------------------------------------------------- 1 | package app.web.drjackycv.tmdbmultiplatform.presentation.extension 2 | 3 | import android.view.LayoutInflater 4 | import android.view.View 5 | import android.view.ViewGroup 6 | import android.widget.ImageView 7 | import androidx.annotation.DrawableRes 8 | import androidx.annotation.LayoutRes 9 | import androidx.appcompat.content.res.AppCompatResources 10 | import app.web.drjackycv.tmdbmultiplatform.R 11 | import app.web.drjackycv.tmdbmultiplatform.presentation.base.util.GlideApp 12 | import com.bumptech.glide.request.RequestOptions 13 | 14 | fun View.gone() { 15 | visibility = View.GONE 16 | } 17 | 18 | fun View.visible() { 19 | visibility = View.VISIBLE 20 | } 21 | 22 | fun View.invisible() { 23 | visibility = View.INVISIBLE 24 | } 25 | 26 | fun ViewGroup.inflate(@LayoutRes layoutRes: Int, attachToRoot: Boolean = false): View = 27 | LayoutInflater.from(context).inflate(layoutRes, this, attachToRoot) 28 | 29 | fun ImageView.load( 30 | url: String, 31 | @DrawableRes placeholderRes: Int = R.drawable.ic_cloud_download 32 | ) { 33 | val safePlaceholderDrawable = AppCompatResources.getDrawable(context, placeholderRes) 34 | val requestOptions = RequestOptions().apply { 35 | placeholder(safePlaceholderDrawable) 36 | error(safePlaceholderDrawable) 37 | } 38 | val glideRequest = GlideApp 39 | .with(context) 40 | .setDefaultRequestOptions(requestOptions) 41 | .load(url) 42 | 43 | glideRequest.into(this) 44 | } 45 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | Android 20 | 21 | 22 | CorrectnessLintAndroid 23 | 24 | 25 | Gradle 26 | 27 | 28 | LintAndroid 29 | 30 | 31 | Probable bugsGradle 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 43 | 44 | 46 | -------------------------------------------------------------------------------- /ios/iosUITests/iosUITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // iosUITests.swift 3 | // iosUITests 4 | // 5 | // Created by Hossein Abbasi on 6/4/20. 6 | // Copyright © 2020 Hossein Abbasi. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | 11 | class iosUITests: XCTestCase { 12 | 13 | override func setUpWithError() throws { 14 | // Put setup code here. This method is called before the invocation of each test method in the class. 15 | 16 | // In UI tests it is usually best to stop immediately when a failure occurs. 17 | continueAfterFailure = false 18 | 19 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. 20 | } 21 | 22 | override func tearDownWithError() throws { 23 | // Put teardown code here. This method is called after the invocation of each test method in the class. 24 | } 25 | 26 | func testExample() throws { 27 | // UI tests must launch the application that they test. 28 | let app = XCUIApplication() 29 | app.launch() 30 | 31 | // Use recording to get started writing UI tests. 32 | // Use XCTAssert and related functions to verify your tests produce the correct results. 33 | } 34 | 35 | func testLaunchPerformance() throws { 36 | if #available(macOS 10.15, iOS 13.0, tvOS 13.0, *) { 37 | // This measures how long it takes to launch your application. 38 | measure(metrics: [XCTOSSignpostMetric.applicationLaunch]) { 39 | XCUIApplication().launch() 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /ios/ios/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /android/src/main/kotlin/app/web/drjackycv/tmdbmultiplatform/presentation/adapter/MoviesAdapter.kt: -------------------------------------------------------------------------------- 1 | package app.web.drjackycv.tmdbmultiplatform.presentation.adapter 2 | 3 | import android.view.LayoutInflater 4 | import android.view.View 5 | import android.view.ViewGroup 6 | import android.widget.ImageView 7 | import android.widget.TextView 8 | import androidx.recyclerview.widget.RecyclerView 9 | import app.web.drjackycv.tmdbmultiplatform.R 10 | import app.web.drjackycv.tmdbmultiplatform.presentation.entity.MovieUI 11 | import app.web.drjackycv.tmdbmultiplatform.presentation.extension.load 12 | 13 | class MoviesAdapter : RecyclerView.Adapter() { 14 | 15 | private var mItems = mutableListOf() 16 | 17 | fun setList(list: List) { 18 | mItems.clear() 19 | mItems.addAll(list) 20 | notifyDataSetChanged() 21 | } 22 | 23 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MovieViewHolder { 24 | return MovieViewHolder( 25 | LayoutInflater.from(parent.context).inflate(R.layout.item_movie, parent, false) 26 | ) 27 | } 28 | 29 | override fun getItemCount() = mItems.size 30 | 31 | override fun onBindViewHolder(holder: MovieViewHolder, position: Int) { 32 | val movie = mItems[position] 33 | 34 | holder.itemMovieImv.load(movie.fullPosterPath) 35 | holder.itemMovieTitleTxv.text = movie.title 36 | } 37 | 38 | class MovieViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { 39 | val itemMovieImv: ImageView = itemView.findViewById(R.id.itemMovieImv) 40 | val itemMovieTitleTxv: TextView = itemView.findViewById(R.id.itemMovieTitleTxv) 41 | } 42 | 43 | } -------------------------------------------------------------------------------- /ios/ios/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /ios/ios.xcodeproj/project.xcworkspace/xcuserdata/drjacky.xcuserdatad/xcdebugger/Expressions.xcexplist: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 7 | 8 | 10 | 11 | 13 | 14 | 16 | 17 | 19 | 20 | 21 | 22 | 24 | 25 | 27 | 28 | 30 | 31 | 33 | 34 | 35 | 36 | 38 | 39 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/data/remote/MoviesApiImpl.kt: -------------------------------------------------------------------------------- 1 | package data.remote 2 | 3 | import data.base.mapper.Mapper 4 | import data.base.remote.Api 5 | import data.entity.Movie 6 | import data.entity.MoviesResponse 7 | import io.ktor.client.* 8 | import io.ktor.client.request.* 9 | import io.ktor.client.statement.* 10 | import io.ktor.http.* 11 | //import kotlinx.serialization.UnstableDefault 12 | import kotlinx.serialization.json.Json 13 | 14 | class MoviesApiImpl( 15 | private val client: HttpClient, 16 | private val key: String, 17 | private val hostUrl: String, 18 | private val mapper: Mapper> 19 | ) : Api> { 20 | 21 | //@OptIn(UnstableDefault::class)// = deprecated: @UseExperimental(UnstableDefault::class) 22 | override suspend fun execute(request: String?): List { 23 | 24 | val httpStatement = client.get { 25 | apiUrl() 26 | parameter("query", request) 27 | } 28 | 29 | val httpResponse = httpStatement.execute() 30 | 31 | val json = httpResponse.readText() 32 | 33 | //val response = Json.nonstrict.parse(MoviesResponse.serializer(), json) 34 | val response = Json { 35 | ignoreUnknownKeys = true 36 | isLenient = true 37 | } 38 | .decodeFromString(MoviesResponse.serializer(), json) 39 | 40 | return mapper.mapTo(response) 41 | 42 | } 43 | 44 | private fun HttpRequestBuilder.apiUrl(path: String? = null) { 45 | header(HttpHeaders.CacheControl, "no-cache") 46 | url { 47 | takeFrom(hostUrl).parameters.append("api_key", key) 48 | path?.let { 49 | encodedPath = it 50 | } 51 | } 52 | } 53 | 54 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TMDbMultiplatform 2 | ![License](https://img.shields.io/github/license/Drjacky/TMDbMultiplatform) 3 | [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2FDrjacky%2FTMDbMultiplatform.svg?type=shield)](https://app.fossa.com/projects/git%2Bgithub.com%2FDrjacky%2FTMDbMultiplatform?ref=badge_shield) 4 | [![Android Weekly](https://img.shields.io/badge/Android%20Weekly-%23425-blue)](https://androidweekly.net/issues/issue-425) 5 | ![Article](https://raw.githubusercontent.com/Drjacky/TMDbMultiplatform/master/cover.jpg) 6 | Link to the [article](https://medium.com/@drjacky/kotlin-multiplatform-rx-mvvm-1fb21280a0b5) 7 | 8 | A Multiplatform project to show list of movies on iOS and Android. 9 | 10 | ## Features 🕹 11 | 12 | - Android: 100% Kotlin 13 | - iOS: 100% Swift 14 | - Following Clean Architecture approach 15 | - Following Input/Output MVVM Architectural Design Pattern 16 | 17 | ## Under Development 🚧 18 | 19 | - Improve architecture 20 | - Add search box 21 | - Use Sqldelight to show how to use the database in Multiplatform apps 22 | - Improve Clean Architecture approach and separate modules with the power of gradle 23 | - Categorize dependencies 24 | - Add tests 25 | - ~~Migrate from OMDb API to TMDb API~~ 26 | - ~~Use a DI framework or a Service Locator~~ 27 | - ~~Migrate to Kotlin 1.4.X. To see changes, click [here](https://github.com/Drjacky/TMDbMultiplatform/pull/3/files).~~ 28 | 29 | ## Contributing 🤝 30 | 31 | Feel free to open a issue or submit a pull request for any bugs/improvements. 32 | 33 | ## Result 📺 34 | ![Screenshot](https://raw.githubusercontent.com/Drjacky/TMDbMultiplatform/master/list.png) 35 | 36 | ## License ⚖️ 37 | [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2FDrjacky%2FTMDbMultiplatform.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2FDrjacky%2FTMDbMultiplatform?ref=badge_large) -------------------------------------------------------------------------------- /android/src/main/res/layout/item_movie.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 18 | 19 | 28 | 29 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /android/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /ios/ios/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "1x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "76x76" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "scale" : "2x", 86 | "size" : "83.5x83.5" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "scale" : "1x", 91 | "size" : "1024x1024" 92 | } 93 | ], 94 | "info" : { 95 | "author" : "xcode", 96 | "version" : 1 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/di/DI.kt: -------------------------------------------------------------------------------- 1 | package di 2 | 3 | import data.base.mapper.Mapper 4 | import data.base.remote.Api 5 | import data.entity.Movie 6 | import data.entity.MoviesResponse 7 | import data.mapper.MoviesMapper 8 | import data.remote.MoviesApiImpl 9 | import data.repository.MoviesRepositoryImpl 10 | import domain.base.repository.Repository 11 | import domain.base.usecase.UseCase 12 | import domain.usecase.GetMoviesUseCaseImpl 13 | import io.ktor.client.* 14 | import io.ktor.client.features.json.* 15 | import io.ktor.client.features.json.serializer.* 16 | import io.ktor.client.features.logging.* 17 | import org.kodein.di.* 18 | import kotlin.native.concurrent.ThreadLocal 19 | 20 | @ThreadLocal 21 | object Di { 22 | 23 | val di = DI.lazy { 24 | bind>>() with singleton { MoviesMapper() } 25 | bind("ApiHost") with provider { "https://api.themoviedb.org/3/search/movie" } 26 | bind("ApiKey") with provider { "238af7cc48b4305ff3fb75d7af217de4" } 27 | bind() with singleton { 28 | HttpClient { 29 | install(JsonFeature) { 30 | serializer = KotlinxSerializer() 31 | } 32 | install(Logging) { 33 | logger = Logger.DEFAULT 34 | level = LogLevel.ALL 35 | } 36 | } 37 | 38 | } 39 | bind>>("Api") with provider { 40 | val apiKey by di.instance("ApiKey") 41 | val apiHost by di.instance("ApiHost") 42 | 43 | MoviesApiImpl( 44 | client = instance(), 45 | key = apiKey, 46 | hostUrl = apiHost, 47 | mapper = instance() 48 | ) 49 | } 50 | bind>>("Repository") with provider { 51 | MoviesRepositoryImpl(instance(tag = "Api")) 52 | } 53 | bind>>("UseCase") with provider { 54 | GetMoviesUseCaseImpl(instance(tag = "Repository")) 55 | } 56 | } 57 | 58 | } -------------------------------------------------------------------------------- /ios/ios/MovieItem.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MovieItem.swift 3 | // ios 4 | // 5 | // Created by Hossein Abbasi on 7/27/20. 6 | // Copyright © 2020 Hossein Abbasi. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import shared 11 | import Nuke 12 | 13 | class MovieItem: UICollectionViewCell { 14 | 15 | let imgCover: UIImageView = { 16 | let v = UIImageView() 17 | 18 | v.translatesAutoresizingMaskIntoConstraints = false 19 | v.contentMode = .scaleAspectFill 20 | v.clipsToBounds = true 21 | 22 | return v 23 | }() 24 | 25 | let lblTitle: UILabel = { 26 | let v = UILabel() 27 | 28 | v.translatesAutoresizingMaskIntoConstraints = false 29 | v.numberOfLines = 2 30 | v.textAlignment = .center 31 | 32 | return v 33 | }() 34 | 35 | var movie: Movie? { 36 | didSet { 37 | guard let movie = movie else { return } 38 | 39 | if let poster = URL(string: movie.fullPosterPath) { 40 | Nuke.loadImage(with: poster, into: imgCover) 41 | } 42 | 43 | lblTitle.text = movie.title 44 | } 45 | } 46 | 47 | required init(coder aDecoder: NSCoder) { 48 | fatalError("init(coder:)") 49 | } 50 | 51 | override init(frame: CGRect) { 52 | super.init(frame: frame) 53 | 54 | contentView.addSubview(imgCover) 55 | contentView.addSubview(lblTitle) 56 | 57 | NSLayoutConstraint.activate([ 58 | imgCover.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), 59 | imgCover.topAnchor.constraint(equalTo: contentView.topAnchor), 60 | imgCover.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), 61 | imgCover.heightAnchor.constraint(equalToConstant: 200), 62 | 63 | lblTitle.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 10), 64 | lblTitle.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -10), 65 | lblTitle.topAnchor.constraint(equalTo: imgCover.bottomAnchor, constant: 10) 66 | ]) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /ios/ios/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | $(MARKETING_VERSION) 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | LSRequiresIPhoneOS 22 | 23 | UIApplicationSceneManifest 24 | 25 | UIApplicationSupportsMultipleScenes 26 | 27 | UISceneConfigurations 28 | 29 | UIWindowSceneSessionRoleApplication 30 | 31 | 32 | UISceneConfigurationName 33 | Default Configuration 34 | UISceneDelegateClassName 35 | $(PRODUCT_MODULE_NAME).SceneDelegate 36 | UISceneStoryboardFile 37 | Main 38 | 39 | 40 | 41 | 42 | UILaunchStoryboardName 43 | LaunchScreen 44 | UIMainStoryboardFile 45 | Main 46 | UIRequiredDeviceCapabilities 47 | 48 | armv7 49 | 50 | UISupportedInterfaceOrientations 51 | 52 | UIInterfaceOrientationPortrait 53 | UIInterfaceOrientationLandscapeLeft 54 | UIInterfaceOrientationLandscapeRight 55 | 56 | UISupportedInterfaceOrientations~ipad 57 | 58 | UIInterfaceOrientationPortrait 59 | UIInterfaceOrientationPortraitUpsideDown 60 | UIInterfaceOrientationLandscapeLeft 61 | UIInterfaceOrientationLandscapeRight 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /.idea/jarRepositories.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 | 15 | 19 | 20 | 24 | 25 | 29 | 30 | 34 | 35 | 39 | 40 | 44 | 45 | 49 | 50 | 54 | 55 | -------------------------------------------------------------------------------- /ios/ios/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.swift 3 | // ios 4 | // 5 | // Created by Hossein Abbasi on 6/4/20. 6 | // Copyright © 2020 Hossein Abbasi. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 12 | 13 | var window: UIWindow? 14 | 15 | 16 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 17 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. 18 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. 19 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). 20 | guard let _ = (scene as? UIWindowScene) else { return } 21 | } 22 | 23 | func sceneDidDisconnect(_ scene: UIScene) { 24 | // Called as the scene is being released by the system. 25 | // This occurs shortly after the scene enters the background, or when its session is discarded. 26 | // Release any resources associated with this scene that can be re-created the next time the scene connects. 27 | // The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead). 28 | } 29 | 30 | func sceneDidBecomeActive(_ scene: UIScene) { 31 | // Called when the scene has moved from an inactive state to an active state. 32 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. 33 | } 34 | 35 | func sceneWillResignActive(_ scene: UIScene) { 36 | // Called when the scene will move from an active state to an inactive state. 37 | // This may occur due to temporary interruptions (ex. an incoming phone call). 38 | } 39 | 40 | func sceneWillEnterForeground(_ scene: UIScene) { 41 | // Called as the scene transitions from the background to the foreground. 42 | // Use this method to undo the changes made on entering the background. 43 | } 44 | 45 | func sceneDidEnterBackground(_ scene: UIScene) { 46 | // Called as the scene transitions from the foreground to the background. 47 | // Use this method to save data, release shared resources, and store enough scene-specific state information 48 | // to restore the scene back to its current state. 49 | } 50 | 51 | 52 | } 53 | 54 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | /local.properties 3 | /.idea/caches 4 | /.idea/libraries 5 | /.idea/modules.xml 6 | /.idea/workspace.xml 7 | /.idea/navEditor.xml 8 | /.idea/assetWizardSettings.xml 9 | .DS_Store 10 | /build 11 | /shared/build 12 | /captures 13 | .cxx 14 | /.idea/sonarlint 15 | 16 | # Created by https://www.toptal.com/developers/gitignore/api/android,kotlin 17 | # Edit at https://www.toptal.com/developers/gitignore?templates=android,kotlin 18 | 19 | ### Android ### 20 | # Built application files 21 | *.apk 22 | *.ap_ 23 | *.aab 24 | 25 | # Files for the ART/Dalvik VM 26 | *.dex 27 | 28 | # Java class files 29 | *.class 30 | 31 | # Generated files 32 | bin/ 33 | gen/ 34 | out/ 35 | 36 | # Gradle files 37 | .gradle/ 38 | build/ 39 | 40 | # Local configuration file (sdk path, etc) 41 | local.properties 42 | 43 | # Proguard folder generated by Eclipse 44 | proguard/ 45 | 46 | # Log Files 47 | *.log 48 | 49 | # Android Studio Navigation editor temp files 50 | .navigation/ 51 | 52 | # Android Studio captures folder 53 | captures/ 54 | 55 | # IntelliJ 56 | *.iml 57 | .idea/workspace.xml 58 | .idea/tasks.xml 59 | .idea/gradle.xml 60 | .idea/assetWizardSettings.xml 61 | .idea/dictionaries 62 | .idea/libraries 63 | .idea/caches 64 | # Android Studio 3 in .gitignore file. 65 | .idea/caches/build_file_checksums.ser 66 | .idea/modules.xml 67 | 68 | # Keystore files 69 | # Uncomment the following lines if you do not want to check your keystore files in. 70 | #*.jks 71 | #*.keystore 72 | 73 | # External native build folder generated in Android Studio 2.2 and later 74 | .externalNativeBuild 75 | 76 | # Google Services (e.g. APIs or Firebase) 77 | # google-services.json 78 | 79 | # Freeline 80 | freeline.py 81 | freeline/ 82 | freeline_project_description.json 83 | 84 | # fastlane 85 | fastlane/report.xml 86 | fastlane/Preview.html 87 | fastlane/screenshots 88 | fastlane/test_output 89 | fastlane/readme.md 90 | 91 | # Version control 92 | vcs.xml 93 | 94 | # lint 95 | lint/intermediates/ 96 | lint/generated/ 97 | lint/outputs/ 98 | lint/tmp/ 99 | # lint/reports/ 100 | 101 | ### Android Patch ### 102 | gen-external-apklibs 103 | output.json 104 | 105 | ### Kotlin ### 106 | # Compiled class file 107 | 108 | # Log file 109 | 110 | # BlueJ files 111 | *.ctxt 112 | 113 | # Mobile Tools for Java (J2ME) 114 | .mtj.tmp/ 115 | 116 | # Package Files # 117 | *.jar 118 | *.war 119 | *.nar 120 | *.ear 121 | *.zip 122 | *.tar.gz 123 | *.rar 124 | 125 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 126 | hs_err_pid* 127 | 128 | # End of https://www.toptal.com/developers/gitignore/api/android,kotlin -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/presentation/movies/MoviesListViewModel.kt: -------------------------------------------------------------------------------- 1 | package presentation.movies 2 | 3 | import com.badoo.reaktive.coroutinesinterop.singleFromCoroutine 4 | import com.badoo.reaktive.observable.* 5 | import com.badoo.reaktive.subject.publish.PublishSubject 6 | import data.base.mapper.Mapper 7 | import data.entity.Movie 8 | import di.Di 9 | import domain.base.usecase.UseCase 10 | import org.kodein.di.instance 11 | import presentation.base.ListViewModel 12 | import presentation.base.ListViewModelInput 13 | import presentation.base.ListViewModelOutput 14 | 15 | class MoviesListViewModel( 16 | mapper: Mapper, List>? 17 | ) : ListViewModel, ListViewModelInput, ListViewModelOutput { 18 | override val inputs: ListViewModelInput = this 19 | override val outputs: ListViewModelOutput = this 20 | override val loading: Observable 21 | override val result: Observable> 22 | 23 | val useCase: UseCase> by Di.di.instance("UseCase") 24 | private val mListProperty = PublishSubject()// publishSubject() 25 | private val mLoadMoreProperty = PublishSubject()//publishSubject() 26 | 27 | init { 28 | val loadingProperty = PublishSubject()//publishSubject() 29 | 30 | val items = mutableListOf() 31 | 32 | var clearItems = false 33 | 34 | loading = loadingProperty 35 | 36 | val initialRequest = mListProperty 37 | .doOnBeforeNext { loadingProperty.onNext(true) } 38 | .flatMapSingle { request -> 39 | singleFromCoroutine { useCase.execute(request) } 40 | } 41 | .doOnBeforeNext { 42 | loadingProperty.onNext(false) 43 | clearItems = true 44 | } 45 | 46 | val nextRequest = mLoadMoreProperty 47 | .doOnBeforeNext { loadingProperty.onNext(true) } 48 | .flatMapSingle { request -> 49 | singleFromCoroutine { useCase.execute(request) } 50 | } 51 | .doOnBeforeNext { 52 | loadingProperty.onNext(false) 53 | clearItems = false 54 | } 55 | 56 | result = merge(initialRequest, nextRequest).map { 57 | if (clearItems) { 58 | items.clear() 59 | } 60 | 61 | @Suppress("UNCHECKED_CAST") 62 | val list = mapper?.mapTo(it) ?: it as List 63 | 64 | items.addAll(list) 65 | 66 | items 67 | } 68 | 69 | } 70 | 71 | override fun get(request: String) { 72 | mListProperty.onNext(request) 73 | } 74 | 75 | override fun loadMore(request: String) { 76 | mLoadMoreProperty.onNext(request) 77 | } 78 | 79 | } -------------------------------------------------------------------------------- /android/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile 2 | 3 | val buildToolsVersion: String by project 4 | val kotlinVersion: String by project 5 | val androidxBaseVersion: String by project 6 | val androidxUiVersion: String by project 7 | val androidMaterialVersion: String by project 8 | val androidxSwipeRefreshLayoutVersion: String by project 9 | val androidxRecyclerviewVersion: String by project 10 | val androidConstraintLayoutVersion: String by project 11 | val androidMultidexVersion: String by project 12 | val ktorVersion: String by project 13 | val glideVersion: String by project 14 | val reactiveVersion: String by project 15 | val kodeinVersion: String by project 16 | 17 | plugins { 18 | id("com.android.application") 19 | kotlin("android") // = id("kotlin-android") 20 | kotlin("android.extensions") 21 | kotlin("kapt") 22 | } 23 | 24 | androidExtensions { 25 | isExperimental = true 26 | } 27 | 28 | android { 29 | compileSdkVersion(29) 30 | buildToolsVersion = buildToolsVersion 31 | defaultConfig { 32 | applicationId = "app.web.drjackycv.tmdbmultiplatform" 33 | minSdkVersion(21) 34 | targetSdkVersion(29) 35 | multiDexEnabled = true 36 | versionCode = 3 37 | versionName = "1.2.0" 38 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 39 | } 40 | 41 | buildTypes { 42 | val debug by getting {} 43 | val release by getting { 44 | isMinifyEnabled = false 45 | proguardFiles( 46 | getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" 47 | ) 48 | } 49 | } 50 | sourceSets { 51 | val main by getting { 52 | java.srcDirs("src/main/kotlin") 53 | manifest.srcFile("src/main/AndroidManifest.xml") 54 | res.srcDirs("src/main/res") 55 | } 56 | } 57 | packagingOptions { 58 | exclude("META-INF/*.kotlin_module") 59 | } 60 | } 61 | 62 | tasks.withType { 63 | kotlinOptions { 64 | jvmTarget = "1.8" 65 | } 66 | } 67 | 68 | dependencies { 69 | implementation("androidx.appcompat:appcompat:$androidxBaseVersion") 70 | implementation("androidx.core:core-ktx:$androidxBaseVersion") 71 | implementation("com.google.android.material:material:$androidMaterialVersion") 72 | implementation("androidx.swiperefreshlayout:swiperefreshlayout:$androidxSwipeRefreshLayoutVersion") 73 | implementation("androidx.recyclerview:recyclerview:$androidxRecyclerviewVersion") 74 | implementation("androidx.constraintlayout:constraintlayout:$androidConstraintLayoutVersion") 75 | implementation("com.android.support:multidex:$androidMultidexVersion") 76 | implementation("io.ktor:ktor-client-android:$ktorVersion") 77 | implementation("com.github.bumptech.glide:glide:$glideVersion") 78 | kapt("com.github.bumptech.glide:compiler:$glideVersion") 79 | implementation("com.badoo.reaktive:reaktive:$reactiveVersion") 80 | implementation("org.kodein.di:kodein-di-framework-android-x:$kodeinVersion") 81 | 82 | implementation(project(":shared")) 83 | } -------------------------------------------------------------------------------- /android/src/main/kotlin/app/web/drjackycv/tmdbmultiplatform/presentation/ui/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package app.web.drjackycv.tmdbmultiplatform.presentation.ui 2 | 3 | import android.os.Bundle 4 | import androidx.appcompat.app.AppCompatActivity 5 | import androidx.recyclerview.widget.GridLayoutManager 6 | import androidx.recyclerview.widget.LinearLayoutManager 7 | import androidx.recyclerview.widget.RecyclerView 8 | import app.web.drjackycv.tmdbmultiplatform.R 9 | import app.web.drjackycv.tmdbmultiplatform.presentation.adapter.MoviesAdapter 10 | import app.web.drjackycv.tmdbmultiplatform.presentation.entity.MovieUI 11 | import app.web.drjackycv.tmdbmultiplatform.presentation.mapper.MoviesUIMapper 12 | import com.badoo.reaktive.observable.observeOn 13 | import com.badoo.reaktive.scheduler.mainScheduler 14 | import kotlinx.android.synthetic.main.activity_main.* 15 | import presentation.base.ListViewModel 16 | import presentation.base.ViewModelBinding 17 | import presentation.movies.MoviesListViewModel 18 | 19 | private const val KEYWORD = "avengers" 20 | 21 | class MainActivity : AppCompatActivity() { 22 | 23 | private lateinit var mMoviesAdapter: MoviesAdapter 24 | private var mIsRefreshing = false 25 | private val mBinding = ViewModelBinding() 26 | private val mViewModel: ListViewModel by lazy { 27 | val moviesUiMapper = MoviesUIMapper() 28 | 29 | MoviesListViewModel(moviesUiMapper) 30 | } 31 | 32 | override fun onCreate(savedInstanceState: Bundle?) { 33 | binding() 34 | super.onCreate(savedInstanceState) 35 | setContentView(R.layout.activity_main) 36 | setupUI() 37 | } 38 | 39 | private fun setupUI() { 40 | mMoviesAdapter = MoviesAdapter() 41 | mainList.layoutManager = GridLayoutManager(this, 2) 42 | mainList.adapter = mMoviesAdapter 43 | 44 | mainList.addOnScrollListener(object : RecyclerView.OnScrollListener() { 45 | override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { 46 | super.onScrolled(recyclerView, dx, dy) 47 | 48 | val manager = mainList.layoutManager as LinearLayoutManager 49 | 50 | val totalItemCount = manager.itemCount 51 | val lastVisibleItem = manager.findLastVisibleItemPosition() 52 | 53 | if (!mIsRefreshing && totalItemCount <= lastVisibleItem + 2) { 54 | loadMore() 55 | } 56 | } 57 | }) 58 | 59 | mainSwipeRefresh.setOnRefreshListener { 60 | mViewModel.inputs.get(KEYWORD) 61 | } 62 | 63 | mViewModel.inputs.get(KEYWORD) 64 | } 65 | 66 | override fun onDestroy() { 67 | mBinding.dispose() //TODO Move it into view model 68 | super.onDestroy() 69 | } 70 | 71 | private fun binding() { 72 | mBinding.subscribe(mViewModel.outputs.loading.observeOn(mainScheduler), onNext = ::loading) 73 | mBinding.subscribe(mViewModel.outputs.result.observeOn(mainScheduler), onNext = ::result) 74 | } 75 | 76 | private fun loading(isLoading: Boolean) { 77 | mIsRefreshing = isLoading 78 | 79 | mainSwipeRefresh.isRefreshing = isLoading 80 | } 81 | 82 | private fun result(movies: List) { 83 | mMoviesAdapter.setList(movies) 84 | } 85 | 86 | private fun loadMore() = mViewModel.inputs.loadMore(KEYWORD) 87 | 88 | } 89 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at dr.jacky.2005@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | xmlns:android 17 | 18 | ^$ 19 | 20 | 21 | 22 |
23 |
24 | 25 | 26 | 27 | xmlns:.* 28 | 29 | ^$ 30 | 31 | 32 | BY_NAME 33 | 34 |
35 |
36 | 37 | 38 | 39 | .*:id 40 | 41 | http://schemas.android.com/apk/res/android 42 | 43 | 44 | 45 |
46 |
47 | 48 | 49 | 50 | .*:name 51 | 52 | http://schemas.android.com/apk/res/android 53 | 54 | 55 | 56 |
57 |
58 | 59 | 60 | 61 | name 62 | 63 | ^$ 64 | 65 | 66 | 67 |
68 |
69 | 70 | 71 | 72 | style 73 | 74 | ^$ 75 | 76 | 77 | 78 |
79 |
80 | 81 | 82 | 83 | .* 84 | 85 | ^$ 86 | 87 | 88 | BY_NAME 89 | 90 |
91 |
92 | 93 | 94 | 95 | .* 96 | 97 | http://schemas.android.com/apk/res/android 98 | 99 | 100 | ANDROID_ATTRIBUTE_ORDER 101 | 102 |
103 |
104 | 105 | 106 | 107 | .* 108 | 109 | .* 110 | 111 | 112 | BY_NAME 113 | 114 |
115 |
116 |
117 |
118 | 119 | 121 |
122 |
-------------------------------------------------------------------------------- /ios/ios/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MovieItem.swift 3 | // ios 4 | // 5 | // Created by Hossein Abbasi on 7/27/20. 6 | // Copyright © 2020 Hossein Abbasi. All rights reserved. 7 | // 8 | 9 | 10 | import UIKit 11 | import shared 12 | 13 | private var KEYWORD = "avengers" 14 | 15 | class ViewController: UIViewController { 16 | private var _movies: [Movie]? 17 | private var _isRefreshing = false 18 | 19 | lazy var uiRefreshControl: UIRefreshControl = { 20 | let view = UIRefreshControl() 21 | 22 | view.addTarget(self, action: #selector(refresh), for: .valueChanged) 23 | 24 | return view 25 | }() 26 | 27 | lazy var uiCollectionView: UICollectionView = { 28 | let layout = UICollectionViewFlowLayout() 29 | 30 | layout.minimumLineSpacing = 15 31 | layout.scrollDirection = .vertical 32 | layout.sectionInset = UIEdgeInsets(top: 15, left: 8, bottom: 8, right: 8) 33 | 34 | let marginsAndInsets = layout.sectionInset.left + layout.sectionInset.right + layout.minimumInteritemSpacing * CGFloat(2 - 1) 35 | let itemWidth = ((UIScreen.main.bounds.size.width - marginsAndInsets) / CGFloat(2)).rounded(.down) 36 | let itemSize = CGSize(width: itemWidth, height: 270) 37 | 38 | layout.itemSize = itemSize 39 | 40 | let v = UICollectionView(frame: .zero, collectionViewLayout: layout) 41 | 42 | v.backgroundColor = UIColor(named: "ListBackground") 43 | v.delegate = self 44 | v.dataSource = self 45 | v.alwaysBounceVertical = true 46 | v.refreshControl = uiRefreshControl 47 | v.translatesAutoresizingMaskIntoConstraints = false 48 | v.register(MovieItem.self, forCellWithReuseIdentifier: "MovieItem") 49 | 50 | return v 51 | }() 52 | 53 | private lazy var _viewModel: MoviesListViewModel = { 54 | let delegate = UIApplication.shared.delegate as! AppDelegate 55 | let viewModel = MoviesListViewModel(mapper: nil) 56 | 57 | return viewModel 58 | }() 59 | 60 | private lazy var _binding: ViewModelBinding = { 61 | return ViewModelBinding() 62 | }() 63 | 64 | deinit { 65 | _binding.dispose() 66 | } 67 | 68 | override func viewDidLoad() { 69 | super.viewDidLoad() 70 | 71 | binding() 72 | 73 | view.addSubview(uiCollectionView) 74 | 75 | NSLayoutConstraint.activate([ 76 | uiCollectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor), 77 | uiCollectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor), 78 | uiCollectionView.topAnchor.constraint(equalTo: view.topAnchor), 79 | uiCollectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor) 80 | ]) 81 | 82 | _viewModel.inputs.get(request: KEYWORD) 83 | } 84 | 85 | // MARK - Selector 86 | @objc func refresh() { 87 | _viewModel.inputs.get(request: KEYWORD) 88 | } 89 | 90 | // MARK - Private 91 | private func binding() { 92 | 93 | _binding.subscribe(observable: _viewModel.outputs.loading) { [weak self] result in 94 | guard let strongSelf = self, let loading = result as? Bool else { return } 95 | 96 | strongSelf._isRefreshing = loading 97 | 98 | if loading { 99 | strongSelf.uiRefreshControl.beginRefreshing() 100 | } else { 101 | strongSelf.uiRefreshControl.endRefreshing() 102 | } 103 | } 104 | 105 | _binding.subscribe(observable: _viewModel.outputs.result) { [weak self] result in 106 | guard let strongSelf = self, let list = result as? [Movie] else { return } 107 | 108 | strongSelf._movies = list 109 | strongSelf.uiCollectionView.reloadData() 110 | } 111 | } 112 | } 113 | 114 | extension ViewController: UICollectionViewDataSource, UICollectionViewDelegate { 115 | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 116 | return _movies?.count ?? 0 117 | } 118 | 119 | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 120 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "MovieItem", for: indexPath) as! MovieItem 121 | 122 | cell.movie = _movies?[indexPath.row] 123 | 124 | return cell 125 | } 126 | 127 | func scrollViewDidScroll(_ scrollView: UIScrollView) { 128 | let bottomEdge = scrollView.contentOffset.y + scrollView.frame.size.height; 129 | 130 | if (bottomEdge + 200 >= scrollView.contentSize.height && scrollView.contentOffset.y > 0 && !_isRefreshing) { 131 | _viewModel.inputs.loadMore(request: KEYWORD) 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /shared/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget 2 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile 3 | 4 | val buildToolsVersion: String by project 5 | val kotlinVersion: String by extra 6 | val reactiveVersion: String by extra 7 | val reactiveNmtcVersion: String by extra 8 | val kotlinSerializationVersion: String by extra 9 | val ktorVersion: String by extra 10 | val coroutinesVersion: String by extra 11 | val kodeinVersion: String by extra 12 | 13 | plugins { 14 | id("com.android.library") 15 | kotlin("multiplatform") 16 | kotlin("plugin.serialization") version "1.5.0" //org.jetbrains.kotlin.config.KotlinCompilerVersion.VERSION 17 | } 18 | 19 | android { 20 | compileSdkVersion(29) 21 | buildToolsVersion = buildToolsVersion 22 | defaultConfig { 23 | minSdkVersion(21) 24 | targetSdkVersion(29) 25 | } 26 | sourceSets { 27 | val main by getting { 28 | java.srcDirs("src/androidMain/kotlin") 29 | manifest.srcFile("src/androidMain/AndroidManifest.xml") 30 | res.srcDirs("src/androidMain/res") 31 | } 32 | } 33 | } 34 | 35 | kotlin { 36 | jvm() 37 | //jvm("android") 38 | android() 39 | 40 | //select iOS target platform depending on the Xcode environment variables 41 | val iOSTarget: (String, KotlinNativeTarget.() -> Unit) -> KotlinNativeTarget = 42 | if (System.getenv("SDK_NAME")?.startsWith("iphoneos") == true) 43 | ::iosArm64 44 | else 45 | ::iosX64 46 | 47 | iOSTarget("ios") { 48 | binaries { 49 | framework { 50 | baseName = "shared" 51 | } 52 | } 53 | } 54 | 55 | sourceSets { 56 | val commonMain by getting { 57 | dependencies { 58 | //implementation("org.jetbrains.kotlin:kotlin-stdlib") 59 | //implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core-common:$coroutinesVersion") 60 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion") { 61 | version { 62 | strictly(coroutinesVersion) 63 | } 64 | } 65 | implementation("com.badoo.reaktive:reaktive:$reactiveVersion") 66 | implementation("com.badoo.reaktive:coroutines-interop:$reactiveNmtcVersion") 67 | //implementation("org.jetbrains.kotlinx:kotlinx-serialization-runtime-common:$kotlinSerializationVersion") 68 | //implementation("org.jetbrains.kotlinx:kotlinx-serialization-core:$kotlinSerializationVersion") 69 | implementation("io.ktor:ktor-client-core:$ktorVersion") 70 | implementation("io.ktor:ktor-client-serialization:$ktorVersion") 71 | implementation("io.ktor:ktor-client-logging:$ktorVersion") 72 | implementation("org.kodein.di:kodein-di:$kodeinVersion") 73 | } 74 | } 75 | 76 | val mobileMain by creating { 77 | dependsOn(commonMain) 78 | } 79 | 80 | val jvmMain by getting { 81 | dependencies { 82 | //api("org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion") 83 | //api("org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion") 84 | //api("org.jetbrains.kotlinx:kotlinx-serialization-runtime:$kotlinSerializationVersion") 85 | //api("io.ktor:ktor-client-serialization-jvm:$ktorVersion") 86 | //api("io.ktor:ktor-client-core-jvm:$ktorVersion") 87 | //api("io.ktor:ktor-client-logging-jvm:$ktorVersion") 88 | } 89 | } 90 | 91 | val androidMain by getting { 92 | dependsOn(mobileMain) 93 | dependsOn(jvmMain) 94 | dependencies { 95 | //implementation("io.ktor:ktor-client-android:$ktorVersion") 96 | } 97 | } 98 | 99 | val iosMain by getting { 100 | dependsOn(mobileMain) 101 | dependencies { 102 | //implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core-native:$coroutinesVersion") 103 | //implementation("org.jetbrains.kotlinx:kotlinx-serialization-runtime-native:$kotlinSerializationVersion") 104 | implementation("io.ktor:ktor-client-ios:$ktorVersion") 105 | //implementation("io.ktor:ktor-client-serialization-native:$ktorVersion") 106 | //implementation("io.ktor:ktor-client-logging-native:$ktorVersion") 107 | } 108 | } 109 | 110 | all { 111 | languageSettings.useExperimentalAnnotation("kotlin.RequiresOptIn") 112 | } 113 | } 114 | 115 | } 116 | 117 | tasks.withType { 118 | kotlinOptions { 119 | jvmTarget = "1.8" 120 | } 121 | } 122 | 123 | val packForXcode by tasks.creating(Sync::class) { 124 | val targetDir = File(buildDir, "xcode-frameworks") 125 | //selecting the right configuration for the iOS framework depending on the Xcode environment variables 126 | val mode = System.getenv("CONFIGURATION") ?: "DEBUG" 127 | val framework = kotlin.targets.getByName("ios").binaries.getFramework(mode) 128 | 129 | inputs.property("mode", mode) 130 | dependsOn(framework.linkTask) 131 | 132 | from({ framework.outputDirectory }) 133 | into(targetDir) 134 | 135 | /// generate a helpful ./gradlew wrapper with embedded Java path 136 | doLast { 137 | val gradlew = File(targetDir, "gradlew") 138 | gradlew.writeText("#!/bin/bash\nexport 'JAVA_HOME=${System.getProperty("java.home")}'\ncd '${rootProject.rootDir}'\n./gradlew \$@\n") 139 | gradlew.setExecutable(true) 140 | } 141 | } 142 | 143 | tasks.getByName("build").dependsOn(packForXcode) -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /android/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /ios/ios.xcodeproj/xcuserdata/drjacky.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 9 | 21 | 22 | 23 | 25 | 37 | 38 | 39 | 41 | 53 | 54 | 55 | 57 | 69 | 70 | 71 | 73 | 85 | 86 | 100 | 101 | 115 | 116 | 117 | 118 | 119 | 121 | 133 | 134 | 135 | 137 | 149 | 150 | 151 | 153 | 165 | 166 | 167 | 169 | 181 | 182 | 183 | 185 | 197 | 198 | 212 | 213 | 227 | 228 | 229 | 230 | 231 | 233 | 245 | 246 | 247 | 249 | 261 | 262 | 263 | 265 | 277 | 278 | 279 | 281 | 293 | 294 | 295 | 297 | 309 | 310 | 311 | 313 | 325 | 326 | 327 | 329 | 341 | 342 | 343 | 345 | 357 | 358 | 359 | 361 | 373 | 374 | 375 | 377 | 389 | 390 | 391 | 393 | 405 | 406 | 407 | 409 | 421 | 422 | 423 | 425 | 437 | 438 | 439 | 441 | 453 | 454 | 455 | 457 | 469 | 470 | 471 | 473 | 485 | 486 | 487 | 489 | 501 | 502 | 503 | 505 | 517 | 518 | 519 | 521 | 533 | 534 | 535 | 537 | 549 | 550 | 551 | 553 | 565 | 566 | 567 | 568 | 569 | -------------------------------------------------------------------------------- /ios/ios.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 52; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 371C2C6B24898F6500BC04CB /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 371C2C6A24898F6500BC04CB /* AppDelegate.swift */; }; 11 | 371C2C6D24898F6500BC04CB /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 371C2C6C24898F6500BC04CB /* SceneDelegate.swift */; }; 12 | 371C2C6F24898F6500BC04CB /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 371C2C6E24898F6500BC04CB /* ViewController.swift */; }; 13 | 371C2C7224898F6500BC04CB /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 371C2C7024898F6500BC04CB /* Main.storyboard */; }; 14 | 371C2C7424898F6700BC04CB /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 371C2C7324898F6700BC04CB /* Assets.xcassets */; }; 15 | 371C2C7724898F6700BC04CB /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 371C2C7524898F6700BC04CB /* LaunchScreen.storyboard */; }; 16 | 371C2C8224898F6700BC04CB /* iosTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 371C2C8124898F6700BC04CB /* iosTests.swift */; }; 17 | 371C2C8D24898F6700BC04CB /* iosUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 371C2C8C24898F6700BC04CB /* iosUITests.swift */; }; 18 | 371C2C9C2489942D00BC04CB /* shared.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 371C2C9B2489942D00BC04CB /* shared.framework */; }; 19 | 371C2C9D2489942D00BC04CB /* shared.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 371C2C9B2489942D00BC04CB /* shared.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 20 | 37EC695224CED34C005AFB10 /* MovieItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37EC695124CED34C005AFB10 /* MovieItem.swift */; }; 21 | 37EC695524CEF684005AFB10 /* Nuke in Frameworks */ = {isa = PBXBuildFile; productRef = 37EC695424CEF684005AFB10 /* Nuke */; }; 22 | /* End PBXBuildFile section */ 23 | 24 | /* Begin PBXContainerItemProxy section */ 25 | 371C2C7E24898F6700BC04CB /* PBXContainerItemProxy */ = { 26 | isa = PBXContainerItemProxy; 27 | containerPortal = 371C2C5F24898F6500BC04CB /* Project object */; 28 | proxyType = 1; 29 | remoteGlobalIDString = 371C2C6624898F6500BC04CB; 30 | remoteInfo = ios; 31 | }; 32 | 371C2C8924898F6700BC04CB /* PBXContainerItemProxy */ = { 33 | isa = PBXContainerItemProxy; 34 | containerPortal = 371C2C5F24898F6500BC04CB /* Project object */; 35 | proxyType = 1; 36 | remoteGlobalIDString = 371C2C6624898F6500BC04CB; 37 | remoteInfo = ios; 38 | }; 39 | /* End PBXContainerItemProxy section */ 40 | 41 | /* Begin PBXCopyFilesBuildPhase section */ 42 | 371C2C9E2489942D00BC04CB /* Embed Frameworks */ = { 43 | isa = PBXCopyFilesBuildPhase; 44 | buildActionMask = 2147483647; 45 | dstPath = ""; 46 | dstSubfolderSpec = 10; 47 | files = ( 48 | 371C2C9D2489942D00BC04CB /* shared.framework in Embed Frameworks */, 49 | ); 50 | name = "Embed Frameworks"; 51 | runOnlyForDeploymentPostprocessing = 0; 52 | }; 53 | /* End PBXCopyFilesBuildPhase section */ 54 | 55 | /* Begin PBXFileReference section */ 56 | 371C2C6724898F6500BC04CB /* ios.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ios.app; sourceTree = BUILT_PRODUCTS_DIR; }; 57 | 371C2C6A24898F6500BC04CB /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 58 | 371C2C6C24898F6500BC04CB /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 59 | 371C2C6E24898F6500BC04CB /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 60 | 371C2C7124898F6500BC04CB /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 61 | 371C2C7324898F6700BC04CB /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 62 | 371C2C7624898F6700BC04CB /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 63 | 371C2C7824898F6700BC04CB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 64 | 371C2C7D24898F6700BC04CB /* iosTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = iosTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 65 | 371C2C8124898F6700BC04CB /* iosTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iosTests.swift; sourceTree = ""; }; 66 | 371C2C8324898F6700BC04CB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 67 | 371C2C8824898F6700BC04CB /* iosUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = iosUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 68 | 371C2C8C24898F6700BC04CB /* iosUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iosUITests.swift; sourceTree = ""; }; 69 | 371C2C8E24898F6700BC04CB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 70 | 371C2C9B2489942D00BC04CB /* shared.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = shared.framework; path = "../shared/build/xcode-frameworks/shared.framework"; sourceTree = ""; }; 71 | 37EC695124CED34C005AFB10 /* MovieItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieItem.swift; sourceTree = ""; }; 72 | /* End PBXFileReference section */ 73 | 74 | /* Begin PBXFrameworksBuildPhase section */ 75 | 371C2C6424898F6500BC04CB /* Frameworks */ = { 76 | isa = PBXFrameworksBuildPhase; 77 | buildActionMask = 2147483647; 78 | files = ( 79 | 371C2C9C2489942D00BC04CB /* shared.framework in Frameworks */, 80 | 37EC695524CEF684005AFB10 /* Nuke in Frameworks */, 81 | ); 82 | runOnlyForDeploymentPostprocessing = 0; 83 | }; 84 | 371C2C7A24898F6700BC04CB /* Frameworks */ = { 85 | isa = PBXFrameworksBuildPhase; 86 | buildActionMask = 2147483647; 87 | files = ( 88 | ); 89 | runOnlyForDeploymentPostprocessing = 0; 90 | }; 91 | 371C2C8524898F6700BC04CB /* Frameworks */ = { 92 | isa = PBXFrameworksBuildPhase; 93 | buildActionMask = 2147483647; 94 | files = ( 95 | ); 96 | runOnlyForDeploymentPostprocessing = 0; 97 | }; 98 | /* End PBXFrameworksBuildPhase section */ 99 | 100 | /* Begin PBXGroup section */ 101 | 371C2C5E24898F6500BC04CB = { 102 | isa = PBXGroup; 103 | children = ( 104 | 371C2C6924898F6500BC04CB /* ios */, 105 | 371C2C8024898F6700BC04CB /* iosTests */, 106 | 371C2C8B24898F6700BC04CB /* iosUITests */, 107 | 371C2C6824898F6500BC04CB /* Products */, 108 | 371C2C9A2489942D00BC04CB /* Frameworks */, 109 | ); 110 | sourceTree = ""; 111 | }; 112 | 371C2C6824898F6500BC04CB /* Products */ = { 113 | isa = PBXGroup; 114 | children = ( 115 | 371C2C6724898F6500BC04CB /* ios.app */, 116 | 371C2C7D24898F6700BC04CB /* iosTests.xctest */, 117 | 371C2C8824898F6700BC04CB /* iosUITests.xctest */, 118 | ); 119 | name = Products; 120 | sourceTree = ""; 121 | }; 122 | 371C2C6924898F6500BC04CB /* ios */ = { 123 | isa = PBXGroup; 124 | children = ( 125 | 371C2C6A24898F6500BC04CB /* AppDelegate.swift */, 126 | 371C2C6C24898F6500BC04CB /* SceneDelegate.swift */, 127 | 371C2C6E24898F6500BC04CB /* ViewController.swift */, 128 | 371C2C7024898F6500BC04CB /* Main.storyboard */, 129 | 371C2C7324898F6700BC04CB /* Assets.xcassets */, 130 | 371C2C7524898F6700BC04CB /* LaunchScreen.storyboard */, 131 | 371C2C7824898F6700BC04CB /* Info.plist */, 132 | 37EC695124CED34C005AFB10 /* MovieItem.swift */, 133 | ); 134 | path = ios; 135 | sourceTree = ""; 136 | }; 137 | 371C2C8024898F6700BC04CB /* iosTests */ = { 138 | isa = PBXGroup; 139 | children = ( 140 | 371C2C8124898F6700BC04CB /* iosTests.swift */, 141 | 371C2C8324898F6700BC04CB /* Info.plist */, 142 | ); 143 | path = iosTests; 144 | sourceTree = ""; 145 | }; 146 | 371C2C8B24898F6700BC04CB /* iosUITests */ = { 147 | isa = PBXGroup; 148 | children = ( 149 | 371C2C8C24898F6700BC04CB /* iosUITests.swift */, 150 | 371C2C8E24898F6700BC04CB /* Info.plist */, 151 | ); 152 | path = iosUITests; 153 | sourceTree = ""; 154 | }; 155 | 371C2C9A2489942D00BC04CB /* Frameworks */ = { 156 | isa = PBXGroup; 157 | children = ( 158 | 371C2C9B2489942D00BC04CB /* shared.framework */, 159 | ); 160 | name = Frameworks; 161 | sourceTree = ""; 162 | }; 163 | /* End PBXGroup section */ 164 | 165 | /* Begin PBXNativeTarget section */ 166 | 371C2C6624898F6500BC04CB /* ios */ = { 167 | isa = PBXNativeTarget; 168 | buildConfigurationList = 371C2C9124898F6700BC04CB /* Build configuration list for PBXNativeTarget "ios" */; 169 | buildPhases = ( 170 | 371C2C9F2489954A00BC04CB /* ShellScript */, 171 | 371C2C6324898F6500BC04CB /* Sources */, 172 | 371C2C6424898F6500BC04CB /* Frameworks */, 173 | 371C2C6524898F6500BC04CB /* Resources */, 174 | 371C2C9E2489942D00BC04CB /* Embed Frameworks */, 175 | ); 176 | buildRules = ( 177 | ); 178 | dependencies = ( 179 | ); 180 | name = ios; 181 | packageProductDependencies = ( 182 | 37EC695424CEF684005AFB10 /* Nuke */, 183 | ); 184 | productName = ios; 185 | productReference = 371C2C6724898F6500BC04CB /* ios.app */; 186 | productType = "com.apple.product-type.application"; 187 | }; 188 | 371C2C7C24898F6700BC04CB /* iosTests */ = { 189 | isa = PBXNativeTarget; 190 | buildConfigurationList = 371C2C9424898F6700BC04CB /* Build configuration list for PBXNativeTarget "iosTests" */; 191 | buildPhases = ( 192 | 371C2C7924898F6700BC04CB /* Sources */, 193 | 371C2C7A24898F6700BC04CB /* Frameworks */, 194 | 371C2C7B24898F6700BC04CB /* Resources */, 195 | ); 196 | buildRules = ( 197 | ); 198 | dependencies = ( 199 | 371C2C7F24898F6700BC04CB /* PBXTargetDependency */, 200 | ); 201 | name = iosTests; 202 | productName = iosTests; 203 | productReference = 371C2C7D24898F6700BC04CB /* iosTests.xctest */; 204 | productType = "com.apple.product-type.bundle.unit-test"; 205 | }; 206 | 371C2C8724898F6700BC04CB /* iosUITests */ = { 207 | isa = PBXNativeTarget; 208 | buildConfigurationList = 371C2C9724898F6700BC04CB /* Build configuration list for PBXNativeTarget "iosUITests" */; 209 | buildPhases = ( 210 | 371C2C8424898F6700BC04CB /* Sources */, 211 | 371C2C8524898F6700BC04CB /* Frameworks */, 212 | 371C2C8624898F6700BC04CB /* Resources */, 213 | ); 214 | buildRules = ( 215 | ); 216 | dependencies = ( 217 | 371C2C8A24898F6700BC04CB /* PBXTargetDependency */, 218 | ); 219 | name = iosUITests; 220 | productName = iosUITests; 221 | productReference = 371C2C8824898F6700BC04CB /* iosUITests.xctest */; 222 | productType = "com.apple.product-type.bundle.ui-testing"; 223 | }; 224 | /* End PBXNativeTarget section */ 225 | 226 | /* Begin PBXProject section */ 227 | 371C2C5F24898F6500BC04CB /* Project object */ = { 228 | isa = PBXProject; 229 | attributes = { 230 | LastSwiftUpdateCheck = 1150; 231 | LastUpgradeCheck = 1150; 232 | ORGANIZATIONNAME = "Hossein Abbasi"; 233 | TargetAttributes = { 234 | 371C2C6624898F6500BC04CB = { 235 | CreatedOnToolsVersion = 11.5; 236 | }; 237 | 371C2C7C24898F6700BC04CB = { 238 | CreatedOnToolsVersion = 11.5; 239 | TestTargetID = 371C2C6624898F6500BC04CB; 240 | }; 241 | 371C2C8724898F6700BC04CB = { 242 | CreatedOnToolsVersion = 11.5; 243 | TestTargetID = 371C2C6624898F6500BC04CB; 244 | }; 245 | }; 246 | }; 247 | buildConfigurationList = 371C2C6224898F6500BC04CB /* Build configuration list for PBXProject "ios" */; 248 | compatibilityVersion = "Xcode 9.3"; 249 | developmentRegion = en; 250 | hasScannedForEncodings = 0; 251 | knownRegions = ( 252 | en, 253 | Base, 254 | ); 255 | mainGroup = 371C2C5E24898F6500BC04CB; 256 | packageReferences = ( 257 | 37EC695324CEF684005AFB10 /* XCRemoteSwiftPackageReference "Nuke" */, 258 | ); 259 | productRefGroup = 371C2C6824898F6500BC04CB /* Products */; 260 | projectDirPath = ""; 261 | projectRoot = ""; 262 | targets = ( 263 | 371C2C6624898F6500BC04CB /* ios */, 264 | 371C2C7C24898F6700BC04CB /* iosTests */, 265 | 371C2C8724898F6700BC04CB /* iosUITests */, 266 | ); 267 | }; 268 | /* End PBXProject section */ 269 | 270 | /* Begin PBXResourcesBuildPhase section */ 271 | 371C2C6524898F6500BC04CB /* Resources */ = { 272 | isa = PBXResourcesBuildPhase; 273 | buildActionMask = 2147483647; 274 | files = ( 275 | 371C2C7724898F6700BC04CB /* LaunchScreen.storyboard in Resources */, 276 | 371C2C7424898F6700BC04CB /* Assets.xcassets in Resources */, 277 | 371C2C7224898F6500BC04CB /* Main.storyboard in Resources */, 278 | ); 279 | runOnlyForDeploymentPostprocessing = 0; 280 | }; 281 | 371C2C7B24898F6700BC04CB /* Resources */ = { 282 | isa = PBXResourcesBuildPhase; 283 | buildActionMask = 2147483647; 284 | files = ( 285 | ); 286 | runOnlyForDeploymentPostprocessing = 0; 287 | }; 288 | 371C2C8624898F6700BC04CB /* Resources */ = { 289 | isa = PBXResourcesBuildPhase; 290 | buildActionMask = 2147483647; 291 | files = ( 292 | ); 293 | runOnlyForDeploymentPostprocessing = 0; 294 | }; 295 | /* End PBXResourcesBuildPhase section */ 296 | 297 | /* Begin PBXShellScriptBuildPhase section */ 298 | 371C2C9F2489954A00BC04CB /* ShellScript */ = { 299 | isa = PBXShellScriptBuildPhase; 300 | buildActionMask = 2147483647; 301 | files = ( 302 | ); 303 | inputFileListPaths = ( 304 | ); 305 | inputPaths = ( 306 | ); 307 | outputFileListPaths = ( 308 | ); 309 | outputPaths = ( 310 | ); 311 | runOnlyForDeploymentPostprocessing = 0; 312 | shellPath = /bin/sh; 313 | shellScript = "# Type a script or drag a script file from your workspace to insert its path.\ncd \"$SRCROOT/../shared/build/xcode-frameworks\"\n./gradlew :shared:packForXCode -PXCODE_CONFIGURATION=${CONFIGURATION}\n"; 314 | }; 315 | /* End PBXShellScriptBuildPhase section */ 316 | 317 | /* Begin PBXSourcesBuildPhase section */ 318 | 371C2C6324898F6500BC04CB /* Sources */ = { 319 | isa = PBXSourcesBuildPhase; 320 | buildActionMask = 2147483647; 321 | files = ( 322 | 371C2C6F24898F6500BC04CB /* ViewController.swift in Sources */, 323 | 371C2C6B24898F6500BC04CB /* AppDelegate.swift in Sources */, 324 | 37EC695224CED34C005AFB10 /* MovieItem.swift in Sources */, 325 | 371C2C6D24898F6500BC04CB /* SceneDelegate.swift in Sources */, 326 | ); 327 | runOnlyForDeploymentPostprocessing = 0; 328 | }; 329 | 371C2C7924898F6700BC04CB /* Sources */ = { 330 | isa = PBXSourcesBuildPhase; 331 | buildActionMask = 2147483647; 332 | files = ( 333 | 371C2C8224898F6700BC04CB /* iosTests.swift in Sources */, 334 | ); 335 | runOnlyForDeploymentPostprocessing = 0; 336 | }; 337 | 371C2C8424898F6700BC04CB /* Sources */ = { 338 | isa = PBXSourcesBuildPhase; 339 | buildActionMask = 2147483647; 340 | files = ( 341 | 371C2C8D24898F6700BC04CB /* iosUITests.swift in Sources */, 342 | ); 343 | runOnlyForDeploymentPostprocessing = 0; 344 | }; 345 | /* End PBXSourcesBuildPhase section */ 346 | 347 | /* Begin PBXTargetDependency section */ 348 | 371C2C7F24898F6700BC04CB /* PBXTargetDependency */ = { 349 | isa = PBXTargetDependency; 350 | target = 371C2C6624898F6500BC04CB /* ios */; 351 | targetProxy = 371C2C7E24898F6700BC04CB /* PBXContainerItemProxy */; 352 | }; 353 | 371C2C8A24898F6700BC04CB /* PBXTargetDependency */ = { 354 | isa = PBXTargetDependency; 355 | target = 371C2C6624898F6500BC04CB /* ios */; 356 | targetProxy = 371C2C8924898F6700BC04CB /* PBXContainerItemProxy */; 357 | }; 358 | /* End PBXTargetDependency section */ 359 | 360 | /* Begin PBXVariantGroup section */ 361 | 371C2C7024898F6500BC04CB /* Main.storyboard */ = { 362 | isa = PBXVariantGroup; 363 | children = ( 364 | 371C2C7124898F6500BC04CB /* Base */, 365 | ); 366 | name = Main.storyboard; 367 | sourceTree = ""; 368 | }; 369 | 371C2C7524898F6700BC04CB /* LaunchScreen.storyboard */ = { 370 | isa = PBXVariantGroup; 371 | children = ( 372 | 371C2C7624898F6700BC04CB /* Base */, 373 | ); 374 | name = LaunchScreen.storyboard; 375 | sourceTree = ""; 376 | }; 377 | /* End PBXVariantGroup section */ 378 | 379 | /* Begin XCBuildConfiguration section */ 380 | 371C2C8F24898F6700BC04CB /* Debug */ = { 381 | isa = XCBuildConfiguration; 382 | buildSettings = { 383 | ALWAYS_SEARCH_USER_PATHS = NO; 384 | CLANG_ANALYZER_NONNULL = YES; 385 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 386 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 387 | CLANG_CXX_LIBRARY = "libc++"; 388 | CLANG_ENABLE_MODULES = YES; 389 | CLANG_ENABLE_OBJC_ARC = YES; 390 | CLANG_ENABLE_OBJC_WEAK = YES; 391 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 392 | CLANG_WARN_BOOL_CONVERSION = YES; 393 | CLANG_WARN_COMMA = YES; 394 | CLANG_WARN_CONSTANT_CONVERSION = YES; 395 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 396 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 397 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 398 | CLANG_WARN_EMPTY_BODY = YES; 399 | CLANG_WARN_ENUM_CONVERSION = YES; 400 | CLANG_WARN_INFINITE_RECURSION = YES; 401 | CLANG_WARN_INT_CONVERSION = YES; 402 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 403 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 404 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 405 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 406 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 407 | CLANG_WARN_STRICT_PROTOTYPES = YES; 408 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 409 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 410 | CLANG_WARN_UNREACHABLE_CODE = YES; 411 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 412 | COPY_PHASE_STRIP = NO; 413 | DEBUG_INFORMATION_FORMAT = dwarf; 414 | ENABLE_STRICT_OBJC_MSGSEND = YES; 415 | ENABLE_TESTABILITY = YES; 416 | GCC_C_LANGUAGE_STANDARD = gnu11; 417 | GCC_DYNAMIC_NO_PIC = NO; 418 | GCC_NO_COMMON_BLOCKS = YES; 419 | GCC_OPTIMIZATION_LEVEL = 0; 420 | GCC_PREPROCESSOR_DEFINITIONS = ( 421 | "DEBUG=1", 422 | "$(inherited)", 423 | ); 424 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 425 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 426 | GCC_WARN_UNDECLARED_SELECTOR = YES; 427 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 428 | GCC_WARN_UNUSED_FUNCTION = YES; 429 | GCC_WARN_UNUSED_VARIABLE = YES; 430 | IPHONEOS_DEPLOYMENT_TARGET = 13.5; 431 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 432 | MTL_FAST_MATH = YES; 433 | ONLY_ACTIVE_ARCH = YES; 434 | SDKROOT = iphoneos; 435 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 436 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 437 | }; 438 | name = Debug; 439 | }; 440 | 371C2C9024898F6700BC04CB /* Release */ = { 441 | isa = XCBuildConfiguration; 442 | buildSettings = { 443 | ALWAYS_SEARCH_USER_PATHS = NO; 444 | CLANG_ANALYZER_NONNULL = YES; 445 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 446 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 447 | CLANG_CXX_LIBRARY = "libc++"; 448 | CLANG_ENABLE_MODULES = YES; 449 | CLANG_ENABLE_OBJC_ARC = YES; 450 | CLANG_ENABLE_OBJC_WEAK = YES; 451 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 452 | CLANG_WARN_BOOL_CONVERSION = YES; 453 | CLANG_WARN_COMMA = YES; 454 | CLANG_WARN_CONSTANT_CONVERSION = YES; 455 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 456 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 457 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 458 | CLANG_WARN_EMPTY_BODY = YES; 459 | CLANG_WARN_ENUM_CONVERSION = YES; 460 | CLANG_WARN_INFINITE_RECURSION = YES; 461 | CLANG_WARN_INT_CONVERSION = YES; 462 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 463 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 464 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 465 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 466 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 467 | CLANG_WARN_STRICT_PROTOTYPES = YES; 468 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 469 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 470 | CLANG_WARN_UNREACHABLE_CODE = YES; 471 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 472 | COPY_PHASE_STRIP = NO; 473 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 474 | ENABLE_NS_ASSERTIONS = NO; 475 | ENABLE_STRICT_OBJC_MSGSEND = YES; 476 | GCC_C_LANGUAGE_STANDARD = gnu11; 477 | GCC_NO_COMMON_BLOCKS = YES; 478 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 479 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 480 | GCC_WARN_UNDECLARED_SELECTOR = YES; 481 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 482 | GCC_WARN_UNUSED_FUNCTION = YES; 483 | GCC_WARN_UNUSED_VARIABLE = YES; 484 | IPHONEOS_DEPLOYMENT_TARGET = 13.5; 485 | MTL_ENABLE_DEBUG_INFO = NO; 486 | MTL_FAST_MATH = YES; 487 | SDKROOT = iphoneos; 488 | SWIFT_COMPILATION_MODE = wholemodule; 489 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 490 | VALIDATE_PRODUCT = YES; 491 | }; 492 | name = Release; 493 | }; 494 | 371C2C9224898F6700BC04CB /* Debug */ = { 495 | isa = XCBuildConfiguration; 496 | buildSettings = { 497 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 498 | CODE_SIGN_STYLE = Automatic; 499 | CURRENT_PROJECT_VERSION = 3; 500 | DEVELOPMENT_TEAM = G8F77R494D; 501 | "FRAMEWORK_SEARCH_PATHS[arch=*]" = "$(SRCROOT)/../shared/build/xcode-frameworks"; 502 | INFOPLIST_FILE = ios/Info.plist; 503 | LD_RUNPATH_SEARCH_PATHS = ( 504 | "$(inherited)", 505 | "@executable_path/Frameworks", 506 | ); 507 | MARKETING_VERSION = 1.2.0; 508 | PRODUCT_BUNDLE_IDENTIFIER = app.web.drjackycv.ios.debug; 509 | PRODUCT_NAME = "$(TARGET_NAME)"; 510 | SWIFT_VERSION = 5.0; 511 | TARGETED_DEVICE_FAMILY = "1,2"; 512 | }; 513 | name = Debug; 514 | }; 515 | 371C2C9324898F6700BC04CB /* Release */ = { 516 | isa = XCBuildConfiguration; 517 | buildSettings = { 518 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 519 | CODE_SIGN_STYLE = Automatic; 520 | CURRENT_PROJECT_VERSION = 3; 521 | DEVELOPMENT_TEAM = G8F77R494D; 522 | "FRAMEWORK_SEARCH_PATHS[arch=*]" = "$(SRCROOT)/../shared/build/xcode-frameworks"; 523 | INFOPLIST_FILE = ios/Info.plist; 524 | LD_RUNPATH_SEARCH_PATHS = ( 525 | "$(inherited)", 526 | "@executable_path/Frameworks", 527 | ); 528 | MARKETING_VERSION = 1.2.0; 529 | PRODUCT_BUNDLE_IDENTIFIER = app.web.drjackycv.ios.debug; 530 | PRODUCT_NAME = "$(TARGET_NAME)"; 531 | SWIFT_VERSION = 5.0; 532 | TARGETED_DEVICE_FAMILY = "1,2"; 533 | }; 534 | name = Release; 535 | }; 536 | 371C2C9524898F6700BC04CB /* Debug */ = { 537 | isa = XCBuildConfiguration; 538 | buildSettings = { 539 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 540 | BUNDLE_LOADER = "$(TEST_HOST)"; 541 | CODE_SIGN_STYLE = Automatic; 542 | INFOPLIST_FILE = iosTests/Info.plist; 543 | IPHONEOS_DEPLOYMENT_TARGET = 13.5; 544 | LD_RUNPATH_SEARCH_PATHS = ( 545 | "$(inherited)", 546 | "@executable_path/Frameworks", 547 | "@loader_path/Frameworks", 548 | ); 549 | PRODUCT_BUNDLE_IDENTIFIER = app.web.drjackycv.iosTests; 550 | PRODUCT_NAME = "$(TARGET_NAME)"; 551 | SWIFT_VERSION = 5.0; 552 | TARGETED_DEVICE_FAMILY = "1,2"; 553 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/ios.app/ios"; 554 | }; 555 | name = Debug; 556 | }; 557 | 371C2C9624898F6700BC04CB /* Release */ = { 558 | isa = XCBuildConfiguration; 559 | buildSettings = { 560 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 561 | BUNDLE_LOADER = "$(TEST_HOST)"; 562 | CODE_SIGN_STYLE = Automatic; 563 | INFOPLIST_FILE = iosTests/Info.plist; 564 | IPHONEOS_DEPLOYMENT_TARGET = 13.5; 565 | LD_RUNPATH_SEARCH_PATHS = ( 566 | "$(inherited)", 567 | "@executable_path/Frameworks", 568 | "@loader_path/Frameworks", 569 | ); 570 | PRODUCT_BUNDLE_IDENTIFIER = app.web.drjackycv.iosTests; 571 | PRODUCT_NAME = "$(TARGET_NAME)"; 572 | SWIFT_VERSION = 5.0; 573 | TARGETED_DEVICE_FAMILY = "1,2"; 574 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/ios.app/ios"; 575 | }; 576 | name = Release; 577 | }; 578 | 371C2C9824898F6700BC04CB /* Debug */ = { 579 | isa = XCBuildConfiguration; 580 | buildSettings = { 581 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 582 | CODE_SIGN_STYLE = Automatic; 583 | INFOPLIST_FILE = iosUITests/Info.plist; 584 | LD_RUNPATH_SEARCH_PATHS = ( 585 | "$(inherited)", 586 | "@executable_path/Frameworks", 587 | "@loader_path/Frameworks", 588 | ); 589 | PRODUCT_BUNDLE_IDENTIFIER = app.web.drjackycv.iosUITests; 590 | PRODUCT_NAME = "$(TARGET_NAME)"; 591 | SWIFT_VERSION = 5.0; 592 | TARGETED_DEVICE_FAMILY = "1,2"; 593 | TEST_TARGET_NAME = ios; 594 | }; 595 | name = Debug; 596 | }; 597 | 371C2C9924898F6700BC04CB /* Release */ = { 598 | isa = XCBuildConfiguration; 599 | buildSettings = { 600 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 601 | CODE_SIGN_STYLE = Automatic; 602 | INFOPLIST_FILE = iosUITests/Info.plist; 603 | LD_RUNPATH_SEARCH_PATHS = ( 604 | "$(inherited)", 605 | "@executable_path/Frameworks", 606 | "@loader_path/Frameworks", 607 | ); 608 | PRODUCT_BUNDLE_IDENTIFIER = app.web.drjackycv.iosUITests; 609 | PRODUCT_NAME = "$(TARGET_NAME)"; 610 | SWIFT_VERSION = 5.0; 611 | TARGETED_DEVICE_FAMILY = "1,2"; 612 | TEST_TARGET_NAME = ios; 613 | }; 614 | name = Release; 615 | }; 616 | /* End XCBuildConfiguration section */ 617 | 618 | /* Begin XCConfigurationList section */ 619 | 371C2C6224898F6500BC04CB /* Build configuration list for PBXProject "ios" */ = { 620 | isa = XCConfigurationList; 621 | buildConfigurations = ( 622 | 371C2C8F24898F6700BC04CB /* Debug */, 623 | 371C2C9024898F6700BC04CB /* Release */, 624 | ); 625 | defaultConfigurationIsVisible = 0; 626 | defaultConfigurationName = Release; 627 | }; 628 | 371C2C9124898F6700BC04CB /* Build configuration list for PBXNativeTarget "ios" */ = { 629 | isa = XCConfigurationList; 630 | buildConfigurations = ( 631 | 371C2C9224898F6700BC04CB /* Debug */, 632 | 371C2C9324898F6700BC04CB /* Release */, 633 | ); 634 | defaultConfigurationIsVisible = 0; 635 | defaultConfigurationName = Release; 636 | }; 637 | 371C2C9424898F6700BC04CB /* Build configuration list for PBXNativeTarget "iosTests" */ = { 638 | isa = XCConfigurationList; 639 | buildConfigurations = ( 640 | 371C2C9524898F6700BC04CB /* Debug */, 641 | 371C2C9624898F6700BC04CB /* Release */, 642 | ); 643 | defaultConfigurationIsVisible = 0; 644 | defaultConfigurationName = Release; 645 | }; 646 | 371C2C9724898F6700BC04CB /* Build configuration list for PBXNativeTarget "iosUITests" */ = { 647 | isa = XCConfigurationList; 648 | buildConfigurations = ( 649 | 371C2C9824898F6700BC04CB /* Debug */, 650 | 371C2C9924898F6700BC04CB /* Release */, 651 | ); 652 | defaultConfigurationIsVisible = 0; 653 | defaultConfigurationName = Release; 654 | }; 655 | /* End XCConfigurationList section */ 656 | 657 | /* Begin XCRemoteSwiftPackageReference section */ 658 | 37EC695324CEF684005AFB10 /* XCRemoteSwiftPackageReference "Nuke" */ = { 659 | isa = XCRemoteSwiftPackageReference; 660 | repositoryURL = "https://github.com/kean/Nuke.git"; 661 | requirement = { 662 | branch = master; 663 | kind = branch; 664 | }; 665 | }; 666 | /* End XCRemoteSwiftPackageReference section */ 667 | 668 | /* Begin XCSwiftPackageProductDependency section */ 669 | 37EC695424CEF684005AFB10 /* Nuke */ = { 670 | isa = XCSwiftPackageProductDependency; 671 | package = 37EC695324CEF684005AFB10 /* XCRemoteSwiftPackageReference "Nuke" */; 672 | productName = Nuke; 673 | }; 674 | /* End XCSwiftPackageProductDependency section */ 675 | }; 676 | rootObject = 371C2C5F24898F6500BC04CB /* Project object */; 677 | } 678 | --------------------------------------------------------------------------------