Skip to content

Instantly share code, notes, and snippets.

@Ajimi
Last active December 4, 2019 21:56
Show Gist options
  • Select an option

  • Save Ajimi/6505df8a63a9cc2e951ac043b6e278b0 to your computer and use it in GitHub Desktop.

Select an option

Save Ajimi/6505df8a63a9cc2e951ac043b6e278b0 to your computer and use it in GitHub Desktop.
package com.esprit.core.util
open class Event<out T>(private val content: T) {
var consumed = false
private set // Allow external read but not write
/**
* Consumes the content if it's not been consumed yet.
* @return The unconsumed content or `null` if it was consumed already.
*/
fun consume(): T? {
return if (consumed) {
null
} else {
consumed = true
content
}
}
/**
* @return The content whether it's been handled or not.
*/
fun peek(): T = content
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Event<*>
if (content != other.content) return false
if (consumed != other.consumed) return false
return true
}
override fun hashCode(): Int {
var result = content?.hashCode() ?: 0
result = 31 * result + consumed.hashCode()
return result
}
}
package com.esprit.core.util
import androidx.lifecycle.Observer
class EventObserver<T>(private val onEventUnconsumedContent: (T) -> Unit) : Observer<Event<T>> {
override fun onChanged(event: Event<T>?) {
event?.consume()?.run(onEventUnconsumedContent)
}
}
package com.esprit.core.extensions
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModel
import com.esprit.core.util.Event
fun <T> ViewModel.emitUiState(
showProgress: Boolean = false,
showError: Event<String>? = null,
showSuccess: Event<T>? = null
): UiState<T>? = UiState<T>(
showProgress = showProgress,
showError = showError,
showSuccess = showSuccess
)
data class UiState<T>(
var showProgress: Boolean,
var showError: Event<String>?,
var showSuccess: Event<T>?
)
fun <T> Fragment.observeUIState(
uiModel: UiState<T>,
progress: (Boolean) -> Unit,
error: (String) -> Unit,
success: (T) -> Unit
) {
progress(uiModel.showProgress)
if (uiModel.showError != null && !uiModel.showError!!.consumed) {
uiModel.showError!!.consume()?.let {
error(it)
}
}
if (uiModel.showSuccess != null && !uiModel.showSuccess!!.consumed) {
uiModel.showSuccess!!.consume()?.let {
success(it)
}
}
}
fun <T> AppCompatActivity.observeUIState(
uiModel: UiState<T>,
progress: (Boolean) -> Unit,
error: (String) -> Unit,
success: (T) -> Unit
) {
progress(uiModel.showProgress)
if (uiModel.showError != null && !uiModel.showError!!.consumed) {
uiModel.showError!!.consume()?.let {
error(it)
}
}
if (uiModel.showSuccess != null && !uiModel.showSuccess!!.consumed) {
uiModel.showSuccess!!.consume()?.let {
success(it)
}
}
}
package com.esprit.marketo.ui.home
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.net.ConnectivityManager
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer
import androidx.recyclerview.widget.LinearLayoutManager
import com.esprit.core.data.pavilion.model.Pavilion
import com.esprit.core.data.products.model.Product
import com.esprit.core.extensions.observeUIState
import com.esprit.core.util.EventObserver
import com.esprit.core.util.gone
import com.esprit.core.util.visible
import com.esprit.marketo.GlideApp
import com.esprit.marketo.R
import com.esprit.marketo.ui.MainActivity
import com.esprit.marketo.ui.MainNavigationFragment
import com.esprit.marketo.ui.MainViewModel
import com.esprit.marketo.ui.category.CategoryActivity
import com.esprit.marketo.ui.home.adapters.ProductDiscountItem
import com.esprit.marketo.ui.home.adapters.ProductTrendingItem
import com.esprit.marketo.ui.pavilion.adapters.PavilionItem
import com.esprit.marketo.ui.productdetail.ProductDetailActivity
import com.google.android.material.snackbar.Snackbar
import com.synnapps.carouselview.ImageListener
import com.xwray.groupie.GroupAdapter
import com.xwray.groupie.ViewHolder
import kotlinx.android.synthetic.main.error_network_state.*
import kotlinx.android.synthetic.main.fragment_home.*
import kotlinx.android.synthetic.main.include_category_home.*
import kotlinx.android.synthetic.main.include_discount_product_home.*
import kotlinx.android.synthetic.main.include_image_carousel.*
import kotlinx.android.synthetic.main.include_trending_product_home.*
import org.koin.androidx.viewmodel.ext.android.sharedViewModel
import org.koin.androidx.viewmodel.ext.android.viewModel
class HomeFragment : Fragment(), MainNavigationFragment {
val homeViewModel: HomeViewModel by viewModel()
val mainViewModel: MainViewModel by sharedViewModel()
private val trendingProductAdapter = GroupAdapter<ViewHolder>()
private val discountedProductAdapter = GroupAdapter<ViewHolder>()
private val pavilionAdapter = GroupAdapter<ViewHolder>()
private var connectionStateReceiver: BroadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val notConnected = intent.getBooleanExtra(
ConnectivityManager
.EXTRA_NO_CONNECTIVITY, false
)
if (notConnected) {
// showErrorState()
} else {
homeViewModel.updateUI()
hideErrorState()
}
}
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_home, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupAppbar()
initUI()
homeViewModel.uiDiscountedState.observe(this, Observer {
val uiModel = it ?: return@Observer
observeUIState(uiModel, {
if (it) {
discount_product_home_recycler_view.gone()
discount_shimmer.apply {
visible()
startShimmer()
}
} else {
discount_product_home_recycler_view.visible()
discount_shimmer.apply {
gone()
stopShimmer()
}
swipe_refresh_home.isRefreshing = false
}
}, {
swipe_refresh_home.isRefreshing = false
}, {
swipe_refresh_home.isRefreshing = false
discountedProductAdapter.clear()
val discountedProductsItems =
it.map {
ProductDiscountItem(
requireContext(),
it
) { isChecked, product ->
homeViewModel.toggleFavorite(isChecked, product)
}
}
discountedProductAdapter.addAll(discountedProductsItems)
})
})
homeViewModel.uiTrendingState.observe(this, Observer {
val uiModel = it ?: return@Observer
observeUIState(uiModel, {
if (it) {
trending_product_home_recycler_view.gone()
trending_shimmer.apply {
visible()
startShimmer()
}
} else {
trending_product_home_recycler_view.visible()
trending_shimmer.apply {
gone()
stopShimmer()
}
swipe_refresh_home.isRefreshing = false
}
}, {
swipe_refresh_home.isRefreshing = false
}, { products ->
trendingProductAdapter.clear()
if (!products.isEmpty()) {
val trendingProductsItems = (1..5).map { index ->
val productTrendingItem = ProductTrendingItem(
requireContext(),
products[index]
) { isChecked, product ->
homeViewModel.toggleFavorite(isChecked, product)
}
productTrendingItem
}.toList()
trendingProductAdapter.addAll(trendingProductsItems)
}
})
})
homeViewModel.uiProductState.observe(this, Observer {
val uiModel = it ?: return@Observer
observeUIState(uiModel, {
}, {
}, {
})
})
homeViewModel.uiPavilionsState.observe(this, Observer {
val uiModel = it ?: return@Observer
observeUIState(uiModel, {
if (it) {
pavilion_recycler_view.gone()
pavilion_shimmer.apply {
visible()
startShimmer()
}
} else {
pavilion_recycler_view.visible()
pavilion_shimmer.apply {
gone()
stopShimmer()
}
swipe_refresh_home.isRefreshing = false
}
}, {
swipe_refresh_home.isRefreshing = false
}, {
pavilionAdapter.clear()
it.forEach {
pavilionAdapter.add(PavilionItem(it, requireContext()))
}
swipe_refresh_home.isRefreshing = false
})
})
homeViewModel.shouldDisplayError.observe(this, EventObserver {
if (it) {
showErrorState()
} else {
hideErrorState()
}
})
layout_no_connection.setOnClickListener {
layout_no_connection.gone()
progress_bar.visible()
homeViewModel.updateUI()
}
swipe_refresh_home.setOnRefreshListener {
homeViewModel.updateUI()
}
trendingProductAdapter.setOnItemClickListener { item, _ ->
homeViewModel.onTrendingProductClicked((item as ProductTrendingItem).product)
}
discountedProductAdapter.setOnItemClickListener { item, _ ->
homeViewModel.onDiscountedProductClicked((item as ProductDiscountItem).product)
}
pavilionAdapter.setOnItemClickListener { item, _ ->
homeViewModel.onCategoryClicked((item as PavilionItem).pavilion)
}
homeViewModel.navigateToProductDetailAction.observe(this, EventObserver { product ->
openProductActivity(product)
})
homeViewModel.navigateToCategoryAction.observe(this, EventObserver { pavilion ->
openCategoryActivity(pavilion)
})
homeViewModel.uiFavoriteSnackBar.observe(this, EventObserver {
val snackBar = Snackbar.make(
(requireActivity() as MainActivity).findViewById(R.id.navigation),
it,
Snackbar.LENGTH_LONG
)
snackBar.show()
})
view_all_categories.setOnClickListener {
mainViewModel.onViewAllPavilionClicked()
}
}
private fun showErrorState() {
swipe_refresh_home.gone()
progress_bar.gone()
network_errorstate.visible()
layout_no_connection.visible()
swipe_refresh_home.isRefreshing = false
}
private fun hideErrorState() {
network_errorstate.gone()
layout_no_connection.gone()
swipe_refresh_home.visible()
swipe_refresh_home.isRefreshing = false
}
private fun openProductActivity(product: Product) {
requireActivity().run {
startActivity(ProductDetailActivity.starterIntent(this, product))
}
}
private fun openCategoryActivity(pavilion: Pavilion) {
requireActivity().run {
startActivity(CategoryActivity.starterIntent(this, pavilion))
}
}
private fun initUI() {
initCarousel()
initTrendingProducts()
initDiscountedProducts()
initPavilion()
}
private fun initCarousel() {
val list = listOf(R.drawable.grocery1, R.drawable.grocery2, R.drawable.grocory3)
val imageListener = ImageListener { position, imageView ->
GlideApp
.with(requireContext())
.load(list[position])
.into(imageView)
}
carouselView.setImageListener(imageListener)
carouselView.pageCount = list.size
}
private fun initTrendingProducts() {
trending_product_home_recycler_view.apply {
layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
adapter = trendingProductAdapter
}
}
private fun initDiscountedProducts() {
discount_product_home_recycler_view.apply {
layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
adapter = discountedProductAdapter
}
}
private fun initPavilion() {
pavilion_recycler_view.apply {
layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
adapter = pavilionAdapter
}
}
private fun setupAppbar() {
home_toolbar.title = "HOME"
}
override fun onStart() {
super.onStart()
requireContext().registerReceiver(
connectionStateReceiver,
IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)
)
homeViewModel.updateUI()
}
override fun onPause() {
super.onPause()
carouselView.pauseCarousel()
}
override fun onResume() {
super.onResume()
carouselView.playCarousel()
}
override fun onStop() {
super.onStop()
requireContext().unregisterReceiver(connectionStateReceiver)
}
companion object {
fun newInstance() = HomeFragment()
}
}
package com.esprit.marketo.ui.home
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.esprit.core.data.Result
import com.esprit.core.data.favorite.local.FavoriteProduct
import com.esprit.core.data.pavilion.model.Pavilion
import com.esprit.core.data.products.model.Product
import com.esprit.core.domain.usecases.favorite.AddFavoriteProductUseCase
import com.esprit.core.domain.usecases.favorite.IsFavoriteProductExistUseCase
import com.esprit.core.domain.usecases.favorite.LoadFavoriteProductsUseCase
import com.esprit.core.domain.usecases.favorite.RemoveFavoriteProductUseCase
import com.esprit.core.domain.usecases.pavilion.LoadPavilionUseCase
import com.esprit.core.domain.usecases.products.LoadDiscountedProductUseCase
import com.esprit.core.domain.usecases.products.LoadProductsUseCase
import com.esprit.core.domain.usecases.products.LoadTrendingProductUseCase
import com.esprit.core.extensions.UiState
import com.esprit.core.extensions.emitUiState
import com.esprit.core.util.Event
import kotlinx.coroutines.*
class HomeViewModel(
private val loadProductsUseCase: LoadProductsUseCase,
private val loadTrendingProductUseCase: LoadTrendingProductUseCase,
private val loadDiscountedProductUseCase: LoadDiscountedProductUseCase,
private val loadPavilionUseCase: LoadPavilionUseCase,
private val addFavoriteProductUseCase: AddFavoriteProductUseCase,
private val removeFavoriteProductUseCase: RemoveFavoriteProductUseCase,
private val loadFavoriteProductsUseCase: LoadFavoriteProductsUseCase,
private val isFavoriteProductExistUseCase: IsFavoriteProductExistUseCase
) : ViewModel(), HomeEventListener {
private val viewModelJob = Job()
private val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)
private val _navigateToProductDetailAction = MutableLiveData<Event<Product>>()
val navigateToProductDetailAction: LiveData<Event<Product>>
get() = _navigateToProductDetailAction
private val _navigateToCategoryAction = MutableLiveData<Event<Pavilion>>()
val navigateToCategoryAction: LiveData<Event<Pavilion>>
get() = _navigateToCategoryAction
private val _uiProductState = MutableLiveData<UiState<List<Product>>>()
val uiProductState: LiveData<UiState<List<Product>>>
get() = _uiProductState
private val _uiPavilionsState = MutableLiveData<UiState<List<Pavilion>>>()
val uiPavilionsState: LiveData<UiState<List<Pavilion>>>
get() = _uiPavilionsState
private val _uiTrendingState = MutableLiveData<UiState<List<Product>>>()
val uiTrendingState: LiveData<UiState<List<Product>>>
get() = _uiTrendingState
private val _uiDiscountedState = MutableLiveData<UiState<List<Product>>>()
val uiDiscountedState: LiveData<UiState<List<Product>>>
get() = _uiDiscountedState
private val _favoritesProducts = MutableLiveData<List<FavoriteProduct>>()
val favoritesProducts: LiveData<List<FavoriteProduct>>
get() = _favoritesProducts
private val _displayError = MutableLiveData<Event<Boolean>>()
val shouldDisplayError: LiveData<Event<Boolean>>
get() = _displayError
private val _uiFavoriteSnackBar = MutableLiveData<Event<String>>()
val uiFavoriteSnackBar: LiveData<Event<String>>
get() = _uiFavoriteSnackBar
fun updateUI() {
loadAllPavilions()
loadAllProducts()
loadTrendingProducts()
loadDiscountedProducts()
}
private fun loadFavoriteProducts() = uiScope.launch(Dispatchers.IO) {
// val favoriteProducts = loadFavoriteProductsUseCase()
withContext(Dispatchers.Main) {
// _favoriteProducts.value = favoriteProducts.value
}
}
private fun loadDiscountedProducts() = uiScope.launch(Dispatchers.IO) {
withContext(Dispatchers.Main) {
_uiDiscountedState.value = emitUiState(showProgress = true)
}
val result = loadDiscountedProductUseCase()
when (result) {
is Result.Success -> {
val checkedFavorite = async {
result.data.map {
it.isFavorite = isFavoriteProductExistUseCase(it)
return@map it
}.toList()
}
withContext(Dispatchers.Main) {
_uiDiscountedState.value = emitUiState(showSuccess = Event(checkedFavorite.await()))
delay(300)
_displayError.value = Event(false)
}
}
is Result.Error -> withContext(Dispatchers.Main) {
_uiDiscountedState.value = emitUiState(showError = Event(result.toString()))
delay(300)
_displayError.value = Event(true)
}
}
}
private fun loadTrendingProducts() = uiScope.launch(Dispatchers.IO) {
withContext(Dispatchers.Main) {
_uiTrendingState.value = emitUiState(showProgress = true)
}
val result = loadTrendingProductUseCase()
when (result) {
is Result.Success -> {
val checkedFavorite = async {
result.data.map {
it.isFavorite = isFavoriteProductExistUseCase(it)
return@map it
}.toList()
}
withContext(Dispatchers.Main) {
_uiTrendingState.value = emitUiState(showSuccess = Event(checkedFavorite.await()))
delay(300)
_displayError.value = Event(false)
}
}
is Result.Error -> withContext(Dispatchers.Main) {
_uiTrendingState.value = emitUiState(showError = Event(result.toString()))
delay(300)
_displayError.value = Event(true)
}
}
}
private fun loadAllProducts() = uiScope.launch(Dispatchers.IO) {
withContext(Dispatchers.Main) {
_uiProductState.value = emitUiState(showProgress = true)
}
val result = loadProductsUseCase()
when (result) {
is Result.Success -> {
withContext(Dispatchers.Main) {
_uiProductState.value = emitUiState(showSuccess = Event(result.data))
delay(300)
_displayError.value = Event(false)
}
}
is Result.Error -> withContext(Dispatchers.Main) {
_uiProductState.value = emitUiState(showError = Event(result.toString()))
delay(300)
_displayError.value = Event(true)
}
}
}
private fun loadAllPavilions() = uiScope.launch(Dispatchers.IO) {
withContext(Dispatchers.Main) {
_uiPavilionsState.value = emitUiState(showProgress = true)
}
val result = loadPavilionUseCase()
when (result) {
is Result.Success -> {
withContext(Dispatchers.Main) {
_uiPavilionsState.value = emitUiState(showSuccess = Event(result.data))
delay(300)
_displayError.value = Event(false)
}
}
is Result.Error -> withContext(Dispatchers.Main) {
_uiPavilionsState.value = emitUiState(showError = Event(result.toString()))
delay(300)
_displayError.value = Event(true)
}
}
}
override fun onCategoryClicked(pavilion: Pavilion) {
_navigateToCategoryAction.postValue(Event(pavilion))
}
override fun onTrendingProductClicked(product: Product) {
_navigateToProductDetailAction.postValue(Event(product))
}
override fun onDiscountedProductClicked(product: Product) {
_navigateToProductDetailAction.postValue(Event(product))
}
override fun onCleared() {
super.onCleared()
viewModelJob.cancel()
}
fun toggleFavorite(isChecked: Boolean, product: Product) {
val message = if (isChecked) {
addToFavorite(product)
"Added successfully"
} else {
removeFromFavorite(product)
"Removed from products"
}
_uiFavoriteSnackBar.value = Event(message)
}
fun addToFavorite(product: Product) {
val favoriteProduct =
FavoriteProduct(product.id, product.name, product.image)
uiScope.launch(Dispatchers.IO) {
addFavoriteProductUseCase(favoriteProduct)
}
}
fun removeFromFavorite(product: Product) {
uiScope.launch(Dispatchers.IO) {
removeFavoriteProductUseCase(
FavoriteProduct(
product.id,
product.name,
product.image
)
)
}
}
}
interface HomeEventListener {
fun onTrendingProductClicked(product: Product)
fun onCategoryClicked(pavilion: Pavilion)
fun onDiscountedProductClicked(product: Product)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment