From 917fdc2ac329e85405f1fc081d49a299a54fb232 Mon Sep 17 00:00:00 2001 From: Mohamed Ebrahim Date: Thu, 18 Nov 2021 14:45:22 +0400 Subject: [PATCH 1/4] add support library --- app/build.gradle | 26 +++++++++++++++++++------- dependencies.gradle | 8 ++++++-- 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 371cb7d..59aaa1c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -8,16 +8,25 @@ plugins { apply from: "$rootDir/dependencies.gradle" android { - compileSdk 31 - + compileSdkVersion versions.compileSdk defaultConfig { applicationId "com.namshi.customer" - minSdk 21 - targetSdk 31 - versionCode 1 - versionName "1.0" - + minSdkVersion versions.minSdk + targetSdkVersion versions.compileSdk + versionCode versions.versionCode + versionName versions.versionName + vectorDrawables.useSupportLibrary = true testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + javaCompileOptions { + annotationProcessorOptions { + arguments += ["room.schemaLocation" : "$projectDir/schemas".toString()] + } + } + javaCompileOptions { + annotationProcessorOptions { + arguments["dagger.hilt.disableModulesHaveInstallInCheck"] = "true" + } + } } buildTypes { @@ -64,6 +73,9 @@ android { } dependencies { + // android supports + implementation "com.google.android.material:material:$versions.materialVersion" + implementation "androidx.constraintlayout:constraintlayout:$versions.constraintVersion" // architecture components implementation "androidx.fragment:fragment-ktx:$versions.fragmentVersion" diff --git a/dependencies.gradle b/dependencies.gradle index 7362ef0..f41e97b 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -1,8 +1,8 @@ ext.versions = [ minSdk : 21, compileSdk : 31, - versionCode : 6, - versionName : '1.0.5', + versionCode : 1, + versionName : '1.0.0', // gradle plugins gradleBuildTool: '7.0.2', @@ -13,6 +13,10 @@ ext.versions = [ // kotlin kotlin : '1.4.32', + // support library + materialVersion : '1.2.0-alpha06', + constraintVersion : '2.0.4', + // binding bindablesVersion : '1.0.9', From d9c2f58af5c9deae6099faa0ec17dfd6476bc819 Mon Sep 17 00:00:00 2001 From: Mohamed Ebrahim Date: Thu, 18 Nov 2021 14:53:06 +0400 Subject: [PATCH 2/4] add NetworkModule and HttpRequestInterceptor --- app/build.gradle | 10 +++++++ .../com/namshi/customer/di/NetworkModule.kt | 28 +++++++++++++++++++ .../network/HttpRequestInterceptor.kt | 20 +++++++++++++ dependencies.gradle | 8 ++++++ 4 files changed, 66 insertions(+) create mode 100644 app/src/main/java/com/namshi/customer/di/NetworkModule.kt create mode 100644 app/src/main/java/com/namshi/customer/network/HttpRequestInterceptor.kt diff --git a/app/build.gradle b/app/build.gradle index 59aaa1c..f8942b1 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -100,6 +100,16 @@ dependencies { androidTestImplementation "com.google.dagger:hilt-android-testing:$versions.hiltCoreVersion" kaptAndroidTest "com.google.dagger:hilt-compiler:$versions.hiltCoreVersion" + // network + implementation "com.github.skydoves:sandwich:$versions.sandwichVersion" + implementation "com.squareup.retrofit2:retrofit:$versions.retrofitVersion" + implementation "com.squareup.retrofit2:converter-moshi:$versions.retrofitVersion" + implementation "com.squareup.okhttp3:logging-interceptor:$versions.okhttpVersion" + testImplementation "com.squareup.okhttp3:mockwebserver:$versions.okhttpVersion" + + // debugging + implementation "com.jakewharton.timber:timber:$versions.timberVersion" + implementation 'androidx.core:core-ktx:1.7.0' implementation 'androidx.appcompat:appcompat:1.3.1' diff --git a/app/src/main/java/com/namshi/customer/di/NetworkModule.kt b/app/src/main/java/com/namshi/customer/di/NetworkModule.kt new file mode 100644 index 0000000..84eb647 --- /dev/null +++ b/app/src/main/java/com/namshi/customer/di/NetworkModule.kt @@ -0,0 +1,28 @@ +package com.namshi.customer.di + +import com.namshi.customer.network.HttpRequestInterceptor +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import okhttp3.OkHttpClient +import javax.inject.Singleton + + +/** + * Created by @mohamedebrahim96 on 18,November,2021 + * ShopiniWorld, Inc + * ebrahimm131@gmail.com + */ +@Module +@InstallIn(SingletonComponent::class) +object NetworkModule { + + @Provides + @Singleton + fun provideOkHttpClient(): OkHttpClient { + return OkHttpClient.Builder() + .addInterceptor(HttpRequestInterceptor()) + .build() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/namshi/customer/network/HttpRequestInterceptor.kt b/app/src/main/java/com/namshi/customer/network/HttpRequestInterceptor.kt new file mode 100644 index 0000000..4eb835e --- /dev/null +++ b/app/src/main/java/com/namshi/customer/network/HttpRequestInterceptor.kt @@ -0,0 +1,20 @@ +package com.namshi.customer.network + +import okhttp3.Interceptor +import okhttp3.Response +import timber.log.Timber + + +/** + * Created by @mohamedebrahim96 on 18,November,2021 + * ShopiniWorld, Inc + * ebrahimm131@gmail.com + */ +class HttpRequestInterceptor : Interceptor { + override fun intercept(chain: Interceptor.Chain): Response { + val originalRequest = chain.request() + val request = originalRequest.newBuilder().url(originalRequest.url).build() + Timber.d(request.toString()) + return chain.proceed(request) + } +} \ No newline at end of file diff --git a/dependencies.gradle b/dependencies.gradle index f41e97b..dd5d4ab 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -29,4 +29,12 @@ ext.versions = [ lifecycleVersion : '2.2.0', roomVersion : '2.4.0-alpha04', archCompomentVersion : '2.1.0', + + // network + retrofitVersion : '2.9.0', + okhttpVersion : '4.9.0', + sandwichVersion : '1.2.1', + + // debugging + timberVersion : '5.0.0', ] \ No newline at end of file From d9c23d88c3e8db3f6ae838acc48aa7e2b850bce2 Mon Sep 17 00:00:00 2001 From: Mohamed Ebrahim Date: Thu, 18 Nov 2021 17:03:50 +0400 Subject: [PATCH 3/4] add main viewmodel --- .idea/misc.xml | 2 + .../com/namshi/customer/di/NetworkModule.kt | 30 +++++++++ .../namshi/customer/model/NamshiResponse.kt | 28 ++++++++ .../namshi/customer/network/NamshiClient.kt | 19 ++++++ .../namshi/customer/network/NamshiService.kt | 18 +++++ .../namshi/customer/ui/main/MainActivity.kt | 12 +++- .../namshi/customer/ui/main/MainViewModel.kt | 36 +++++++++- .../customer/ui/main/adapter/HomeAdapter.kt | 65 +++++++++++++++++++ .../com/namshi/customer/utils/Constants.kt | 13 ++++ app/src/main/res/layout/activity_main.xml | 51 +++++++++++++-- app/src/main/res/layout/item_home.xml | 38 +++++++++++ app/src/main/res/layout/toolbar_home.xml | 17 +++++ app/src/main/res/values/colors.xml | 2 + 13 files changed, 324 insertions(+), 7 deletions(-) create mode 100644 app/src/main/java/com/namshi/customer/model/NamshiResponse.kt create mode 100644 app/src/main/java/com/namshi/customer/network/NamshiClient.kt create mode 100644 app/src/main/java/com/namshi/customer/network/NamshiService.kt create mode 100644 app/src/main/java/com/namshi/customer/ui/main/adapter/HomeAdapter.kt create mode 100644 app/src/main/java/com/namshi/customer/utils/Constants.kt create mode 100644 app/src/main/res/layout/item_home.xml create mode 100644 app/src/main/res/layout/toolbar_home.xml diff --git a/.idea/misc.xml b/.idea/misc.xml index dac35dc..cd8b250 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -6,6 +6,8 @@ + + diff --git a/app/src/main/java/com/namshi/customer/di/NetworkModule.kt b/app/src/main/java/com/namshi/customer/di/NetworkModule.kt index 84eb647..f9424ee 100644 --- a/app/src/main/java/com/namshi/customer/di/NetworkModule.kt +++ b/app/src/main/java/com/namshi/customer/di/NetworkModule.kt @@ -1,11 +1,17 @@ package com.namshi.customer.di import com.namshi.customer.network.HttpRequestInterceptor +import com.namshi.customer.network.NamshiClient +import com.namshi.customer.network.NamshiService +import com.namshi.customer.utils.Constants +import com.skydoves.sandwich.coroutines.CoroutinesResponseCallAdapterFactory import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import okhttp3.OkHttpClient +import retrofit2.Retrofit +import retrofit2.converter.moshi.MoshiConverterFactory import javax.inject.Singleton @@ -25,4 +31,28 @@ object NetworkModule { .addInterceptor(HttpRequestInterceptor()) .build() } + + @Provides + @Singleton + fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit { + return Retrofit.Builder() + .client(okHttpClient) + .baseUrl(Constants.BASE_URL) + .addConverterFactory(MoshiConverterFactory.create()) + .addCallAdapterFactory(CoroutinesResponseCallAdapterFactory.create()) + .build() + } + + @Provides + @Singleton + fun provideNamshiService(retrofit: Retrofit): NamshiService { + return retrofit.create(NamshiService::class.java) + } + + @Provides + @Singleton + fun provideNamshiClient(namshiService: NamshiService): NamshiClient { + return NamshiClient(namshiService) + } + } \ No newline at end of file diff --git a/app/src/main/java/com/namshi/customer/model/NamshiResponse.kt b/app/src/main/java/com/namshi/customer/model/NamshiResponse.kt new file mode 100644 index 0000000..ecf7ceb --- /dev/null +++ b/app/src/main/java/com/namshi/customer/model/NamshiResponse.kt @@ -0,0 +1,28 @@ +package com.namshi.customer.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + + +/** + * Created by @mohamedebrahim96 on 18,November,2021 + * ShopiniWorld, Inc + * ebrahimm131@gmail.com + */ +@JsonClass(generateAdapter = true) +data class NamshiResponse( + @field:Json(name = "content") val content: List +) { + data class Content( + @field:Json(name = "type") var type: String, + @field:Json(name = "cols") var cols: Int, + @field:Json(name = "images") var images: List + ) { + data class Images( + @field:Json(name = "url") var url: String, + @field:Json(name = "width") var width: Int, + @field:Json(name = "height") var height: Int, + @field:Json(name = "format") var format: String + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/namshi/customer/network/NamshiClient.kt b/app/src/main/java/com/namshi/customer/network/NamshiClient.kt new file mode 100644 index 0000000..6cb3f76 --- /dev/null +++ b/app/src/main/java/com/namshi/customer/network/NamshiClient.kt @@ -0,0 +1,19 @@ +package com.namshi.customer.network + +import com.namshi.customer.model.NamshiResponse +import com.skydoves.sandwich.ApiResponse +import javax.inject.Inject + + +/** + * Created by @mohamedebrahim96 on 18,November,2021 + * ShopiniWorld, Inc + * ebrahimm131@gmail.com + */ +class NamshiClient @Inject constructor( + private val namshiService: NamshiService +) { + + suspend fun fetchHomeList(): ApiResponse = + namshiService.fetchHomeList() +} diff --git a/app/src/main/java/com/namshi/customer/network/NamshiService.kt b/app/src/main/java/com/namshi/customer/network/NamshiService.kt new file mode 100644 index 0000000..b0476de --- /dev/null +++ b/app/src/main/java/com/namshi/customer/network/NamshiService.kt @@ -0,0 +1,18 @@ +package com.namshi.customer.network + +import com.namshi.customer.model.NamshiResponse +import com.skydoves.sandwich.ApiResponse +import retrofit2.http.GET + + +/** + * Created by @mohamedebrahim96 on 18,November,2021 + * ShopiniWorld, Inc + * ebrahimm131@gmail.com + */ +interface NamshiService { + + @GET("content") + suspend fun fetchHomeList(): ApiResponse + +} diff --git a/app/src/main/java/com/namshi/customer/ui/main/MainActivity.kt b/app/src/main/java/com/namshi/customer/ui/main/MainActivity.kt index 45becc9..f86400e 100644 --- a/app/src/main/java/com/namshi/customer/ui/main/MainActivity.kt +++ b/app/src/main/java/com/namshi/customer/ui/main/MainActivity.kt @@ -1,15 +1,25 @@ package com.namshi.customer.ui.main import android.os.Bundle +import androidx.activity.viewModels +import androidx.annotation.VisibleForTesting import com.namshi.customer.R import com.namshi.customer.databinding.ActivityMainBinding +import com.namshi.customer.ui.main.adapter.HomeAdapter import com.skydoves.bindables.BindingActivity import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint class MainActivity : BindingActivity(R.layout.activity_main) { + + @get:VisibleForTesting + internal val viewModel: MainViewModel by viewModels() + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.activity_main) + binding { + adapter = HomeAdapter() + vm = viewModel + } } } \ No newline at end of file diff --git a/app/src/main/java/com/namshi/customer/ui/main/MainViewModel.kt b/app/src/main/java/com/namshi/customer/ui/main/MainViewModel.kt index 166a079..17d0c02 100644 --- a/app/src/main/java/com/namshi/customer/ui/main/MainViewModel.kt +++ b/app/src/main/java/com/namshi/customer/ui/main/MainViewModel.kt @@ -1,7 +1,15 @@ package com.namshi.customer.ui.main +import androidx.databinding.Bindable +import androidx.lifecycle.viewModelScope +import com.namshi.customer.model.NamshiResponse +import com.namshi.customer.repository.MainRepository import com.skydoves.bindables.BindingViewModel +import com.skydoves.bindables.bindingProperty import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.flatMapLatest +import timber.log.Timber import javax.inject.Inject @@ -11,7 +19,33 @@ import javax.inject.Inject * ebrahimm131@gmail.com */ @HiltViewModel -class MainViewModel @Inject constructor() : BindingViewModel() { +class MainViewModel @Inject constructor( + private val mainRepository: MainRepository +) : BindingViewModel() { + @get:Bindable + var isLoading: Boolean by bindingProperty(false) + private set + + @get:Bindable + var toastMessage: String? by bindingProperty(null) + private set + + private val homeFetchingIndex: MutableStateFlow = MutableStateFlow(0) + private val homeListFlow = homeFetchingIndex.flatMapLatest { page -> + mainRepository.fetchHomeList( + page = page, + onStart = { isLoading = true }, + onComplete = { isLoading = false }, + onError = { toastMessage = it } + ) + } + + @get:Bindable + val homeList: List by homeListFlow.asBindingProperty(viewModelScope, emptyList()) + + init { + Timber.d("init MainViewModel") + } } \ No newline at end of file diff --git a/app/src/main/java/com/namshi/customer/ui/main/adapter/HomeAdapter.kt b/app/src/main/java/com/namshi/customer/ui/main/adapter/HomeAdapter.kt new file mode 100644 index 0000000..d5bd0f8 --- /dev/null +++ b/app/src/main/java/com/namshi/customer/ui/main/adapter/HomeAdapter.kt @@ -0,0 +1,65 @@ +package com.namshi.customer.ui.main.adapter + + +/** + * Created by @mohamedebrahim96 on 18,November,2021 + * ShopiniWorld, Inc + * ebrahimm131@gmail.com + */ + +import android.os.SystemClock +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.RecyclerView.NO_POSITION +import com.namshi.customer.R +import com.skydoves.bindables.BindingListAdapter +import com.skydoves.bindables.binding +import com.skydoves.pokedex.R +import com.skydoves.pokedex.databinding.ItemPokemonBinding +import com.skydoves.pokedex.model.Pokemon +import com.skydoves.pokedex.ui.details.DetailActivity + +class HomeAdapter : BindingListAdapter(diffUtil) { + + private var onClickedAt = 0L + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PokemonViewHolder = + parent.binding(R.layout.item_pokemon).let(::PokemonViewHolder) + + override fun onBindViewHolder(holder: PokemonViewHolder, position: Int) = + holder.bindPokemon(getItem(position)) + + inner class PokemonViewHolder constructor( + private val binding: ItemPokemonBinding + ) : RecyclerView.ViewHolder(binding.root) { + + init { + binding.root.setOnClickListener { + val position = bindingAdapterPosition.takeIf { it != NO_POSITION } + ?: return@setOnClickListener + val currentClickedAt = SystemClock.elapsedRealtime() + if (currentClickedAt - onClickedAt > binding.transformationLayout.duration) { + DetailActivity.startActivity(binding.transformationLayout, getItem(position)) + onClickedAt = currentClickedAt + } + } + } + + fun bindPokemon(pokemon: Pokemon) { + binding.pokemon = pokemon + binding.executePendingBindings() + } + } + + companion object { + private val diffUtil = object : DiffUtil.ItemCallback() { + + override fun areItemsTheSame(oldItem: Pokemon, newItem: Pokemon): Boolean = + oldItem.name == newItem.name + + override fun areContentsTheSame(oldItem: Pokemon, newItem: Pokemon): Boolean = + oldItem == newItem + } + } +} diff --git a/app/src/main/java/com/namshi/customer/utils/Constants.kt b/app/src/main/java/com/namshi/customer/utils/Constants.kt new file mode 100644 index 0000000..81d48a8 --- /dev/null +++ b/app/src/main/java/com/namshi/customer/utils/Constants.kt @@ -0,0 +1,13 @@ +package com.namshi.customer.utils + + +/** + * Created by @mohamedebrahim96 on 18,November,2021 + * ShopiniWorld, Inc + * ebrahimm131@gmail.com + */ +object Constants { + + const val BASE_URL = "https://demo8082631.mockable.io/" + +} \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index b93f460..f721996 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -5,21 +5,62 @@ + + + + android:background="@color/grey" + android:clipToPadding="false"> + + + + + + + + - diff --git a/app/src/main/res/layout/item_home.xml b/app/src/main/res/layout/item_home.xml new file mode 100644 index 0000000..1ce2a1f --- /dev/null +++ b/app/src/main/res/layout/item_home.xml @@ -0,0 +1,38 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/toolbar_home.xml b/app/src/main/res/layout/toolbar_home.xml new file mode 100644 index 0000000..45bde19 --- /dev/null +++ b/app/src/main/res/layout/toolbar_home.xml @@ -0,0 +1,17 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index f8c6127..674c74a 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -7,4 +7,6 @@ #FF018786 #FF000000 #FFFFFFFF + #E8E8E8 + \ No newline at end of file From b693d27adc41cf8ba0171f795dc0809913a34326 Mon Sep 17 00:00:00 2001 From: Mohamed Ebrahim Date: Thu, 18 Nov 2021 19:14:22 +0400 Subject: [PATCH 4/4] add recylerview and the adapter --- .idea/misc.xml | 4 +- app/build.gradle | 13 +++ app/src/main/AndroidManifest.xml | 12 ++- .../customer/binding/RecyclerViewBinding.kt | 33 ++++++++ .../namshi/customer/binding/ViewBinding.kt | 49 +++++++++++ .../namshi/customer/di/DispatcherModule.kt | 26 ++++++ .../namshi/customer/di/RepositoryModule.kt | 29 +++++++ .../namshi/customer/model/NamshiResponse.kt | 17 ++-- .../customer/repository/MainRepository.kt | 37 +++++++++ .../namshi/customer/repository/Repository.kt | 9 +++ .../namshi/customer/ui/main/MainViewModel.kt | 7 +- .../customer/ui/main/adapter/HomeAdapter.kt | 55 ++++++------- app/src/main/res/layout/activity_main.xml | 2 +- app/src/main/res/layout/item_home.xml | 81 +++++++++++-------- app/src/main/res/values-night/themes.xml | 23 +++--- app/src/main/res/values/themes.xml | 23 +++--- dependencies.gradle | 15 +++- 17 files changed, 328 insertions(+), 107 deletions(-) create mode 100644 app/src/main/java/com/namshi/customer/binding/RecyclerViewBinding.kt create mode 100644 app/src/main/java/com/namshi/customer/binding/ViewBinding.kt create mode 100644 app/src/main/java/com/namshi/customer/di/DispatcherModule.kt create mode 100644 app/src/main/java/com/namshi/customer/di/RepositoryModule.kt create mode 100644 app/src/main/java/com/namshi/customer/repository/MainRepository.kt create mode 100644 app/src/main/java/com/namshi/customer/repository/Repository.kt diff --git a/.idea/misc.xml b/.idea/misc.xml index cd8b250..80af9f9 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -5,8 +5,8 @@ - - + + diff --git a/app/build.gradle b/app/build.gradle index f8942b1..7606cf8 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -107,9 +107,22 @@ dependencies { implementation "com.squareup.okhttp3:logging-interceptor:$versions.okhttpVersion" testImplementation "com.squareup.okhttp3:mockwebserver:$versions.okhttpVersion" + // moshi + implementation "com.squareup.moshi:moshi-kotlin:$versions.moshiVersion" + kapt "com.squareup.moshi:moshi-kotlin-codegen:$versions.moshiVersion" + // debugging implementation "com.jakewharton.timber:timber:$versions.timberVersion" + // whatIf + implementation "com.github.skydoves:whatif:$versions.whatIfVersion" + + + // glide + implementation "com.github.bumptech.glide:glide:$versions.glideVersion" + implementation "com.github.florent37:glidepalette:$versions.glidePaletteVersion" + kapt "com.github.bumptech.glide:compiler:$versions.glideVersion" + implementation 'androidx.core:core-ktx:1.7.0' implementation 'androidx.appcompat:appcompat:1.3.1' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a871116..16a97c3 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -3,6 +3,9 @@ xmlns:tools="http://schemas.android.com/tools" package="com.namshi.customer"> + + + + android:exported="true" + android:screenOrientation="portrait"> - + \ No newline at end of file diff --git a/app/src/main/java/com/namshi/customer/binding/RecyclerViewBinding.kt b/app/src/main/java/com/namshi/customer/binding/RecyclerViewBinding.kt new file mode 100644 index 0000000..48d9dff --- /dev/null +++ b/app/src/main/java/com/namshi/customer/binding/RecyclerViewBinding.kt @@ -0,0 +1,33 @@ +package com.namshi.customer.binding + +import androidx.databinding.BindingAdapter +import androidx.recyclerview.widget.RecyclerView +import com.skydoves.bindables.BindingListAdapter +import com.skydoves.whatif.whatIfNotNullAs + + +/** + * Created by @mohamedebrahim96 on 18,November,2021 + * ShopiniWorld, Inc + * ebrahimm131@gmail.com + */ +object RecyclerViewBinding { + + @JvmStatic + @BindingAdapter("adapter") + fun bindAdapter(view: RecyclerView, adapter: RecyclerView.Adapter<*>) { + view.adapter = adapter.apply { + stateRestorationPolicy = RecyclerView.Adapter.StateRestorationPolicy.PREVENT_WHEN_EMPTY + } + } + + @JvmStatic + @BindingAdapter("submitList") + fun bindSubmitList(view: RecyclerView, itemList: List?) { + view.adapter.whatIfNotNullAs> { adapter -> + adapter.submitList(itemList) + } + } + + +} \ No newline at end of file diff --git a/app/src/main/java/com/namshi/customer/binding/ViewBinding.kt b/app/src/main/java/com/namshi/customer/binding/ViewBinding.kt new file mode 100644 index 0000000..d02d881 --- /dev/null +++ b/app/src/main/java/com/namshi/customer/binding/ViewBinding.kt @@ -0,0 +1,49 @@ +package com.namshi.customer.binding + +import android.view.View +import android.widget.Toast +import androidx.appcompat.widget.AppCompatImageView +import androidx.databinding.BindingAdapter +import com.bumptech.glide.Glide +import com.github.florent37.glidepalette.BitmapPalette +import com.github.florent37.glidepalette.GlidePalette +import com.skydoves.whatif.whatIfNotNullOrEmpty + + +/** + * Created by @mohamedebrahim96 on 18,November,2021 + * ShopiniWorld, Inc + * ebrahimm131@gmail.com + */ +object ViewBinding { + + @JvmStatic + @BindingAdapter("toast") + fun bindToast(view: View, text: String?) { + text.whatIfNotNullOrEmpty { + Toast.makeText(view.context, it, Toast.LENGTH_SHORT).show() + } + } + + @JvmStatic + @BindingAdapter("loadImage") + fun bindLoadImage(view: AppCompatImageView, url: String) { + Glide.with(view.context) + .load(url) + .listener( + GlidePalette.with(url) + .use(BitmapPalette.Profile.MUTED_LIGHT) + .crossfade(true) + ).into(view) + } + + @JvmStatic + @BindingAdapter("gone") + fun bindGone(view: View, shouldBeGone: Boolean) { + view.visibility = if (shouldBeGone) { + View.GONE + } else { + View.VISIBLE + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/namshi/customer/di/DispatcherModule.kt b/app/src/main/java/com/namshi/customer/di/DispatcherModule.kt new file mode 100644 index 0000000..8a5173c --- /dev/null +++ b/app/src/main/java/com/namshi/customer/di/DispatcherModule.kt @@ -0,0 +1,26 @@ +package com.namshi.customer.di + +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import javax.inject.Singleton + + +/** + * Created by @mohamedebrahim96 on 18,November,2021 + * ShopiniWorld, Inc + * ebrahimm131@gmail.com + */ +@Module +@InstallIn(SingletonComponent::class) +object DispatcherModule { + + @Provides + @Singleton + fun provideIODispatcher(): CoroutineDispatcher { + return Dispatchers.IO + } +} diff --git a/app/src/main/java/com/namshi/customer/di/RepositoryModule.kt b/app/src/main/java/com/namshi/customer/di/RepositoryModule.kt new file mode 100644 index 0000000..0fd9095 --- /dev/null +++ b/app/src/main/java/com/namshi/customer/di/RepositoryModule.kt @@ -0,0 +1,29 @@ +package com.namshi.customer.di + +import com.namshi.customer.network.NamshiClient +import com.namshi.customer.repository.MainRepository +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ViewModelComponent +import dagger.hilt.android.scopes.ViewModelScoped +import kotlinx.coroutines.CoroutineDispatcher + + +/** + * Created by @mohamedebrahim96 on 18,November,2021 + * ShopiniWorld, Inc + * ebrahimm131@gmail.com + */ +@Module +@InstallIn(ViewModelComponent::class) +object RepositoryModule { + @Provides + @ViewModelScoped + fun provideMainRepository( + namshiClient: NamshiClient, + coroutineDispatcher: CoroutineDispatcher + ): MainRepository { + return MainRepository(namshiClient, coroutineDispatcher) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/namshi/customer/model/NamshiResponse.kt b/app/src/main/java/com/namshi/customer/model/NamshiResponse.kt index ecf7ceb..a33e15e 100644 --- a/app/src/main/java/com/namshi/customer/model/NamshiResponse.kt +++ b/app/src/main/java/com/namshi/customer/model/NamshiResponse.kt @@ -1,7 +1,9 @@ package com.namshi.customer.model +import android.os.Parcelable import com.squareup.moshi.Json import com.squareup.moshi.JsonClass +import kotlinx.parcelize.Parcelize /** @@ -9,20 +11,25 @@ import com.squareup.moshi.JsonClass * ShopiniWorld, Inc * ebrahimm131@gmail.com */ +@Parcelize @JsonClass(generateAdapter = true) data class NamshiResponse( @field:Json(name = "content") val content: List -) { +) : Parcelable { + @Parcelize + @JsonClass(generateAdapter = true) data class Content( @field:Json(name = "type") var type: String, - @field:Json(name = "cols") var cols: Int, - @field:Json(name = "images") var images: List - ) { + @field:Json(name = "cols") var cols: Int?, + @field:Json(name = "images") var images: List? + ) : Parcelable { + @Parcelize + @JsonClass(generateAdapter = true) data class Images( @field:Json(name = "url") var url: String, @field:Json(name = "width") var width: Int, @field:Json(name = "height") var height: Int, @field:Json(name = "format") var format: String - ) + ) : Parcelable } } \ No newline at end of file diff --git a/app/src/main/java/com/namshi/customer/repository/MainRepository.kt b/app/src/main/java/com/namshi/customer/repository/MainRepository.kt new file mode 100644 index 0000000..4d6ba83 --- /dev/null +++ b/app/src/main/java/com/namshi/customer/repository/MainRepository.kt @@ -0,0 +1,37 @@ +package com.namshi.customer.repository + +import androidx.annotation.WorkerThread +import com.namshi.customer.network.NamshiClient +import com.skydoves.sandwich.message +import com.skydoves.sandwich.onError +import com.skydoves.sandwich.onException +import com.skydoves.sandwich.suspendOnSuccess +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.onCompletion +import kotlinx.coroutines.flow.onStart +import javax.inject.Inject + +class MainRepository @Inject constructor( + private val namshiClient: NamshiClient, + private val ioDispatcher: CoroutineDispatcher +) : Repository { + + @WorkerThread + fun fetchHomeList( + onStart: () -> Unit, + onComplete: () -> Unit, + onError: (String?) -> Unit + ) = flow { + val response = namshiClient.fetchHomeList() + response.suspendOnSuccess { + emit(data.content) + } + .onError { + onError(message()) + } + .onException { onError(message) } + + }.onStart { onStart() }.onCompletion { onComplete() }.flowOn(ioDispatcher) +} diff --git a/app/src/main/java/com/namshi/customer/repository/Repository.kt b/app/src/main/java/com/namshi/customer/repository/Repository.kt new file mode 100644 index 0000000..81bf029 --- /dev/null +++ b/app/src/main/java/com/namshi/customer/repository/Repository.kt @@ -0,0 +1,9 @@ +package com.namshi.customer.repository + + +/** + * Created by @mohamedebrahim96 on 18,November,2021 + * ShopiniWorld, Inc + * ebrahimm131@gmail.com + */ +interface Repository \ No newline at end of file diff --git a/app/src/main/java/com/namshi/customer/ui/main/MainViewModel.kt b/app/src/main/java/com/namshi/customer/ui/main/MainViewModel.kt index 17d0c02..6751b32 100644 --- a/app/src/main/java/com/namshi/customer/ui/main/MainViewModel.kt +++ b/app/src/main/java/com/namshi/customer/ui/main/MainViewModel.kt @@ -5,6 +5,7 @@ import androidx.lifecycle.viewModelScope import com.namshi.customer.model.NamshiResponse import com.namshi.customer.repository.MainRepository import com.skydoves.bindables.BindingViewModel +import com.skydoves.bindables.asBindingProperty import com.skydoves.bindables.bindingProperty import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow @@ -34,7 +35,6 @@ class MainViewModel @Inject constructor( private val homeFetchingIndex: MutableStateFlow = MutableStateFlow(0) private val homeListFlow = homeFetchingIndex.flatMapLatest { page -> mainRepository.fetchHomeList( - page = page, onStart = { isLoading = true }, onComplete = { isLoading = false }, onError = { toastMessage = it } @@ -42,7 +42,10 @@ class MainViewModel @Inject constructor( } @get:Bindable - val homeList: List by homeListFlow.asBindingProperty(viewModelScope, emptyList()) + val homeList: List by homeListFlow.asBindingProperty( + viewModelScope, + emptyList() + ) init { Timber.d("init MainViewModel") diff --git a/app/src/main/java/com/namshi/customer/ui/main/adapter/HomeAdapter.kt b/app/src/main/java/com/namshi/customer/ui/main/adapter/HomeAdapter.kt index d5bd0f8..7df7cab 100644 --- a/app/src/main/java/com/namshi/customer/ui/main/adapter/HomeAdapter.kt +++ b/app/src/main/java/com/namshi/customer/ui/main/adapter/HomeAdapter.kt @@ -7,58 +7,51 @@ package com.namshi.customer.ui.main.adapter * ebrahimm131@gmail.com */ -import android.os.SystemClock import android.view.ViewGroup import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView -import androidx.recyclerview.widget.RecyclerView.NO_POSITION import com.namshi.customer.R +import com.namshi.customer.databinding.ItemHomeBinding +import com.namshi.customer.model.NamshiResponse import com.skydoves.bindables.BindingListAdapter import com.skydoves.bindables.binding -import com.skydoves.pokedex.R -import com.skydoves.pokedex.databinding.ItemPokemonBinding -import com.skydoves.pokedex.model.Pokemon -import com.skydoves.pokedex.ui.details.DetailActivity -class HomeAdapter : BindingListAdapter(diffUtil) { +class HomeAdapter : BindingListAdapter(diffUtil) { - private var onClickedAt = 0L - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PokemonViewHolder = - parent.binding(R.layout.item_pokemon).let(::PokemonViewHolder) + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder = + parent.binding(R.layout.item_home).let(::ViewHolder) - override fun onBindViewHolder(holder: PokemonViewHolder, position: Int) = - holder.bindPokemon(getItem(position)) + override fun onBindViewHolder(holder: ViewHolder, position: Int) = + holder.bindContent(getItem(position)) - inner class PokemonViewHolder constructor( - private val binding: ItemPokemonBinding + inner class ViewHolder constructor( + private val binding: ItemHomeBinding ) : RecyclerView.ViewHolder(binding.root) { init { - binding.root.setOnClickListener { - val position = bindingAdapterPosition.takeIf { it != NO_POSITION } - ?: return@setOnClickListener - val currentClickedAt = SystemClock.elapsedRealtime() - if (currentClickedAt - onClickedAt > binding.transformationLayout.duration) { - DetailActivity.startActivity(binding.transformationLayout, getItem(position)) - onClickedAt = currentClickedAt - } - } + } - fun bindPokemon(pokemon: Pokemon) { - binding.pokemon = pokemon + fun bindContent(content: NamshiResponse.Content) { + binding.content = content binding.executePendingBindings() } } companion object { - private val diffUtil = object : DiffUtil.ItemCallback() { - - override fun areItemsTheSame(oldItem: Pokemon, newItem: Pokemon): Boolean = - oldItem.name == newItem.name - - override fun areContentsTheSame(oldItem: Pokemon, newItem: Pokemon): Boolean = + private val diffUtil = object : DiffUtil.ItemCallback() { + + override fun areItemsTheSame( + oldItem: NamshiResponse.Content, + newItem: NamshiResponse.Content + ): Boolean = + oldItem.type == newItem.type + + override fun areContentsTheSame( + oldItem: NamshiResponse.Content, + newItem: NamshiResponse.Content + ): Boolean = oldItem == newItem } } diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index f721996..4913184 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -18,7 +18,7 @@ - - - - - + + + + + + + + - - \ No newline at end of file + android:foreground="?attr/selectableItemBackground" + tools:background="@color/white" + tools:ignore="UnusedAttribute"> + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml index 99914b1..f98ebbe 100644 --- a/app/src/main/res/values-night/themes.xml +++ b/app/src/main/res/values-night/themes.xml @@ -1,16 +1,11 @@ - - - \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index f6696ca..f98ebbe 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -1,16 +1,11 @@ - - - \ No newline at end of file diff --git a/dependencies.gradle b/dependencies.gradle index dd5d4ab..2060457 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -32,9 +32,20 @@ ext.versions = [ // network retrofitVersion : '2.9.0', - okhttpVersion : '4.9.0', + okhttpVersion : '4.9.1', sandwichVersion : '1.2.1', + // moshi + moshiVersion : '1.12.0', + + // glide + glideVersion : '4.12.0', + glidePaletteVersion : '2.1.2', + + // debugging - timberVersion : '5.0.0', + timberVersion : '5.0.1', + + // whatIf + whatIfVersion : '1.1.0', ] \ No newline at end of file