Skip to content

Commit

Permalink
Merge pull request woocommerce#2731 from woocommerce/issue/2630-displ…
Browse files Browse the repository at this point in the history
…ay-grouped-products

Issue/2630 display grouped products
  • Loading branch information
AmandaRiu authored Aug 18, 2020
2 parents 05f4d99 + 50391fc commit 1ae7ef2
Show file tree
Hide file tree
Showing 31 changed files with 782 additions and 184 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ data class Product(
val menuOrder: Int,
val categories: List<ProductCategory>,
val tags: List<ProductTag>,
val groupedProductIds: List<Long>,
override val length: Float,
override val width: Float,
override val height: Float,
Expand Down Expand Up @@ -136,7 +137,8 @@ data class Product(
menuOrder == product.menuOrder &&
isSameImages(product.images) &&
isSameCategories(product.categories) &&
isSameTags(product.tags)
isSameTags(product.tags) &&
groupedProductIds == product.groupedProductIds
}

val hasCategories get() = categories.isNotEmpty()
Expand Down Expand Up @@ -354,7 +356,8 @@ data class Product(
menuOrder = updatedProduct.menuOrder,
categories = updatedProduct.categories,
tags = updatedProduct.tags,
type = updatedProduct.type
type = updatedProduct.type,
groupedProductIds = updatedProduct.groupedProductIds
)
} ?: this.copy()
}
Expand Down Expand Up @@ -454,6 +457,11 @@ fun Product.toDataModel(storedProductModel: WCProductModel?): WCProductModel {
it.categories = categoriesToJson()
it.tags = tagsToJson()
it.type = type.value
it.groupedProductIds = groupedProductIds.joinToString(
separator = ",",
prefix = "[",
postfix = "]"
)
}
}

Expand Down Expand Up @@ -536,7 +544,8 @@ fun WCProductModel.toAppModel(): Product {
it.name,
it.slug
)
}
},
groupedProductIds = this.getGroupedProductIds()
)
}

Expand All @@ -549,6 +558,20 @@ fun MediaModel.toAppModel(): Product.Image {
)
}

fun List<Product>.isSameList(productList: List<Product>): Boolean {
if (this.size != productList.size) {
return false
}
for (index in this.indices) {
val oldItem = productList[index]
val newItem = this[index]
if (!oldItem.isSameProduct(newItem)) {
return false
}
}
return true
}

/**
* Returns the product as a [ProductReviewProduct] for use with the product reviews feature.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.woocommerce.android.ui.products

import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import com.woocommerce.android.model.Product
import com.woocommerce.android.model.isSameList

class GroupedProductListAdapter(
private val onItemDeleted: (product: Product) -> Unit
) : RecyclerView.Adapter<ProductItemViewHolder>() {
private val productList = ArrayList<Product>()

init {
setHasStableIds(true)
}

override fun getItemId(position: Int) = productList[position].remoteId

override fun getItemCount() = productList.size

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ProductItemViewHolder(parent)

override fun onBindViewHolder(holder: ProductItemViewHolder, position: Int) {
val product = productList[position]

holder.bind(product)
holder.setOnDeleteClickListener(product, onItemDeleted)
}

fun setProductList(products: List<Product>) {
if (!productList.isSameList(products)) {
val diffResult = DiffUtil.calculateDiff(ProductItemDiffUtil(productList, products))
productList.clear()
productList.addAll(products)
diffResult.dispatchUpdatesTo(this)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
package com.woocommerce.android.ui.products

import android.os.Bundle
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.core.view.isVisible
import androidx.fragment.app.viewModels
import androidx.lifecycle.Observer
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.LinearLayoutManager
import com.woocommerce.android.R
import com.woocommerce.android.analytics.AnalyticsTracker
import com.woocommerce.android.extensions.navigateBackWithResult
import com.woocommerce.android.extensions.takeIfNotEqualTo
import com.woocommerce.android.ui.base.BaseFragment
import com.woocommerce.android.ui.base.UIMessageResolver
import com.woocommerce.android.ui.dialog.CustomDiscardDialog
import com.woocommerce.android.ui.main.MainActivity.Companion.BackPressListener
import com.woocommerce.android.viewmodel.MultiLiveEvent.Event.Exit
import com.woocommerce.android.viewmodel.MultiLiveEvent.Event.ExitWithResult
import com.woocommerce.android.viewmodel.MultiLiveEvent.Event.ShowDiscardDialog
import com.woocommerce.android.viewmodel.MultiLiveEvent.Event.ShowSnackbar
import com.woocommerce.android.viewmodel.ViewModelFactory
import com.woocommerce.android.widgets.SkeletonView
import kotlinx.android.synthetic.main.fragment_grouped_product_list.*
import javax.inject.Inject

class GroupedProductListFragment : BaseFragment(), BackPressListener {
companion object {
const val KEY_GROUPED_PRODUCT_IDS_RESULT = "key_grouped_product_ids_result"
}

@Inject lateinit var uiMessageResolver: UIMessageResolver

@Inject lateinit var viewModelFactory: ViewModelFactory
val viewModel: GroupedProductListViewModel by viewModels { viewModelFactory }

private val skeletonView = SkeletonView()
private val productListAdapter: GroupedProductListAdapter by lazy {
GroupedProductListAdapter(viewModel::onGroupedProductDeleted)
}

private var doneMenuItem: MenuItem? = null

override fun getFragmentTitle() = getString(R.string.grouped_products)

override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
setHasOptionsMenu(true)
return inflater.inflate(R.layout.fragment_grouped_product_list, container, false)
}

override fun onDestroyView() {
// hide the skeleton view if fragment is destroyed
skeletonView.hide()
super.onDestroyView()
}

override fun onResume() {
super.onResume()
AnalyticsTracker.trackViewShown(this)
}

override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
menu.clear()
inflater.inflate(R.menu.menu_done, menu)
doneMenuItem = menu.findItem(R.id.menu_done)
doneMenuItem?.isVisible = false
super.onCreateOptionsMenu(menu, inflater)
}

override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.menu_done -> {
viewModel.onDoneButtonClicked()
true
}
else -> super.onOptionsItemSelected(item)
}
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupObservers()
}

override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)

val activity = requireActivity()

productsRecycler.layoutManager = LinearLayoutManager(activity)
productsRecycler.adapter = productListAdapter
productsRecycler.isMotionEventSplittingEnabled = false
}

private fun setupObservers() {
viewModel.productListViewStateData.observe(viewLifecycleOwner) { old, new ->
new.isSkeletonShown?.takeIfNotEqualTo(old?.isSkeletonShown) { showSkeleton(it) }
new.isLoadingMore?.takeIfNotEqualTo(old?.isLoadingMore) { loadMoreProgress.isVisible = it }
new.hasChanges?.takeIfNotEqualTo(old?.hasChanges) {
doneMenuItem?.isVisible = it
}
}

viewModel.event.observe(viewLifecycleOwner, Observer { event ->
when (event) {
is ShowSnackbar -> uiMessageResolver.showSnack(event.message)
is ShowDiscardDialog -> CustomDiscardDialog.showDiscardDialog(
requireActivity(),
event.positiveBtnAction,
event.negativeBtnAction,
event.messageId,
negativeButtonId = event.negativeButtonId
)
is Exit -> findNavController().navigateUp()
is ExitWithResult<*> -> {
navigateBackWithResult(KEY_GROUPED_PRODUCT_IDS_RESULT, event.item as? List<*>)
}
else -> event.isHandled = false
}
})

viewModel.productList.observe(viewLifecycleOwner, Observer {
productListAdapter.setProductList(it)
})
}

private fun showSkeleton(show: Boolean) {
when (show) {
true -> {
skeletonView.show(productsRecycler, R.layout.skeleton_product_list, delayed = true)
}
false -> skeletonView.hide()
}
}

override fun onRequestAllowBackPress() = viewModel.onBackButtonClicked()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.woocommerce.android.ui.products

import android.os.Bundle
import androidx.lifecycle.ViewModel
import androidx.savedstate.SavedStateRegistryOwner
import com.woocommerce.android.di.ViewModelAssistedFactory
import com.woocommerce.android.viewmodel.ViewModelKey
import dagger.Binds
import dagger.Module
import dagger.Provides
import dagger.multibindings.IntoMap

@Module
abstract class GroupedProductListModule {
@Module
companion object {
@JvmStatic
@Provides
fun provideDefaultArgs(fragment: GroupedProductListFragment): Bundle? {
return fragment.arguments
}
}

@Binds
@IntoMap
@ViewModelKey(GroupedProductListViewModel::class)
abstract fun bindFactory(factory: GroupedProductListViewModel.Factory): ViewModelAssistedFactory<out ViewModel>

@Binds
abstract fun bindSavedStateRegistryOwner(fragment: GroupedProductListFragment): SavedStateRegistryOwner
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package com.woocommerce.android.ui.products

import com.woocommerce.android.model.Product
import com.woocommerce.android.model.toAppModel
import com.woocommerce.android.tools.SelectedSite
import com.woocommerce.android.util.WooLog
import com.woocommerce.android.util.WooLog.T
import com.woocommerce.android.util.suspendCancellableCoroutineWithTimeout
import kotlinx.coroutines.CancellableContinuation
import kotlinx.coroutines.CancellationException
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import org.wordpress.android.fluxc.action.WCProductAction.FETCH_PRODUCTS
import org.wordpress.android.fluxc.Dispatcher
import org.wordpress.android.fluxc.generated.WCProductActionBuilder
import org.wordpress.android.fluxc.store.WCProductStore
import org.wordpress.android.fluxc.store.WCProductStore.FetchProductsPayload
import org.wordpress.android.fluxc.store.WCProductStore.OnProductChanged
import javax.inject.Inject
import kotlin.coroutines.resume

class GroupedProductListRepository @Inject constructor(
private val dispatcher: Dispatcher,
private val selectedSite: SelectedSite,
private val productStore: WCProductStore
) {
companion object {
private const val ACTION_TIMEOUT = 10L * 1000
private const val PRODUCT_PAGE_SIZE = WCProductStore.DEFAULT_PRODUCT_PAGE_SIZE
}

private var loadContinuation: CancellableContinuation<Boolean>? = null
private var offset = 0

var canLoadMoreProducts = true
private set

init {
dispatcher.register(this)
}

fun onCleanup() {
dispatcher.unregister(this)
}

/**
* Submits a fetch request to get a page of products for the [groupedProductIds] and returns the full
* list of products from the database
*/
suspend fun fetchGroupedProductList(
groupedProductIds: List<Long>,
loadMore: Boolean = false
): List<Product> {
try {
suspendCancellableCoroutineWithTimeout<Boolean>(ACTION_TIMEOUT) {
offset = if (loadMore) offset + PRODUCT_PAGE_SIZE else 0
loadContinuation = it
val payload = FetchProductsPayload(
selectedSite.get(),
PRODUCT_PAGE_SIZE,
offset,
remoteProductIds = groupedProductIds
)
dispatcher.dispatch(WCProductActionBuilder.newFetchProductsAction(payload))
}
} catch (e: CancellationException) {
WooLog.d(T.PRODUCTS, "CancellationException while fetching grouped products")
}

return getGroupedProductList(groupedProductIds)
}

/**
* Returns all products for the [groupedProductIds] that are in the database
*/
fun getGroupedProductList(groupedProductIds: List<Long>): List<Product> {
return if (selectedSite.exists()) {
val wcProducts = productStore.getProductsByRemoteIds(
selectedSite.get(),
remoteProductIds = groupedProductIds
)
wcProducts.map { it.toAppModel() }
} else {
WooLog.w(T.PRODUCTS, "No site selected - unable to load products")
emptyList()
}
}

@SuppressWarnings("unused")
@Subscribe(threadMode = ThreadMode.MAIN)
fun onProductChanged(event: OnProductChanged) {
if (event.causeOfChange == FETCH_PRODUCTS) {
if (event.isError) {
// TODO: add tracking event
loadContinuation?.resume(false)
} else {
// TODO: add tracking event
canLoadMoreProducts = event.canLoadMore
loadContinuation?.resume(true)
}
loadContinuation = null
}
}
}
Loading

0 comments on commit 1ae7ef2

Please sign in to comment.