diff --git a/README.md b/README.md new file mode 100755 index 0000000..799783d --- /dev/null +++ b/README.md @@ -0,0 +1,78 @@ +# MVVM Clean Architecture with RxJava3+Coroutines Flow, Static Code Analysis, Dagger Hilt, Dynamic Features + +[![ktlint](https://img.shields.io/badge/code%20style-%E2%9D%A4-FF4081.svg)](https://ktlint.github.io/) +[![Kotlin Version](https://img.shields.io/badge/kotlin-1.4.0-blue.svg)](https://kotlinlang.org) +[![API](https://img.shields.io/badge/API-21%2B-brightgreen.svg?style=flat)](https://android-arsenal.com/api?level=21) +[![codecov](https://codecov.io/gh/andremion/Theatre/graph/badge.svg)](https://codecov.io/gh/andremion/Theatre) + + +## About + +Sample project that build with MVVM clean architure and various cool techs including RxJava3 and Coroutines Flow, Dynamic Feature modules as base of BottomNavigationView or ViewPager2, with both OfflineFirst and OfflineLast approaches as database Single Source of Truth and TDD. + +Unit tests are written with JUnit4, JUnit5, MockK, Truth, MockWebServer. + +| Flow | RxJava3 | Pagination | Favorites +| ------------------|-------------| -----|--------------| +| | | | | + + +## Overview +* Gradle Kotlin DSL is used for setting up gradle files with ```buildSrc``` folder and extensions. +* KtLint, Detekt, and Git Hooks is used for checking, and formatting code and validating code before commits. +* Dagger Hilt, Dynamic Feature Modules with Navigation Components, ViewModel, Retrofit, Room, RxJava, Coroutines libraries adn dependencies are set up. +* ```features``` and ```libraries``` folders are used to include android libraries and dynamic feature modules +* In core module dagger hilt dependencies and ```@EntryPoint``` is created +* Data module uses Retrofit and Room to provide Local and Remote data sources +* Repository provides offline and remote fetch function with mapping and local save, delete and fetch functions +* Domain module uses useCase classes to implment business logic to fetch and forward data +* ViewModel uses LiveData with data-binding to display LOADING, and ERROR or SUCCESS states. + +## Built With 🛠 + +Some of the popular libraries and MVVM clean architecture used with offline-first and offline-last with Room database and Retrofit as data source + +* [Kotlin](https://kotlinlang.org/) - First class and official programming language for Android development. + +* [Coroutines](https://kotlinlang.org/docs/reference/coroutines-overview.html) - Threads on steroids for Kotlin +* [Flow](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-flow/) - A cold asynchronous data stream that sequentially emits values and completes normally or with an exception. +* [RxJava3](https://github.com/ReactiveX/RxJava) - Newest version of famous reactive programming library for Java, and other languages +* [Android JetPack](https://developer.android.com/jetpack) - Collection of libraries that help you design robust, testable, and maintainable apps. + * [LiveData](https://developer.android.com/topic/libraries/architecture/livedata) - Data objects that notify views when the underlying database changes. + * [ViewModel](https://developer.android.com/topic/libraries/architecture/viewmodel) - Stores UI-related data that isn't destroyed on UI changes. + * [DataBinding](https://developer.android.com/topic/libraries/data-binding) - Generates a binding class for each XML layout file present in that module and allows you to more easily write code that interacts with views. + * [Navigation Components](https://developer.android.com/guide/navigation/navigation-getting-started) Navigate fragments as never easier before + * [Dynamic Feature Modules](https://developer.android.com/guide/playcore/dynamic-delivery) Dynamic modules for adding or removing based on preference +* [Material Components for Android](https://github.com/material-components/material-components-android) - Modular and customizable Material Design UI components for Android. +* [Dependency Injection](https://developer.android.com/training/dependency-injection) - + * [Hilt-Dagger](https://dagger.dev/hilt/) - Standard library to incorporate Dagger dependency injection into an Android application. + * [Hilt-ViewModel](https://developer.android.com/training/dependency-injection/hilt-jetpack) - DI for injecting `ViewModel`. +* [Retrofit](https://square.github.io/retrofit/) - A type-safe HTTP client for Android and Java. +* [Glide](https://github.com/bumptech/glide) - Image loading library. +* [Lottie](http://airbnb.io/lottie) - animation library + +* Architecture + * Clean Architecture + * MVVM + MVI + * Offline first/last with Room an Retrofit + * [Dynamic feature modules](https://developer.android.com/studio/projects/dynamic-delivery) +* Tests + * [Unit Tests](https://en.wikipedia.org/wiki/Unit_testing) ([JUnit5](https://junit.org/junit5/)) ([JUnit4](https://junit.org/junit4/)) + * [MockWebServer](https://github.com/square/okhttp/tree/master/mockwebserver) Mock server for testing Api requests with OkHttp and Retrofit + * [Mockk](https://mockk.io/) Mockking library for Kotlin + * [Truth](https://truth.dev) Assertion library +* Gradle + * [Gradle Kotlin DSL](https://docs.gradle.org/current/userguide/kotlin_dsl.html) + * Custom tasks + * Plugins ([Ktlint](https://github.com/JLLeitschuh/ktlint-gradle), [Detekt](https://github.com/arturbosch/detekt#with-gradle), [SafeArgs](https://developer.android.com/guide/navigation/navigation-pass-data#Safe-args)), [Git Hooks](https://githooks.com) + + +### Modularaization, Library and Feature Modules + +Uses both library modules and dynamic feature modules + +## Architecture + +Uses concepts of clean architecture + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 057036e..961cdeb 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,5 +1,5 @@ - PropertyFindAR + Property FindAR Home Favorites Notification diff --git a/features/account/src/main/res/layout/fragment_account.xml b/features/account/src/main/res/layout/fragment_account.xml index 572ed67..aecbe2f 100644 --- a/features/account/src/main/res/layout/fragment_account.xml +++ b/features/account/src/main/res/layout/fragment_account.xml @@ -13,23 +13,20 @@ android:text="ACCOUNT\n Under Construction" android:textSize="30dp" android:textStyle="bold" - app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintBottom_toTopOf="@+id/lavUnderConstruction" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent" - app:layout_constraintVertical_bias="0.25" /> + app:layout_constraintTop_toTopOf="parent" /> diff --git a/features/favorites/src/main/res/layout/fragment_favorites.xml b/features/favorites/src/main/res/layout/fragment_favorites.xml index 605e4fa..ce72408 100644 --- a/features/favorites/src/main/res/layout/fragment_favorites.xml +++ b/features/favorites/src/main/res/layout/fragment_favorites.xml @@ -1,6 +1,7 @@ + @@ -14,22 +15,20 @@ android:textSize="30dp" android:textStyle="bold" app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintBottom_toTopOf="@+id/lavUnderConstruction" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent" - app:layout_constraintVertical_bias="0.25" /> + app:layout_constraintTop_toTopOf="parent" /> diff --git a/features/home/src/main/java/com/smarttoolfactory/home/adapter/HomeViewPager2FragmentStateAdapter.kt b/features/home/src/main/java/com/smarttoolfactory/home/adapter/HomeViewPager2FragmentStateAdapter.kt index 31eeeec..fbe7651 100644 --- a/features/home/src/main/java/com/smarttoolfactory/home/adapter/HomeViewPager2FragmentStateAdapter.kt +++ b/features/home/src/main/java/com/smarttoolfactory/home/adapter/HomeViewPager2FragmentStateAdapter.kt @@ -23,18 +23,22 @@ class HomeViewPager2FragmentStateAdapter(fragmentManager: FragmentManager, lifec override fun createFragment(position: Int): Fragment { return when (position) { - - // Fragment with Flow 0 -> NavHostContainerFragment.createNavHostContainerFragment( R.layout.fragment_navhost_property_list_flow, R.id.nested_nav_host_fragment_property_list ) - // Fragment with Pagination - else -> NavHostContainerFragment.createNavHostContainerFragment( + // Fragment with RxJava3 + 1 -> NavHostContainerFragment.createNavHostContainerFragment( R.layout.fragment_navhost_property_list_rxjava3, R.id.nested_nav_host_fragment_property_list ) + + // Fragment with Flow + Pagination + else -> NavHostContainerFragment.createNavHostContainerFragment( + R.layout.fragment_navhost_property_list_paged, + R.id.nested_nav_host_fragment_property_list + ) } } } diff --git a/features/home/src/main/java/com/smarttoolfactory/home/adapter/LoadingAdapter.kt b/features/home/src/main/java/com/smarttoolfactory/home/adapter/LoadingAdapter.kt new file mode 100644 index 0000000..209dd93 --- /dev/null +++ b/features/home/src/main/java/com/smarttoolfactory/home/adapter/LoadingAdapter.kt @@ -0,0 +1,3 @@ +package com.smarttoolfactory.home.adapter + +class LoadingAdapter diff --git a/features/home/src/main/java/com/smarttoolfactory/home/adapter/PropertyListAdapter.kt b/features/home/src/main/java/com/smarttoolfactory/home/adapter/PropertyListAdapter.kt index 3326cd7..89b36f9 100644 --- a/features/home/src/main/java/com/smarttoolfactory/home/adapter/PropertyListAdapter.kt +++ b/features/home/src/main/java/com/smarttoolfactory/home/adapter/PropertyListAdapter.kt @@ -1,5 +1,6 @@ package com.smarttoolfactory.home.adapter +import android.graphics.Color import android.widget.ImageButton import androidx.annotation.LayoutRes import androidx.databinding.ViewDataBinding @@ -57,12 +58,15 @@ class PropertyItemListAdapter( onLikeButtonClick(this) // Set image source of like button + val likeImageButton = (likeButton as? ImageButton) val imageResource = if (isFavorite) { - R.drawable.ic_baseline_favorite_24 + likeImageButton?.setColorFilter(Color.rgb(244, 81, 30)) + R.drawable.ic_baseline_favorite_30 } else { - R.drawable.ic_baseline_favorite_border_24 + likeImageButton?.setColorFilter(Color.rgb(41, 182, 246)) + R.drawable.ic_baseline_favorite_border_30 } - (likeButton as? ImageButton)?.setImageResource(imageResource) + likeImageButton?.setImageResource(imageResource) } } } @@ -78,17 +82,11 @@ class PropertyItemListAdapter( */ class PropertyItemDiffCallback : DiffUtil.ItemCallback() { - override fun areItemsTheSame( - oldItem: PropertyItem, - newItem: PropertyItem - ): Boolean { - return oldItem === newItem + override fun areItemsTheSame(oldItem: PropertyItem, newItem: PropertyItem): Boolean { + return oldItem.id == newItem.id } - override fun areContentsTheSame( - oldItem: PropertyItem, - newItem: PropertyItem - ): Boolean { - return oldItem.id == newItem.id + override fun areContentsTheSame(oldItem: PropertyItem, newItem: PropertyItem): Boolean { + return oldItem == newItem } } diff --git a/features/home/src/main/java/com/smarttoolfactory/home/di/HomeComponent.kt b/features/home/src/main/java/com/smarttoolfactory/home/di/HomeComponent.kt index c18a031..f323e14 100644 --- a/features/home/src/main/java/com/smarttoolfactory/home/di/HomeComponent.kt +++ b/features/home/src/main/java/com/smarttoolfactory/home/di/HomeComponent.kt @@ -2,8 +2,9 @@ package com.smarttoolfactory.home.di import androidx.fragment.app.Fragment import com.smarttoolfactory.core.di.CoreModuleDependencies -import com.smarttoolfactory.home.propertylist.PropertyListFlowFragment -import com.smarttoolfactory.home.propertylist.PropertyListRxjava3Fragment +import com.smarttoolfactory.home.propertylist.flow.PropertyListFlowFragment +import com.smarttoolfactory.home.propertylist.paged.PagedPropertyListFragment +import com.smarttoolfactory.home.propertylist.rxjava.PropertyListRxjava3Fragment import dagger.BindsInstance import dagger.Component @@ -15,6 +16,7 @@ interface HomeComponent { fun inject(fragment: PropertyListFlowFragment) fun inject(fragment: PropertyListRxjava3Fragment) + fun inject(fragment: PagedPropertyListFragment) @Component.Factory interface Factory { diff --git a/features/home/src/main/java/com/smarttoolfactory/home/di/HomeModule.kt b/features/home/src/main/java/com/smarttoolfactory/home/di/HomeModule.kt index 5081a1e..da89f75 100644 --- a/features/home/src/main/java/com/smarttoolfactory/home/di/HomeModule.kt +++ b/features/home/src/main/java/com/smarttoolfactory/home/di/HomeModule.kt @@ -2,10 +2,12 @@ package com.smarttoolfactory.home.di import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModelProvider +import com.smarttoolfactory.home.propertylist.flow.PropertyListViewModelFlow +import com.smarttoolfactory.home.propertylist.paged.PagedPropertyListViewModel +import com.smarttoolfactory.home.propertylist.rxjava.PropertyListViewModelRxJava3 +import com.smarttoolfactory.home.viewmodel.PagedPropertyListViewModelFactory import com.smarttoolfactory.home.viewmodel.PropertyListFlowViewModelFactory import com.smarttoolfactory.home.viewmodel.PropertyListRxJava3ViewModelFactory -import com.smarttoolfactory.home.viewmodel.PropertyListViewModelFlow -import com.smarttoolfactory.home.viewmodel.PropertyListViewModelRxJava3 import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -18,6 +20,9 @@ import kotlinx.coroutines.SupervisorJob @Module class HomeModule { + /** + * Property ViewModel that uses Flow for data operation + */ @Provides fun providePropertyListViewModelFlow( fragment: Fragment, @@ -25,6 +30,19 @@ class HomeModule { ) = ViewModelProvider(fragment, factory).get(PropertyListViewModelFlow::class.java) + /** + * Property ViewModel that uses Flow for data operation with Pagaination + */ + @Provides + fun providePagedPropertyListViewModel( + fragment: Fragment, + factory: PagedPropertyListViewModelFactory + ) = + ViewModelProvider(fragment, factory).get(PagedPropertyListViewModel::class.java) + + /** + * Property ViewModel that uses Rxjava for data operations + */ @Provides fun providePropertyListViewModelRxJava3( fragment: Fragment, diff --git a/features/home/src/main/java/com/smarttoolfactory/home/viewmodel/AbstractPropertyListVM.kt b/features/home/src/main/java/com/smarttoolfactory/home/propertylist/AbstractPropertyListVM.kt similarity index 94% rename from features/home/src/main/java/com/smarttoolfactory/home/viewmodel/AbstractPropertyListVM.kt rename to features/home/src/main/java/com/smarttoolfactory/home/propertylist/AbstractPropertyListVM.kt index 7ef4617..ec20cbb 100644 --- a/features/home/src/main/java/com/smarttoolfactory/home/viewmodel/AbstractPropertyListVM.kt +++ b/features/home/src/main/java/com/smarttoolfactory/home/propertylist/AbstractPropertyListVM.kt @@ -1,4 +1,4 @@ -package com.smarttoolfactory.home.viewmodel +package com.smarttoolfactory.home.propertylist import androidx.lifecycle.LiveData import androidx.lifecycle.ViewModel diff --git a/features/home/src/main/java/com/smarttoolfactory/home/propertylist/PropertyListFlowFragment.kt b/features/home/src/main/java/com/smarttoolfactory/home/propertylist/flow/PropertyListFlowFragment.kt similarity index 96% rename from features/home/src/main/java/com/smarttoolfactory/home/propertylist/PropertyListFlowFragment.kt rename to features/home/src/main/java/com/smarttoolfactory/home/propertylist/flow/PropertyListFlowFragment.kt index 806403c..0b04fd3 100644 --- a/features/home/src/main/java/com/smarttoolfactory/home/propertylist/PropertyListFlowFragment.kt +++ b/features/home/src/main/java/com/smarttoolfactory/home/propertylist/flow/PropertyListFlowFragment.kt @@ -1,4 +1,4 @@ -package com.smarttoolfactory.home.propertylist +package com.smarttoolfactory.home.propertylist.flow import android.os.Bundle import androidx.core.os.bundleOf @@ -12,7 +12,6 @@ import com.smarttoolfactory.home.adapter.PropertyItemListAdapter import com.smarttoolfactory.home.databinding.FragmentPropertyListBinding import com.smarttoolfactory.home.di.DaggerHomeComponent import com.smarttoolfactory.home.viewmodel.HomeToolbarVM -import com.smarttoolfactory.home.viewmodel.PropertyListViewModelFlow import dagger.hilt.android.EntryPointAccessors import javax.inject.Inject diff --git a/features/home/src/main/java/com/smarttoolfactory/home/viewmodel/PropertyListViewModelFlow.kt b/features/home/src/main/java/com/smarttoolfactory/home/propertylist/flow/PropertyListViewModelFlow.kt similarity index 74% rename from features/home/src/main/java/com/smarttoolfactory/home/viewmodel/PropertyListViewModelFlow.kt rename to features/home/src/main/java/com/smarttoolfactory/home/propertylist/flow/PropertyListViewModelFlow.kt index 84ab745..59900aa 100644 --- a/features/home/src/main/java/com/smarttoolfactory/home/viewmodel/PropertyListViewModelFlow.kt +++ b/features/home/src/main/java/com/smarttoolfactory/home/propertylist/flow/PropertyListViewModelFlow.kt @@ -1,4 +1,4 @@ -package com.smarttoolfactory.home.viewmodel +package com.smarttoolfactory.home.propertylist.flow import androidx.hilt.lifecycle.ViewModelInject import androidx.lifecycle.LiveData @@ -10,7 +10,11 @@ import com.smarttoolfactory.core.viewstate.ViewState import com.smarttoolfactory.domain.ORDER_BY_NONE import com.smarttoolfactory.domain.model.PropertyItem import com.smarttoolfactory.domain.usecase.GetPropertiesUseCaseFlow +import com.smarttoolfactory.home.propertylist.AbstractPropertyListVM import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.flatMapConcat import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onStart @@ -34,17 +38,17 @@ class PropertyListViewModelFlow @ViewModelInject constructor( var orderKey = MutableLiveData().apply { value = _orderByKey } - init { - updateOrderByKey() - } - - fun updateOrderByKey() { - getPropertiesUseCase.getCurrentSortKey() + private fun getOrderByKey(): Flow { + return getPropertiesUseCase.getCurrentSortKey() .onEach { - _orderByKey = it - orderKey.value = _orderByKey + println("🍏 AbstractPropertyListVM init orderKey: $it") + _orderByKey = it ?: _orderByKey + orderKey.postValue(_orderByKey) + } + .catch { + orderKey.postValue(_orderByKey) + println("❌ AbstractPropertyListVM init error: $it") } - .launchIn(coroutineScope) } /** @@ -60,12 +64,18 @@ class PropertyListViewModelFlow @ViewModelInject constructor( */ override fun getPropertyList() { - getPropertiesUseCase.getPropertiesOfflineFirst(_orderByKey) + getOrderByKey() + .flatMapConcat { + getPropertiesUseCase + .getPropertiesOfflineFirst(_orderByKey) + } .convertToFlowViewState() .onStart { + println("🍏 FlowViewModel getPropertyList() START") _propertyViewState.value = ViewState(status = Status.LOADING) } .onEach { + println("🍎 FlowViewModel getPropertyList() RES: $it") _propertyViewState.value = it } .launchIn(coroutineScope) diff --git a/features/home/src/main/java/com/smarttoolfactory/home/propertylist/paged/PagedPropertyListFragment.kt b/features/home/src/main/java/com/smarttoolfactory/home/propertylist/paged/PagedPropertyListFragment.kt new file mode 100644 index 0000000..9d1b785 --- /dev/null +++ b/features/home/src/main/java/com/smarttoolfactory/home/propertylist/paged/PagedPropertyListFragment.kt @@ -0,0 +1,145 @@ +package com.smarttoolfactory.home.propertylist.paged + +import android.os.Bundle +import androidx.core.os.bundleOf +import androidx.fragment.app.activityViewModels +import androidx.recyclerview.widget.LinearLayoutManager +import com.smarttoolfactory.core.di.CoreModuleDependencies +import com.smarttoolfactory.core.ui.fragment.DynamicNavigationFragment +import com.smarttoolfactory.core.util.EndlessScrollListener +import com.smarttoolfactory.core.util.observe +import com.smarttoolfactory.home.R +import com.smarttoolfactory.home.adapter.PropertyItemListAdapter +import com.smarttoolfactory.home.databinding.FragmentPropertyListPagedBinding +import com.smarttoolfactory.home.di.DaggerHomeComponent +import com.smarttoolfactory.home.viewmodel.HomeToolbarVM +import dagger.hilt.android.EntryPointAccessors +import javax.inject.Inject + +class PagedPropertyListFragment : + DynamicNavigationFragment(), + EndlessScrollListener.ScrollToBottomListener { + + @Inject + lateinit var viewModel: PagedPropertyListViewModel + + lateinit var itemListAdapter: PropertyItemListAdapter + + /** + * Listener for listening scrolling to last item of RecyclerView + */ + private lateinit var endlessScrollListener: EndlessScrollListener + + /** + * ViewModel for setting sort filter on top menu and property list fragments + */ + private val toolbarVM by activityViewModels() + + override fun getLayoutRes(): Int = R.layout.fragment_property_list_paged + + override fun onCreate(savedInstanceState: Bundle?) { + initCoreDependentInjection() + super.onCreate(savedInstanceState) + viewModel.refreshPropertyList() + } + + override fun bindViews() { + dataBinding.viewModel = viewModel + + dataBinding.recyclerView.apply { + + // Set Layout manager + val linearLayoutManager = + LinearLayoutManager(activity, LinearLayoutManager.VERTICAL, false) + + this.layoutManager = linearLayoutManager + + // Set RecyclerViewAdapter + itemListAdapter = PropertyItemListAdapter( + R.layout.row_property, + viewModel::onClick, + viewModel::onLikeButtonClick + + ) + + // Set Adapter + this.adapter = itemListAdapter + + // Set RecyclerView layout manager, adapter, and scroll listener for infinite scrolling + endlessScrollListener = + EndlessScrollListener(linearLayoutManager, this@PagedPropertyListFragment) + this.addOnScrollListener(endlessScrollListener) + } + + val swipeRefreshLayout = dataBinding.swipeRefreshLayout + + swipeRefreshLayout.setOnRefreshListener { + swipeRefreshLayout.isRefreshing = false + viewModel.refreshPropertyList() + } + + subscribeViewModelSortChange() + + subscribeGoToDetailScreen() + } + + /** + * When sort key is fetched from database change the one belong to Toolbar + */ + private fun subscribeViewModelSortChange() { + viewLifecycleOwner.observe(viewModel.orderKey) { + toolbarVM.currentSortFilter = it + } + } + + private fun subscribeToolbarSortChange() { + + viewLifecycleOwner.observe(toolbarVM.queryBySort) { + it.getContentIfNotHandled()?.let { orderBy -> + viewModel.refreshPropertyList(orderBy) + toolbarVM.currentSortFilter = orderBy + } + } + } + + private fun subscribeGoToDetailScreen() { + + viewModel.goToDetailScreen.observe( + viewLifecycleOwner, + { + + it.getContentIfNotHandled()?.let { propertyItem -> + val bundle = bundleOf("property" to propertyItem) + } + } + ) + } + + private fun initCoreDependentInjection() { + + val coreModuleDependencies = EntryPointAccessors.fromApplication( + requireActivity().applicationContext, + CoreModuleDependencies::class.java + ) + + DaggerHomeComponent.factory().create( + dependentModule = coreModuleDependencies, + fragment = this + ) + .inject(this) + } + + override fun onResume() { + super.onResume() + subscribeToolbarSortChange() + } + + override fun onPause() { + super.onPause() + toolbarVM.queryBySort.removeObservers(viewLifecycleOwner) + } + + override fun onScrollToBottom() { + viewModel.getPropertyList() + } +} diff --git a/features/home/src/main/java/com/smarttoolfactory/home/propertylist/paged/PagedPropertyListViewModel.kt b/features/home/src/main/java/com/smarttoolfactory/home/propertylist/paged/PagedPropertyListViewModel.kt new file mode 100644 index 0000000..fd5a7cd --- /dev/null +++ b/features/home/src/main/java/com/smarttoolfactory/home/propertylist/paged/PagedPropertyListViewModel.kt @@ -0,0 +1,95 @@ +package com.smarttoolfactory.home.propertylist.paged + +import androidx.hilt.lifecycle.ViewModelInject +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import com.smarttoolfactory.core.util.Event +import com.smarttoolfactory.core.util.convertToFlowViewState +import com.smarttoolfactory.core.viewstate.Status +import com.smarttoolfactory.core.viewstate.ViewState +import com.smarttoolfactory.domain.ORDER_BY_NONE +import com.smarttoolfactory.domain.model.PropertyItem +import com.smarttoolfactory.domain.usecase.GetPropertiesUseCasePaged +import com.smarttoolfactory.home.propertylist.AbstractPropertyListVM +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.flatMapConcat +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.onStart + +class PagedPropertyListViewModel @ViewModelInject constructor( + private val coroutineScope: CoroutineScope, + private val getPropertiesUseCase: GetPropertiesUseCasePaged +) : AbstractPropertyListVM() { + + private val _goToDetailScreen = MutableLiveData>() + + override val goToDetailScreen: LiveData> + get() = _goToDetailScreen + + private val _propertyViewState = MutableLiveData>>() + + override val propertyListViewState: LiveData>> + get() = _propertyViewState + + private var _orderByKey = ORDER_BY_NONE + + var orderKey = MutableLiveData().apply { value = _orderByKey } + + private fun getOrderByKey(): Flow { + return getPropertiesUseCase.getCurrentSortKey() + .onEach { + println("🍏 AbstractPropertyListVM init orderKey: $it") + _orderByKey = it ?: _orderByKey + orderKey.postValue(_orderByKey) + } + .catch { + orderKey.postValue(_orderByKey) + println("❌ AbstractPropertyListVM init error: $it") + } + } + + override fun getPropertyList() { + + getOrderByKey() + .flatMapConcat { + println("🔥 refreshPropertyList: $it") + getPropertiesUseCase.getPagedOfflineLast(_orderByKey) + } + .convertToFlowViewState() + .onStart { + _propertyViewState.value = ViewState(status = Status.LOADING) + } + .onEach { + _propertyViewState.value = it + } + .launchIn(coroutineScope) + } + + override fun refreshPropertyList(orderBy: String?) { + + getOrderByKey() + .flatMapConcat { + println("🔥 refreshPropertyList: $it") + getPropertiesUseCase.refreshData(orderBy ?: _orderByKey) + } + .convertToFlowViewState() + .onStart { + _propertyViewState.value = ViewState(status = Status.LOADING) + } + .onEach { + _propertyViewState.value = it + } + .launchIn(coroutineScope) + } + + override fun onClick(item: PropertyItem) { + _goToDetailScreen.value = Event(item) + } + + fun onLikeButtonClick(item: PropertyItem) { + println("🔥 Like: $item") + } +} diff --git a/features/home/src/main/java/com/smarttoolfactory/home/propertylist/PropertyListRxjava3Fragment.kt b/features/home/src/main/java/com/smarttoolfactory/home/propertylist/rxjava/PropertyListRxjava3Fragment.kt similarity index 96% rename from features/home/src/main/java/com/smarttoolfactory/home/propertylist/PropertyListRxjava3Fragment.kt rename to features/home/src/main/java/com/smarttoolfactory/home/propertylist/rxjava/PropertyListRxjava3Fragment.kt index 83992a4..32dd303 100644 --- a/features/home/src/main/java/com/smarttoolfactory/home/propertylist/PropertyListRxjava3Fragment.kt +++ b/features/home/src/main/java/com/smarttoolfactory/home/propertylist/rxjava/PropertyListRxjava3Fragment.kt @@ -1,4 +1,4 @@ -package com.smarttoolfactory.home.propertylist +package com.smarttoolfactory.home.propertylist.rxjava import android.os.Bundle import androidx.core.os.bundleOf @@ -12,7 +12,6 @@ import com.smarttoolfactory.home.adapter.PropertyItemListAdapter import com.smarttoolfactory.home.databinding.FragmentPropertyListBinding import com.smarttoolfactory.home.di.DaggerHomeComponent import com.smarttoolfactory.home.viewmodel.HomeToolbarVM -import com.smarttoolfactory.home.viewmodel.PropertyListViewModelRxJava3 import dagger.hilt.android.EntryPointAccessors import javax.inject.Inject diff --git a/features/home/src/main/java/com/smarttoolfactory/home/viewmodel/PropertyListViewModelRxJava3.kt b/features/home/src/main/java/com/smarttoolfactory/home/propertylist/rxjava/PropertyListViewModelRxJava3.kt similarity index 80% rename from features/home/src/main/java/com/smarttoolfactory/home/viewmodel/PropertyListViewModelRxJava3.kt rename to features/home/src/main/java/com/smarttoolfactory/home/propertylist/rxjava/PropertyListViewModelRxJava3.kt index fa36400..9674206 100644 --- a/features/home/src/main/java/com/smarttoolfactory/home/viewmodel/PropertyListViewModelRxJava3.kt +++ b/features/home/src/main/java/com/smarttoolfactory/home/propertylist/rxjava/PropertyListViewModelRxJava3.kt @@ -1,4 +1,4 @@ -package com.smarttoolfactory.home.viewmodel +package com.smarttoolfactory.home.propertylist.rxjava import androidx.hilt.lifecycle.ViewModelInject import androidx.lifecycle.LiveData @@ -10,7 +10,9 @@ import com.smarttoolfactory.core.viewstate.ViewState import com.smarttoolfactory.domain.ORDER_BY_NONE import com.smarttoolfactory.domain.model.PropertyItem import com.smarttoolfactory.domain.usecase.GetPropertiesUseCaseRxJava3 +import com.smarttoolfactory.home.propertylist.AbstractPropertyListVM import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers +import io.reactivex.rxjava3.core.Single import io.reactivex.rxjava3.schedulers.Schedulers class PropertyListViewModelRxJava3 @ViewModelInject constructor( @@ -31,28 +33,25 @@ class PropertyListViewModelRxJava3 @ViewModelInject constructor( var orderKey = MutableLiveData().apply { value = _orderByKey } - init { - updateOrderByKey() - } - - private fun updateOrderByKey() { - getPropertiesUseCase.getCurrentSortKey() + private fun getOrderByKey(): Single { + return getPropertiesUseCase.getCurrentSortKey() .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - { - _orderByKey = it - - orderKey.value = _orderByKey - }, - { - println("PropertyListViewModelRxJava3 init error: $it") - } - ) + .doOnSuccess { + _orderByKey = it ?: _orderByKey + orderKey.postValue(_orderByKey) + } + .onErrorResumeNext { + Single.just(_orderByKey) + } } override fun getPropertyList() { - getPropertiesUseCase.getPropertiesOfflineFirst(_orderByKey) + + getOrderByKey() + .flatMap { + getPropertiesUseCase.getPropertiesOfflineFirst(_orderByKey) + } .convertFromSingleToObservableViewStateWithLoading() .observeOn(AndroidSchedulers.mainThread()) .subscribe( diff --git a/features/home/src/main/java/com/smarttoolfactory/home/viewbindings/ViewBindings.kt b/features/home/src/main/java/com/smarttoolfactory/home/viewbindings/ViewBindings.kt index 7fd2e25..74d6267 100755 --- a/features/home/src/main/java/com/smarttoolfactory/home/viewbindings/ViewBindings.kt +++ b/features/home/src/main/java/com/smarttoolfactory/home/viewbindings/ViewBindings.kt @@ -1,5 +1,6 @@ package com.smarttoolfactory.home.viewbindings +import android.graphics.Color import android.view.View import android.widget.ImageButton import android.widget.ImageView @@ -64,6 +65,12 @@ fun View.visibilityBasedOn(condition: Boolean) { @BindingAdapter("favoriteImageSrc") fun ImageButton.setFavoriteImageSrc(favorite: Boolean) { + if (favorite) { + setColorFilter(Color.rgb(244, 81, 30)) + } else { + setColorFilter(Color.rgb(41, 182, 246)) + } + val imageResource = if (favorite) R.drawable.ic_baseline_favorite_30 else R.drawable.ic_baseline_favorite_border_30 diff --git a/features/home/src/main/java/com/smarttoolfactory/home/viewmodel/ViewModelFactory.kt b/features/home/src/main/java/com/smarttoolfactory/home/viewmodel/ViewModelFactory.kt index 6f7f2cb..d5e6ca5 100644 --- a/features/home/src/main/java/com/smarttoolfactory/home/viewmodel/ViewModelFactory.kt +++ b/features/home/src/main/java/com/smarttoolfactory/home/viewmodel/ViewModelFactory.kt @@ -3,7 +3,11 @@ package com.smarttoolfactory.home.viewmodel import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import com.smarttoolfactory.domain.usecase.GetPropertiesUseCaseFlow +import com.smarttoolfactory.domain.usecase.GetPropertiesUseCasePaged import com.smarttoolfactory.domain.usecase.GetPropertiesUseCaseRxJava3 +import com.smarttoolfactory.home.propertylist.flow.PropertyListViewModelFlow +import com.smarttoolfactory.home.propertylist.paged.PagedPropertyListViewModel +import com.smarttoolfactory.home.propertylist.rxjava.PropertyListViewModelRxJava3 import javax.inject.Inject import kotlinx.coroutines.CoroutineScope @@ -26,6 +30,25 @@ class PropertyListFlowViewModelFactory @Inject constructor( } } +class PagedPropertyListViewModelFactory @Inject constructor( + private val coroutineScope: CoroutineScope, + private val getPropertiesUseCase: GetPropertiesUseCasePaged +) : ViewModelProvider.Factory { + + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T { + + if (modelClass != PagedPropertyListViewModel::class.java) { + throw IllegalArgumentException("Unknown ViewModel class") + } + + return PagedPropertyListViewModel( + coroutineScope, + getPropertiesUseCase + ) as T + } +} + class PropertyListRxJava3ViewModelFactory @Inject constructor( private val getPropertiesUseCase: GetPropertiesUseCaseRxJava3 ) : ViewModelProvider.Factory { diff --git a/features/home/src/main/res/layout/fragment_navhost_property_list_paged.xml b/features/home/src/main/res/layout/fragment_navhost_property_list_paged.xml new file mode 100644 index 0000000..a7cd307 --- /dev/null +++ b/features/home/src/main/res/layout/fragment_navhost_property_list_paged.xml @@ -0,0 +1,23 @@ + + + + + + + + + + \ No newline at end of file diff --git a/features/home/src/main/res/layout/fragment_property_list.xml b/features/home/src/main/res/layout/fragment_property_list.xml index 2199fc8..095f0a4 100644 --- a/features/home/src/main/res/layout/fragment_property_list.xml +++ b/features/home/src/main/res/layout/fragment_property_list.xml @@ -9,7 +9,7 @@ + type="com.smarttoolfactory.home.propertylist.AbstractPropertyListVM" /> + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/features/home/src/main/res/layout/row_property.xml b/features/home/src/main/res/layout/row_property.xml index 2710b0b..4a391c6 100644 --- a/features/home/src/main/res/layout/row_property.xml +++ b/features/home/src/main/res/layout/row_property.xml @@ -14,16 +14,15 @@ diff --git a/features/home/src/main/res/navigation/nav_graph_property_list_paged.xml b/features/home/src/main/res/navigation/nav_graph_property_list_paged.xml new file mode 100644 index 0000000..a5528db --- /dev/null +++ b/features/home/src/main/res/navigation/nav_graph_property_list_paged.xml @@ -0,0 +1,14 @@ + + + + + + \ No newline at end of file diff --git a/features/home/src/main/res/navigation/nav_graph_property_list_rxjava3.xml b/features/home/src/main/res/navigation/nav_graph_property_list_rxjava3.xml index 15b0d89..91287d4 100644 --- a/features/home/src/main/res/navigation/nav_graph_property_list_rxjava3.xml +++ b/features/home/src/main/res/navigation/nav_graph_property_list_rxjava3.xml @@ -7,7 +7,7 @@ diff --git a/features/home/src/test/java/com/smarttoolfactory/home/viewmodel/PropertyListViewModelFlowTest.kt b/features/home/src/test/java/com/smarttoolfactory/home/viewmodel/PropertyListViewModelFlowTest.kt index 39fef61..e49370e 100644 --- a/features/home/src/test/java/com/smarttoolfactory/home/viewmodel/PropertyListViewModelFlowTest.kt +++ b/features/home/src/test/java/com/smarttoolfactory/home/viewmodel/PropertyListViewModelFlowTest.kt @@ -3,8 +3,10 @@ package com.smarttoolfactory.home.viewmodel import androidx.arch.core.executor.testing.InstantTaskExecutorRule import com.google.common.truth.Truth import com.smarttoolfactory.core.viewstate.Status +import com.smarttoolfactory.domain.ORDER_BY_NONE import com.smarttoolfactory.domain.model.PropertyItem import com.smarttoolfactory.domain.usecase.GetPropertiesUseCaseFlow +import com.smarttoolfactory.home.propertylist.flow.PropertyListViewModelFlow import com.smarttoolfactory.test_utils.RESPONSE_JSON_PATH import com.smarttoolfactory.test_utils.rule.TestCoroutineRule import com.smarttoolfactory.test_utils.test_observer.test @@ -20,6 +22,9 @@ import org.junit.Before import org.junit.Rule import org.junit.Test +/** + * ❌ FIXME Either [LiveDataTestObserver] or Flow is bugged with tests, solve the issue + */ class PropertyListViewModelFlowTest { // Run tasks synchronously @@ -72,12 +77,20 @@ class PropertyListViewModelFlowTest { emit(throw Exception("Network Exception")) } + every { + useCase.getCurrentSortKey() + } returns flow { + emit((ORDER_BY_NONE)) + } + val testObserver = viewModel.propertyListViewState.test() // WHEN + viewModel.getPropertyList() // THEN + println("💀 THEN") testObserver .assertValue { states -> ( @@ -92,9 +105,6 @@ class PropertyListViewModelFlowTest { verify(atMost = 1) { useCase.getPropertiesOfflineFirst() } } - /** - * ❌ FIXME This test is flaky, find out the cause, sometimes null is returned - */ @Test fun `given useCase fetched data, should have ViewState SUCCESS and data offlineFirst`() = testCoroutineRule.runBlockingTest { @@ -104,14 +114,19 @@ class PropertyListViewModelFlowTest { emit(itemList) } + every { + useCase.getCurrentSortKey() + } returns flow { + emit((ORDER_BY_NONE)) + } + val testObserver = viewModel.propertyListViewState.test() // WHEN viewModel.getPropertyList() - advanceUntilIdle() - // THEN + println("💀 THEN") val viewStates = testObserver.values() Truth.assertThat(viewStates.first().status).isEqualTo(Status.LOADING) @@ -128,7 +143,7 @@ class PropertyListViewModelFlowTest { // GIVEN every { - useCase.getPropertiesOfflineLast() + useCase.getPropertiesOfflineLast(ORDER_BY_NONE) } returns flow> { emit(throw Exception("Network Exception")) } @@ -137,6 +152,7 @@ class PropertyListViewModelFlowTest { // WHEN viewModel.refreshPropertyList() + advanceUntilIdle() // THEN testObserver @@ -151,7 +167,7 @@ class PropertyListViewModelFlowTest { val finalState = testObserver.values()[1] Truth.assertThat(finalState.error?.message).isEqualTo("Network Exception") Truth.assertThat(finalState.error).isInstanceOf(Exception::class.java) - verify(atMost = 1) { useCase.getPropertiesOfflineLast() } + verify(atMost = 1) { useCase.getPropertiesOfflineLast(ORDER_BY_NONE) } } /** @@ -163,7 +179,7 @@ class PropertyListViewModelFlowTest { // GIVEN every { - useCase.getPropertiesOfflineLast() + useCase.getPropertiesOfflineLast(ORDER_BY_NONE) } returns flow { emit(itemList) } @@ -172,6 +188,7 @@ class PropertyListViewModelFlowTest { // WHEN viewModel.refreshPropertyList() + advanceUntilIdle() // THEN val viewStates = testObserver.values() @@ -179,7 +196,7 @@ class PropertyListViewModelFlowTest { val actual = viewStates.last().data Truth.assertThat(actual?.size).isEqualTo(itemList.size) - verify(exactly = 1) { useCase.getPropertiesOfflineLast() } + verify(exactly = 1) { useCase.getPropertiesOfflineLast(ORDER_BY_NONE) } testObserver.dispose() } diff --git a/features/home/src/test/java/com/smarttoolfactory/home/viewmodel/PropertyListViewModelRxJava3Test.kt b/features/home/src/test/java/com/smarttoolfactory/home/viewmodel/PropertyListViewModelRxJava3Test.kt index 483f198..16f414d 100644 --- a/features/home/src/test/java/com/smarttoolfactory/home/viewmodel/PropertyListViewModelRxJava3Test.kt +++ b/features/home/src/test/java/com/smarttoolfactory/home/viewmodel/PropertyListViewModelRxJava3Test.kt @@ -3,9 +3,10 @@ package com.smarttoolfactory.home.viewmodel import androidx.arch.core.executor.testing.InstantTaskExecutorRule import com.google.common.truth.Truth import com.smarttoolfactory.core.viewstate.Status +import com.smarttoolfactory.domain.ORDER_BY_NONE import com.smarttoolfactory.domain.model.PropertyItem import com.smarttoolfactory.domain.usecase.GetPropertiesUseCaseRxJava3 -import com.smarttoolfactory.home.viewmodel.AbstractPropertyListVM.Companion.ORDER_BY_NONE +import com.smarttoolfactory.home.propertylist.rxjava.PropertyListViewModelRxJava3 import com.smarttoolfactory.test_utils.RESPONSE_JSON_PATH import com.smarttoolfactory.test_utils.rule.RxImmediateSchedulerRule import com.smarttoolfactory.test_utils.test_observer.test @@ -71,6 +72,10 @@ class PropertyListViewModelRxJava3Test { useCase.getPropertiesOfflineFirst(ORDER_BY_NONE) } returns Single.error(Exception("Network Exception")) + every { + useCase.getCurrentSortKey() + } returns Single.just(ORDER_BY_NONE) + val testObserver = viewModel.propertyListViewState.test() // WHEN @@ -96,6 +101,9 @@ class PropertyListViewModelRxJava3Test { // GIVEN every { useCase.getPropertiesOfflineFirst(ORDER_BY_NONE) } returns Single.just(itemList) + every { + useCase.getCurrentSortKey() + } returns Single.just(ORDER_BY_NONE) val testObserver = viewModel.propertyListViewState.test() @@ -114,7 +122,9 @@ class PropertyListViewModelRxJava3Test { val finalState = testObserver.values()[1] val actual = finalState.data Truth.assertThat(actual?.size).isEqualTo(itemList.size) + verify(exactly = 1) { useCase.getPropertiesOfflineFirst(ORDER_BY_NONE) } + verify(exactly = 1) { useCase.getCurrentSortKey(ORDER_BY_NONE) } testObserver.dispose() } diff --git a/features/notification/src/main/res/layout/fragment_notification.xml b/features/notification/src/main/res/layout/fragment_notification.xml index 650549f..268c085 100644 --- a/features/notification/src/main/res/layout/fragment_notification.xml +++ b/features/notification/src/main/res/layout/fragment_notification.xml @@ -1,6 +1,7 @@ + @@ -14,22 +15,20 @@ android:textSize="30dp" android:textStyle="bold" app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintBottom_toTopOf="@+id/lavUnderConstruction" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent" - app:layout_constraintVertical_bias="0.25" /> + app:layout_constraintTop_toTopOf="parent" /> diff --git a/libraries/core/src/main/java/com/smarttoolfactory/core/di/CoreModuleDependencies.kt b/libraries/core/src/main/java/com/smarttoolfactory/core/di/CoreModuleDependencies.kt index 5f12a3a..e2d77d2 100644 --- a/libraries/core/src/main/java/com/smarttoolfactory/core/di/CoreModuleDependencies.kt +++ b/libraries/core/src/main/java/com/smarttoolfactory/core/di/CoreModuleDependencies.kt @@ -1,13 +1,14 @@ package com.smarttoolfactory.core.di import com.smarttoolfactory.domain.usecase.GetPropertiesUseCaseFlow +import com.smarttoolfactory.domain.usecase.GetPropertiesUseCasePaged import com.smarttoolfactory.domain.usecase.GetPropertiesUseCaseRxJava3 import dagger.hilt.EntryPoint import dagger.hilt.InstallIn import dagger.hilt.android.components.ApplicationComponent /** - * This component is required for adding component to Dynamic Feature Module dependencies + * This component is required for adding dependencies to Dy */ @EntryPoint @InstallIn(ApplicationComponent::class) @@ -18,4 +19,5 @@ interface CoreModuleDependencies { */ fun getPropertiesUseCaseFlow(): GetPropertiesUseCaseFlow fun getPropertiesUseCaseRxJava3(): GetPropertiesUseCaseRxJava3 + fun getPropertiesUseCasePaged(): GetPropertiesUseCasePaged } diff --git a/libraries/core/src/main/java/com/smarttoolfactory/core/di/DataModule.kt b/libraries/core/src/main/java/com/smarttoolfactory/core/di/DataModule.kt index e697b93..a973749 100644 --- a/libraries/core/src/main/java/com/smarttoolfactory/core/di/DataModule.kt +++ b/libraries/core/src/main/java/com/smarttoolfactory/core/di/DataModule.kt @@ -3,11 +3,15 @@ package com.smarttoolfactory.core.di import com.smarttoolfactory.data.di.DatabaseModule import com.smarttoolfactory.data.di.NetworkModule import com.smarttoolfactory.data.mapper.PropertyDTOtoEntityListMapper +import com.smarttoolfactory.data.repository.PagedPropertyRepository +import com.smarttoolfactory.data.repository.PagedPropertyRepositoryImpl import com.smarttoolfactory.data.repository.PropertyRepositoryCoroutines import com.smarttoolfactory.data.repository.PropertyRepositoryImlRxJava3 import com.smarttoolfactory.data.repository.PropertyRepositoryImplCoroutines import com.smarttoolfactory.data.repository.PropertyRepositoryRxJava3 import com.smarttoolfactory.data.source.LocalDataSourceRxJava3Impl +import com.smarttoolfactory.data.source.LocalPagedPropertyDataSource +import com.smarttoolfactory.data.source.LocalPagedPropertySourceImpl import com.smarttoolfactory.data.source.LocalPropertyDataSourceCoroutines import com.smarttoolfactory.data.source.LocalPropertyDataSourceImpl import com.smarttoolfactory.data.source.LocalPropertyDataSourceRxJava3 @@ -43,6 +47,21 @@ interface DataModule { @Binds fun bindRepositoryCoroutines(repository: PropertyRepositoryImplCoroutines): PropertyRepositoryCoroutines + + /* + Coroutines + Pagination + */ + + @Singleton + @Binds + fun bindPagedLocalDataSource(localDataSource: LocalPagedPropertySourceImpl): + LocalPagedPropertyDataSource + + @Singleton + @Binds + fun bindPagedRepository(repository: PagedPropertyRepositoryImpl): + PagedPropertyRepository + /* RxJava */ diff --git a/libraries/core/src/main/java/com/smarttoolfactory/core/util/EndlessScrollListener.kt b/libraries/core/src/main/java/com/smarttoolfactory/core/util/EndlessScrollListener.kt new file mode 100644 index 0000000..c241977 --- /dev/null +++ b/libraries/core/src/main/java/com/smarttoolfactory/core/util/EndlessScrollListener.kt @@ -0,0 +1,46 @@ +package com.smarttoolfactory.core.util + +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView + +class EndlessScrollListener( + private val linearLayoutManager: LinearLayoutManager, + private val listener: ScrollToBottomListener +) : RecyclerView.OnScrollListener() { + + private var previousTotal = 0 + private var loading = true + private val visibleThreshold = 8 + private var firstVisibleItem = 0 + private var visibleItemCount = 0 + private var totalItemCount = 0 + + fun onRefresh() { + previousTotal = 0 + } + + override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { + super.onScrolled(recyclerView, dx, dy) + + visibleItemCount = recyclerView.childCount + totalItemCount = linearLayoutManager.itemCount + firstVisibleItem = linearLayoutManager.findFirstVisibleItemPosition() + + if (loading) { + if (totalItemCount > previousTotal) { + loading = false + previousTotal = totalItemCount + } + } + if (!loading && totalItemCount - visibleItemCount + <= firstVisibleItem + visibleThreshold + ) { + listener.onScrollToBottom() + loading = true + } + } + + interface ScrollToBottomListener { + fun onScrollToBottom() + } +} diff --git a/libraries/data/schemas/com.smarttoolfactory.data.db.PropertyDatabase/3.json b/libraries/data/schemas/com.smarttoolfactory.data.db.PropertyDatabase/3.json new file mode 100644 index 0000000..2eb8412 --- /dev/null +++ b/libraries/data/schemas/com.smarttoolfactory.data.db.PropertyDatabase/3.json @@ -0,0 +1,597 @@ +{ + "formatVersion": 1, + "database": { + "version": 3, + "identityHash": "4e4855a4623740f1eebc51d968f31cb3", + "entities": [ + { + "tableName": "property", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`insert_order` INTEGER NOT NULL, `id` INTEGER NOT NULL, `update` INTEGER NOT NULL, `category_id` INTEGER NOT NULL, `title` TEXT NOT NULL, `subject` TEXT NOT NULL, `type` TEXT NOT NULL, `type_id` INTEGER NOT NULL, `thumbnail` TEXT, `thumbnail_big` TEXT, `image_count` INTEGER NOT NULL, `price` TEXT NOT NULL, `price_period` TEXT, `price_period_raw` TEXT NOT NULL, `price_label` TEXT, `price_value` TEXT, `price_value_raw` INTEGER NOT NULL, `currency` TEXT NOT NULL, `featured` INTEGER NOT NULL, `location` TEXT NOT NULL, `area` TEXT NOT NULL, `poa` INTEGER NOT NULL, `rera_permit` TEXT, `bathrooms` TEXT NOT NULL, `bedrooms` TEXT NOT NULL, `date_insert` TEXT NOT NULL, `date_update` TEXT NOT NULL, `agent_name` TEXT NOT NULL, `broker_name` TEXT NOT NULL, `agent_license` TEXT, `location_id` INTEGER NOT NULL, `hide_location` INTEGER NOT NULL, `broker` TEXT NOT NULL, `amenities` TEXT NOT NULL, `amenities_keys` TEXT NOT NULL, `latitude` REAL NOT NULL, `longitude` REAL NOT NULL, `premium` INTEGER NOT NULL, `livingrooms` TEXT NOT NULL, `verified` INTEGER NOT NULL, `gallery` TEXT, `phone` TEXT NOT NULL, `lead_email_receivers` TEXT NOT NULL, `reference` TEXT NOT NULL, PRIMARY KEY(`insert_order`))", + "fields": [ + { + "fieldPath": "insertOrder", + "columnName": "insert_order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "update", + "columnName": "update", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "categoryId", + "columnName": "category_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "typeId", + "columnName": "type_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "thumbnail", + "columnName": "thumbnail", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "thumbnailBig", + "columnName": "thumbnail_big", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "imageCount", + "columnName": "image_count", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "price", + "columnName": "price", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pricePeriod", + "columnName": "price_period", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pricePeriodRaw", + "columnName": "price_period_raw", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "priceLabel", + "columnName": "price_label", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "priceValue", + "columnName": "price_value", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "priceValueRaw", + "columnName": "price_value_raw", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "currency", + "columnName": "currency", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "featured", + "columnName": "featured", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "location", + "columnName": "location", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "area", + "columnName": "area", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "poa", + "columnName": "poa", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reraPermit", + "columnName": "rera_permit", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bathrooms", + "columnName": "bathrooms", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bedrooms", + "columnName": "bedrooms", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "dateInsert", + "columnName": "date_insert", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "dateUpdate", + "columnName": "date_update", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "agentName", + "columnName": "agent_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "brokerName", + "columnName": "broker_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "agentLicense", + "columnName": "agent_license", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "locationId", + "columnName": "location_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hideLocation", + "columnName": "hide_location", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "broker", + "columnName": "broker", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "amenities", + "columnName": "amenities", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "amenitiesKeys", + "columnName": "amenities_keys", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "latitude", + "columnName": "latitude", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "longitude", + "columnName": "longitude", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "premium", + "columnName": "premium", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "livingrooms", + "columnName": "livingrooms", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "verified", + "columnName": "verified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "gallery", + "columnName": "gallery", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "phone", + "columnName": "phone", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "leadEmailReceivers", + "columnName": "lead_email_receivers", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "reference", + "columnName": "reference", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "insert_order" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "paged_property", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`insert_order` INTEGER NOT NULL, `id` INTEGER NOT NULL, `update` INTEGER NOT NULL, `category_id` INTEGER NOT NULL, `title` TEXT NOT NULL, `subject` TEXT NOT NULL, `type` TEXT NOT NULL, `type_id` INTEGER NOT NULL, `thumbnail` TEXT, `thumbnail_big` TEXT, `image_count` INTEGER NOT NULL, `price` TEXT NOT NULL, `price_period` TEXT, `price_period_raw` TEXT NOT NULL, `price_label` TEXT, `price_value` TEXT, `price_value_raw` INTEGER NOT NULL, `currency` TEXT NOT NULL, `featured` INTEGER NOT NULL, `location` TEXT NOT NULL, `area` TEXT NOT NULL, `poa` INTEGER NOT NULL, `rera_permit` TEXT, `bathrooms` TEXT NOT NULL, `bedrooms` TEXT NOT NULL, `date_insert` TEXT NOT NULL, `date_update` TEXT NOT NULL, `agent_name` TEXT NOT NULL, `broker_name` TEXT NOT NULL, `agent_license` TEXT, `location_id` INTEGER NOT NULL, `hide_location` INTEGER NOT NULL, `broker` TEXT NOT NULL, `amenities` TEXT NOT NULL, `amenities_keys` TEXT NOT NULL, `latitude` REAL NOT NULL, `longitude` REAL NOT NULL, `premium` INTEGER NOT NULL, `livingrooms` TEXT NOT NULL, `verified` INTEGER NOT NULL, `gallery` TEXT, `phone` TEXT NOT NULL, `lead_email_receivers` TEXT NOT NULL, `reference` TEXT NOT NULL, PRIMARY KEY(`insert_order`))", + "fields": [ + { + "fieldPath": "insertOrder", + "columnName": "insert_order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "update", + "columnName": "update", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "categoryId", + "columnName": "category_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "typeId", + "columnName": "type_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "thumbnail", + "columnName": "thumbnail", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "thumbnailBig", + "columnName": "thumbnail_big", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "imageCount", + "columnName": "image_count", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "price", + "columnName": "price", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pricePeriod", + "columnName": "price_period", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pricePeriodRaw", + "columnName": "price_period_raw", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "priceLabel", + "columnName": "price_label", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "priceValue", + "columnName": "price_value", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "priceValueRaw", + "columnName": "price_value_raw", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "currency", + "columnName": "currency", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "featured", + "columnName": "featured", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "location", + "columnName": "location", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "area", + "columnName": "area", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "poa", + "columnName": "poa", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reraPermit", + "columnName": "rera_permit", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bathrooms", + "columnName": "bathrooms", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bedrooms", + "columnName": "bedrooms", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "dateInsert", + "columnName": "date_insert", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "dateUpdate", + "columnName": "date_update", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "agentName", + "columnName": "agent_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "brokerName", + "columnName": "broker_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "agentLicense", + "columnName": "agent_license", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "locationId", + "columnName": "location_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hideLocation", + "columnName": "hide_location", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "broker", + "columnName": "broker", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "amenities", + "columnName": "amenities", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "amenitiesKeys", + "columnName": "amenities_keys", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "latitude", + "columnName": "latitude", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "longitude", + "columnName": "longitude", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "premium", + "columnName": "premium", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "livingrooms", + "columnName": "livingrooms", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "verified", + "columnName": "verified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "gallery", + "columnName": "gallery", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "phone", + "columnName": "phone", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "leadEmailReceivers", + "columnName": "lead_email_receivers", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "reference", + "columnName": "reference", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "insert_order" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "sort_order", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `order_by` TEXT NOT NULL DEFAULT '', PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "orderBy", + "columnName": "order_by", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '4e4855a4623740f1eebc51d968f31cb3')" + ] + } +} \ No newline at end of file diff --git a/libraries/data/src/main/java/com/smarttoolfactory/data/constant/Constants.kt b/libraries/data/src/main/java/com/smarttoolfactory/data/constant/Constants.kt index b4f1d33..3485236 100644 --- a/libraries/data/src/main/java/com/smarttoolfactory/data/constant/Constants.kt +++ b/libraries/data/src/main/java/com/smarttoolfactory/data/constant/Constants.kt @@ -19,4 +19,4 @@ const val ORDER_BY_DES_DESCENDING = "bd" DBConstants */ const val DATABASE_NAME = "property.db" -const val DATABASE_VERSION = 2 +const val DATABASE_VERSION = 3 diff --git a/libraries/data/src/main/java/com/smarttoolfactory/data/db/PagedPropertyDao.kt b/libraries/data/src/main/java/com/smarttoolfactory/data/db/PagedPropertyDao.kt new file mode 100755 index 0000000..65a36d7 --- /dev/null +++ b/libraries/data/src/main/java/com/smarttoolfactory/data/db/PagedPropertyDao.kt @@ -0,0 +1,38 @@ +package com.smarttoolfactory.data.db + +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import com.smarttoolfactory.data.model.local.PagedPropertyEntity + +@Dao +interface PagedPropertyDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(entity: PagedPropertyEntity): Long + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(entities: List): List + + @Delete + suspend fun deletePagedPropertyEntity(entity: PagedPropertyEntity): Int + + @Query("DELETE FROM paged_property") + suspend fun deleteAll() + + /** + * Get number of properties in db + */ + @Query("SELECT COUNT(*) FROM paged_property") + suspend fun getPropertyCount(): Int + + /** + * Get properties from database. + * + * *If database is empty returns empty list [] + */ + @Query("SELECT * FROM paged_property") + suspend fun getPropertyList(): List +} diff --git a/libraries/data/src/main/java/com/smarttoolfactory/data/db/PropertyDatabase.kt b/libraries/data/src/main/java/com/smarttoolfactory/data/db/PropertyDatabase.kt index 154d905..40eafcf 100644 --- a/libraries/data/src/main/java/com/smarttoolfactory/data/db/PropertyDatabase.kt +++ b/libraries/data/src/main/java/com/smarttoolfactory/data/db/PropertyDatabase.kt @@ -6,11 +6,16 @@ import androidx.room.TypeConverters import androidx.room.migration.Migration import androidx.sqlite.db.SupportSQLiteDatabase import com.smarttoolfactory.data.constant.DATABASE_VERSION +import com.smarttoolfactory.data.model.local.PagedPropertyEntity import com.smarttoolfactory.data.model.local.PropertyEntity import com.smarttoolfactory.data.model.local.SortOrderEntity @Database( - entities = [PropertyEntity::class, SortOrderEntity::class], + entities = [ + PropertyEntity::class, + PagedPropertyEntity::class, + SortOrderEntity::class + ], version = DATABASE_VERSION, exportSchema = true ) @@ -24,6 +29,8 @@ abstract class PropertyDatabase : RoomDatabase() { abstract fun propertySortDaoCoroutines(): SortOrderDaoCoroutines abstract fun propertySortDaoRxJava(): SortOrderDaoRxJava3 + + abstract fun pagedPropertyDao(): PagedPropertyDao } /** @@ -40,3 +47,61 @@ val MIGRATION_1_2: Migration = object : Migration(1, 2) { ) } } + +/** + * Add new Property table for paging, this could have been done with [PropertyEntity] but + * used this as another sample. + */ +val MIGRATION_2_3: Migration = object : Migration(2, 3) { + + override fun migrate(database: SupportSQLiteDatabase) { + + database.execSQL( + "CREATE TABLE IF NOT EXISTS `paged_property` (" + + "`insert_order` INTEGER NOT NULL, " + + "`id` INTEGER NOT NULL, " + + "`update` INTEGER NOT NULL, " + + "`category_id` INTEGER NOT NULL, " + + "`title` TEXT NOT NULL, " + + "`subject` TEXT NOT NULL, " + + "`type` TEXT NOT NULL, " + + "`type_id` INTEGER NOT NULL, " + + "`thumbnail` TEXT, " + + "`thumbnail_big` TEXT, " + + "`image_count` INTEGER NOT NULL, " + + "`price` TEXT NOT NULL, " + + "`price_period` TEXT, " + + "`price_period_raw` TEXT NOT NULL, " + + "`price_label` TEXT, " + + "`price_value` TEXT, " + + "`price_value_raw` INTEGER NOT NULL, " + + "`currency` TEXT NOT NULL, " + + "`featured` INTEGER NOT NULL, " + + "`location` TEXT NOT NULL, " + + "`area` TEXT NOT NULL, " + + "`poa` INTEGER NOT NULL, " + + "`rera_permit` TEXT, " + + "`bathrooms` TEXT NOT NULL, " + + "`bedrooms` TEXT NOT NULL, " + + "`date_insert` TEXT NOT NULL, " + + "`date_update` TEXT NOT NULL, " + + "`agent_name` TEXT NOT NULL, " + + "`broker_name` TEXT NOT NULL, " + + "`agent_license` TEXT, " + + "`location_id` INTEGER NOT NULL, " + + "`hide_location` INTEGER NOT NULL, " + + "`broker` TEXT NOT NULL, " + + "`amenities` TEXT NOT NULL, " + + "`amenities_keys` TEXT NOT NULL, " + + "`latitude` REAL NOT NULL, " + + "`longitude` REAL NOT NULL, " + + "`premium` INTEGER NOT NULL, " + + "`livingrooms` TEXT NOT NULL, " + + "`verified` INTEGER NOT NULL, " + + "`gallery` TEXT, " + + "`phone` TEXT NOT NULL, " + + "`lead_email_receivers` TEXT NOT NULL, " + + "`reference` TEXT NOT NULL, PRIMARY KEY(`insert_order`))" + ) + } +} diff --git a/libraries/data/src/main/java/com/smarttoolfactory/data/di/DatabaseModule.kt b/libraries/data/src/main/java/com/smarttoolfactory/data/di/DatabaseModule.kt index c260655..38ecaa3 100644 --- a/libraries/data/src/main/java/com/smarttoolfactory/data/di/DatabaseModule.kt +++ b/libraries/data/src/main/java/com/smarttoolfactory/data/di/DatabaseModule.kt @@ -4,6 +4,8 @@ import android.app.Application import androidx.room.Room import com.smarttoolfactory.data.constant.DATABASE_NAME import com.smarttoolfactory.data.db.MIGRATION_1_2 +import com.smarttoolfactory.data.db.MIGRATION_2_3 +import com.smarttoolfactory.data.db.PagedPropertyDao import com.smarttoolfactory.data.db.PropertyDaoCoroutines import com.smarttoolfactory.data.db.PropertyDaoRxJava3 import com.smarttoolfactory.data.db.PropertyDatabase @@ -27,7 +29,7 @@ class DatabaseModule { PropertyDatabase::class.java, DATABASE_NAME ) - .addMigrations(MIGRATION_1_2) + .addMigrations(MIGRATION_1_2, MIGRATION_2_3) .build() } @@ -50,4 +52,9 @@ class DatabaseModule { @Provides fun provideSortOrderDaoRxJava3(appDatabase: PropertyDatabase): SortOrderDaoRxJava3 = appDatabase.propertySortDaoRxJava() + + @Singleton + @Provides + fun providePagedPropertyDao(appDatabase: PropertyDatabase): PagedPropertyDao = + appDatabase.pagedPropertyDao() } diff --git a/libraries/data/src/main/java/com/smarttoolfactory/data/mapper/MappingFactory.kt b/libraries/data/src/main/java/com/smarttoolfactory/data/mapper/MappingFactory.kt index 6424d2f..345cc6e 100755 --- a/libraries/data/src/main/java/com/smarttoolfactory/data/mapper/MappingFactory.kt +++ b/libraries/data/src/main/java/com/smarttoolfactory/data/mapper/MappingFactory.kt @@ -3,6 +3,7 @@ package com.smarttoolfactory.data.mapper import com.smarttoolfactory.data.model.IEntity import com.smarttoolfactory.data.model.Mappable import com.smarttoolfactory.data.model.local.BrokerEntity +import com.smarttoolfactory.data.model.local.PagedPropertyEntity import com.smarttoolfactory.data.model.local.PropertyEntity import com.smarttoolfactory.data.model.remote.BrokerDTO import com.smarttoolfactory.data.model.remote.PropertyDTO @@ -110,6 +111,70 @@ class PropertyDTOtoEntityListMapper @Inject constructor() : } } +class PropertyDTOtoPagedEntityListMapper @Inject constructor() : + ListMapper { + + override fun map(input: List): List { + + return input.map { input -> + PagedPropertyEntity( + id = input.id, + update = input.update, + categoryId = input.categoryId, + title = input.title, + subject = input.subject, + type = input.type, + typeId = input.typeId, + thumbnail = input.thumbnail, + thumbnailBig = input.thumbnailBig, + imageCount = input.imageCount, + price = input.price, + pricePeriod = input.pricePeriod, + pricePeriodRaw = input.pricePeriodRaw, + priceLabel = input.priceLabel, + priceValue = input.priceValue, + priceValueRaw = input.priceValueRaw, + currency = input.currency, + featured = input.featured, + location = input.location, + area = input.area, + poa = input.poa, + reraPermit = input.reraPermit, + bathrooms = input.bathrooms, + bedrooms = input.bedrooms, + dateInsert = input.dateInsert, + dateUpdate = input.dateUpdate, + agentName = input.agentName, + brokerName = input.brokerName, + agentLicense = input.agentLicense, + locationId = input.locationId, + hideLocation = input.hideLocation, + + // Maps BrokerEntity + broker = + MapperFactory.createMapper().map(input.broker), + // Maps List + amenities = input.amenities, + amenitiesKeys = input.amenitiesKeys, + + latitude = input.lat, + longitude = input.long, + premium = input.premium, + livingrooms = input.livingrooms, + verified = input.verified, + + // Maps List + gallery = input.gallery, + phone = input.phone, + + // Maps List + leadEmailReceivers = input.leadEmailReceivers, + reference = input.reference, + ) + } + } +} + /** * Create [Mapper] or [ListMapper] using Reflection api and factory pattern */ diff --git a/libraries/data/src/main/java/com/smarttoolfactory/data/model/IEntity.kt b/libraries/data/src/main/java/com/smarttoolfactory/data/model/Mappables.kt similarity index 100% rename from libraries/data/src/main/java/com/smarttoolfactory/data/model/IEntity.kt rename to libraries/data/src/main/java/com/smarttoolfactory/data/model/Mappables.kt diff --git a/libraries/data/src/main/java/com/smarttoolfactory/data/model/local/PagedPropertyEntity.kt b/libraries/data/src/main/java/com/smarttoolfactory/data/model/local/PagedPropertyEntity.kt new file mode 100644 index 0000000..5224e58 --- /dev/null +++ b/libraries/data/src/main/java/com/smarttoolfactory/data/model/local/PagedPropertyEntity.kt @@ -0,0 +1,111 @@ +package com.smarttoolfactory.data.model.local + +import androidx.room.ColumnInfo +import androidx.room.Entity +import com.smarttoolfactory.data.model.IEntity + +/** + * Properties for PAGING which are retrieved via REST is converted + * to [PagedPropertyEntity] to store in database for offline-first or online-first. + * + * * Implements [IEntity] marker interface for mapping this database items from REST DTOs or + * to UI items + */ +@Entity(tableName = "paged_property", primaryKeys = ["insert_order"]) +data class PagedPropertyEntity( + + @ColumnInfo(name = "insert_order") + var insertOrder: Int = 0, + + @ColumnInfo(name = "id") + val id: Int, + @ColumnInfo(name = "update") + val update: Int, + @ColumnInfo(name = "category_id") + val categoryId: Int, + @ColumnInfo(name = "title") + val title: String, + @ColumnInfo(name = "subject") + val subject: String, + @ColumnInfo(name = "type") + val type: String, + @ColumnInfo(name = "type_id") + val typeId: Int, + @ColumnInfo(name = "thumbnail") + val thumbnail: String?, + @ColumnInfo(name = "thumbnail_big") + val thumbnailBig: String?, + @ColumnInfo(name = "image_count") + val imageCount: Int, + @ColumnInfo(name = "price") + val price: String, + @ColumnInfo(name = "price_period") + val pricePeriod: String?, + @ColumnInfo(name = "price_period_raw") + val pricePeriodRaw: String, + @ColumnInfo(name = "price_label") + val priceLabel: String?, + @ColumnInfo(name = "price_value") + val priceValue: String?, + @ColumnInfo(name = "price_value_raw") + val priceValueRaw: Int, + @ColumnInfo(name = "currency") + val currency: String, + @ColumnInfo(name = "featured") + val featured: Boolean, + @ColumnInfo(name = "location") + val location: String, + @ColumnInfo(name = "area") + val area: String, + @ColumnInfo(name = "poa") + val poa: Boolean, + @ColumnInfo(name = "rera_permit") + val reraPermit: String?, + @ColumnInfo(name = "bathrooms") + val bathrooms: String, + @ColumnInfo(name = "bedrooms") + val bedrooms: String, + @ColumnInfo(name = "date_insert") + val dateInsert: String, + @ColumnInfo(name = "date_update") + val dateUpdate: String, + @ColumnInfo(name = "agent_name") + val agentName: String, + @ColumnInfo(name = "broker_name") + val brokerName: String, + @ColumnInfo(name = "agent_license") + val agentLicense: String?, + @ColumnInfo(name = "location_id") + val locationId: Int, + @ColumnInfo(name = "hide_location") + val hideLocation: Boolean, + @ColumnInfo(name = "broker") + + val broker: BrokerEntity, + @ColumnInfo(name = "amenities") + val amenities: List, + @ColumnInfo(name = "amenities_keys") + val amenitiesKeys: List, + + @ColumnInfo(name = "latitude") + val latitude: Double, + @ColumnInfo(name = "longitude") + val longitude: Double, + @ColumnInfo(name = "premium") + val premium: Boolean, + @ColumnInfo(name = "livingrooms") + val livingrooms: String, + @ColumnInfo(name = "verified") + val verified: Boolean, + + @ColumnInfo(name = "gallery") + val gallery: List?, + @ColumnInfo(name = "phone") + val phone: String, + + @ColumnInfo(name = "lead_email_receivers") + val leadEmailReceivers: List, + + @ColumnInfo(name = "reference") + val reference: String +) : IEntity diff --git a/libraries/data/src/main/java/com/smarttoolfactory/data/model/local/PropertyEntity.kt b/libraries/data/src/main/java/com/smarttoolfactory/data/model/local/PropertyEntity.kt index 4d9211b..9dac095 100644 --- a/libraries/data/src/main/java/com/smarttoolfactory/data/model/local/PropertyEntity.kt +++ b/libraries/data/src/main/java/com/smarttoolfactory/data/model/local/PropertyEntity.kt @@ -10,6 +10,9 @@ import com.smarttoolfactory.data.model.IEntity * * * Implements [IEntity] marker interface for mapping this database items from REST DTOs or * to UI items + * + * * Insert order is required for getting data same order they are inserted since ordering is + * done in server side. */ @Entity(tableName = "property", primaryKeys = ["insert_order"]) data class PropertyEntity( diff --git a/libraries/data/src/main/java/com/smarttoolfactory/data/model/local/SavedProperty.kt b/libraries/data/src/main/java/com/smarttoolfactory/data/model/local/SavedProperty.kt new file mode 100644 index 0000000..16ff648 --- /dev/null +++ b/libraries/data/src/main/java/com/smarttoolfactory/data/model/local/SavedProperty.kt @@ -0,0 +1,72 @@ +package com.smarttoolfactory.data.model.local + +import androidx.room.Embedded +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.Index +import androidx.room.PrimaryKey +import androidx.room.Relation + +@Entity(tableName = "post") +data class PostEntity( + @PrimaryKey + val id: Int, + val userId: Int, + val title: String, + val body: String +) + +/** + * * Data class that contains [PostStatus] data. + * [PostEntity.id] is in [PostEntity] class, [PostStatus.postId] is in [PostStatus] + * both points to same value. + * + * * [PostStatus.id] is auto generated by insertion to table. + * + * * Index let's this table to be sorted by postId which makes all + * rows with same postId to be found faster. + * + * * Status of the [PostEntity] with [PostEntity.id] or [PostStatus.postId] belong to current user + * logged in with [PostStatus.userAccountId] or -1 if any user hasn't logged in + */ +@Entity( + tableName = "post_status", + indices = [Index(value = ["userAccountId", "postId"])], + foreignKeys = [ + ForeignKey( + entity = PostEntity::class, + parentColumns = ["id"], + childColumns = ["postId"], + onDelete = ForeignKey.NO_ACTION + ) + ] +) +data class PostStatus( + @PrimaryKey(autoGenerate = true) + val id: Int = 0, + val userAccountId: Int = -1, + val postId: Int, + val displayCount: Int = 0, + val isFavorite: Boolean = false +) + +/** + * @Embedded tag is for having nested entities that are contained inside another entity. For + * instance Songs are embedded inside an Album. + * + * @Relation is for having relation between entities based on pairing one or more properties, + * such as ids. For instance Person with id, having Pets that has userId that is exactly same + * with each other. + * + * * ParentColumn name from [PostEntity] class is matched with entityColumn + * from [PostStatus.postId] + */ +data class PostAndStatus( + + @Embedded + val postEntity: PostEntity, + + // 🔥 'id' comes from Post, 'postId' comes from Post. Both are the same ids + @Relation(parentColumn = "id", entityColumn = "postId") + var postStatus: PostStatus? = null +) diff --git a/libraries/data/src/main/java/com/smarttoolfactory/data/repository/Repository.kt b/libraries/data/src/main/java/com/smarttoolfactory/data/repository/Repository.kt index 3e3df0b..0d3c49a 100644 --- a/libraries/data/src/main/java/com/smarttoolfactory/data/repository/Repository.kt +++ b/libraries/data/src/main/java/com/smarttoolfactory/data/repository/Repository.kt @@ -1,6 +1,7 @@ package com.smarttoolfactory.data.repository import com.smarttoolfactory.data.constant.ORDER_BY_NONE +import com.smarttoolfactory.data.model.local.PagedPropertyEntity import com.smarttoolfactory.data.model.local.PropertyEntity import io.reactivex.rxjava3.core.Completable import io.reactivex.rxjava3.core.Single @@ -12,15 +13,8 @@ import io.reactivex.rxjava3.core.Single */ interface PropertyRepositoryCoroutines { - fun getCurrentPageNumber(): Int - suspend fun fetchEntitiesFromRemote(orderBy: String = ORDER_BY_NONE): List - suspend fun fetchEntitiesFromRemoteByPage( - page: Int, - orderBy: String = ORDER_BY_NONE - ): List - suspend fun getPropertyEntitiesFromLocal(): List suspend fun savePropertyEntities(propertyEntities: List) @@ -38,15 +32,8 @@ interface PropertyRepositoryCoroutines { */ interface PropertyRepositoryRxJava3 { - fun getCurrentPageNumber(): Int - fun fetchEntitiesFromRemote(orderBy: String = ORDER_BY_NONE): Single> - fun fetchEntitiesFromRemoteByPage( - page: Int, - orderBy: String = ORDER_BY_NONE - ): Single> - fun getPropertyEntitiesFromLocal(): Single> fun savePropertyEntities(propertyEntities: List): Completable @@ -56,3 +43,26 @@ interface PropertyRepositoryRxJava3 { fun saveSortOrderKey(orderBy: String): Completable fun getSortOrderKey(): Single } + +// TODO This can be single interface with generic input and output types +interface PagedPropertyRepository { + + fun getCurrentPageNumber(): Int + + suspend fun fetchEntitiesFromRemoteByPage( + orderBy: String = ORDER_BY_NONE + ): List + + suspend fun getPropertyCount(): Int + + fun resetPageCount() + + suspend fun getPropertyEntitiesFromLocal(): List + + suspend fun savePropertyEntities(propertyEntities: List) + + suspend fun deletePropertyEntities() + + suspend fun saveSortOrderKey(orderBy: String) + suspend fun getSortOrderKey(): String +} diff --git a/libraries/data/src/main/java/com/smarttoolfactory/data/repository/RepositoryImpl.kt b/libraries/data/src/main/java/com/smarttoolfactory/data/repository/RepositoryImpl.kt index 1c43533..6915c02 100644 --- a/libraries/data/src/main/java/com/smarttoolfactory/data/repository/RepositoryImpl.kt +++ b/libraries/data/src/main/java/com/smarttoolfactory/data/repository/RepositoryImpl.kt @@ -1,7 +1,10 @@ package com.smarttoolfactory.data.repository import com.smarttoolfactory.data.mapper.PropertyDTOtoEntityListMapper +import com.smarttoolfactory.data.mapper.PropertyDTOtoPagedEntityListMapper +import com.smarttoolfactory.data.model.local.PagedPropertyEntity import com.smarttoolfactory.data.model.local.PropertyEntity +import com.smarttoolfactory.data.source.LocalPagedPropertyDataSource import com.smarttoolfactory.data.source.LocalPropertyDataSourceCoroutines import com.smarttoolfactory.data.source.LocalPropertyDataSourceRxJava3 import com.smarttoolfactory.data.source.RemotePropertyDataSourceCoroutines @@ -16,24 +19,10 @@ class PropertyRepositoryImplCoroutines @Inject constructor( private val mapper: PropertyDTOtoEntityListMapper ) : PropertyRepositoryCoroutines { - private var currentPageNumber = 0 - - override fun getCurrentPageNumber(): Int { - return currentPageNumber - } - override suspend fun fetchEntitiesFromRemote(orderBy: String): List { + val data = remoteDataSource.getPropertyDTOs(orderBy) saveSortOrderKey(orderBy) - return mapper.map(remoteDataSource.getPropertyDTOs(orderBy)) - } - - override suspend fun fetchEntitiesFromRemoteByPage( - page: Int, - orderBy: String - ): List { - currentPageNumber = page - saveSortOrderKey(orderBy) - return mapper.map(remoteDataSource.getPropertyDTOsWithPagination(page, orderBy)) + return mapper.map(data) } override suspend fun getPropertyEntitiesFromLocal(): List { @@ -63,12 +52,6 @@ class PropertyRepositoryImlRxJava3 @Inject constructor( private val mapper: PropertyDTOtoEntityListMapper ) : PropertyRepositoryRxJava3 { - private var currentPageNumber = 0 - - override fun getCurrentPageNumber(): Int { - return currentPageNumber - } - override fun fetchEntitiesFromRemote(orderBy: String): Single> { return remoteDataSource.getPropertyDTOs(orderBy) @@ -78,17 +61,6 @@ class PropertyRepositoryImlRxJava3 @Inject constructor( } } - override fun fetchEntitiesFromRemoteByPage( - page: Int, - orderBy: String - ): Single> { - return remoteDataSource.getPropertyDTOsWithPagination(page, orderBy).map { - this.currentPageNumber = page - saveSortOrderKey(orderBy) - mapper.map(it) - } - } - override fun getPropertyEntitiesFromLocal(): Single> { return localDataSource.getPropertyEntities() } @@ -106,6 +78,59 @@ class PropertyRepositoryImlRxJava3 @Inject constructor( } override fun getSortOrderKey(): Single { - return localDataSource.getOrderkey() + return localDataSource.getOrderKey() + } +} + +class PagedPropertyRepositoryImpl @Inject constructor( + private val localDataSource: LocalPagedPropertyDataSource, + private val remoteDataSource: RemotePropertyDataSourceCoroutines, + private val mapper: PropertyDTOtoPagedEntityListMapper +) : PagedPropertyRepository { + + private var currentPageNumber = 1 + + override fun getCurrentPageNumber(): Int { + return currentPageNumber + } + + override suspend fun fetchEntitiesFromRemoteByPage( + orderBy: String + ): List { + + val data = remoteDataSource.getPropertyDTOsWithPagination( + currentPageNumber++, + orderBy + ) + saveSortOrderKey(orderBy) + return mapper.map(data) + } + + override suspend fun getPropertyCount(): Int { + return localDataSource.getPropertyCount() + } + + override fun resetPageCount() { + currentPageNumber = 1 + } + + override suspend fun getPropertyEntitiesFromLocal(): List { + return localDataSource.getPropertyEntities() + } + + override suspend fun savePropertyEntities(propertyEntities: List) { + localDataSource.saveEntities(propertyEntities) + } + + override suspend fun deletePropertyEntities() { + localDataSource.deletePropertyEntities() + } + + override suspend fun saveSortOrderKey(orderBy: String) { + localDataSource.saveOrderKey(orderBy) + } + + override suspend fun getSortOrderKey(): String { + return localDataSource.getOrderKey() } } diff --git a/libraries/data/src/main/java/com/smarttoolfactory/data/source/PropertyDataSource.kt b/libraries/data/src/main/java/com/smarttoolfactory/data/source/PropertyDataSource.kt index 14100b4..d03fb1f 100644 --- a/libraries/data/src/main/java/com/smarttoolfactory/data/source/PropertyDataSource.kt +++ b/libraries/data/src/main/java/com/smarttoolfactory/data/source/PropertyDataSource.kt @@ -1,6 +1,7 @@ package com.smarttoolfactory.data.source import com.smarttoolfactory.data.constant.ORDER_BY_NONE +import com.smarttoolfactory.data.model.local.PagedPropertyEntity import com.smarttoolfactory.data.model.local.PropertyEntity import com.smarttoolfactory.data.model.remote.PropertyDTO import io.reactivex.rxjava3.core.Completable @@ -27,6 +28,18 @@ interface LocalPropertyDataSourceCoroutines : PropertyDataSource { suspend fun getOrderKey(): String } +/* + Pagination + Coroutines + */ +interface LocalPagedPropertyDataSource : PropertyDataSource { + suspend fun getPropertyEntities(): List + suspend fun saveEntities(properties: List): List + suspend fun deletePropertyEntities() + suspend fun getPropertyCount(): Int + suspend fun saveOrderKey(orderBy: String) + suspend fun getOrderKey(): String +} + /* RxJava3 */ @@ -43,5 +56,5 @@ interface LocalPropertyDataSourceRxJava3 : PropertyDataSource { fun saveEntities(properties: List): Completable fun deletePropertyEntities(): Completable fun saveOrderKey(orderBy: String): Completable - fun getOrderkey(): Single + fun getOrderKey(): Single } diff --git a/libraries/data/src/main/java/com/smarttoolfactory/data/source/PropertyDataSourceImpl.kt b/libraries/data/src/main/java/com/smarttoolfactory/data/source/PropertyDataSourceImpl.kt index 07b8ec2..63525c0 100644 --- a/libraries/data/src/main/java/com/smarttoolfactory/data/source/PropertyDataSourceImpl.kt +++ b/libraries/data/src/main/java/com/smarttoolfactory/data/source/PropertyDataSourceImpl.kt @@ -2,10 +2,12 @@ package com.smarttoolfactory.data.source import com.smarttoolfactory.data.api.PropertyApiCoroutines import com.smarttoolfactory.data.api.PropertyApiRxJava +import com.smarttoolfactory.data.db.PagedPropertyDao import com.smarttoolfactory.data.db.PropertyDaoCoroutines import com.smarttoolfactory.data.db.PropertyDaoRxJava3 import com.smarttoolfactory.data.db.SortOrderDaoCoroutines import com.smarttoolfactory.data.db.SortOrderDaoRxJava3 +import com.smarttoolfactory.data.model.local.PagedPropertyEntity import com.smarttoolfactory.data.model.local.PropertyEntity import com.smarttoolfactory.data.model.local.SortOrderEntity import com.smarttoolfactory.data.model.remote.PropertyDTO @@ -98,7 +100,40 @@ class LocalDataSourceRxJava3Impl @Inject constructor( return sortDao.insert(SortOrderEntity(orderBy = orderBy)) } - override fun getOrderkey(): Single { + override fun getOrderKey(): Single { return sortDao.getSortOrderSingle() } } + +/* + Paged Local Data Source + */ +class LocalPagedPropertySourceImpl @Inject constructor( + private val dao: PagedPropertyDao, + private val sortDao: SortOrderDaoCoroutines +) : LocalPagedPropertyDataSource { + + override suspend fun getPropertyEntities(): List { + return dao.getPropertyList() + } + + override suspend fun saveEntities(properties: List): List { + return dao.insert(properties) + } + + override suspend fun deletePropertyEntities() { + return dao.deleteAll() + } + + override suspend fun getPropertyCount(): Int { + return dao.getPropertyCount() + } + + override suspend fun saveOrderKey(orderBy: String) { + sortDao.insert(SortOrderEntity(orderBy = orderBy)) + } + + override suspend fun getOrderKey(): String { + return sortDao.getSortOrderEntity() + } +} diff --git a/libraries/data/src/test/java/com/smarttoolfactory/data/repository/PagedPropertyRepositoryImplTest.kt b/libraries/data/src/test/java/com/smarttoolfactory/data/repository/PagedPropertyRepositoryImplTest.kt new file mode 100644 index 0000000..4b68f0b --- /dev/null +++ b/libraries/data/src/test/java/com/smarttoolfactory/data/repository/PagedPropertyRepositoryImplTest.kt @@ -0,0 +1,205 @@ +package com.smarttoolfactory.data.repository + +import com.google.common.truth.Truth +import com.smarttoolfactory.data.constant.ORDER_BY_NONE +import com.smarttoolfactory.data.mapper.MapperFactory +import com.smarttoolfactory.data.mapper.PropertyDTOtoPagedEntityListMapper +import com.smarttoolfactory.data.model.local.PagedPropertyEntity +import com.smarttoolfactory.data.model.remote.PropertyResponse +import com.smarttoolfactory.data.source.LocalPagedPropertyDataSource +import com.smarttoolfactory.data.source.RemotePropertyDataSourceCoroutines +import com.smarttoolfactory.test_utils.RESPONSE_JSON_PATH +import com.smarttoolfactory.test_utils.RESPONSE_JSON_PATH_PAGE_1 +import com.smarttoolfactory.test_utils.RESPONSE_JSON_PATH_PAGE_2 +import com.smarttoolfactory.test_utils.util.convertToObjectFromJson +import com.smarttoolfactory.test_utils.util.getResourceAsText +import io.mockk.clearMocks +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.coVerifyOrder +import io.mockk.every +import io.mockk.just +import io.mockk.mockk +import io.mockk.runs +import io.mockk.slot +import kotlinx.coroutines.test.runBlockingTest +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test + +internal class PagedPropertyRepositoryImplTest { + + private lateinit var repository: PagedPropertyRepository + + private val localDataSource: LocalPagedPropertyDataSource = mockk() + private val remoteDataSource: RemotePropertyDataSourceCoroutines = mockk() + private val mapper: PropertyDTOtoPagedEntityListMapper = mockk() + + companion object { + + private val propertyResponse = convertToObjectFromJson( + getResourceAsText(RESPONSE_JSON_PATH) + )!! + + private val propertyDTOList = propertyResponse.res + + private val entityList = + MapperFactory.createListMapper() + .map(propertyDTOList) + + // FIXME Cannot convert from Json to Entity even with wrapper, check out Moshi or Jackson + + private val propertyResponsePage1 = convertToObjectFromJson( + getResourceAsText(RESPONSE_JSON_PATH_PAGE_1) + )!! + + private val propertyResponsePage2 = convertToObjectFromJson( + getResourceAsText(RESPONSE_JSON_PATH_PAGE_2) + )!! + + private val propertyDTOListPage1 = propertyResponsePage1.res + private val propertyDTOListPage2 = propertyResponsePage2.res + + private val entityListPage1 = + MapperFactory.createListMapper() + .map( + convertToObjectFromJson( + getResourceAsText(RESPONSE_JSON_PATH_PAGE_1) + )!!.res + ) + + private val entityListPage2 = + MapperFactory.createListMapper() + .map( + convertToObjectFromJson( + getResourceAsText(RESPONSE_JSON_PATH_PAGE_1) + )!!.res + ) + } + + @Test + fun `given page 2 returned data returned should have current page number 2 with Pagination`() = + runBlockingTest { + + // GIVEN + val slot = slot() + + // Page 1 Pagination + val page1DTO = propertyDTOListPage1 + val page1Data = entityListPage1 + + coEvery { + remoteDataSource.getPropertyDTOsWithPagination(1) + } returns page1DTO + + every { mapper.map(page1DTO) } returns page1Data + coEvery { localDataSource.saveOrderKey(capture(slot)) } just runs + + // Page 2 Pagination + val page2DTO = propertyDTOListPage2 + val page2Data = entityListPage2 + + coEvery { + remoteDataSource.getPropertyDTOsWithPagination(2) + } returns page2DTO + + every { mapper.map(page2DTO) } returns page2Data + + // WHEN + val page1 = repository.getCurrentPageNumber() + val expected1 = repository.fetchEntitiesFromRemoteByPage() + + val page2 = repository.getCurrentPageNumber() + val expected2 = repository.fetchEntitiesFromRemoteByPage() + + // THEN + Truth.assertThat(expected1).isEqualTo(page1Data) + Truth.assertThat(page1).isEqualTo(1) + + Truth.assertThat(expected2).isEqualTo(page2Data) + Truth.assertThat(page2).isEqualTo(2) + + coVerifyOrder { + remoteDataSource.getPropertyDTOsWithPagination(1) + localDataSource.saveOrderKey(ORDER_BY_NONE) + mapper.map(page1DTO) + remoteDataSource.getPropertyDTOsWithPagination(2) + localDataSource.saveOrderKey(ORDER_BY_NONE) + mapper.map(page2DTO) + } + } + + @Test + fun `given DB is empty should return an empty list`() = runBlockingTest { + + // GIVEN + val expected = listOf() + coEvery { localDataSource.getPropertyEntities() } returns expected + + // WHEN + val actual = repository.getPropertyEntitiesFromLocal() + + // THEN + Truth.assertThat(actual).isEmpty() + coVerify(exactly = 1) { localDataSource.getPropertyEntities() } + } + + @Test + fun `given DB is populated should return data list`() = runBlockingTest { + + // GIVEN + coEvery { localDataSource.getPropertyEntities() } returns entityList + + // WHEN + val actual = repository.getPropertyEntitiesFromLocal() + + // THEN + Truth.assertThat(actual) + .containsExactlyElementsIn(entityList) + coVerify(exactly = 1) { localDataSource.getPropertyEntities() } + } + + @Test + fun `given entities, should save entities`() = runBlockingTest { + + // GIVEN + val idList = entityList.map { + it.id.toLong() + } + + coEvery { + localDataSource.saveEntities(entityList) + } returns idList + + // WHEN + repository.savePropertyEntities(entityList) + + // THEN + coVerify(exactly = 1) { localDataSource.saveEntities(entityList) } + } + + @Test + fun `given no error should delete entities`() = runBlockingTest { + + // GIVEN + coEvery { localDataSource.deletePropertyEntities() } just runs + + // WHEN + repository.deletePropertyEntities() + + // THEN + coVerify(exactly = 1) { + localDataSource.deletePropertyEntities() + } + } + + @BeforeEach + fun setUp() { + repository = PagedPropertyRepositoryImpl(localDataSource, remoteDataSource, mapper) + } + + @AfterEach + fun tearDown() { + clearMocks(localDataSource, remoteDataSource, mapper) + } +} diff --git a/libraries/data/src/test/java/com/smarttoolfactory/data/repository/PropertyRepositoryCoroutinesTest.kt b/libraries/data/src/test/java/com/smarttoolfactory/data/repository/PropertyRepositoryCoroutinesTest.kt index 2d85bd0..4fd4c72 100644 --- a/libraries/data/src/test/java/com/smarttoolfactory/data/repository/PropertyRepositoryCoroutinesTest.kt +++ b/libraries/data/src/test/java/com/smarttoolfactory/data/repository/PropertyRepositoryCoroutinesTest.kt @@ -9,8 +9,6 @@ import com.smarttoolfactory.data.model.remote.PropertyResponse import com.smarttoolfactory.data.source.LocalPropertyDataSourceCoroutines import com.smarttoolfactory.data.source.RemotePropertyDataSourceCoroutines import com.smarttoolfactory.test_utils.RESPONSE_JSON_PATH -import com.smarttoolfactory.test_utils.RESPONSE_JSON_PATH_PAGE_1 -import com.smarttoolfactory.test_utils.RESPONSE_JSON_PATH_PAGE_2 import com.smarttoolfactory.test_utils.util.convertToObjectFromJson import com.smarttoolfactory.test_utils.util.getResourceAsText import io.mockk.clearMocks @@ -21,6 +19,7 @@ import io.mockk.every import io.mockk.just import io.mockk.mockk import io.mockk.runs +import io.mockk.slot import io.mockk.verify import kotlinx.coroutines.test.runBlockingTest import org.junit.jupiter.api.AfterEach @@ -54,35 +53,6 @@ internal class PropertyRepositoryCoroutinesTest { private val entityList = MapperFactory.createListMapper() .map(propertyDTOList) - - // FIXME Cannot convert from Json to Entity even with wrapper, check out Moshi or Jackson - - private val propertyResponsePage1 = convertToObjectFromJson( - getResourceAsText(RESPONSE_JSON_PATH_PAGE_1) - )!! - - private val propertyResponsePage2 = convertToObjectFromJson( - getResourceAsText(RESPONSE_JSON_PATH_PAGE_2) - )!! - - private val propertyDTOListPage1 = propertyResponsePage1.res - private val propertyDTOListPage2 = propertyResponsePage2.res - - private val entityListPage1 = - MapperFactory.createListMapper() - .map( - convertToObjectFromJson( - getResourceAsText(RESPONSE_JSON_PATH_PAGE_1) - )!!.res - ) - - private val entityListPage2 = - MapperFactory.createListMapper() - .map( - convertToObjectFromJson( - getResourceAsText(RESPONSE_JSON_PATH_PAGE_1) - )!!.res - ) } @Test @@ -110,69 +80,27 @@ internal class PropertyRepositoryCoroutinesTest { runBlockingTest { // GIVEN + val slot = slot() + val actual = entityList coEvery { remoteDataSource.getPropertyDTOs() } returns propertyDTOList every { mapper.map(propertyDTOList) } returns entityList + coEvery { localDataSource.saveOrderKey(capture(slot)) } just runs // WHEN val expected = repository.fetchEntitiesFromRemote() // THEN Truth.assertThat(expected).isEqualTo(actual) + Truth.assertThat(slot.captured).isEqualTo(ORDER_BY_NONE) + coVerifyOrder { remoteDataSource.getPropertyDTOs() + localDataSource.saveOrderKey(ORDER_BY_NONE) mapper.map(propertyDTOList) } } - @Test - fun `given page 2 returned data returned should have current page number 2 with Pagination`() = - runBlockingTest { - - // GIVEN - - // Page 1 Pagination - val page1DTO = propertyDTOListPage1 - val page1Data = entityListPage1 - - coEvery { - remoteDataSource.getPropertyDTOsWithPagination(1) - } returns page1DTO - - every { mapper.map(page1DTO) } returns page1Data - - // Page 2 Pagination - val page2DTO = propertyDTOListPage2 - val page2Data = entityListPage2 - - coEvery { - remoteDataSource.getPropertyDTOsWithPagination(2) - } returns page2DTO - - every { mapper.map(page2DTO) } returns page2Data - - // WHEN - val expected1 = repository.fetchEntitiesFromRemoteByPage(1) - val page1 = repository.getCurrentPageNumber() - - val expected2 = repository.fetchEntitiesFromRemoteByPage(2) - val page2 = repository.getCurrentPageNumber() - - // THEN - Truth.assertThat(expected1).isEqualTo(page1Data) - Truth.assertThat(page1).isEqualTo(1) - - Truth.assertThat(expected2).isEqualTo(page2Data) - Truth.assertThat(page2).isEqualTo(2) - - coVerifyOrder { - remoteDataSource.getPropertyDTOsWithPagination(1) - mapper.map(page1DTO) - remoteDataSource.getPropertyDTOsWithPagination(2) - mapper.map(page2DTO) - } - } - @Test fun `given DB is empty should return an empty list`() = runBlockingTest { diff --git a/libraries/data/src/test/java/com/smarttoolfactory/data/repository/PropertyRepositoryRxJava3Test.kt b/libraries/data/src/test/java/com/smarttoolfactory/data/repository/PropertyRepositoryRxJava3Test.kt index eea813f..c94033b 100644 --- a/libraries/data/src/test/java/com/smarttoolfactory/data/repository/PropertyRepositoryRxJava3Test.kt +++ b/libraries/data/src/test/java/com/smarttoolfactory/data/repository/PropertyRepositoryRxJava3Test.kt @@ -1,6 +1,7 @@ package com.smarttoolfactory.data.repository import com.google.common.truth.Truth +import com.smarttoolfactory.data.constant.ORDER_BY_NONE import com.smarttoolfactory.data.mapper.MapperFactory import com.smarttoolfactory.data.mapper.PropertyDTOtoEntityListMapper import com.smarttoolfactory.data.model.local.PropertyEntity @@ -8,15 +9,12 @@ import com.smarttoolfactory.data.model.remote.PropertyResponse import com.smarttoolfactory.data.source.LocalPropertyDataSourceRxJava3 import com.smarttoolfactory.data.source.RemotePropertyDataSourceRxJava3 import com.smarttoolfactory.test_utils.RESPONSE_JSON_PATH -import com.smarttoolfactory.test_utils.RESPONSE_JSON_PATH_PAGE_1 -import com.smarttoolfactory.test_utils.RESPONSE_JSON_PATH_PAGE_2 import com.smarttoolfactory.test_utils.util.convertToObjectFromJson import com.smarttoolfactory.test_utils.util.getResourceAsText import io.mockk.clearMocks -import io.mockk.coEvery -import io.mockk.coVerifyOrder import io.mockk.every import io.mockk.mockk +import io.mockk.slot import io.mockk.verify import io.mockk.verifyOrder import io.reactivex.rxjava3.core.Completable @@ -52,35 +50,6 @@ class PropertyRepositoryRxJava3Test { private val entityList = MapperFactory.createListMapper() .map(propertyDTOList) - - // FIXME Cannot convert from Json to Entity even with wrapper, check out Moshi or Jackson - - private val propertyResponsePage1 = convertToObjectFromJson( - getResourceAsText(RESPONSE_JSON_PATH_PAGE_1) - )!! - - private val propertyResponsePage2 = convertToObjectFromJson( - getResourceAsText(RESPONSE_JSON_PATH_PAGE_2) - )!! - - private val propertyDTOListPage1 = propertyResponsePage1.res - private val propertyDTOListPage2 = propertyResponsePage2.res - - private val entityListPage1 = - MapperFactory.createListMapper() - .map( - convertToObjectFromJson( - getResourceAsText(RESPONSE_JSON_PATH_PAGE_1) - )!!.res - ) - - private val entityListPage2 = - MapperFactory.createListMapper() - .map( - convertToObjectFromJson( - getResourceAsText(RESPONSE_JSON_PATH_PAGE_1) - )!!.res - ) } @Test @@ -112,72 +81,26 @@ class PropertyRepositoryRxJava3Test { fun `given remote data source return PropertyDTO list, should return PropertyEntity list`() { // GIVEN + val slot = slot() + every { remoteDataSource.getPropertyDTOs() } returns Single.just(propertyDTOList) every { mapper.map(propertyDTOList) } returns entityList + every { localDataSource.saveOrderKey(capture(slot)) } returns Completable.complete() // WHEN val expected = repository.fetchEntitiesFromRemote().blockingGet() // THEN Truth.assertThat(expected).containsExactlyElementsIn(entityList) + Truth.assertThat(slot.captured).isEqualTo(ORDER_BY_NONE) + verifyOrder { remoteDataSource.getPropertyDTOs() + localDataSource.saveOrderKey(ORDER_BY_NONE) mapper.map(propertyDTOList) } } - @Test - fun `given page 2 returned data returned should have current page number 2 with Pagination`() { - - // GIVEN - - // Page 1 Pagination - val page1DTO = propertyDTOListPage1 - val page1Data = entityListPage1 - - coEvery { - remoteDataSource.getPropertyDTOsWithPagination(1) - } returns Single.just(page1DTO) - - every { mapper.map(page1DTO) } returns page1Data - - // Page 2 Pagination - val page2DTO = propertyDTOListPage2 - val page2Data = entityListPage2 - - coEvery { - remoteDataSource.getPropertyDTOsWithPagination(2) - } returns Single.just(page2DTO) - - every { mapper.map(page2DTO) } returns page2Data - - // WHEN - val expected1 = repository - .fetchEntitiesFromRemoteByPage(1) - .blockingGet() - - val page1 = repository.getCurrentPageNumber() - - val expected2 = repository - .fetchEntitiesFromRemoteByPage(2) - .blockingGet() - val page2 = repository.getCurrentPageNumber() - - // THEN - Truth.assertThat(expected1).isEqualTo(page1Data) - Truth.assertThat(page1).isEqualTo(1) - - Truth.assertThat(expected2).isEqualTo(page2Data) - Truth.assertThat(page2).isEqualTo(2) - - coVerifyOrder { - remoteDataSource.getPropertyDTOsWithPagination(1) - mapper.map(page1DTO) - remoteDataSource.getPropertyDTOsWithPagination(2) - mapper.map(page2DTO) - } - } - @Test fun `given DB is empty should return an empty list`() { diff --git a/libraries/data/src/test/java/com/smarttoolfactory/data/source/PropertyDataSourceCoroutinesTest.kt b/libraries/data/src/test/java/com/smarttoolfactory/data/source/PropertyDataSourceCoroutinesTest.kt index 5227a0f..5a2291c 100644 --- a/libraries/data/src/test/java/com/smarttoolfactory/data/source/PropertyDataSourceCoroutinesTest.kt +++ b/libraries/data/src/test/java/com/smarttoolfactory/data/source/PropertyDataSourceCoroutinesTest.kt @@ -3,6 +3,7 @@ package com.smarttoolfactory.data.source import com.google.common.truth.Truth import com.smarttoolfactory.data.api.PropertyApiCoroutines import com.smarttoolfactory.data.db.PropertyDaoCoroutines +import com.smarttoolfactory.data.db.SortOrderDaoCoroutines import com.smarttoolfactory.data.mapper.MapperFactory import com.smarttoolfactory.data.mapper.PropertyDTOtoEntityListMapper import com.smarttoolfactory.data.model.local.PropertyEntity @@ -100,6 +101,7 @@ class PropertyDataSourceCoroutinesTest { private val dao = mockk() private lateinit var localDataSource: LocalPropertyDataSourceCoroutines + private val sortDao = mockk() @Test fun `given DB is empty should return an empty list`() = runBlockingTest { @@ -163,12 +165,12 @@ class PropertyDataSourceCoroutinesTest { @BeforeEach fun setUp() { - localDataSource = LocalPropertyDataSourceImpl(dao) + localDataSource = LocalPropertyDataSourceImpl(dao, sortDao) } @AfterEach fun tearDown() { - clearMocks(dao) + clearMocks(dao, sortDao) } } } diff --git a/libraries/data/src/test/java/com/smarttoolfactory/data/source/PropertyDataSourceRxJava3Test.kt b/libraries/data/src/test/java/com/smarttoolfactory/data/source/PropertyDataSourceRxJava3Test.kt index 08fcd1e..4f8d34e 100644 --- a/libraries/data/src/test/java/com/smarttoolfactory/data/source/PropertyDataSourceRxJava3Test.kt +++ b/libraries/data/src/test/java/com/smarttoolfactory/data/source/PropertyDataSourceRxJava3Test.kt @@ -3,6 +3,7 @@ package com.smarttoolfactory.data.source import com.google.common.truth.Truth import com.smarttoolfactory.data.api.PropertyApiRxJava import com.smarttoolfactory.data.db.PropertyDaoRxJava3 +import com.smarttoolfactory.data.db.SortOrderDaoRxJava3 import com.smarttoolfactory.data.mapper.MapperFactory import com.smarttoolfactory.data.mapper.PropertyDTOtoEntityListMapper import com.smarttoolfactory.data.model.local.PropertyEntity @@ -100,6 +101,7 @@ class PropertyDataSourceRxJava3Test { inner class LocalDataSourceTest { private val dao = mockk() + private val sortDao = mockk() private lateinit var localDataSource: LocalDataSourceRxJava3Impl @@ -167,12 +169,12 @@ class PropertyDataSourceRxJava3Test { @BeforeEach fun setUp() { - localDataSource = LocalDataSourceRxJava3Impl(dao) + localDataSource = LocalDataSourceRxJava3Impl(dao, sortDao) } @AfterEach fun tearDown() { - clearMocks(dao) + clearMocks(dao, sortDao) } } } diff --git a/libraries/domain/src/main/java/com/smarttoolfactory/domain/mapper/Mappers.kt b/libraries/domain/src/main/java/com/smarttoolfactory/domain/mapper/Mappers.kt index f3b6c6e..d08bb1c 100755 --- a/libraries/domain/src/main/java/com/smarttoolfactory/domain/mapper/Mappers.kt +++ b/libraries/domain/src/main/java/com/smarttoolfactory/domain/mapper/Mappers.kt @@ -4,6 +4,7 @@ import com.smarttoolfactory.data.mapper.ListMapper import com.smarttoolfactory.data.mapper.Mapper import com.smarttoolfactory.data.mapper.MapperFactory import com.smarttoolfactory.data.model.local.BrokerEntity +import com.smarttoolfactory.data.model.local.PagedPropertyEntity import com.smarttoolfactory.data.model.local.PropertyEntity import com.smarttoolfactory.domain.model.BrokerItem import com.smarttoolfactory.domain.model.PropertyItem @@ -96,3 +97,68 @@ class PropertyEntityToItemListMapper @Inject constructor() : } } } + +class PagedEntityToItemListMapper @Inject constructor() : + ListMapper { + + override fun map(input: List): List { + + return input.map { input -> + + PropertyItem( + id = input.id, + update = input.update, + categoryId = input.categoryId, + title = input.title, + subject = input.subject, + type = input.type, + typeId = input.typeId, + thumbnail = input.thumbnail, + thumbnailBig = input.thumbnailBig, + imageCount = input.imageCount, + price = input.price, + pricePeriod = input.pricePeriod, + pricePeriodRaw = input.pricePeriodRaw, + priceLabel = input.priceLabel, + priceValue = input.priceValue, + priceValueRaw = input.priceValueRaw, + currency = input.currency, + featured = input.featured, + location = input.location, + area = input.area, + poa = input.poa, + reraPermit = input.reraPermit, + bathrooms = input.bathrooms, + bedrooms = input.bedrooms, + dateInsert = input.dateInsert, + dateUpdate = input.dateUpdate, + agentName = input.agentName, + brokerName = input.brokerName, + agentLicense = input.agentLicense, + locationId = input.locationId, + hideLocation = input.hideLocation, + + // Maps BrokerEntity + broker = + MapperFactory.createMapper().map(input.broker), + // Maps List + amenities = input.amenities, + amenitiesKeys = input.amenitiesKeys, + + latitude = input.latitude, + longitude = input.longitude, + premium = input.premium, + livingrooms = input.livingrooms, + verified = input.verified, + + // Maps List + gallery = input.gallery, + phone = input.phone, + + // Maps List + leadEmailReceivers = input.leadEmailReceivers, + reference = input.reference, + ) + } + } +} diff --git a/libraries/domain/src/main/java/com/smarttoolfactory/domain/usecase/GetPropertiesUseCaseFlow.kt b/libraries/domain/src/main/java/com/smarttoolfactory/domain/usecase/GetPropertiesUseCaseFlow.kt index fbb4291..dd57e47 100644 --- a/libraries/domain/src/main/java/com/smarttoolfactory/domain/usecase/GetPropertiesUseCaseFlow.kt +++ b/libraries/domain/src/main/java/com/smarttoolfactory/domain/usecase/GetPropertiesUseCaseFlow.kt @@ -109,49 +109,7 @@ class GetPropertiesUseCaseFlow @Inject constructor( data.forEachIndexed { index, propertyEntity -> propertyEntity.insertOrder = index } - - data - } - } else { - it - } - } - .flowOn(dispatcherProvider.ioDispatcher) - .catch { throwable -> - emitAll(flowOf(listOf())) - } - .map { - toPropertyListOrError(it) - } - -// return flow { emit((repository.getSortOrderKey() ?: ORDER_BY_NONE) == orderBy) } -// .flatMapConcat { sameFilter -> -// -// if (sameFilter) { -// getOfflineFirstPropertyListFlow() -// } else { -// getPropertiesOfflineLast(orderBy) -// } -// } - } - - private fun getOfflineFirstPropertyListFlow(): Flow> { - return flow { - emit(repository.getPropertyEntitiesFromLocal()) - } - .catch { throwable -> - emitAll(flowOf(listOf())) - } - .map { - if (it.isEmpty()) { - repository.run { - val data = fetchEntitiesFromRemote() - deletePropertyEntities() - - // 🔥 Add an insert order since we are not using Room's ORDER BY - data.forEachIndexed { index, propertyEntity -> - propertyEntity.insertOrder = index - } + savePropertyEntities(data) data } @@ -176,7 +134,10 @@ class GetPropertiesUseCaseFlow @Inject constructor( } } - fun getCurrentSortKey(defaultKey: String = ORDER_BY_NONE): Flow { + /** + * Get current sort key from db + */ + fun getCurrentSortKey(defaultKey: String = ORDER_BY_NONE): Flow { return flow { emit(repository.getSortOrderKey()) } .catch { emitAll(flowOf(defaultKey)) diff --git a/libraries/domain/src/main/java/com/smarttoolfactory/domain/usecase/GetPropertiesUseCasePaged.kt b/libraries/domain/src/main/java/com/smarttoolfactory/domain/usecase/GetPropertiesUseCasePaged.kt index 9eab3b5..d055228 100644 --- a/libraries/domain/src/main/java/com/smarttoolfactory/domain/usecase/GetPropertiesUseCasePaged.kt +++ b/libraries/domain/src/main/java/com/smarttoolfactory/domain/usecase/GetPropertiesUseCasePaged.kt @@ -1,70 +1,54 @@ package com.smarttoolfactory.domain.usecase import com.smarttoolfactory.data.constant.ORDER_BY_NONE -import com.smarttoolfactory.data.model.local.PropertyEntity -import com.smarttoolfactory.data.repository.PropertyRepositoryCoroutines +import com.smarttoolfactory.data.model.local.PagedPropertyEntity +import com.smarttoolfactory.data.repository.PagedPropertyRepository import com.smarttoolfactory.domain.dispatcher.UseCaseDispatchers import com.smarttoolfactory.domain.error.EmptyDataException -import com.smarttoolfactory.domain.mapper.PropertyEntityToItemListMapper +import com.smarttoolfactory.domain.mapper.PagedEntityToItemListMapper import com.smarttoolfactory.domain.model.PropertyItem import javax.inject.Inject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.emitAll +import kotlinx.coroutines.flow.flatMapConcat import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map -/** - * UseCase for getting UI Item list with offline first or offline last approach. - * - * * *Offline-first* first source to look for data is local data source, or database, - * if database is empty or if caching is used it's expiry date is over, looks for remote source - * for data. If both of the sources are empty, either return empty list or error to notify UI. - * - * This approach is good for when user is offline or have no internet connection, additional logic - * can be added to check if user is offline to not deleted cached data. - * - * * *Offline-last* always checks remote data source for data and applies to database or offline - * source as last resort. Offline-last is used especially when user refreshes data with a UI - * element to get the latest data or new data is always first preference. - */ class GetPropertiesUseCasePaged @Inject constructor( - private val repository: PropertyRepositoryCoroutines, - private val mapper: PropertyEntityToItemListMapper, + private val repository: PagedPropertyRepository, + private val mapper: PagedEntityToItemListMapper, private val dispatcherProvider: UseCaseDispatchers ) { /** - * Function to retrieve data from repository with offline-last which checks - * REMOTE data source first. - * - * * Check out Remote Source first - * * If empty data or null returned throw empty set exception - * * If error occurred while fetching data from remote: Try to fetch data from db - * * If data is fetched from remote source: delete old data, save new data and return new data - * * If both network and db don't have any data throw empty set exception + * Fetches data from REMOTE source with PAGINATION + * using specified parameter parameter. Page number i s kept in [PagedPropertyRepository]. * + * * Since [PagedPropertyEntity] insert order is not kept + * it's required to add an index for insertion or to db to get valid data which is ordered + * in server side */ - fun getPagedOfflineLast(page: Int, orderBy: String): Flow> { - return flow { emit(repository.fetchEntitiesFromRemote(orderBy)) } + fun getPagedOfflineLast(orderBy: String): Flow> { + + return flow { emit(repository.fetchEntitiesFromRemoteByPage(orderBy)) } .map { if (it.isNullOrEmpty()) { throw EmptyDataException("No Data is available in Remote source!") } else { repository.run { - if (page == 1) { - deletePropertyEntities() - } +// deletePropertyEntities() + + val propertyCount = repository.getPropertyCount() // 🔥 Add an insert order since we are not using Room's ORDER BY it.forEachIndexed { index, propertyEntity -> - propertyEntity.insertOrder = index + propertyEntity.insertOrder = propertyCount + index } savePropertyEntities(it) - getPropertyEntitiesFromLocal() } } @@ -79,7 +63,19 @@ class GetPropertiesUseCasePaged @Inject constructor( } } - private fun toPropertyListOrError(entityList: List): List { + /** + * Resets page number to 1, clears previous data from database and fetches new data + * from REMOTE source with given param + */ + fun refreshData(orderBy: String): Flow> { + return flow { emit(repository.resetPageCount()) } + .flatMapConcat { + repository.deletePropertyEntities() + getPagedOfflineLast(orderBy) + } + } + + private fun toPropertyListOrError(entityList: List): List { return if (!entityList.isNullOrEmpty()) { mapper.map(entityList) } else { @@ -87,7 +83,10 @@ class GetPropertiesUseCasePaged @Inject constructor( } } - fun getCurrentSortKey(defaultKey: String = ORDER_BY_NONE): Flow { + /** + * Get current sort key from db + */ + fun getCurrentSortKey(defaultKey: String = ORDER_BY_NONE): Flow { return flow { emit(repository.getSortOrderKey()) } .catch { emitAll(flowOf(defaultKey)) diff --git a/libraries/domain/src/main/java/com/smarttoolfactory/domain/usecase/GetPropertiesUseCaseRxJava3.kt b/libraries/domain/src/main/java/com/smarttoolfactory/domain/usecase/GetPropertiesUseCaseRxJava3.kt index 23ffc9a..5bba88d 100644 --- a/libraries/domain/src/main/java/com/smarttoolfactory/domain/usecase/GetPropertiesUseCaseRxJava3.kt +++ b/libraries/domain/src/main/java/com/smarttoolfactory/domain/usecase/GetPropertiesUseCaseRxJava3.kt @@ -121,7 +121,10 @@ class GetPropertiesUseCaseRxJava3 @Inject constructor( ) } - fun getCurrentSortKey(defaultKey: String = ORDER_BY_NONE): Single { + /** + * Get current sort key from db + */ + fun getCurrentSortKey(defaultKey: String = ORDER_BY_NONE): Single { return repository.getSortOrderKey() .onErrorResumeNext { Single.just(defaultKey) diff --git a/libraries/domain/src/test/java/com/smarttoolfactory/domain/mapper/PropertyEntityToItemListMapperTest.kt b/libraries/domain/src/test/java/com/smarttoolfactory/domain/mapper/PropertyEntityToItemListMapperTest.kt index 745294e..1c2af4a 100644 --- a/libraries/domain/src/test/java/com/smarttoolfactory/domain/mapper/PropertyEntityToItemListMapperTest.kt +++ b/libraries/domain/src/test/java/com/smarttoolfactory/domain/mapper/PropertyEntityToItemListMapperTest.kt @@ -1,13 +1,11 @@ package com.smarttoolfactory.domain.mapper -import com.google.common.truth.Truth import com.smarttoolfactory.data.mapper.MapperFactory import com.smarttoolfactory.data.mapper.PropertyDTOtoEntityListMapper import com.smarttoolfactory.data.model.remote.PropertyResponse import com.smarttoolfactory.test_utils.RESPONSE_JSON_PATH import com.smarttoolfactory.test_utils.util.convertToObjectFromJson import com.smarttoolfactory.test_utils.util.getResourceAsText -import org.junit.jupiter.api.Test class PropertyEntityToItemListMapperTest { @@ -22,21 +20,21 @@ class PropertyEntityToItemListMapperTest { .map(propertyDTOList) } - @Test - fun `Given a valid propertyEntity list, should map to propertyItem list`() { - - // GIVEN - val mapper = - MapperFactory.createListMapper() - - // WHEN - val actual = mapper.map(entityList) - - // THEN - actual.forEachIndexed { index, propertyItem -> - Truth.assertThat(propertyItem.id).isEqualTo(entityList[index].id) - Truth.assertThat(propertyItem.update).isEqualTo(entityList[index].update) - Truth.assertThat(propertyItem.price).isEqualTo(entityList[index].price) - } - } +// @Test +// fun `Given a valid propertyEntity list, should map to propertyItem list`() { +// +// // GIVEN +// val mapper = +// MapperFactory.createListMapper() +// +// // WHEN +// val actual = mapper.map(entityList) +// +// // THEN +// actual.forEachIndexed { index, propertyItem -> +// Truth.assertThat(propertyItem.id).isEqualTo(entityList[index].id) +// Truth.assertThat(propertyItem.update).isEqualTo(entityList[index].update) +// Truth.assertThat(propertyItem.price).isEqualTo(entityList[index].price) +// } +// } } diff --git a/libraries/domain/src/test/java/com/smarttoolfactory/domain/usecase/GetPropertiesUseCaseFlowTest.kt b/libraries/domain/src/test/java/com/smarttoolfactory/domain/usecase/GetPropertiesUseCaseFlowTest.kt index 560503a..9412e7d 100644 --- a/libraries/domain/src/test/java/com/smarttoolfactory/domain/usecase/GetPropertiesUseCaseFlowTest.kt +++ b/libraries/domain/src/test/java/com/smarttoolfactory/domain/usecase/GetPropertiesUseCaseFlowTest.kt @@ -252,7 +252,6 @@ class GetPropertiesUseCaseFlowTest { testCoroutineExtension.runBlockingTest { // GIVEN - coEvery { repository.getOrderFilter() } returns ORDER_BY_NONE coEvery { repository.getPropertyEntitiesFromLocal() } returns entityList coEvery { mapper.map(entityList) } returns itemList // WHEN @@ -268,7 +267,6 @@ class GetPropertiesUseCaseFlowTest { .dispose() coVerifySequence { - repository.getOrderFilter() repository.getPropertyEntitiesFromLocal() mapper.map(entityList) } @@ -279,7 +277,6 @@ class GetPropertiesUseCaseFlowTest { testCoroutineExtension.runBlockingTest { // GIVEN - coEvery { repository.getOrderFilter() } returns ORDER_BY_NONE coEvery { repository.getPropertyEntitiesFromLocal() } returns listOf() coEvery { repository.fetchEntitiesFromRemote() } returns entityList coEvery { repository.deletePropertyEntities() } just runs @@ -298,7 +295,6 @@ class GetPropertiesUseCaseFlowTest { .dispose() coVerifySequence { - repository.getOrderFilter() repository.getPropertyEntitiesFromLocal() repository.fetchEntitiesFromRemote() repository.deletePropertyEntities() @@ -312,7 +308,6 @@ class GetPropertiesUseCaseFlowTest { testCoroutineExtension.runBlockingTest { // GIVEN - coEvery { repository.getOrderFilter() } returns ORDER_BY_NONE coEvery { repository.getPropertyEntitiesFromLocal() } throws SQLException("Database Exception") @@ -334,7 +329,6 @@ class GetPropertiesUseCaseFlowTest { .dispose() coVerifySequence { - repository.getOrderFilter() repository.getPropertyEntitiesFromLocal() repository.fetchEntitiesFromRemote() repository.deletePropertyEntities() @@ -348,7 +342,6 @@ class GetPropertiesUseCaseFlowTest { testCoroutineExtension.runBlockingTest { // GIVEN - coEvery { repository.getOrderFilter() } returns ORDER_BY_NONE coEvery { repository.getPropertyEntitiesFromLocal() } returns listOf() coEvery { @@ -367,7 +360,6 @@ class GetPropertiesUseCaseFlowTest { .dispose() coVerifySequence { - repository.getOrderFilter() repository.getPropertyEntitiesFromLocal() repository.fetchEntitiesFromRemote() } diff --git a/libraries/domain/src/test/java/com/smarttoolfactory/domain/usecase/GetPropertiesUseCaseRxJava3Test.kt b/libraries/domain/src/test/java/com/smarttoolfactory/domain/usecase/GetPropertiesUseCaseRxJava3Test.kt index 01d7b77..c7b931c 100644 --- a/libraries/domain/src/test/java/com/smarttoolfactory/domain/usecase/GetPropertiesUseCaseRxJava3Test.kt +++ b/libraries/domain/src/test/java/com/smarttoolfactory/domain/usecase/GetPropertiesUseCaseRxJava3Test.kt @@ -211,7 +211,6 @@ class GetPropertiesUseCaseRxJava3Test { fun `given Local source has data, should return data`() { // GIVEN - every { repository.getOrderFilter() } returns ORDER_BY_NONE every { repository.getPropertyEntitiesFromLocal() } returns Single.just(entityList) every { mapper.map(entityList) } returns itemList @@ -228,7 +227,6 @@ class GetPropertiesUseCaseRxJava3Test { .dispose() verifySequence { - repository.getOrderFilter() repository.getPropertyEntitiesFromLocal() mapper.map(entityList) } @@ -238,7 +236,6 @@ class GetPropertiesUseCaseRxJava3Test { fun `given Local source is empty, should fetch data from Remote`() { // GIVEN - every { repository.getOrderFilter() } returns ORDER_BY_NONE every { repository.getPropertyEntitiesFromLocal() } returns Single.just(listOf()) every { repository.fetchEntitiesFromRemote() } returns Single.just(entityList) every { repository.deletePropertyEntities() } returns Completable.complete() @@ -260,7 +257,6 @@ class GetPropertiesUseCaseRxJava3Test { .dispose() verifySequence { - repository.getOrderFilter() repository.getPropertyEntitiesFromLocal() repository.fetchEntitiesFromRemote() repository.deletePropertyEntities() @@ -273,7 +269,6 @@ class GetPropertiesUseCaseRxJava3Test { fun `given exception returned from Local source should fetch data from Remote`() { // GIVEN - every { repository.getOrderFilter() } returns ORDER_BY_NONE every { repository.getPropertyEntitiesFromLocal() } returns Single.error(SQLException("Database Exception")) @@ -297,7 +292,6 @@ class GetPropertiesUseCaseRxJava3Test { .dispose() verifySequence { - repository.getOrderFilter() repository.getPropertyEntitiesFromLocal() repository.fetchEntitiesFromRemote() repository.deletePropertyEntities() @@ -310,7 +304,6 @@ class GetPropertiesUseCaseRxJava3Test { fun `given Local source is empty and Remote returned error, should throw exception`() { // GIVEN - every { repository.getOrderFilter() } returns ORDER_BY_NONE every { repository.getPropertyEntitiesFromLocal() } returns Single.just(listOf()) every { @@ -329,7 +322,6 @@ class GetPropertiesUseCaseRxJava3Test { .dispose() verifySequence { - repository.getOrderFilter() repository.getPropertyEntitiesFromLocal() repository.fetchEntitiesFromRemote() } diff --git a/screenshots/favorites.png b/screenshots/favorites.png new file mode 100644 index 0000000..8d56f9e Binary files /dev/null and b/screenshots/favorites.png differ diff --git a/screenshots/modules.png b/screenshots/modules.png new file mode 100644 index 0000000..b7c4595 Binary files /dev/null and b/screenshots/modules.png differ diff --git a/screenshots/property_flow.png b/screenshots/property_flow.png new file mode 100644 index 0000000..29fbdbd Binary files /dev/null and b/screenshots/property_flow.png differ diff --git a/screenshots/property_pagination.png b/screenshots/property_pagination.png new file mode 100644 index 0000000..77d6457 Binary files /dev/null and b/screenshots/property_pagination.png differ diff --git a/screenshots/property_rxjava3.png b/screenshots/property_rxjava3.png new file mode 100644 index 0000000..92f5dfe Binary files /dev/null and b/screenshots/property_rxjava3.png differ