diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..5c98b42 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,2 @@ +# Default ignored files +/workspace.xml \ No newline at end of file diff --git a/.idea/CleanArchitectureStudy02.iml b/.idea/CleanArchitectureStudy02.iml new file mode 100644 index 0000000..d6ebd48 --- /dev/null +++ b/.idea/CleanArchitectureStudy02.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 0000000..681f41a --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,116 @@ + + + + + + + +
+ + + + xmlns:android + + ^$ + + + +
+
+ + + + xmlns:.* + + ^$ + + + BY_NAME + +
+
+ + + + .*:id + + http://schemas.android.com/apk/res/android + + + +
+
+ + + + .*:name + + http://schemas.android.com/apk/res/android + + + +
+
+ + + + name + + ^$ + + + +
+
+ + + + style + + ^$ + + + +
+
+ + + + .* + + ^$ + + + BY_NAME + +
+
+ + + + .* + + http://schemas.android.com/apk/res/android + + + ANDROID_ATTRIBUTE_ORDER + +
+
+ + + + .* + + .* + + + BY_NAME + +
+
+
+
+
+
\ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..3733e0d --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,19 @@ + + + + + + + 1.8 + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..aae1561 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/workspace.xml b/.idea/workspace.xml new file mode 100644 index 0000000..33fa8d0 --- /dev/null +++ b/.idea/workspace.xml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1584022362892 + + + + + + + \ No newline at end of file diff --git a/ToyProject/.gitignore b/ToyProject/.gitignore new file mode 100644 index 0000000..603b140 --- /dev/null +++ b/ToyProject/.gitignore @@ -0,0 +1,14 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx diff --git a/ToyProject/.idea/.name b/ToyProject/.idea/.name new file mode 100644 index 0000000..f6ea2a8 --- /dev/null +++ b/ToyProject/.idea/.name @@ -0,0 +1 @@ +MovieSearch \ No newline at end of file diff --git a/ToyProject/.idea/codeStyles/Project.xml b/ToyProject/.idea/codeStyles/Project.xml new file mode 100644 index 0000000..88ea3aa --- /dev/null +++ b/ToyProject/.idea/codeStyles/Project.xml @@ -0,0 +1,122 @@ + + + + + + + + + +
+ + + + xmlns:android + + ^$ + + + +
+
+ + + + xmlns:.* + + ^$ + + + BY_NAME + +
+
+ + + + .*:id + + http://schemas.android.com/apk/res/android + + + +
+
+ + + + .*:name + + http://schemas.android.com/apk/res/android + + + +
+
+ + + + name + + ^$ + + + +
+
+ + + + style + + ^$ + + + +
+
+ + + + .* + + ^$ + + + BY_NAME + +
+
+ + + + .* + + http://schemas.android.com/apk/res/android + + + ANDROID_ATTRIBUTE_ORDER + +
+
+ + + + .* + + .* + + + BY_NAME + +
+
+
+
+ + +
+
\ No newline at end of file diff --git a/ToyProject/.idea/codeStyles/codeStyleConfig.xml b/ToyProject/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 0000000..79ee123 --- /dev/null +++ b/ToyProject/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/ToyProject/.idea/gradle.xml b/ToyProject/.idea/gradle.xml new file mode 100644 index 0000000..5cd135a --- /dev/null +++ b/ToyProject/.idea/gradle.xml @@ -0,0 +1,20 @@ + + + + + + + \ No newline at end of file diff --git a/ToyProject/.idea/misc.xml b/ToyProject/.idea/misc.xml new file mode 100644 index 0000000..9159540 --- /dev/null +++ b/ToyProject/.idea/misc.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + 1.8 + + + + + + + + \ No newline at end of file diff --git a/ToyProject/.idea/render.experimental.xml b/ToyProject/.idea/render.experimental.xml new file mode 100644 index 0000000..8ec256a --- /dev/null +++ b/ToyProject/.idea/render.experimental.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/ToyProject/.idea/runConfigurations.xml b/ToyProject/.idea/runConfigurations.xml new file mode 100644 index 0000000..7f68460 --- /dev/null +++ b/ToyProject/.idea/runConfigurations.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/ToyProject/.idea/vcs.xml b/ToyProject/.idea/vcs.xml new file mode 100644 index 0000000..6c0b863 --- /dev/null +++ b/ToyProject/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/ToyProject/README.md b/ToyProject/README.md deleted file mode 100644 index 324911f..0000000 --- a/ToyProject/README.md +++ /dev/null @@ -1 +0,0 @@ -# Toy Project \ No newline at end of file diff --git a/ToyProject/android_features.gradle b/ToyProject/android_features.gradle new file mode 100644 index 0000000..16c7a87 --- /dev/null +++ b/ToyProject/android_features.gradle @@ -0,0 +1,31 @@ +dependencies { + // material, constraintLayout + implementation 'com.google.android.material:material:1.2.0-alpha05' + implementation 'androidx.constraintlayout:constraintlayout:1.1.3' + + // Retrofit + implementation 'com.squareup.retrofit2:retrofit:2.7.1' + implementation 'com.squareup.retrofit2:converter-gson:2.7.1' + implementation 'com.squareup.okhttp3:logging-interceptor:4.3.0' + implementation "com.squareup.retrofit2:adapter-rxjava2:2.7.0" + + // Glide + implementation "com.github.bumptech.glide:glide:4.11.0" + + // rxJava + implementation "io.reactivex.rxjava2:rxandroid:2.1.1" + implementation "io.reactivex.rxjava2:rxjava:2.2.16" + implementation "io.reactivex.rxjava2:rxkotlin:2.4.0" + implementation "com.jakewharton.rxbinding3:rxbinding:3.1.0" + + // koin + implementation 'org.koin:koin-androidx-viewmodel:2.1.3' + + // Paging + implementation "androidx.paging:paging-runtime:2.1.1" + implementation 'androidx.paging:paging-rxjava2:2.1.1' + + testImplementation 'junit:junit:4.13' + androidTestImplementation 'androidx.test.ext:junit:1.1.1' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' +} \ No newline at end of file diff --git a/ToyProject/app/.gitignore b/ToyProject/app/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/ToyProject/app/.gitignore @@ -0,0 +1 @@ +/build diff --git a/ToyProject/app/build.gradle b/ToyProject/app/build.gradle new file mode 100644 index 0000000..05a53bf --- /dev/null +++ b/ToyProject/app/build.gradle @@ -0,0 +1,58 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-android-extensions' +apply plugin: 'kotlin-kapt' + +apply from: "$rootDir/android_features.gradle" + +def keysPropertiesFile = rootProject.file("keys.properties") +def keysProperties = new Properties() +keysProperties.load(new FileInputStream(keysPropertiesFile)) + +android { + compileSdkVersion 29 + + defaultConfig { + applicationId "com.egiwon.moviesearch" + minSdkVersion 23 + targetSdkVersion 29 + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + debug { + buildConfigField 'String', "API_KEY", keysProperties['API_KEY'] + } + + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + + dataBinding { + enabled = true + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = JavaVersion.VERSION_1_8 + } + +} + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation 'androidx.core:core-ktx:1.2.0' + + testImplementation 'junit:junit:4.12' + androidTestImplementation 'androidx.test.ext:junit:1.1.1' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' +} diff --git a/ToyProject/app/proguard-rules.pro b/ToyProject/app/proguard-rules.pro new file mode 100644 index 0000000..f1b4245 --- /dev/null +++ b/ToyProject/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/ToyProject/app/src/androidTest/java/com/egiwon/moviesearch/ExampleInstrumentedTest.kt b/ToyProject/app/src/androidTest/java/com/egiwon/moviesearch/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..83374f4 --- /dev/null +++ b/ToyProject/app/src/androidTest/java/com/egiwon/moviesearch/ExampleInstrumentedTest.kt @@ -0,0 +1,22 @@ +package com.egiwon.moviesearch + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.egiwon.moviesearch", appContext.packageName) + } +} diff --git a/ToyProject/app/src/main/AndroidManifest.xml b/ToyProject/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..adf4a0e --- /dev/null +++ b/ToyProject/app/src/main/AndroidManifest.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ToyProject/app/src/main/java/com/egiwon/moviesearch/MovieSearchApplication.kt b/ToyProject/app/src/main/java/com/egiwon/moviesearch/MovieSearchApplication.kt new file mode 100644 index 0000000..918a648 --- /dev/null +++ b/ToyProject/app/src/main/java/com/egiwon/moviesearch/MovieSearchApplication.kt @@ -0,0 +1,28 @@ +package com.egiwon.moviesearch + +import android.app.Application +import com.egiwon.moviesearch.di.dataSourceModule +import com.egiwon.moviesearch.di.networkModule +import com.egiwon.moviesearch.di.remoteDataSourceModule +import com.egiwon.moviesearch.di.viewModelModule +import org.koin.android.ext.koin.androidContext +import org.koin.android.logger.AndroidLogger +import org.koin.core.context.startKoin +import org.koin.core.logger.EmptyLogger + +class MovieSearchApplication : Application() { + + override fun onCreate() { + super.onCreate() + startKoin { + logger(if (BuildConfig.DEBUG) AndroidLogger() else EmptyLogger()) + androidContext(this@MovieSearchApplication) + modules( + viewModelModule, + dataSourceModule, + remoteDataSourceModule, + networkModule + ) + } + } +} \ No newline at end of file diff --git a/ToyProject/app/src/main/java/com/egiwon/moviesearch/base/BaseActivity.kt b/ToyProject/app/src/main/java/com/egiwon/moviesearch/base/BaseActivity.kt new file mode 100644 index 0000000..bc1862d --- /dev/null +++ b/ToyProject/app/src/main/java/com/egiwon/moviesearch/base/BaseActivity.kt @@ -0,0 +1,42 @@ +package com.egiwon.moviesearch.base + +import android.os.Bundle +import android.widget.Toast +import androidx.annotation.LayoutRes +import androidx.appcompat.app.AppCompatActivity +import androidx.databinding.DataBindingUtil +import androidx.databinding.ViewDataBinding +import androidx.lifecycle.Observer + +abstract class BaseActivity( + @LayoutRes private val layoutResId: Int +) : AppCompatActivity(layoutResId) { + + protected abstract val viewModel: VM + + protected lateinit var binding: VDB + private set + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = DataBindingUtil.setContentView(this, layoutResId) + binding.lifecycleOwner = this + binding.initAdapter() + addObserves() + } + + open fun VDB.initAdapter() = Unit + + private fun addObserves() { + viewModel.showErrorTextResId.observe(this, Observer { showToast(it) }) + } + + protected fun bind(action: VDB.() -> Unit) { + binding.run(action) + } + + protected fun showToast(textResId: Int) { + Toast.makeText(this, textResId, Toast.LENGTH_SHORT).show() + } + +} \ No newline at end of file diff --git a/ToyProject/app/src/main/java/com/egiwon/moviesearch/base/BaseIdentifier.kt b/ToyProject/app/src/main/java/com/egiwon/moviesearch/base/BaseIdentifier.kt new file mode 100644 index 0000000..20ff1eb --- /dev/null +++ b/ToyProject/app/src/main/java/com/egiwon/moviesearch/base/BaseIdentifier.kt @@ -0,0 +1,21 @@ +package com.egiwon.moviesearch.base + +abstract class BaseIdentifier : Any() { + abstract val id: Any + + override fun hashCode(): Int { + return id.hashCode() + } + + override operator fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + (other as? BaseIdentifier)?.run { + if (id != other.id) return false + return true + } + + return false + } +} \ No newline at end of file diff --git a/ToyProject/app/src/main/java/com/egiwon/moviesearch/base/BaseListAdapter.kt b/ToyProject/app/src/main/java/com/egiwon/moviesearch/base/BaseListAdapter.kt new file mode 100644 index 0000000..fc026ca --- /dev/null +++ b/ToyProject/app/src/main/java/com/egiwon/moviesearch/base/BaseListAdapter.kt @@ -0,0 +1,43 @@ +package com.egiwon.moviesearch.base + +import android.view.ViewGroup +import androidx.annotation.LayoutRes +import androidx.databinding.ViewDataBinding +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter + +abstract class BaseListAdapter( + @LayoutRes private val layoutResId: Int, + private val bindingId: Int, + private val viewModels: Map = mapOf() +) : ListAdapter>(object : + DiffUtil.ItemCallback() { + + override fun areItemsTheSame(oldItem: IDENTIFIER, newItem: IDENTIFIER): Boolean { + return oldItem.id == newItem.id + } + + override fun areContentsTheSame(oldItem: IDENTIFIER, newItem: IDENTIFIER): Boolean { + return oldItem == newItem + } + +}) { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder = + object : BaseViewHolder(parent, layoutResId, bindingId, viewModels) {} + + override fun onBindViewHolder(holder: BaseViewHolder, position: Int) = + holder.onBindViewHolder(getItem(position)) + + override fun onViewRecycled(holder: BaseViewHolder) { + super.onViewRecycled(holder) + holder.onRecycledViewHolder() + } + + fun replaceAll(items: List?) { + if (items != null) { + submitList(items) + } + } + +} \ No newline at end of file diff --git a/ToyProject/app/src/main/java/com/egiwon/moviesearch/base/BasePagedAdapter.kt b/ToyProject/app/src/main/java/com/egiwon/moviesearch/base/BasePagedAdapter.kt new file mode 100644 index 0000000..c508ae0 --- /dev/null +++ b/ToyProject/app/src/main/java/com/egiwon/moviesearch/base/BasePagedAdapter.kt @@ -0,0 +1,43 @@ +package com.egiwon.moviesearch.base + +import android.view.ViewGroup +import androidx.annotation.LayoutRes +import androidx.databinding.ViewDataBinding +import androidx.paging.PagedList +import androidx.paging.PagedListAdapter +import androidx.recyclerview.widget.DiffUtil + +abstract class BasePagedAdapter( + @LayoutRes private val layoutResId: Int, + private val bindingId: Int, + private val viewModels: Map = mapOf() +) : PagedListAdapter>( + object : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: IDENTIFIER, newItem: IDENTIFIER): Boolean { + return oldItem.id == newItem.id + } + + override fun areContentsTheSame(oldItem: IDENTIFIER, newItem: IDENTIFIER): Boolean { + return oldItem == newItem + } + + }) { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder = + object : BaseViewHolder(parent, layoutResId, bindingId, viewModels) {} + + override fun onBindViewHolder(holder: BaseViewHolder, position: Int) = + holder.onBindViewHolder(getItem(position)) + + override fun onViewRecycled(holder: BaseViewHolder) { + super.onViewRecycled(holder) + holder.onRecycledViewHolder() + } + + fun replaceAllPagedItems(items: PagedList?) { + if (items != null) { + submitList(items) + } + } + +} \ No newline at end of file diff --git a/ToyProject/app/src/main/java/com/egiwon/moviesearch/base/BaseViewHolder.kt b/ToyProject/app/src/main/java/com/egiwon/moviesearch/base/BaseViewHolder.kt new file mode 100644 index 0000000..4909c1d --- /dev/null +++ b/ToyProject/app/src/main/java/com/egiwon/moviesearch/base/BaseViewHolder.kt @@ -0,0 +1,40 @@ +package com.egiwon.moviesearch.base + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.annotation.LayoutRes +import androidx.databinding.DataBindingUtil +import androidx.databinding.ViewDataBinding +import androidx.recyclerview.widget.RecyclerView + +abstract class BaseViewHolder( + parent: ViewGroup, + @LayoutRes resourceId: Int, + private val bindingId: Int?, + private val viewModels: Map = mapOf() +) : RecyclerView.ViewHolder( + LayoutInflater.from(parent.context) + .inflate(resourceId, parent, false) +) { + + protected val binding: VDB = DataBindingUtil.bind(itemView)!! + + open fun onBindViewHolder(item: Any?) { + if (bindingId == null) return + if (item == null) return + + binding.run { + viewModels.let { + for (key in it.keys) { + if (key == null) continue + setVariable(key, it[key]) + } + } + + setVariable(bindingId, item) + executePendingBindings() + } + } + + open fun onRecycledViewHolder() = Unit +} \ No newline at end of file diff --git a/ToyProject/app/src/main/java/com/egiwon/moviesearch/base/BaseViewModel.kt b/ToyProject/app/src/main/java/com/egiwon/moviesearch/base/BaseViewModel.kt new file mode 100644 index 0000000..102ada4 --- /dev/null +++ b/ToyProject/app/src/main/java/com/egiwon/moviesearch/base/BaseViewModel.kt @@ -0,0 +1,21 @@ +package com.egiwon.moviesearch.base + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import io.reactivex.disposables.CompositeDisposable + +abstract class BaseViewModel : ViewModel() { + protected val compositeDisposable = CompositeDisposable() + + protected val mutableShowErrorTextResId = MutableLiveData() + val showErrorTextResId: LiveData get() = mutableShowErrorTextResId + + override fun onCleared() { + compositeDisposable.dispose() + super.onCleared() + } + + open fun onClick(model: Any) = Unit + +} \ No newline at end of file diff --git a/ToyProject/app/src/main/java/com/egiwon/moviesearch/data/MovieRepository.kt b/ToyProject/app/src/main/java/com/egiwon/moviesearch/data/MovieRepository.kt new file mode 100644 index 0000000..1246141 --- /dev/null +++ b/ToyProject/app/src/main/java/com/egiwon/moviesearch/data/MovieRepository.kt @@ -0,0 +1,22 @@ +package com.egiwon.moviesearch.data + +import androidx.lifecycle.LiveData +import androidx.paging.PagedList +import com.egiwon.moviesearch.data.model.MovieDetailEntity +import com.egiwon.moviesearch.data.model.MovieEntity +import com.egiwon.moviesearch.data.model.MovieTrailerEntity +import io.reactivex.Single +import io.reactivex.disposables.CompositeDisposable + +interface MovieRepository { + fun getPopularMovies(page: Int): Single> + + fun getPagingPopularMovies( + compositeDisposable: CompositeDisposable, + onFailure: (Throwable) -> Unit + ): LiveData> + + fun getMovieDetailInfo(movieId: Int): Single + + fun getMovieTrailerInfo(movieId: Int): Single +} \ No newline at end of file diff --git a/ToyProject/app/src/main/java/com/egiwon/moviesearch/data/MovieRepositoryImpl.kt b/ToyProject/app/src/main/java/com/egiwon/moviesearch/data/MovieRepositoryImpl.kt new file mode 100644 index 0000000..8f1aaab --- /dev/null +++ b/ToyProject/app/src/main/java/com/egiwon/moviesearch/data/MovieRepositoryImpl.kt @@ -0,0 +1,66 @@ +package com.egiwon.moviesearch.data + +import androidx.lifecycle.LiveData +import androidx.paging.LivePagedListBuilder +import androidx.paging.PagedList +import com.egiwon.moviesearch.data.model.MovieDetailEntity +import com.egiwon.moviesearch.data.model.MovieEntity +import com.egiwon.moviesearch.data.model.MovieTrailerEntity +import com.egiwon.moviesearch.data.source.paging.MovieDataSourceFactory +import com.egiwon.moviesearch.data.source.remote.MovieRemoteDataSource +import com.egiwon.moviesearch.data.source.remote.response.mapToMovieCreditEntity +import com.egiwon.moviesearch.data.source.remote.response.mapToMovieDetailEntity +import com.egiwon.moviesearch.data.source.remote.response.mapToMovieEntities +import com.egiwon.moviesearch.data.source.remote.response.mapToMovieTrailerEntity +import io.reactivex.Completable +import io.reactivex.Single +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.functions.BiFunction +import io.reactivex.rxkotlin.addTo +import io.reactivex.schedulers.Schedulers + +typealias onFailureRequestMovie = (Throwable) -> Unit + +class MovieRepositoryImpl( + private val movieRemoteDataSource: MovieRemoteDataSource +) : MovieRepository { + + override fun getPopularMovies(page: Int): Single> = + movieRemoteDataSource.getPopularMovies(page) + .map { movieResponses -> movieResponses.mapToMovieEntities() } + + override fun getPagingPopularMovies( + compositeDisposable: CompositeDisposable, + onFailure: onFailureRequestMovie + ): LiveData> { + + val movieDataSourceFactory = + MovieDataSourceFactory(compositeDisposable, movieRemoteDataSource, onFailure) + + return LivePagedListBuilder(movieDataSourceFactory, MovieDataSourceFactory.moviePageConfig) + .setFetchExecutor { + Completable + .fromRunnable(it) + .subscribeOn(Schedulers.single()) + .subscribe() + .addTo(compositeDisposable) + } + .build() + } + + override fun getMovieDetailInfo(movieId: Int): Single = + Single.zip( + movieRemoteDataSource.getMovieDetailInfo(movieId), + movieRemoteDataSource.getMovieCredits(movieId), + BiFunction { movieDetailResult, creditResult -> + movieDetailResult.mapToMovieDetailEntity( + creditResult.mapToMovieCreditEntity().castList + ) + } + ) + + override fun getMovieTrailerInfo(movieId: Int): Single = + movieRemoteDataSource.getMovieTrailerInfo(movieId) + .map { movieTrailerResponse -> movieTrailerResponse.mapToMovieTrailerEntity() } + +} \ No newline at end of file diff --git a/ToyProject/app/src/main/java/com/egiwon/moviesearch/data/model/MovieCreditEntity.kt b/ToyProject/app/src/main/java/com/egiwon/moviesearch/data/model/MovieCreditEntity.kt new file mode 100644 index 0000000..08814a8 --- /dev/null +++ b/ToyProject/app/src/main/java/com/egiwon/moviesearch/data/model/MovieCreditEntity.kt @@ -0,0 +1,21 @@ +package com.egiwon.moviesearch.data.model + +import com.egiwon.moviesearch.ui.model.MovieCastViewObject + +data class MovieCreditEntity( + val id: Int = 0, + val castList: List = emptyList() +) + +data class MovieCastEntity( + val castId: Int = 0, + val name: String = "", + val profilePath: String? = "" +) + +fun MovieCastEntity.mapToMovieCastViewObject(): MovieCastViewObject = + MovieCastViewObject( + castId = castId, + name = name, + profilePath = profilePath + ) diff --git a/ToyProject/app/src/main/java/com/egiwon/moviesearch/data/model/MovieDetailEntity.kt b/ToyProject/app/src/main/java/com/egiwon/moviesearch/data/model/MovieDetailEntity.kt new file mode 100644 index 0000000..cff82c6 --- /dev/null +++ b/ToyProject/app/src/main/java/com/egiwon/moviesearch/data/model/MovieDetailEntity.kt @@ -0,0 +1,28 @@ +package com.egiwon.moviesearch.data.model + +import com.egiwon.moviesearch.ui.model.MovieDetailViewObject + +data class MovieDetailEntity( + val id: Int = 0, + val overview: String = "", + val voteAverage: Double = 0.0, + val posterPath: String = "", + val releaseDate: String = "", + val title: String = "", + val runtime: Int = 0, + val movieCasts: List? = emptyList() +) + +fun MovieDetailEntity.mapToMovieDetailViewObject(): MovieDetailViewObject = + runCatching { + MovieDetailViewObject( + id = id, + overview = overview, + voteAverage = voteAverage, + posterPath = posterPath, + releaseDate = releaseDate, + title = title, + runtime = runtime, + castList = movieCasts?.map { it.mapToMovieCastViewObject() } ?: emptyList() + ) + }.getOrNull() ?: MovieDetailViewObject() \ No newline at end of file diff --git a/ToyProject/app/src/main/java/com/egiwon/moviesearch/data/model/MovieEntity.kt b/ToyProject/app/src/main/java/com/egiwon/moviesearch/data/model/MovieEntity.kt new file mode 100644 index 0000000..c6890de --- /dev/null +++ b/ToyProject/app/src/main/java/com/egiwon/moviesearch/data/model/MovieEntity.kt @@ -0,0 +1,14 @@ +package com.egiwon.moviesearch.data.model + +import com.egiwon.moviesearch.base.BaseIdentifier + +data class MovieEntity( + val movieId: Int = 0, + val title: String = "", + val posterPath: String = "" +) : BaseIdentifier() { + override val id: Any + get() = movieId + +} + diff --git a/ToyProject/app/src/main/java/com/egiwon/moviesearch/data/model/MovieTrailerEntity.kt b/ToyProject/app/src/main/java/com/egiwon/moviesearch/data/model/MovieTrailerEntity.kt new file mode 100644 index 0000000..0140dea --- /dev/null +++ b/ToyProject/app/src/main/java/com/egiwon/moviesearch/data/model/MovieTrailerEntity.kt @@ -0,0 +1,30 @@ +package com.egiwon.moviesearch.data.model + +import com.egiwon.moviesearch.ui.model.MovieTrailer +import com.egiwon.moviesearch.ui.model.MovieTrailerViewObject + +data class MovieTrailerEntity( + val id: Int, + val trailers: List +) + +data class MovieTrailerItem( + val trailerId: String = "", + val name: String = "", + val key: String = "", + val site: String = "" +) + +fun MovieTrailerEntity.mapToMovieTrailerViewObject(): MovieTrailerViewObject = + MovieTrailerViewObject( + id = id, + trailers = trailers.map { it.mapToMovieTrailerEntityItem() } + ) + +fun MovieTrailerItem.mapToMovieTrailerEntityItem(): MovieTrailer = + MovieTrailer( + trailerId = trailerId, + name = name, + key = key, + site = site + ) \ No newline at end of file diff --git a/ToyProject/app/src/main/java/com/egiwon/moviesearch/data/source/paging/MovieDataSourceFactory.kt b/ToyProject/app/src/main/java/com/egiwon/moviesearch/data/source/paging/MovieDataSourceFactory.kt new file mode 100644 index 0000000..247be2a --- /dev/null +++ b/ToyProject/app/src/main/java/com/egiwon/moviesearch/data/source/paging/MovieDataSourceFactory.kt @@ -0,0 +1,84 @@ +package com.egiwon.moviesearch.data.source.paging + +import androidx.paging.DataSource +import androidx.paging.PageKeyedDataSource +import androidx.paging.PagedList +import com.egiwon.moviesearch.data.model.MovieEntity +import com.egiwon.moviesearch.data.source.remote.MovieRemoteDataSource +import com.egiwon.moviesearch.data.source.remote.response.mapToMovieEntities +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.rxkotlin.addTo +import io.reactivex.rxkotlin.subscribeBy +import io.reactivex.schedulers.Schedulers + +class MovieDataSourceFactory( + private val compositeDisposable: CompositeDisposable, + private val remoteDataSource: MovieRemoteDataSource, + private val onFailure: (Throwable) -> Unit +) : DataSource.Factory() { + + private var totalPage = 1 + + override fun create(): DataSource { + return MoviePagingDataSource( + compositeDisposable, + remoteDataSource + ) + } + + inner class MoviePagingDataSource( + private val compositeDisposable: CompositeDisposable, + private val remoteDataSource: MovieRemoteDataSource + ) : PageKeyedDataSource() { + + override fun loadInitial(params: LoadInitialParams, callback: LoadInitialCallback) { + remoteDataSource.getPopularMovies(START_PAGE) + .subscribeOn(Schedulers.io()) + .subscribeBy( + onSuccess = { + totalPage = it.totalPages + val resultList = it.mapToMovieEntities() + callback.onResult(resultList, 0, START_PAGE + 1) + }, + onError = { + onFailure(it) + } + ) + .addTo(compositeDisposable) + } + + override fun loadAfter(params: LoadParams, callback: LoadCallback) { + + if (params.key < totalPage) { + remoteDataSource.getPopularMovies(params.key) + .subscribeOn(Schedulers.io()) + .subscribeBy( + onSuccess = { + totalPage = it.totalPages + val resultList = it.mapToMovieEntities() + callback.onResult(resultList, params.key + 1) + }, + onError = { + onFailure(it) + } + ) + .addTo(compositeDisposable) + } + } + + override fun loadBefore(params: LoadParams, callback: LoadCallback) = Unit + + } + + companion object { + private const val START_PAGE = 1 + private const val PAGE_SIZE = 20 + + val moviePageConfig = PagedList.Config.Builder() + .setPageSize(PAGE_SIZE) + .setInitialLoadSizeHint(PAGE_SIZE) + .setPrefetchDistance(PAGE_SIZE) + .build() + } + +} \ No newline at end of file diff --git a/ToyProject/app/src/main/java/com/egiwon/moviesearch/data/source/remote/MovieRemoteDataSource.kt b/ToyProject/app/src/main/java/com/egiwon/moviesearch/data/source/remote/MovieRemoteDataSource.kt new file mode 100644 index 0000000..f60c10e --- /dev/null +++ b/ToyProject/app/src/main/java/com/egiwon/moviesearch/data/source/remote/MovieRemoteDataSource.kt @@ -0,0 +1,17 @@ +package com.egiwon.moviesearch.data.source.remote + +import com.egiwon.moviesearch.data.source.remote.response.MovieCreditsResponse +import com.egiwon.moviesearch.data.source.remote.response.MovieDetailResponse +import com.egiwon.moviesearch.data.source.remote.response.MovieResponse +import com.egiwon.moviesearch.data.source.remote.response.MovieTrailerResponse +import io.reactivex.Single + +interface MovieRemoteDataSource { + fun getPopularMovies(page: Int): Single + + fun getMovieDetailInfo(id: Int): Single + + fun getMovieCredits(id: Int): Single + + fun getMovieTrailerInfo(id: Int): Single +} \ No newline at end of file diff --git a/ToyProject/app/src/main/java/com/egiwon/moviesearch/data/source/remote/MovieRemoteDataSourceImpl.kt b/ToyProject/app/src/main/java/com/egiwon/moviesearch/data/source/remote/MovieRemoteDataSourceImpl.kt new file mode 100644 index 0000000..6855aa8 --- /dev/null +++ b/ToyProject/app/src/main/java/com/egiwon/moviesearch/data/source/remote/MovieRemoteDataSourceImpl.kt @@ -0,0 +1,30 @@ +package com.egiwon.moviesearch.data.source.remote + +import com.egiwon.moviesearch.data.source.remote.response.MovieCreditsResponse +import com.egiwon.moviesearch.data.source.remote.response.MovieDetailResponse +import com.egiwon.moviesearch.data.source.remote.response.MovieResponse +import com.egiwon.moviesearch.data.source.remote.response.MovieTrailerResponse +import com.egiwon.moviesearch.data.source.remote.service.MovieService +import io.reactivex.Single +import io.reactivex.schedulers.Schedulers + +class MovieRemoteDataSourceImpl( + private val movieService: MovieService +) : MovieRemoteDataSource { + + override fun getPopularMovies(page: Int): Single = + movieService.getPopularMovies(page) + .subscribeOn(Schedulers.io()) + + override fun getMovieDetailInfo(id: Int): Single = + movieService.getMovieDetails(id) + .subscribeOn(Schedulers.io()) + + override fun getMovieCredits(id: Int): Single = + movieService.getMovieCredits(id) + .subscribeOn(Schedulers.io()) + + override fun getMovieTrailerInfo(id: Int): Single = + movieService.getMovieTrailers(id) + .subscribeOn(Schedulers.io()) +} \ No newline at end of file diff --git a/ToyProject/app/src/main/java/com/egiwon/moviesearch/data/source/remote/response/MovieCreditsResponse.kt b/ToyProject/app/src/main/java/com/egiwon/moviesearch/data/source/remote/response/MovieCreditsResponse.kt new file mode 100644 index 0000000..e98c631 --- /dev/null +++ b/ToyProject/app/src/main/java/com/egiwon/moviesearch/data/source/remote/response/MovieCreditsResponse.kt @@ -0,0 +1,38 @@ +package com.egiwon.moviesearch.data.source.remote.response + +import com.egiwon.moviesearch.data.model.MovieCastEntity +import com.egiwon.moviesearch.data.model.MovieCreditEntity +import com.google.gson.annotations.SerializedName + +data class MovieCreditsResponse( + @SerializedName("id") + val id: Int, + @SerializedName("cast") + val cast: List +) + +data class MovieCast( + @SerializedName("cast_id") + val castId: Int, + @SerializedName("character") + val character: String, + @SerializedName("name") + val name: String, + @SerializedName("profile_path") + val profilePath: String? +) + +fun MovieCreditsResponse.mapToMovieCreditEntity(): MovieCreditEntity = + runCatching { + MovieCreditEntity( + id = id, + castList = cast.map { it.mapToMovieCastEntity() } + ) + }.getOrNull() ?: MovieCreditEntity() + +fun MovieCast.mapToMovieCastEntity(): MovieCastEntity = + MovieCastEntity( + castId = castId, + name = name, + profilePath = profilePath + ) diff --git a/ToyProject/app/src/main/java/com/egiwon/moviesearch/data/source/remote/response/MovieDetailResponse.kt b/ToyProject/app/src/main/java/com/egiwon/moviesearch/data/source/remote/response/MovieDetailResponse.kt new file mode 100644 index 0000000..8a46e73 --- /dev/null +++ b/ToyProject/app/src/main/java/com/egiwon/moviesearch/data/source/remote/response/MovieDetailResponse.kt @@ -0,0 +1,44 @@ +package com.egiwon.moviesearch.data.source.remote.response + +import com.egiwon.moviesearch.data.model.MovieCastEntity +import com.egiwon.moviesearch.data.model.MovieDetailEntity +import com.google.gson.annotations.SerializedName + +data class MovieDetailResponse( + @SerializedName("id") + val id: Int, + @SerializedName("imdb_id") + val imdbId: String, + @SerializedName("overview") + val overview: String, + @SerializedName("popularity") + val popularity: Double, + @SerializedName("poster_path") + val posterPath: String, + @SerializedName("release_date") + val releaseDate: String, + @SerializedName("runtime") + val runtime: Int, + @SerializedName("title") + val title: String, + @SerializedName("vote_average") + val voteAverage: Double, + @SerializedName("vote_count") + val voteCount: Int +) + +fun MovieDetailResponse.mapToMovieDetailEntity( + movieCastList: List = emptyList() +) = + runCatching { + MovieDetailEntity( + id = id, + overview = overview, + voteAverage = voteAverage, + posterPath = posterPath, + releaseDate = releaseDate, + title = title, + runtime = runtime, + movieCasts = movieCastList + ) + }.getOrNull() ?: MovieDetailEntity() diff --git a/ToyProject/app/src/main/java/com/egiwon/moviesearch/data/source/remote/response/MovieResponse.kt b/ToyProject/app/src/main/java/com/egiwon/moviesearch/data/source/remote/response/MovieResponse.kt new file mode 100644 index 0000000..8fc2797 --- /dev/null +++ b/ToyProject/app/src/main/java/com/egiwon/moviesearch/data/source/remote/response/MovieResponse.kt @@ -0,0 +1,58 @@ +package com.egiwon.moviesearch.data.source.remote.response + +import com.egiwon.moviesearch.data.model.MovieEntity +import com.google.gson.annotations.SerializedName + +data class MovieResponse( + @SerializedName("page") + val page: Int, + @SerializedName("results") + val results: List, + @SerializedName("total_pages") + val totalPages: Int, + @SerializedName("total_results") + val totalResults: Int +) { + data class Result( + @SerializedName("adult") + val adult: Boolean = false, + @SerializedName("backdrop_path") + val backdropPath: String = "", + @SerializedName("genre_ids") + val genreIds: List = emptyList(), + @SerializedName("id") + val id: Int = 0, + @SerializedName("original_language") + val originalLanguage: String = "", + @SerializedName("original_title") + val originalTitle: String = "", + @SerializedName("overview") + val overview: String = "", + @SerializedName("popularity") + val popularity: Double = 0.0, + @SerializedName("poster_path") + val posterPath: String = "", + @SerializedName("release_date") + val releaseDate: String = "", + @SerializedName("title") + val title: String = "", + @SerializedName("video") + val video: Boolean = false, + @SerializedName("vote_average") + val voteAverage: Double = 0.0, + @SerializedName("vote_count") + val voteCount: Int = 0 + ) +} + +fun MovieResponse.mapToMovieEntities(): List = + this.results.map { movieResponse -> + runCatching { + MovieEntity( + movieId = movieResponse.id, + title = movieResponse.title, + posterPath = movieResponse.posterPath + ) + }.getOrNull() ?: MovieEntity() + } + diff --git a/ToyProject/app/src/main/java/com/egiwon/moviesearch/data/source/remote/response/MovieTrailerResponse.kt b/ToyProject/app/src/main/java/com/egiwon/moviesearch/data/source/remote/response/MovieTrailerResponse.kt new file mode 100644 index 0000000..03a0c59 --- /dev/null +++ b/ToyProject/app/src/main/java/com/egiwon/moviesearch/data/source/remote/response/MovieTrailerResponse.kt @@ -0,0 +1,37 @@ +package com.egiwon.moviesearch.data.source.remote.response + +import com.egiwon.moviesearch.data.model.MovieTrailerEntity +import com.egiwon.moviesearch.data.model.MovieTrailerItem +import com.google.gson.annotations.SerializedName + +data class MovieTrailerResponse( + @SerializedName("id") + val id: Int, + @SerializedName("results") + val results: List +) + +data class MovieTrailerResponseItem( + @SerializedName("id") + val id: String = "", + @SerializedName("name") + val name: String = "", + @SerializedName("site") + val site: String = "", + @SerializedName("key") + val key: String = "" +) + +fun MovieTrailerResponse.mapToMovieTrailerEntity(): MovieTrailerEntity = + MovieTrailerEntity( + id = id, + trailers = results.map { it.mapToMovieTrailerEntityItem() } + ) + +fun MovieTrailerResponseItem.mapToMovieTrailerEntityItem(): MovieTrailerItem = + MovieTrailerItem( + trailerId = id, + name = name, + key = key, + site = site + ) \ No newline at end of file diff --git a/ToyProject/app/src/main/java/com/egiwon/moviesearch/data/source/remote/service/MovieService.kt b/ToyProject/app/src/main/java/com/egiwon/moviesearch/data/source/remote/service/MovieService.kt new file mode 100644 index 0000000..35d2c9a --- /dev/null +++ b/ToyProject/app/src/main/java/com/egiwon/moviesearch/data/source/remote/service/MovieService.kt @@ -0,0 +1,37 @@ +package com.egiwon.moviesearch.data.source.remote.service + +import com.egiwon.moviesearch.data.source.remote.response.MovieCreditsResponse +import com.egiwon.moviesearch.data.source.remote.response.MovieDetailResponse +import com.egiwon.moviesearch.data.source.remote.response.MovieResponse +import com.egiwon.moviesearch.data.source.remote.response.MovieTrailerResponse +import io.reactivex.Single +import retrofit2.http.GET +import retrofit2.http.Path +import retrofit2.http.Query + +interface MovieService { + + @GET("3/movie/popular") + fun getPopularMovies( + @Query("page") page: Int, + @Query("language") language: String = "ko" + ): Single + + @GET("3/movie/{movieId}") + fun getMovieDetails( + @Path("movieId") movieId: Int, + @Query("language") language: String = "ko" + ): Single + + @GET("3/movie/{movieId}/credits") + fun getMovieCredits( + @Path("movieId") movieId: Int, + @Query("language") language: String = "ko" + ): Single + + @GET("3/movie/{movieId}/videos") + fun getMovieTrailers( + @Path("movieId") movieId: Int, + @Query("language") language: String = "ko" + ): Single +} \ No newline at end of file diff --git a/ToyProject/app/src/main/java/com/egiwon/moviesearch/di/DataSourceModule.kt b/ToyProject/app/src/main/java/com/egiwon/moviesearch/di/DataSourceModule.kt new file mode 100644 index 0000000..eabdcdc --- /dev/null +++ b/ToyProject/app/src/main/java/com/egiwon/moviesearch/di/DataSourceModule.kt @@ -0,0 +1,9 @@ +package com.egiwon.moviesearch.di + +import com.egiwon.moviesearch.data.MovieRepository +import com.egiwon.moviesearch.data.MovieRepositoryImpl +import org.koin.dsl.module + +val dataSourceModule = module { + single { MovieRepositoryImpl(get()) } +} \ No newline at end of file diff --git a/ToyProject/app/src/main/java/com/egiwon/moviesearch/di/NetworkModule.kt b/ToyProject/app/src/main/java/com/egiwon/moviesearch/di/NetworkModule.kt new file mode 100644 index 0000000..5b84a6e --- /dev/null +++ b/ToyProject/app/src/main/java/com/egiwon/moviesearch/di/NetworkModule.kt @@ -0,0 +1,70 @@ +package com.egiwon.moviesearch.di + +import com.egiwon.moviesearch.BuildConfig +import com.egiwon.moviesearch.data.source.remote.service.MovieService +import okhttp3.Interceptor +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor +import org.koin.core.parameter.parametersOf +import org.koin.dsl.module +import retrofit2.CallAdapter +import retrofit2.Converter +import retrofit2.Retrofit +import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory +import retrofit2.converter.gson.GsonConverterFactory + +val networkModule = module { + factory { + HttpLoggingInterceptor().apply { + level = if (BuildConfig.DEBUG) { + HttpLoggingInterceptor.Level.BODY + } else { + HttpLoggingInterceptor.Level.NONE + } + } + } + + factory { (chain: Interceptor.Chain) -> + chain.proceed( + chain.request() + .newBuilder() + .url( + chain.request() + .url + .newBuilder() + .addQueryParameter(API_KEY_NAME, BuildConfig.API_KEY) + .build() + ).build() + ) + } + + single { + RxJava2CallAdapterFactory.create() + } + single { + GsonConverterFactory.create() + } + + factory { + OkHttpClient.Builder() + .addInterceptor { get { parametersOf(it) } } + .addInterceptor(get()) + .build() + } + + single { + Retrofit.Builder() + .baseUrl(BASE_URL) + .client(get()) + .addCallAdapterFactory(get()) + .addConverterFactory(get()) + .build() + } + + factory { + get().create(MovieService::class.java) + } +} + +const val BASE_URL = "https://api.themoviedb.org/" +const val API_KEY_NAME = "api_key" \ No newline at end of file diff --git a/ToyProject/app/src/main/java/com/egiwon/moviesearch/di/RemoteDataSourceModule.kt b/ToyProject/app/src/main/java/com/egiwon/moviesearch/di/RemoteDataSourceModule.kt new file mode 100644 index 0000000..efb38f2 --- /dev/null +++ b/ToyProject/app/src/main/java/com/egiwon/moviesearch/di/RemoteDataSourceModule.kt @@ -0,0 +1,9 @@ +package com.egiwon.moviesearch.di + +import com.egiwon.moviesearch.data.source.remote.MovieRemoteDataSource +import com.egiwon.moviesearch.data.source.remote.MovieRemoteDataSourceImpl +import org.koin.dsl.module + +val remoteDataSourceModule = module { + single { MovieRemoteDataSourceImpl(get()) } +} \ No newline at end of file diff --git a/ToyProject/app/src/main/java/com/egiwon/moviesearch/di/ViewModelModule.kt b/ToyProject/app/src/main/java/com/egiwon/moviesearch/di/ViewModelModule.kt new file mode 100644 index 0000000..67424c7 --- /dev/null +++ b/ToyProject/app/src/main/java/com/egiwon/moviesearch/di/ViewModelModule.kt @@ -0,0 +1,12 @@ +package com.egiwon.moviesearch.di + +import com.egiwon.moviesearch.ui.MainViewModel +import com.egiwon.moviesearch.ui.detail.MovieDetailViewModel +import org.koin.androidx.viewmodel.dsl.viewModel +import org.koin.dsl.module + +val viewModelModule = module { + viewModel { MainViewModel(get()) } + + viewModel { MovieDetailViewModel(get()) } +} \ No newline at end of file diff --git a/ToyProject/app/src/main/java/com/egiwon/moviesearch/ext/ImageViewExt.kt b/ToyProject/app/src/main/java/com/egiwon/moviesearch/ext/ImageViewExt.kt new file mode 100644 index 0000000..9a5cad3 --- /dev/null +++ b/ToyProject/app/src/main/java/com/egiwon/moviesearch/ext/ImageViewExt.kt @@ -0,0 +1,12 @@ +package com.egiwon.moviesearch.ext + +import android.widget.ImageView +import androidx.databinding.BindingAdapter +import com.egiwon.moviesearch.wrapper.GlideWrapper + +@BindingAdapter("loadAsyncImage") +fun ImageView.loadAsyncImage(url: String?) = GlideWrapper.asyncLoadImage(this, url) + +@BindingAdapter("loadAsyncCastImage") +fun ImageView.loadAsyncCastImage(url: String?) = GlideWrapper.asyncLoadCircleImage(this, url) + diff --git a/ToyProject/app/src/main/java/com/egiwon/moviesearch/ext/OnThrottleButtonClickListenerExt.kt b/ToyProject/app/src/main/java/com/egiwon/moviesearch/ext/OnThrottleButtonClickListenerExt.kt new file mode 100644 index 0000000..9f06e16 --- /dev/null +++ b/ToyProject/app/src/main/java/com/egiwon/moviesearch/ext/OnThrottleButtonClickListenerExt.kt @@ -0,0 +1,32 @@ +package com.egiwon.moviesearch.ext + +import android.view.View +import androidx.databinding.BindingAdapter + +private typealias OnClickListener = (View) -> Unit + +@BindingAdapter("onSingleClick") +fun View.onSingleClick(listener: View.OnClickListener) { + setOnClickListener(OnSingleClickListener { + run(listener::onClick) + }) +} + +class OnSingleClickListener(private val listener: OnClickListener) : View.OnClickListener { + + override fun onClick(v: View?) { + val now = System.currentTimeMillis() + if (now - lastTime < INTERVAL) return + lastTime = now + if (v != null) { + listener(v) + } + } + + companion object { + + private const val INTERVAL: Long = 300 + + private var lastTime: Long = 0 + } +} \ No newline at end of file diff --git a/ToyProject/app/src/main/java/com/egiwon/moviesearch/ext/RecyclerViewExt.kt b/ToyProject/app/src/main/java/com/egiwon/moviesearch/ext/RecyclerViewExt.kt new file mode 100644 index 0000000..30ce378 --- /dev/null +++ b/ToyProject/app/src/main/java/com/egiwon/moviesearch/ext/RecyclerViewExt.kt @@ -0,0 +1,15 @@ +package com.egiwon.moviesearch.ext + +import androidx.databinding.BindingAdapter +import androidx.paging.PagedList +import androidx.recyclerview.widget.RecyclerView +import com.egiwon.moviesearch.base.BaseIdentifier +import com.egiwon.moviesearch.base.BasePagedAdapter + +@Suppress("UNCHECKED_CAST") +@BindingAdapter("replacePagedItems") +fun RecyclerView.replacePagedItems(items: PagedList?) { + (adapter as? BasePagedAdapter)?.run { + replaceAllPagedItems(items) + } +} \ No newline at end of file diff --git a/ToyProject/app/src/main/java/com/egiwon/moviesearch/ui/MainActivity.kt b/ToyProject/app/src/main/java/com/egiwon/moviesearch/ui/MainActivity.kt new file mode 100644 index 0000000..d396c73 --- /dev/null +++ b/ToyProject/app/src/main/java/com/egiwon/moviesearch/ui/MainActivity.kt @@ -0,0 +1,91 @@ +package com.egiwon.moviesearch.ui + +import android.content.Intent +import android.os.Bundle +import android.view.Menu +import android.view.MenuItem +import androidx.appcompat.app.AppCompatDelegate +import androidx.core.app.ActivityOptionsCompat +import androidx.lifecycle.Observer +import com.egiwon.moviesearch.BR +import com.egiwon.moviesearch.R +import com.egiwon.moviesearch.base.BaseActivity +import com.egiwon.moviesearch.base.BasePagedAdapter +import com.egiwon.moviesearch.base.BaseViewModel +import com.egiwon.moviesearch.data.model.MovieEntity +import com.egiwon.moviesearch.databinding.ActivityMainBinding +import com.egiwon.moviesearch.databinding.ItemMovieBinding +import com.egiwon.moviesearch.ui.detail.MovieDetailActivity +import org.koin.androidx.viewmodel.ext.android.viewModel + +class MainActivity : BaseActivity(R.layout.activity_main) { + + override val viewModel: MainViewModel by viewModel() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + bind { + vm = viewModel + initAdapter() + } + + viewModel.loadPopularMovies() + observingViewModel() + } + + override fun ActivityMainBinding.initAdapter() { + rvMovieList.adapter = object : BasePagedAdapter( + R.layout.item_movie, + BR.movie, + mutableMapOf().apply { + put(BR.vm, viewModel) + } + ) {} + rvMovieList.setHasFixedSize(true) + } + + override fun onCreateOptionsMenu(menu: Menu?): Boolean { + menuInflater.inflate(R.menu.main_menu, menu) + + val nightMode = AppCompatDelegate.getDefaultNightMode() + if (nightMode == AppCompatDelegate.MODE_NIGHT_YES) { + menu?.findItem(R.id.night_mode)?.setIcon(R.drawable.ic_sunny) + } else { + menu?.findItem(R.id.night_mode)?.setIcon(R.drawable.ic_brightness) + } + return true + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.night_mode -> { + val nightMode = AppCompatDelegate.getDefaultNightMode() + if (nightMode == AppCompatDelegate.MODE_NIGHT_YES) { + AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO) + } else { + AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES) + } + recreate() + } + + } + return true + } + + private fun observingViewModel() { + viewModel.movie.observe(this, Observer { + val intent = Intent(this, MovieDetailActivity::class.java).apply { + putExtra(KEY_MOVIE_ID, it.movieId) + } + + val options = ActivityOptionsCompat.makeSceneTransitionAnimation(this) + startActivity(intent, options.toBundle()) + }) + + } + + companion object { + const val KEY_MOVIE_ID = "key_movie_id" + } +} diff --git a/ToyProject/app/src/main/java/com/egiwon/moviesearch/ui/MainViewModel.kt b/ToyProject/app/src/main/java/com/egiwon/moviesearch/ui/MainViewModel.kt new file mode 100644 index 0000000..6a81b47 --- /dev/null +++ b/ToyProject/app/src/main/java/com/egiwon/moviesearch/ui/MainViewModel.kt @@ -0,0 +1,34 @@ +package com.egiwon.moviesearch.ui + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.paging.PagedList +import com.egiwon.moviesearch.R +import com.egiwon.moviesearch.base.BaseViewModel +import com.egiwon.moviesearch.data.MovieRepository +import com.egiwon.moviesearch.data.model.MovieEntity + +class MainViewModel( + private val repository: MovieRepository +) : BaseViewModel() { + + lateinit var resultMovieList: LiveData> + + private val _movie = MutableLiveData() + val movie: LiveData get() = _movie + + fun loadPopularMovies() { + resultMovieList = repository.getPagingPopularMovies( + compositeDisposable = compositeDisposable, + onFailure = { + mutableShowErrorTextResId.value = R.string.error_load_fail_text + } + ) + } + + override fun onClick(model: Any) { + if (model is MovieEntity) { + _movie.value = model + } + } +} \ No newline at end of file diff --git a/ToyProject/app/src/main/java/com/egiwon/moviesearch/ui/detail/MovieDetailActivity.kt b/ToyProject/app/src/main/java/com/egiwon/moviesearch/ui/detail/MovieDetailActivity.kt new file mode 100644 index 0000000..8083274 --- /dev/null +++ b/ToyProject/app/src/main/java/com/egiwon/moviesearch/ui/detail/MovieDetailActivity.kt @@ -0,0 +1,83 @@ +package com.egiwon.moviesearch.ui.detail + +import android.os.Bundle +import androidx.core.os.bundleOf +import androidx.lifecycle.Observer +import com.egiwon.moviesearch.BR +import com.egiwon.moviesearch.R +import com.egiwon.moviesearch.base.BaseActivity +import com.egiwon.moviesearch.base.BaseIdentifier +import com.egiwon.moviesearch.base.BaseListAdapter +import com.egiwon.moviesearch.base.BaseViewModel +import com.egiwon.moviesearch.databinding.ActivityMovieDetailBinding +import com.egiwon.moviesearch.databinding.ItemCastBinding +import com.egiwon.moviesearch.ui.MainActivity +import com.egiwon.moviesearch.ui.model.MovieCastViewObject +import com.egiwon.moviesearch.ui.model.MovieTrailerViewObject +import com.egiwon.moviesearch.ui.preview.PreviewDialog +import org.koin.androidx.viewmodel.ext.android.viewModel + +class MovieDetailActivity : BaseActivity( + R.layout.activity_movie_detail +) { + + override val viewModel: MovieDetailViewModel by viewModel() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val movieId = intent.getIntExtra(MainActivity.KEY_MOVIE_ID, 0) + viewModel.getMovieDetailInfo(movieId) + + bind { + initAdapter() + vm = viewModel + } + observingViewModel() + } + + private fun showDialog(movieTrailer: MovieTrailerViewObject) { + val fragmentManager = supportFragmentManager + val newFragment = PreviewDialog() + + if (movieTrailer.trailers.isNotEmpty()) { + newFragment.arguments = bundleOf(KEY_TRAILER to movieTrailer.trailers[0].key) + } + + newFragment.show(fragmentManager, "dialog") + } + + override fun ActivityMovieDetailBinding.initAdapter() { + rvCreditCast.adapter = + object : BaseListAdapter( + R.layout.item_cast, + BR.castOfCharacter, + mutableMapOf().apply { + put(BR.vm, viewModel) + } + ) {} + + rvCreditCast.setHasFixedSize(true) + } + + private fun observingViewModel() { + viewModel.movieDetailInfo.observe(this, Observer { + binding.movieDetailInfo = it + }) + + @Suppress("UNCHECKED_CAST") + viewModel.movieCastList.observe(this, Observer { + (binding.rvCreditCast.adapter as? BaseListAdapter) + ?.replaceAll(it) + }) + + viewModel.movieTrailerInfo.observe(this, Observer { + showDialog(it) + }) + } + + + companion object { + const val KEY_TRAILER = "key_trailer" + } +} \ No newline at end of file diff --git a/ToyProject/app/src/main/java/com/egiwon/moviesearch/ui/detail/MovieDetailViewModel.kt b/ToyProject/app/src/main/java/com/egiwon/moviesearch/ui/detail/MovieDetailViewModel.kt new file mode 100644 index 0000000..873d098 --- /dev/null +++ b/ToyProject/app/src/main/java/com/egiwon/moviesearch/ui/detail/MovieDetailViewModel.kt @@ -0,0 +1,48 @@ +package com.egiwon.moviesearch.ui.detail + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.Transformations +import com.egiwon.moviesearch.base.BaseViewModel +import com.egiwon.moviesearch.data.MovieRepository +import com.egiwon.moviesearch.data.model.mapToMovieDetailViewObject +import com.egiwon.moviesearch.data.model.mapToMovieTrailerViewObject +import com.egiwon.moviesearch.ui.model.MovieCastViewObject +import com.egiwon.moviesearch.ui.model.MovieDetailViewObject +import com.egiwon.moviesearch.ui.model.MovieTrailerViewObject +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.rxkotlin.addTo +import io.reactivex.rxkotlin.subscribeBy + +class MovieDetailViewModel( + private val repository: MovieRepository +) : BaseViewModel() { + + private val _movieDetailInfo = MutableLiveData() + val movieDetailInfo: LiveData get() = _movieDetailInfo + + private val _movieTrailerInfo = MutableLiveData() + val movieTrailerInfo: LiveData get() = _movieTrailerInfo + + val movieCastList: LiveData> = Transformations.map(movieDetailInfo) { + it.castList + } + + fun getMovieDetailInfo(movieId: Int) { + repository.getMovieDetailInfo(movieId) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeBy { + _movieDetailInfo.value = it.mapToMovieDetailViewObject() + } + .addTo(compositeDisposable) + } + + fun getMovieTrailerInfo(movieId: Int) { + repository.getMovieTrailerInfo(movieId) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeBy { + _movieTrailerInfo.value = it.mapToMovieTrailerViewObject() + } + .addTo(compositeDisposable) + } +} \ No newline at end of file diff --git a/ToyProject/app/src/main/java/com/egiwon/moviesearch/ui/model/MovieCastViewObject.kt b/ToyProject/app/src/main/java/com/egiwon/moviesearch/ui/model/MovieCastViewObject.kt new file mode 100644 index 0000000..8083eb9 --- /dev/null +++ b/ToyProject/app/src/main/java/com/egiwon/moviesearch/ui/model/MovieCastViewObject.kt @@ -0,0 +1,12 @@ +package com.egiwon.moviesearch.ui.model + +import com.egiwon.moviesearch.base.BaseIdentifier + +data class MovieCastViewObject( + val castId: Int = 0, + val name: String = "", + val profilePath: String? = "" +) : BaseIdentifier() { + override val id: Any + get() = castId +} \ No newline at end of file diff --git a/ToyProject/app/src/main/java/com/egiwon/moviesearch/ui/model/MovieDetailViewObject.kt b/ToyProject/app/src/main/java/com/egiwon/moviesearch/ui/model/MovieDetailViewObject.kt new file mode 100644 index 0000000..8fe5be8 --- /dev/null +++ b/ToyProject/app/src/main/java/com/egiwon/moviesearch/ui/model/MovieDetailViewObject.kt @@ -0,0 +1,12 @@ +package com.egiwon.moviesearch.ui.model + +data class MovieDetailViewObject( + val id: Int = 0, + val overview: String = "", + val voteAverage: Double = 0.0, + val posterPath: String = "", + val releaseDate: String = "", + val title: String = "", + val runtime: Int = 0, + val castList: List = emptyList() +) \ No newline at end of file diff --git a/ToyProject/app/src/main/java/com/egiwon/moviesearch/ui/model/MovieTrailerViewObject.kt b/ToyProject/app/src/main/java/com/egiwon/moviesearch/ui/model/MovieTrailerViewObject.kt new file mode 100644 index 0000000..b7ca0f9 --- /dev/null +++ b/ToyProject/app/src/main/java/com/egiwon/moviesearch/ui/model/MovieTrailerViewObject.kt @@ -0,0 +1,13 @@ +package com.egiwon.moviesearch.ui.model + +data class MovieTrailerViewObject( + val id: Int, + val trailers: List +) + +data class MovieTrailer( + val trailerId: String = "", + val name: String = "", + val key: String = "", + val site: String = "" +) \ No newline at end of file diff --git a/ToyProject/app/src/main/java/com/egiwon/moviesearch/ui/preview/PreviewDialog.kt b/ToyProject/app/src/main/java/com/egiwon/moviesearch/ui/preview/PreviewDialog.kt new file mode 100644 index 0000000..26b2ef1 --- /dev/null +++ b/ToyProject/app/src/main/java/com/egiwon/moviesearch/ui/preview/PreviewDialog.kt @@ -0,0 +1,39 @@ +package com.egiwon.moviesearch.ui.preview + +import android.annotation.SuppressLint +import android.app.Dialog +import android.os.Bundle +import android.view.Window +import android.webkit.WebView +import android.webkit.WebViewClient +import androidx.appcompat.app.AlertDialog +import androidx.fragment.app.DialogFragment +import com.egiwon.moviesearch.ui.detail.MovieDetailActivity.Companion.KEY_TRAILER + +class PreviewDialog : DialogFragment() { + + private val key by lazy { + arguments?.get(KEY_TRAILER)?.let { + (it as? String) ?: "" + } + } + + @SuppressLint("SetJavaScriptEnabled") + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val view = WebView(requireActivity()) + + val dialog = AlertDialog.Builder(requireActivity()) + .setView(view) + .create() + + view.settings.javaScriptEnabled = true + view.webViewClient = WebViewClient() + + if (!key.isNullOrBlank()) { + view.loadUrl("https://www.themoviedb.org/video/play?key=${key}&height=300") + } + + dialog.requestWindowFeature(Window.FEATURE_NO_TITLE) + return dialog + } +} \ No newline at end of file diff --git a/ToyProject/app/src/main/java/com/egiwon/moviesearch/wrapper/GlideWrapper.kt b/ToyProject/app/src/main/java/com/egiwon/moviesearch/wrapper/GlideWrapper.kt new file mode 100644 index 0000000..152b9fc --- /dev/null +++ b/ToyProject/app/src/main/java/com/egiwon/moviesearch/wrapper/GlideWrapper.kt @@ -0,0 +1,39 @@ +package com.egiwon.moviesearch.wrapper + +import android.widget.ImageView +import com.bumptech.glide.Glide +import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions +import com.bumptech.glide.request.RequestOptions +import com.egiwon.moviesearch.R + +object GlideWrapper { + private fun asyncLoadImage( + target: ImageView, + url: String?, + block: RequestOptions.() -> RequestOptions + ) { + + Glide.with(target) + .load("$POSTER_BASE_URL$url") + .transition(DrawableTransitionOptions.withCrossFade()) + .apply(RequestOptions().block()) + .into(target) + } + + + fun asyncLoadImage(target: ImageView, url: String?) { + asyncLoadImage(target, url) { + RequestOptions() + } + } + + fun asyncLoadCircleImage(target: ImageView, url: String?) { + asyncLoadImage(target, url) { + RequestOptions + .circleCropTransform() + .placeholder(R.drawable.ic_person_outline_24px) + } + } + + private const val POSTER_BASE_URL = "https://image.tmdb.org/t/p/w185_and_h278_bestv2/" +} \ No newline at end of file diff --git a/ToyProject/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/ToyProject/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/ToyProject/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/ToyProject/app/src/main/res/drawable/ic_brightness.xml b/ToyProject/app/src/main/res/drawable/ic_brightness.xml new file mode 100644 index 0000000..682d3e5 --- /dev/null +++ b/ToyProject/app/src/main/res/drawable/ic_brightness.xml @@ -0,0 +1,9 @@ + + + diff --git a/ToyProject/app/src/main/res/drawable/ic_clapperboard.xml b/ToyProject/app/src/main/res/drawable/ic_clapperboard.xml new file mode 100644 index 0000000..fddd8b4 --- /dev/null +++ b/ToyProject/app/src/main/res/drawable/ic_clapperboard.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + diff --git a/ToyProject/app/src/main/res/drawable/ic_launcher_background.xml b/ToyProject/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/ToyProject/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ToyProject/app/src/main/res/drawable/ic_person_outline_24px.xml b/ToyProject/app/src/main/res/drawable/ic_person_outline_24px.xml new file mode 100644 index 0000000..d726d1a --- /dev/null +++ b/ToyProject/app/src/main/res/drawable/ic_person_outline_24px.xml @@ -0,0 +1,9 @@ + + + diff --git a/ToyProject/app/src/main/res/drawable/ic_sunny.xml b/ToyProject/app/src/main/res/drawable/ic_sunny.xml new file mode 100644 index 0000000..d8080c4 --- /dev/null +++ b/ToyProject/app/src/main/res/drawable/ic_sunny.xml @@ -0,0 +1,9 @@ + + + diff --git a/ToyProject/app/src/main/res/layout/activity_main.xml b/ToyProject/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..681c38b --- /dev/null +++ b/ToyProject/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ToyProject/app/src/main/res/layout/activity_movie_detail.xml b/ToyProject/app/src/main/res/layout/activity_movie_detail.xml new file mode 100644 index 0000000..f6c9126 --- /dev/null +++ b/ToyProject/app/src/main/res/layout/activity_movie_detail.xml @@ -0,0 +1,135 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ToyProject/app/src/main/res/layout/item_cast.xml b/ToyProject/app/src/main/res/layout/item_cast.xml new file mode 100644 index 0000000..9365e9a --- /dev/null +++ b/ToyProject/app/src/main/res/layout/item_cast.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ToyProject/app/src/main/res/layout/item_movie.xml b/ToyProject/app/src/main/res/layout/item_movie.xml new file mode 100644 index 0000000..115e296 --- /dev/null +++ b/ToyProject/app/src/main/res/layout/item_movie.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ToyProject/app/src/main/res/menu/main_menu.xml b/ToyProject/app/src/main/res/menu/main_menu.xml new file mode 100644 index 0000000..def8c07 --- /dev/null +++ b/ToyProject/app/src/main/res/menu/main_menu.xml @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file diff --git a/ToyProject/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/ToyProject/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..eca70cf --- /dev/null +++ b/ToyProject/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/ToyProject/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/ToyProject/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..eca70cf --- /dev/null +++ b/ToyProject/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/ToyProject/app/src/main/res/mipmap-hdpi/ic_launcher.png b/ToyProject/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..a571e60 Binary files /dev/null and b/ToyProject/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/ToyProject/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/ToyProject/app/src/main/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 0000000..61da551 Binary files /dev/null and b/ToyProject/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/ToyProject/app/src/main/res/mipmap-mdpi/ic_launcher.png b/ToyProject/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..c41dd28 Binary files /dev/null and b/ToyProject/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/ToyProject/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/ToyProject/app/src/main/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 0000000..db5080a Binary files /dev/null and b/ToyProject/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/ToyProject/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/ToyProject/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..6dba46d Binary files /dev/null and b/ToyProject/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/ToyProject/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/ToyProject/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 0000000..da31a87 Binary files /dev/null and b/ToyProject/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/ToyProject/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/ToyProject/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..15ac681 Binary files /dev/null and b/ToyProject/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/ToyProject/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/ToyProject/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..b216f2d Binary files /dev/null and b/ToyProject/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/ToyProject/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/ToyProject/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..f25a419 Binary files /dev/null and b/ToyProject/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/ToyProject/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/ToyProject/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..e96783c Binary files /dev/null and b/ToyProject/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/ToyProject/app/src/main/res/values-night/colors.xml b/ToyProject/app/src/main/res/values-night/colors.xml new file mode 100644 index 0000000..e2c99d5 --- /dev/null +++ b/ToyProject/app/src/main/res/values-night/colors.xml @@ -0,0 +1,8 @@ + + + #454545 + #777777 + #FFFFFF + + #252525 + diff --git a/ToyProject/app/src/main/res/values/colors.xml b/ToyProject/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..e50d45d --- /dev/null +++ b/ToyProject/app/src/main/res/values/colors.xml @@ -0,0 +1,12 @@ + + + @color/replyBlue + @color/replyLightBlue + @color/replyOrange + #FFFFFF + + #344955 + #4A6572 + #232f34 + #F9AA53 + diff --git a/ToyProject/app/src/main/res/values/strings.xml b/ToyProject/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..0f0bd0b --- /dev/null +++ b/ToyProject/app/src/main/res/values/strings.xml @@ -0,0 +1,8 @@ + + MovieSearch + load fail + Night + Day_Mode + %1$s분 + 예고편 + diff --git a/ToyProject/app/src/main/res/values/styles.xml b/ToyProject/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..1b0195d --- /dev/null +++ b/ToyProject/app/src/main/res/values/styles.xml @@ -0,0 +1,17 @@ + + + + + + + +