├── app
├── .gitignore
├── src
│ ├── main
│ │ ├── res
│ │ │ ├── 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
│ │ │ ├── drawable-hdpi
│ │ │ │ └── img_placeholder.png
│ │ │ ├── drawable-ldpi
│ │ │ │ └── img_placeholder.png
│ │ │ ├── drawable-mdpi
│ │ │ │ └── img_placeholder.png
│ │ │ ├── drawable-xhdpi
│ │ │ │ └── img_placeholder.png
│ │ │ ├── drawable-xxhdpi
│ │ │ │ └── img_placeholder.png
│ │ │ ├── drawable-xxxhdpi
│ │ │ │ └── img_placeholder.png
│ │ │ ├── values
│ │ │ │ ├── colors.xml
│ │ │ │ ├── strings.xml
│ │ │ │ └── styles.xml
│ │ │ ├── drawable
│ │ │ │ ├── bg_shimmer_rect.xml
│ │ │ │ └── ic_launcher_background.xml
│ │ │ ├── mipmap-anydpi-v26
│ │ │ │ ├── ic_launcher.xml
│ │ │ │ └── ic_launcher_round.xml
│ │ │ ├── layout
│ │ │ │ ├── activity_movie_detail.xml
│ │ │ │ ├── layout_common_error.xml
│ │ │ │ ├── activity_dashboard.xml
│ │ │ │ ├── activity_movie_detail_content.xml
│ │ │ │ ├── item_movie.xml
│ │ │ │ ├── layout_item_movie_load.xml
│ │ │ │ └── layout_movie_detail_load.xml
│ │ │ └── drawable-v24
│ │ │ │ └── ic_launcher_foreground.xml
│ │ ├── java
│ │ │ └── com
│ │ │ │ └── andro
│ │ │ │ └── indie
│ │ │ │ └── school
│ │ │ │ ├── common
│ │ │ │ ├── di
│ │ │ │ │ ├── BaseViewModelModuleProvider.kt
│ │ │ │ │ ├── BaseModuleProvider.kt
│ │ │ │ │ ├── DataSourceModules.kt
│ │ │ │ │ ├── DepsModuleProvider.kt
│ │ │ │ │ └── NetworkModules.kt
│ │ │ │ ├── base
│ │ │ │ │ ├── ResponseResult.kt
│ │ │ │ │ ├── BaseActivity.kt
│ │ │ │ │ ├── DiffCallback.kt
│ │ │ │ │ └── BaseDataSource.kt
│ │ │ │ ├── extension
│ │ │ │ │ └── CommonExt.kt
│ │ │ │ └── custom
│ │ │ │ │ └── IntentHelper.kt
│ │ │ │ ├── ui
│ │ │ │ ├── dashboard
│ │ │ │ │ ├── DashboardViewModel.kt
│ │ │ │ │ ├── DashboardModule.kt
│ │ │ │ │ ├── DashboardActivity.kt
│ │ │ │ │ └── DashboardMovieAdapter.kt
│ │ │ │ └── detail
│ │ │ │ │ ├── MovieDetailViewModel.kt
│ │ │ │ │ ├── MovieDetailModule.kt
│ │ │ │ │ └── MovieDetailActivity.kt
│ │ │ │ ├── api
│ │ │ │ ├── MovieApiService.kt
│ │ │ │ └── AuthInterceptor.kt
│ │ │ │ ├── RetrofitCoroutineApp.kt
│ │ │ │ └── data
│ │ │ │ ├── model
│ │ │ │ ├── vo
│ │ │ │ │ └── MovieItem.kt
│ │ │ │ └── response
│ │ │ │ │ ├── MovieResponse.kt
│ │ │ │ │ └── MovieDetailResponse.kt
│ │ │ │ └── source
│ │ │ │ └── remote
│ │ │ │ └── MovieRemoteDataSource.kt
│ │ └── AndroidManifest.xml
│ ├── test
│ │ └── java
│ │ │ └── com
│ │ │ └── andro
│ │ │ └── indie
│ │ │ └── school
│ │ │ └── ExampleUnitTest.kt
│ └── androidTest
│ │ └── java
│ │ └── com
│ │ └── andro
│ │ └── indie
│ │ └── school
│ │ └── ExampleInstrumentedTest.kt
├── proguard-rules.pro
└── build.gradle.kts
├── settings.gradle.kts
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── .gitignore
├── readme.md
├── gradle.properties
├── gradlew.bat
└── gradlew
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | include(":app")
2 | rootProject.name="RetrofitCoroutine"
3 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/herisulistiyanto/Simple-Retrofit-Coroutine/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/herisulistiyanto/Simple-Retrofit-Coroutine/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/herisulistiyanto/Simple-Retrofit-Coroutine/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/herisulistiyanto/Simple-Retrofit-Coroutine/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/herisulistiyanto/Simple-Retrofit-Coroutine/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/herisulistiyanto/Simple-Retrofit-Coroutine/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/img_placeholder.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/herisulistiyanto/Simple-Retrofit-Coroutine/HEAD/app/src/main/res/drawable-hdpi/img_placeholder.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-ldpi/img_placeholder.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/herisulistiyanto/Simple-Retrofit-Coroutine/HEAD/app/src/main/res/drawable-ldpi/img_placeholder.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-mdpi/img_placeholder.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/herisulistiyanto/Simple-Retrofit-Coroutine/HEAD/app/src/main/res/drawable-mdpi/img_placeholder.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/herisulistiyanto/Simple-Retrofit-Coroutine/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/herisulistiyanto/Simple-Retrofit-Coroutine/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/img_placeholder.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/herisulistiyanto/Simple-Retrofit-Coroutine/HEAD/app/src/main/res/drawable-xhdpi/img_placeholder.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/img_placeholder.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/herisulistiyanto/Simple-Retrofit-Coroutine/HEAD/app/src/main/res/drawable-xxhdpi/img_placeholder.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/herisulistiyanto/Simple-Retrofit-Coroutine/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/herisulistiyanto/Simple-Retrofit-Coroutine/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxxhdpi/img_placeholder.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/herisulistiyanto/Simple-Retrofit-Coroutine/HEAD/app/src/main/res/drawable-xxxhdpi/img_placeholder.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/herisulistiyanto/Simple-Retrofit-Coroutine/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /credential.properties
5 | /.idea
6 | .DS_Store
7 | /build
8 | /captures
9 | .externalNativeBuild
10 | .cxx
11 | *.apk
12 | *.aab
13 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #008577
4 | #00574B
5 | #D81B60
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/bg_shimmer_rect.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Thu Oct 31 14:24:54 WIB 2019
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip
7 |
--------------------------------------------------------------------------------
/app/src/main/java/com/andro/indie/school/common/di/BaseViewModelModuleProvider.kt:
--------------------------------------------------------------------------------
1 | package com.andro.indie.school.common.di
2 |
3 | /**
4 | * Created by herisulistiyanto on 07/11/19.
5 | * KjokenKoddinger
6 | */
7 |
8 | interface BaseViewModelModuleProvider {
9 |
10 | fun loadModules()
11 |
12 | }
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/java/com/andro/indie/school/common/di/BaseModuleProvider.kt:
--------------------------------------------------------------------------------
1 | package com.andro.indie.school.common.di
2 |
3 | import org.koin.core.module.Module
4 |
5 | /**
6 | * Created by herisulistiyanto on 07/11/19.
7 | * KjokenKoddinger
8 | */
9 |
10 | interface BaseModuleProvider {
11 |
12 | val modules: List
13 |
14 | }
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | RetrofitCoroutine
3 | Score : %s
4 | Release date : %s
5 | Overview
6 | Movie Detail
7 |
8 |
--------------------------------------------------------------------------------
/app/src/test/java/com/andro/indie/school/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package com.andro.indie.school
2 |
3 | import org.junit.Test
4 |
5 | import org.junit.Assert.*
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * See [testing documentation](http://d.android.com/tools/testing).
11 | */
12 | class ExampleUnitTest {
13 | @Test
14 | fun addition_isCorrect() {
15 | assertEquals(4, 2 + 2)
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/app/src/main/java/com/andro/indie/school/common/base/ResponseResult.kt:
--------------------------------------------------------------------------------
1 | package com.andro.indie.school.common.base
2 |
3 | /**
4 | * Created by herisulistiyanto on 01/11/19.
5 | * KjokenKoddinger
6 | */
7 |
8 | sealed class ResponseResult {
9 |
10 | data class Success(val result: T): ResponseResult()
11 | data class Error(val msg: T): ResponseResult()
12 | object Loading : ResponseResult()
13 |
14 | }
15 |
16 | data class ResponseWrapper(val data: T?, val errorMsg: String?)
--------------------------------------------------------------------------------
/app/src/main/java/com/andro/indie/school/ui/dashboard/DashboardViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.andro.indie.school.ui.dashboard
2 |
3 | import androidx.lifecycle.ViewModel
4 | import androidx.lifecycle.viewModelScope
5 | import com.andro.indie.school.data.source.remote.MovieRemoteDataSource
6 |
7 | /**
8 | * Created by herisulistiyanto on 01/11/19.
9 | * KjokenKoddinger
10 | */
11 |
12 | class DashboardViewModel(private val movieRemoteDataSource: MovieRemoteDataSource) : ViewModel() {
13 |
14 | fun discoverAllMovies() = movieRemoteDataSource.discoverAllMovies(viewModelScope)
15 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/andro/indie/school/ui/detail/MovieDetailViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.andro.indie.school.ui.detail
2 |
3 | import androidx.lifecycle.ViewModel
4 | import androidx.lifecycle.viewModelScope
5 | import com.andro.indie.school.data.source.remote.MovieRemoteDataSource
6 |
7 | /**
8 | * Created by herisulistiyanto on 06/11/19.
9 | * KjokenKoddinger
10 | */
11 |
12 | class MovieDetailViewModel(private val movieRemoteDataSource: MovieRemoteDataSource): ViewModel() {
13 |
14 | fun fetchDetailMovie(movieId: Int) = movieRemoteDataSource.getMovieDetails(movieId, viewModelScope)
15 |
16 | }
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | # Demo Movie App
2 |
3 | Simple demo app using Retrofit & Kotlin Coroutine
4 |
5 | ### In this app using :
6 | * Retrofit + OkHttp
7 | * Coil Image Loader
8 | * Kotlin Coroutine
9 | * Gradle Kotlin DSL
10 | * MVVM Pattern
11 | * Koin
12 | * Shimmer
13 |
14 |
15 | ### How to Compile?
16 | * You need to get `api key` from [themoviedb.org](https://themoviedb.org/), and go to [developer page](https://developers.themoviedb.org/) for complete docs
17 | * Once you get the api key, then create file named `credential.properties` on rootDir project
18 | * Put your api key in `credential.properties` like : `api_key=xxxxxxxxxxxxxxxx`
19 | * And done :)
--------------------------------------------------------------------------------
/app/src/main/java/com/andro/indie/school/common/di/DataSourceModules.kt:
--------------------------------------------------------------------------------
1 | package com.andro.indie.school.common.di
2 |
3 | import com.andro.indie.school.data.source.remote.MovieRemoteDataSource
4 | import org.koin.core.module.Module
5 | import org.koin.dsl.module
6 |
7 | /**
8 | * Created by herisulistiyanto on 07/11/19.
9 | * KjokenKoddinger
10 | */
11 |
12 | object DataSourceModules: BaseModuleProvider {
13 |
14 | override val modules: List
15 | get() = listOf(
16 | remoteDataSourceModule
17 | )
18 |
19 | private val remoteDataSourceModule = module {
20 | single { MovieRemoteDataSource(get()) }
21 | }
22 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/andro/indie/school/api/MovieApiService.kt:
--------------------------------------------------------------------------------
1 | package com.andro.indie.school.api
2 |
3 | import com.andro.indie.school.data.model.response.MovieDetailResponse
4 | import com.andro.indie.school.data.model.response.MovieResponse
5 | import retrofit2.Response
6 | import retrofit2.http.GET
7 | import retrofit2.http.Path
8 |
9 | /**
10 | * Created by herisulistiyanto on 2019-10-31.
11 | * KjokenKoddinger
12 | */
13 |
14 | interface MovieApiService {
15 |
16 | @GET("/3/discover/movie")
17 | suspend fun discoverAllMovies(): Response
18 |
19 | @GET("/3/movie/{movieId}")
20 | suspend fun getMovieDetail(@Path("movieId") movieId: Int): Response
21 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/andro/indie/school/ui/dashboard/DashboardModule.kt:
--------------------------------------------------------------------------------
1 | package com.andro.indie.school.ui.dashboard
2 |
3 | import com.andro.indie.school.common.di.BaseViewModelModuleProvider
4 | import org.koin.androidx.viewmodel.dsl.viewModel
5 | import org.koin.core.context.loadKoinModules
6 | import org.koin.dsl.module
7 |
8 | /**
9 | * Created by herisulistiyanto on 07/11/19.
10 | * KjokenKoddinger
11 | */
12 |
13 | object DashboardModule : BaseViewModelModuleProvider {
14 |
15 | override fun loadModules() = lazyLoadModule
16 |
17 | private val lazyLoadModule by lazy { loadKoinModules(viewModelModule) }
18 |
19 | private val viewModelModule = module {
20 | viewModel { DashboardViewModel(get()) }
21 | }
22 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/andro/indie/school/ui/detail/MovieDetailModule.kt:
--------------------------------------------------------------------------------
1 | package com.andro.indie.school.ui.detail
2 |
3 | import com.andro.indie.school.common.di.BaseViewModelModuleProvider
4 | import org.koin.androidx.viewmodel.dsl.viewModel
5 | import org.koin.core.context.loadKoinModules
6 | import org.koin.dsl.module
7 |
8 | /**
9 | * Created by herisulistiyanto on 07/11/19.
10 | * KjokenKoddinger
11 | */
12 |
13 | object MovieDetailModule: BaseViewModelModuleProvider {
14 |
15 | override fun loadModules() = lazyLoadModule
16 |
17 | private val lazyLoadModule by lazy { loadKoinModules(viewModelModule) }
18 |
19 | private val viewModelModule = module {
20 | viewModel { MovieDetailViewModel(get()) }
21 | }
22 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/andro/indie/school/api/AuthInterceptor.kt:
--------------------------------------------------------------------------------
1 | package com.andro.indie.school.api
2 |
3 | import com.andro.indie.school.BuildConfig
4 | import okhttp3.Interceptor
5 | import okhttp3.Response
6 |
7 | /**
8 | * Created by herisulistiyanto on 2019-10-31.
9 | * KjokenKoddinger
10 | */
11 |
12 | class AuthInterceptor: Interceptor {
13 |
14 | override fun intercept(chain: Interceptor.Chain): Response {
15 | val original = chain.request()
16 | val interceptUrl = original.url.newBuilder()
17 | .addQueryParameter("api_key", BuildConfig.API_KEY)
18 | .build()
19 | val newReq = original.newBuilder().url(interceptUrl).build()
20 | return chain.proceed(newReq)
21 | }
22 | }
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_movie_detail.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/app/src/main/java/com/andro/indie/school/common/di/DepsModuleProvider.kt:
--------------------------------------------------------------------------------
1 | package com.andro.indie.school.common.di
2 |
3 | import android.content.ContentResolver
4 | import org.koin.android.ext.koin.androidContext
5 | import org.koin.core.module.Module
6 | import org.koin.dsl.module
7 |
8 | /**
9 | * Created by herisulistiyanto on 07/11/19.
10 | * KjokenKoddinger
11 | */
12 |
13 | object DepsModuleProvider {
14 |
15 | private val appModule = module {
16 | single { androidContext().contentResolver }
17 | }
18 |
19 | val modules: List
20 | get() {
21 | return ArrayList().apply {
22 | add(appModule)
23 | addAll(NetworkModules.modules)
24 | addAll(DataSourceModules.modules)
25 | }
26 | }
27 |
28 | }
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/andro/indie/school/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package com.andro.indie.school
2 |
3 | import androidx.test.platform.app.InstrumentationRegistry
4 | import androidx.test.ext.junit.runners.AndroidJUnit4
5 |
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | import org.junit.Assert.*
10 |
11 | /**
12 | * Instrumented test, which will execute on an Android device.
13 | *
14 | * See [testing documentation](http://d.android.com/tools/testing).
15 | */
16 | @RunWith(AndroidJUnit4::class)
17 | class ExampleInstrumentedTest {
18 | @Test
19 | fun useAppContext() {
20 | // Context of the app under test.
21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
22 | assertEquals("com.andro.indie.school", appContext.packageName)
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
--------------------------------------------------------------------------------
/app/src/main/java/com/andro/indie/school/RetrofitCoroutineApp.kt:
--------------------------------------------------------------------------------
1 | package com.andro.indie.school
2 |
3 | import android.app.Application
4 | import com.andro.indie.school.common.di.DepsModuleProvider
5 | import org.koin.android.ext.koin.androidContext
6 | import org.koin.android.ext.koin.androidLogger
7 | import org.koin.core.context.startKoin
8 | import timber.log.Timber
9 |
10 | /**
11 | * Created by herisulistiyanto on 01/11/19.
12 | * KjokenKoddinger
13 | */
14 |
15 | class RetrofitCoroutineApp: Application() {
16 |
17 | override fun onCreate() {
18 | super.onCreate()
19 | startKoin {
20 | androidLogger()
21 | androidContext(this@RetrofitCoroutineApp)
22 | modules(DepsModuleProvider.modules)
23 | }
24 |
25 | if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree())
26 | }
27 |
28 | }
--------------------------------------------------------------------------------
/app/src/main/res/layout/layout_common_error.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
17 |
18 |
--------------------------------------------------------------------------------
/app/src/main/java/com/andro/indie/school/common/base/BaseActivity.kt:
--------------------------------------------------------------------------------
1 | package com.andro.indie.school.common.base
2 |
3 | import android.os.Bundle
4 | import androidx.appcompat.app.AppCompatActivity
5 | import androidx.lifecycle.LiveData
6 | import androidx.lifecycle.observe
7 |
8 | /**
9 | * Created by herisulistiyanto on 01/11/19.
10 | * KjokenKoddinger
11 | */
12 |
13 | abstract class BaseActivity : AppCompatActivity() {
14 |
15 | override fun onCreate(savedInstanceState: Bundle?) {
16 | super.onCreate(savedInstanceState)
17 | loadModules()
18 | onViewReady(savedInstanceState)
19 | }
20 |
21 | abstract fun onViewReady(savedInstanceState: Bundle?)
22 |
23 | abstract fun loadModules()
24 |
25 | protected fun LiveData.onResult(action: (T) -> Unit) {
26 | observe(this@BaseActivity) { data ->
27 | data?.let(action)
28 | }
29 | }
30 |
31 | }
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
15 |
16 |
20 |
21 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/app/src/main/java/com/andro/indie/school/data/model/vo/MovieItem.kt:
--------------------------------------------------------------------------------
1 | package com.andro.indie.school.data.model.vo
2 |
3 | import com.google.gson.annotations.SerializedName
4 |
5 |
6 | /**
7 | * Created by herisulistiyanto on 2019-11-05.
8 | * KjokenKoddinger
9 | */
10 |
11 | data class MovieItem(
12 | @SerializedName("backdrop_path")
13 | val backdropPath: String? = "",
14 | @SerializedName("genre_ids")
15 | val genreIds: List? = listOf(),
16 | @SerializedName("id")
17 | val id: Int? = 0,
18 | @SerializedName("overview")
19 | val overview: String? = "",
20 | @SerializedName("popularity")
21 | val popularity: Double? = 0.0,
22 | @SerializedName("poster_path")
23 | val posterPath: String? = "",
24 | @SerializedName("release_date")
25 | val releaseDate: String? = "",
26 | @SerializedName("title")
27 | val title: String? = "",
28 | @SerializedName("vote_average")
29 | val voteAverage: Double? = 0.0
30 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/andro/indie/school/common/base/DiffCallback.kt:
--------------------------------------------------------------------------------
1 | package com.andro.indie.school.common.base
2 |
3 | import androidx.recyclerview.widget.DiffUtil
4 |
5 | /**
6 | * Created by herisulistiyanto on 2019-11-05.
7 | * KjokenKoddinger
8 | */
9 |
10 | class DiffCallback : DiffUtil.Callback() {
11 |
12 | private var oldList: List = emptyList()
13 | private var newList: List = emptyList()
14 |
15 | fun setList(oldList: List, newList: List) {
16 | this.oldList = oldList
17 | this.newList = newList
18 | }
19 |
20 | override fun getOldListSize(): Int = oldList.size
21 |
22 | override fun getNewListSize(): Int = newList.size
23 |
24 | override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
25 | return oldList[oldItemPosition] == newList[newItemPosition]
26 | }
27 |
28 | override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
29 | return oldList[oldItemPosition] == newList[newItemPosition]
30 | }
31 |
32 | }
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2G
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app's APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Automatically convert third-party libraries to use AndroidX
19 | android.enableJetifier=true
20 | # Kotlin code style for this project: "official" or "obsolete":
21 | kotlin.code.style=official
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_dashboard.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
16 |
17 |
18 |
19 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/app/src/main/java/com/andro/indie/school/data/source/remote/MovieRemoteDataSource.kt:
--------------------------------------------------------------------------------
1 | package com.andro.indie.school.data.source.remote
2 |
3 | import androidx.lifecycle.LiveData
4 | import com.andro.indie.school.api.MovieApiService
5 | import com.andro.indie.school.common.base.BaseDataSource
6 | import com.andro.indie.school.common.base.ResponseResult
7 | import com.andro.indie.school.common.base.ResponseWrapper
8 | import com.andro.indie.school.common.base.resultLiveData
9 | import com.andro.indie.school.data.model.response.MovieDetailResponse
10 | import com.andro.indie.school.data.model.response.MovieResponse
11 | import kotlinx.coroutines.CoroutineScope
12 |
13 | /**
14 | * Created by herisulistiyanto on 01/11/19.
15 | * KjokenKoddinger
16 | */
17 |
18 | class MovieRemoteDataSource(private val movieApiService: MovieApiService) : BaseDataSource() {
19 |
20 | fun discoverAllMovies(scope: CoroutineScope): LiveData>> = resultLiveData(scope) {
21 | getResult {
22 | movieApiService.discoverAllMovies()
23 | }
24 | }
25 |
26 | fun getMovieDetails(movieId: Int, scope: CoroutineScope): LiveData>> = resultLiveData(scope) {
27 | getResult {
28 | movieApiService.getMovieDetail(movieId)
29 | }
30 | }
31 |
32 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/andro/indie/school/common/base/BaseDataSource.kt:
--------------------------------------------------------------------------------
1 | package com.andro.indie.school.common.base
2 |
3 | import androidx.lifecycle.LiveData
4 | import androidx.lifecycle.liveData
5 | import kotlinx.coroutines.CoroutineScope
6 | import kotlinx.coroutines.Dispatchers
7 | import kotlinx.coroutines.withContext
8 | import retrofit2.Response
9 | import timber.log.Timber
10 |
11 | /**
12 | * Created by herisulistiyanto on 01/11/19.
13 | * KjokenKoddinger
14 | */
15 |
16 | abstract class BaseDataSource {
17 |
18 | protected suspend fun getResult(call: suspend () -> Response): ResponseResult> {
19 | try {
20 | val response = call()
21 | if (response.isSuccessful) {
22 | val body = response.body()
23 | if (null != body) return ResponseResult.Success(ResponseWrapper(body, null))
24 | }
25 | return error("${response.code()} ${response.message()}")
26 | } catch (e: Exception) {
27 | return error(e.message ?: e.toString())
28 | }
29 | }
30 |
31 | private fun error(msg: String): ResponseResult> {
32 | Timber.e(msg)
33 | return ResponseResult.Error(ResponseWrapper(null, msg))
34 | }
35 |
36 | }
37 |
38 | fun resultLiveData(scope: CoroutineScope, call: suspend () -> ResponseResult): LiveData> {
39 | return liveData(scope.coroutineContext) {
40 | emit(ResponseResult.Loading)
41 |
42 | withContext(Dispatchers.IO) {
43 | emit(call.invoke())
44 | }
45 | }
46 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/andro/indie/school/common/extension/CommonExt.kt:
--------------------------------------------------------------------------------
1 | package com.andro.indie.school.common.extension
2 |
3 | import android.app.Activity
4 | import android.content.Context
5 | import android.widget.ImageView
6 | import coil.api.load
7 | import com.andro.indie.school.R
8 | import com.andro.indie.school.common.custom.IntentHelper
9 | import java.text.SimpleDateFormat
10 | import java.util.Locale
11 |
12 | /**
13 | * Created by herisulistiyanto on 2019-11-05.
14 | * KjokenKoddinger
15 | */
16 |
17 | fun String?.convertDate(inputFormat: String = "yyyy-MM-dd",
18 | outputFormat: String = "dd MMMM yyyy",
19 | localeId: Locale = Locale.getDefault()): String {
20 | if (!this.isNullOrEmpty()) {
21 | val dateFormat = SimpleDateFormat(inputFormat, localeId)
22 | val requiredFormat = SimpleDateFormat(outputFormat, localeId)
23 | try {
24 | val date = dateFormat.parse(this)
25 | date?.let {
26 | return requiredFormat.format(it)
27 | }
28 | } catch (e: Exception) {
29 | e.printStackTrace()
30 | }
31 | }
32 | return ""
33 | }
34 |
35 | fun ImageView.loadImageWithUrl(partUrl: String?, isLarge: Boolean = false) {
36 | val baseUrl = when {
37 | isLarge -> "https://image.tmdb.org/t/p/w500/"
38 | else -> "https://image.tmdb.org/t/p/w300/"
39 | }
40 | val fullUrl = baseUrl + partUrl.orEmpty()
41 | this.load(fullUrl) {
42 | crossfade(false)
43 | placeholder(R.drawable.img_placeholder)
44 | error(R.drawable.img_placeholder)
45 | }
46 | }
47 |
48 | inline fun Context.launchActivity(vararg params: Pair) {
49 | IntentHelper.internalStartActivity(this, T::class.java, params)
50 | }
51 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
12 |
13 |
19 |
22 |
25 |
26 |
27 |
28 |
34 |
35 |
--------------------------------------------------------------------------------
/app/src/main/java/com/andro/indie/school/common/di/NetworkModules.kt:
--------------------------------------------------------------------------------
1 | package com.andro.indie.school.common.di
2 |
3 | import com.andro.indie.school.BuildConfig
4 | import com.andro.indie.school.api.AuthInterceptor
5 | import com.andro.indie.school.api.MovieApiService
6 | import okhttp3.OkHttpClient
7 | import okhttp3.logging.HttpLoggingInterceptor
8 | import org.koin.core.module.Module
9 | import org.koin.dsl.module
10 | import retrofit2.Retrofit
11 | import retrofit2.converter.gson.GsonConverterFactory
12 | import java.util.concurrent.TimeUnit
13 |
14 | /**
15 | * Created by herisulistiyanto on 07/11/19.
16 | * KjokenKoddinger
17 | */
18 |
19 | object NetworkModules: BaseModuleProvider {
20 |
21 | override val modules: List
22 | get() = listOf(
23 | retrofitModule,
24 | webServiceModule
25 | )
26 |
27 | private val webServiceModule = module {
28 | single { get().create(MovieApiService::class.java) }
29 | }
30 |
31 | private val retrofitModule = module {
32 | single { provideAuthInterceptor() }
33 | single { provideOkHttpClient(get()) }
34 | single { provideRetrofit(get(), BuildConfig.BASE_URL) }
35 | }
36 |
37 | private fun provideAuthInterceptor(): AuthInterceptor = AuthInterceptor()
38 |
39 | private fun provideOkHttpClient(authInterceptor: AuthInterceptor): OkHttpClient {
40 | val logging = HttpLoggingInterceptor().apply {
41 | level = if (BuildConfig.DEBUG) HttpLoggingInterceptor.Level.BODY else
42 | HttpLoggingInterceptor.Level.NONE
43 | }
44 |
45 | return OkHttpClient.Builder()
46 | .addInterceptor(authInterceptor)
47 | .addInterceptor(logging)
48 | .connectTimeout(30L, TimeUnit.SECONDS)
49 | .writeTimeout(30L, TimeUnit.SECONDS)
50 | .readTimeout(30L, TimeUnit.SECONDS)
51 | .build()
52 | }
53 |
54 | private fun provideRetrofit(okHttpClient: OkHttpClient, baseUrl: String): Retrofit {
55 | return Retrofit.Builder()
56 | .client(okHttpClient)
57 | .baseUrl(baseUrl)
58 | .addConverterFactory(GsonConverterFactory.create())
59 | .build()
60 | }
61 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/andro/indie/school/data/model/response/MovieResponse.kt:
--------------------------------------------------------------------------------
1 | package com.andro.indie.school.data.model.response
2 |
3 | import com.andro.indie.school.data.model.vo.MovieItem
4 | import com.google.gson.annotations.SerializedName
5 |
6 |
7 | /**
8 | * Created by herisulistiyanto on 01/11/19.
9 | * KjokenKoddinger
10 | */
11 |
12 | data class MovieResponse(
13 | @SerializedName("page")
14 | val page: Int? = 0,
15 | @SerializedName("results")
16 | val results: List? = listOf(),
17 | @SerializedName("total_pages")
18 | val totalPages: Int? = 0,
19 | @SerializedName("total_results")
20 | val totalResults: Int? = 0
21 | ) {
22 | data class Result(
23 | @SerializedName("adult")
24 | val adult: Boolean? = false,
25 | @SerializedName("backdrop_path")
26 | val backdropPath: String? = "",
27 | @SerializedName("genre_ids")
28 | val genreIds: List? = listOf(),
29 | @SerializedName("id")
30 | val id: Int? = 0,
31 | @SerializedName("original_language")
32 | val originalLanguage: String? = "",
33 | @SerializedName("original_title")
34 | val originalTitle: String? = "",
35 | @SerializedName("overview")
36 | val overview: String? = "",
37 | @SerializedName("popularity")
38 | val popularity: Double? = 0.0,
39 | @SerializedName("poster_path")
40 | val posterPath: String? = "",
41 | @SerializedName("release_date")
42 | val releaseDate: String? = "",
43 | @SerializedName("title")
44 | val title: String? = "",
45 | @SerializedName("video")
46 | val video: Boolean? = false,
47 | @SerializedName("vote_average")
48 | val voteAverage: Double? = 0.0,
49 | @SerializedName("vote_count")
50 | val voteCount: Int? = 0
51 | )
52 |
53 | fun mapToMovieItemList(): List {
54 | return results.orEmpty().map {
55 | MovieItem(
56 | it.backdropPath,
57 | it.genreIds,
58 | it.id,
59 | it.overview,
60 | it.popularity,
61 | it.posterPath,
62 | it.releaseDate,
63 | it.title,
64 | it.voteAverage)
65 | }
66 | }
67 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/app/src/main/java/com/andro/indie/school/ui/dashboard/DashboardActivity.kt:
--------------------------------------------------------------------------------
1 | package com.andro.indie.school.ui.dashboard
2 |
3 | import android.os.Bundle
4 | import androidx.recyclerview.widget.LinearLayoutManager
5 | import com.andro.indie.school.R
6 | import com.andro.indie.school.common.base.BaseActivity
7 | import com.andro.indie.school.common.base.ResponseResult
8 | import com.andro.indie.school.common.extension.launchActivity
9 | import com.andro.indie.school.ui.detail.MovieDetailActivity
10 | import kotlinx.android.synthetic.main.activity_dashboard.*
11 | import kotlinx.android.synthetic.main.layout_common_error.*
12 | import org.koin.androidx.viewmodel.ext.android.viewModel
13 |
14 | class DashboardActivity : BaseActivity() {
15 |
16 | private val dashboardViewModel by viewModel()
17 | private val movieAdapter by lazy {
18 | DashboardMovieAdapter {
19 | launchActivity(
20 | MovieDetailActivity.ExtraKey.EXTRA_MOVIE_ID to it.first,
21 | MovieDetailActivity.ExtraKey.EXTRA_MOVIE_TITLE to it.second
22 | )
23 | }
24 | }
25 |
26 | object DashboardStateView {
27 | const val STATE_LOADING = 0
28 | const val STATE_CONTENT = STATE_LOADING + 1
29 | const val STATE_ERROR = STATE_CONTENT + 1
30 | }
31 |
32 | override fun onViewReady(savedInstanceState: Bundle?) {
33 | setContentView(R.layout.activity_dashboard)
34 | initDisplay()
35 |
36 | dashboardViewModel.discoverAllMovies().onResult {
37 | vfDashboardContent.displayedChild = when(it) {
38 | is ResponseResult.Success -> {
39 | val data = it.result.data?.mapToMovieItemList()
40 | movieAdapter.setListMovieData(data)
41 | DashboardStateView.STATE_CONTENT
42 | }
43 | is ResponseResult.Loading -> {
44 | DashboardStateView.STATE_LOADING
45 | }
46 | is ResponseResult.Error -> {
47 | tvCommonError.text = it.msg.errorMsg.orEmpty()
48 | DashboardStateView.STATE_ERROR
49 | }
50 | }
51 | }
52 | }
53 |
54 | override fun loadModules() {
55 | DashboardModule.loadModules()
56 | }
57 |
58 | private fun initDisplay() {
59 | with(rvDashboardMovie) {
60 | layoutManager = LinearLayoutManager(this@DashboardActivity)
61 | adapter = movieAdapter
62 | }
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/app/src/main/java/com/andro/indie/school/ui/dashboard/DashboardMovieAdapter.kt:
--------------------------------------------------------------------------------
1 | package com.andro.indie.school.ui.dashboard
2 |
3 | import android.view.LayoutInflater
4 | import android.view.View
5 | import android.view.ViewGroup
6 | import androidx.recyclerview.widget.DiffUtil
7 | import androidx.recyclerview.widget.RecyclerView
8 | import com.andro.indie.school.R
9 | import com.andro.indie.school.common.base.DiffCallback
10 | import com.andro.indie.school.common.extension.convertDate
11 | import com.andro.indie.school.common.extension.loadImageWithUrl
12 | import com.andro.indie.school.data.model.vo.MovieItem
13 | import kotlinx.android.synthetic.main.item_movie.view.*
14 |
15 | /**
16 | * Created by herisulistiyanto on 2019-11-05.
17 | * KjokenKoddinger
18 | */
19 |
20 | class DashboardMovieAdapter(
21 | private val diffCallback: DiffCallback = DiffCallback(),
22 | private val listener: (Pair) -> Unit
23 | ) : RecyclerView.Adapter() {
24 |
25 | private val movieList = mutableListOf()
26 |
27 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MovieItemViewHolder {
28 | val rootView = LayoutInflater.from(parent.context).inflate(R.layout.item_movie, parent, false)
29 | return MovieItemViewHolder(rootView)
30 | }
31 |
32 | override fun onBindViewHolder(holder: MovieItemViewHolder, position: Int) {
33 | holder.onBind(movieList[holder.adapterPosition])
34 | }
35 |
36 | override fun getItemCount(): Int {
37 | return movieList.size
38 | }
39 |
40 | fun setListMovieData(data: List?) {
41 | calculateDiff(data.orEmpty())
42 | }
43 |
44 | private fun calculateDiff(newData: List) {
45 | diffCallback.setList(movieList, newData)
46 | val result = DiffUtil.calculateDiff(diffCallback)
47 | with(movieList) {
48 | clear()
49 | addAll(newData)
50 | }
51 | result.dispatchUpdatesTo(this)
52 | }
53 |
54 | inner class MovieItemViewHolder(private val curItemView: View) : RecyclerView.ViewHolder(curItemView) {
55 |
56 | fun onBind(data: MovieItem) {
57 | with(curItemView) {
58 | tvMovieTitle.text = data.title
59 | tvMovieVoteAverage.text = curItemView.context.getString(R.string.text_score, (data.voteAverage
60 | ?: 0).toString())
61 | tvMovieReleaseDate.text = curItemView.context.getString(R.string.text_release, data.releaseDate.convertDate())
62 | ivMovieImage.loadImageWithUrl(data.backdropPath)
63 | }
64 |
65 | curItemView.setOnClickListener {
66 | if (adapterPosition != RecyclerView.NO_POSITION) {
67 | data.id?.let {
68 | listener.invoke(it to data.title)
69 | }
70 | }
71 | }
72 | }
73 |
74 | }
75 | }
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_movie_detail_content.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
19 |
20 |
26 |
27 |
33 |
34 |
42 |
43 |
54 |
55 |
65 |
66 |
--------------------------------------------------------------------------------
/app/src/main/java/com/andro/indie/school/common/custom/IntentHelper.kt:
--------------------------------------------------------------------------------
1 | package com.andro.indie.school.common.custom
2 |
3 | import android.app.Activity
4 | import android.content.Context
5 | import android.content.Intent
6 | import android.os.Bundle
7 | import android.os.Parcelable
8 | import java.io.Serializable
9 |
10 | /**
11 | * Created by herisulistiyanto on 06/11/19.
12 | * KjokenKoddinger
13 | */
14 |
15 | object IntentHelper {
16 |
17 | @JvmStatic
18 | fun createIntent(ctx: Context, clazz: Class, params: Array>): Intent {
19 | val intent = Intent(ctx, clazz)
20 | if (params.isNotEmpty()) fillIntentArguments(intent, params)
21 | return intent
22 | }
23 |
24 | @JvmStatic
25 | fun fillIntentArguments(intent: Intent, params: Array>) {
26 | params.forEach {
27 | when (val value = it.second) {
28 | null -> intent.putExtra(it.first, null as Serializable?)
29 | is Int -> intent.putExtra(it.first, value)
30 | is Long -> intent.putExtra(it.first, value)
31 | is CharSequence -> intent.putExtra(it.first, value)
32 | is String -> intent.putExtra(it.first, value)
33 | is Float -> intent.putExtra(it.first, value)
34 | is Double -> intent.putExtra(it.first, value)
35 | is Char -> intent.putExtra(it.first, value)
36 | is Short -> intent.putExtra(it.first, value)
37 | is Boolean -> intent.putExtra(it.first, value)
38 | is Serializable -> intent.putExtra(it.first, value)
39 | is Bundle -> intent.putExtra(it.first, value)
40 | is Parcelable -> intent.putExtra(it.first, value)
41 | is Array<*> -> when {
42 | value.isArrayOf() -> intent.putExtra(it.first, value)
43 | value.isArrayOf() -> intent.putExtra(it.first, value)
44 | value.isArrayOf() -> intent.putExtra(it.first, value)
45 | else -> throw Exception("Intent extra ${it.first} has wrong type ${value.javaClass.name}")
46 | }
47 | is IntArray -> intent.putExtra(it.first, value)
48 | is LongArray -> intent.putExtra(it.first, value)
49 | is FloatArray -> intent.putExtra(it.first, value)
50 | is DoubleArray -> intent.putExtra(it.first, value)
51 | is CharArray -> intent.putExtra(it.first, value)
52 | is ShortArray -> intent.putExtra(it.first, value)
53 | is BooleanArray -> intent.putExtra(it.first, value)
54 | else -> throw Exception("Intent extra ${it.first} has wrong type ${value.javaClass.name}")
55 | }
56 | return@forEach
57 | }
58 | }
59 |
60 | @JvmStatic
61 | fun internalStartActivity(
62 | ctx: Context,
63 | activity: Class,
64 | params: Array>
65 | ) {
66 | ctx.startActivity(createIntent(ctx, activity, params))
67 | }
68 |
69 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/andro/indie/school/data/model/response/MovieDetailResponse.kt:
--------------------------------------------------------------------------------
1 | package com.andro.indie.school.data.model.response
2 | import com.google.gson.annotations.SerializedName
3 |
4 |
5 | /**
6 | * Created by herisulistiyanto on 06/11/19.
7 | * KjokenKoddinger
8 | */
9 |
10 | data class MovieDetailResponse(
11 | @SerializedName("adult")
12 | val adult: Boolean? = false,
13 | @SerializedName("backdrop_path")
14 | val backdropPath: String? = "",
15 | @SerializedName("belongs_to_collection")
16 | val belongsToCollection: Any? = Any(),
17 | @SerializedName("budget")
18 | val budget: Int? = 0,
19 | @SerializedName("genres")
20 | val genres: List? = listOf(),
21 | @SerializedName("homepage")
22 | val homepage: String? = "",
23 | @SerializedName("id")
24 | val id: Int? = 0,
25 | @SerializedName("imdb_id")
26 | val imdbId: String? = "",
27 | @SerializedName("original_language")
28 | val originalLanguage: String? = "",
29 | @SerializedName("original_title")
30 | val originalTitle: String? = "",
31 | @SerializedName("overview")
32 | val overview: String? = "",
33 | @SerializedName("popularity")
34 | val popularity: Double? = 0.0,
35 | @SerializedName("poster_path")
36 | val posterPath: String? = "",
37 | @SerializedName("production_companies")
38 | val productionCompanies: List? = listOf(),
39 | @SerializedName("production_countries")
40 | val productionCountries: List? = listOf(),
41 | @SerializedName("release_date")
42 | val releaseDate: String? = "",
43 | @SerializedName("revenue")
44 | val revenue: Int? = 0,
45 | @SerializedName("runtime")
46 | val runtime: Int? = 0,
47 | @SerializedName("spoken_languages")
48 | val spokenLanguages: List? = listOf(),
49 | @SerializedName("status")
50 | val status: String? = "",
51 | @SerializedName("tagline")
52 | val tagline: String? = "",
53 | @SerializedName("title")
54 | val title: String? = "",
55 | @SerializedName("video")
56 | val video: Boolean? = false,
57 | @SerializedName("vote_average")
58 | val voteAverage: Double? = 0.0,
59 | @SerializedName("vote_count")
60 | val voteCount: Int? = 0
61 | ) {
62 | data class Genre(
63 | @SerializedName("id")
64 | val id: Int? = 0,
65 | @SerializedName("name")
66 | val name: String? = ""
67 | )
68 |
69 | data class ProductionCompany(
70 | @SerializedName("id")
71 | val id: Int? = 0,
72 | @SerializedName("logo_path")
73 | val logoPath: String? = "",
74 | @SerializedName("name")
75 | val name: String? = "",
76 | @SerializedName("origin_country")
77 | val originCountry: String? = ""
78 | )
79 |
80 | data class ProductionCountry(
81 | @SerializedName("iso_3166_1")
82 | val iso31661: String? = "",
83 | @SerializedName("name")
84 | val name: String? = ""
85 | )
86 |
87 | data class SpokenLanguage(
88 | @SerializedName("iso_639_1")
89 | val iso6391: String? = "",
90 | @SerializedName("name")
91 | val name: String? = ""
92 | )
93 | }
--------------------------------------------------------------------------------
/app/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import org.jetbrains.kotlin.gradle.dsl.KotlinJvmOptions
2 | import java.util.Properties
3 |
4 | plugins {
5 | id("com.android.application")
6 | kotlin("android")
7 | kotlin("android.extensions")
8 | kotlin("kapt")
9 | }
10 |
11 | fun readProperties(propertiesFile: File) = Properties().apply {
12 | propertiesFile.inputStream().use { fis ->
13 | load(fis)
14 | }
15 | }
16 |
17 | fun loadProperties(fileName: String, act: (String, String) -> Unit) {
18 | val propFile = File(fileName)
19 | if (propFile.canRead()) {
20 | val data = readProperties(propFile)
21 | data.forEach { key, value ->
22 | act.invoke(key.toString().toUpperCase(), value.toString())
23 | }
24 | }
25 |
26 | }
27 |
28 | android {
29 | compileSdkVersion(29)
30 | buildToolsVersion("29.0.2")
31 | defaultConfig {
32 | applicationId = "com.andro.indie.school"
33 | minSdkVersion(19)
34 | targetSdkVersion(29)
35 | versionCode = 1
36 | versionName = "1.0"
37 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
38 |
39 | loadProperties("credential.properties") { key, value ->
40 | buildConfigField("String", key, "\"$value\"")
41 | }
42 |
43 | buildConfigField("String", "BASE_URL", "\"https://api.themoviedb.org\"")
44 | }
45 |
46 | buildTypes {
47 | getByName("debug") {
48 | isMinifyEnabled = false
49 | isDebuggable = true
50 | proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
51 | }
52 | getByName("release") {
53 | isMinifyEnabled = false
54 | proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
55 | }
56 | }
57 |
58 | compileOptions {
59 | sourceCompatibility = JavaVersion.VERSION_1_8
60 | targetCompatibility = JavaVersion.VERSION_1_8
61 | }
62 |
63 | kotlinOptions {
64 | val options = this as KotlinJvmOptions
65 | options.jvmTarget = "1.8"
66 | }
67 |
68 | }
69 |
70 | fun DependencyHandlerScope.execDeps(vararg deps: List>) {
71 | deps.forEach { collection ->
72 | collection.forEach {
73 | when (it.second) {
74 | is LibType.Library -> implementation(it.first)
75 | is LibType.Compiler -> kapt(it.first)
76 | is LibType.TestLib -> testImplementation(it.first)
77 | is LibType.AndroidTestLib -> androidTestImplementation(it.first)
78 | }
79 | }
80 | }
81 | }
82 |
83 | dependencies {
84 |
85 | implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar"))))
86 | implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.3.50")
87 |
88 | execDeps(
89 | Dependencies.androidLibs,
90 | Dependencies.lifecycleLibs,
91 | Dependencies.concurrentLibs,
92 | Dependencies.injectionLibs,
93 | Dependencies.networkingLib,
94 | Dependencies.persistenceLibs,
95 | Dependencies.imageLoaderLibs,
96 | Dependencies.workerLibs,
97 | Dependencies.debuggingLibs,
98 | Dependencies.commonThirdPartyLibs
99 | )
100 |
101 | testImplementation("junit:junit:4.12")
102 | androidTestImplementation("androidx.test.ext:junit:1.1.1")
103 | androidTestImplementation("androidx.test.espresso:espresso-core:3.2.0")
104 | }
105 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/item_movie.xml:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
14 |
15 |
26 |
27 |
35 |
36 |
44 |
45 |
55 |
56 |
67 |
68 |
75 |
76 |
85 |
86 |
87 |
88 |
--------------------------------------------------------------------------------
/app/src/main/java/com/andro/indie/school/ui/detail/MovieDetailActivity.kt:
--------------------------------------------------------------------------------
1 | package com.andro.indie.school.ui.detail
2 |
3 | import android.os.Bundle
4 | import android.view.MenuItem
5 | import androidx.lifecycle.lifecycleScope
6 | import com.andro.indie.school.R
7 | import com.andro.indie.school.common.base.BaseActivity
8 | import com.andro.indie.school.common.base.ResponseResult
9 | import com.andro.indie.school.common.extension.loadImageWithUrl
10 | import com.andro.indie.school.data.model.response.MovieDetailResponse
11 | import com.google.android.material.chip.Chip
12 | import kotlinx.android.synthetic.main.activity_movie_detail.*
13 | import kotlinx.android.synthetic.main.activity_movie_detail_content.*
14 | import kotlinx.android.synthetic.main.layout_common_error.*
15 | import kotlinx.coroutines.launch
16 | import org.koin.androidx.viewmodel.ext.android.viewModel
17 |
18 | /**
19 | * Created by herisulistiyanto on 06/11/19.
20 | * KjokenKoddinger
21 | */
22 |
23 | class MovieDetailActivity : BaseActivity() {
24 |
25 | private val movieDetailViewModel by viewModel()
26 |
27 | private val movieId by lazy(LazyThreadSafetyMode.NONE) {
28 | intent?.getIntExtra(ExtraKey.EXTRA_MOVIE_ID, 0) ?: 0
29 | }
30 |
31 | private val movieTitle by lazy(LazyThreadSafetyMode.NONE) {
32 | intent?.getStringExtra(ExtraKey.EXTRA_MOVIE_TITLE)
33 | }
34 |
35 | object ExtraKey {
36 | const val EXTRA_MOVIE_ID = "MovieDetailActivity.EXTRA_MOVIE_ID"
37 | const val EXTRA_MOVIE_TITLE = "MovieDetailActivity.EXTRA_MOVIE_TITLE"
38 | }
39 |
40 | object MovieDetailStateView {
41 | const val STATE_LOADING = 0
42 | const val STATE_CONTENT = STATE_LOADING + 1
43 | const val STATE_ERROR = STATE_CONTENT + 1
44 | }
45 |
46 | override fun onViewReady(savedInstanceState: Bundle?) {
47 | setContentView(R.layout.activity_movie_detail)
48 | setupToolbar(movieTitle)
49 |
50 | movieDetailViewModel.fetchDetailMovie(movieId).onResult {
51 | vfMovieDetail.displayedChild = when (it) {
52 | is ResponseResult.Success -> {
53 | it.result.data?.let { response ->
54 | populateDetailDisplay(response)
55 | }
56 | MovieDetailStateView.STATE_CONTENT
57 | }
58 | is ResponseResult.Error -> {
59 | tvCommonError.text = it.msg.errorMsg.orEmpty()
60 | MovieDetailStateView.STATE_ERROR
61 | }
62 | is ResponseResult.Loading -> MovieDetailStateView.STATE_LOADING
63 | }
64 | }
65 | }
66 |
67 | override fun loadModules() {
68 | MovieDetailModule.loadModules()
69 | }
70 |
71 | private fun setupToolbar(movieTitle: String?) {
72 | supportActionBar?.apply {
73 | setDisplayHomeAsUpEnabled(true)
74 | setDisplayShowTitleEnabled(true)
75 | title = movieTitle ?: getString(R.string.text_toolbar_title_movie_detail)
76 | }
77 | }
78 |
79 | private fun populateDetailDisplay(response: MovieDetailResponse) {
80 | ivMovieDetail.loadImageWithUrl(response.backdropPath)
81 | tvDetailOverview.text = response.overview.orEmpty()
82 |
83 | val genres = response.genres.orEmpty()
84 | lifecycleScope.launch {
85 | populateGenre(genres)
86 | }
87 | }
88 |
89 | private fun populateGenre(listGenre: List) {
90 | listGenre.forEach {
91 | val chip = Chip(this@MovieDetailActivity).apply {
92 | text = it.name.orEmpty()
93 | isClickable = false
94 | }
95 | chipGroupGenre.addView(chip)
96 | }
97 | }
98 |
99 | override fun onOptionsItemSelected(item: MenuItem): Boolean {
100 | return when (item.itemId) {
101 | android.R.id.home -> {
102 | onBackPressed()
103 | true
104 | }
105 | else -> super.onOptionsItemSelected(item)
106 | }
107 | }
108 | }
--------------------------------------------------------------------------------
/app/src/main/res/layout/layout_item_movie_load.xml:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
15 |
16 |
25 |
26 |
34 |
35 |
44 |
45 |
53 |
54 |
55 |
56 |
66 |
67 |
75 |
76 |
86 |
87 |
95 |
96 |
97 |
98 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/layout_movie_detail_load.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
11 |
12 |
20 |
21 |
27 |
28 |
34 |
35 |
43 |
44 |
52 |
53 |
61 |
62 |
71 |
72 |
81 |
82 |
91 |
92 |
100 |
101 |
102 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/app/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 |
--------------------------------------------------------------------------------