How can I combine 3 LiveData into one, I have a snippet in my project where I need to display the result of 3 different requests, into one RecyclerView,
I read about it, and found out that it is possible to combine three LiveData into one, but I do not understand how this can be done using Paging 3.
Sample code in my ViewModel:
#HiltViewModel
class SearchViewModel #Inject constructor(
private val getAllCharacters: GetCharactersUseCase,
private val getAllLocations: GetLocationsUseCase,
private val getAllEpisodes: GetEpisodesUseCase
) : BaseViewModel() {
private val _charactersState = MutableLiveData<PagingData<RickAndMorty>>()
val charactersState: LiveData<PagingData<RickAndMorty>> = _charactersState
private val _locationsState = MutableLiveData<PagingData<RickAndMorty>>()
val locationsState: LiveData<PagingData<RickAndMorty>> = _locationsState
private val _episodesState = MutableLiveData<PagingData<RickAndMorty>>()
val episodesState: LiveData<PagingData<RickAndMorty>> = _episodesState
val liveDataMerger: MediatorLiveData<PagingData<RickAndMorty>> =
MediatorLiveData<PagingData<RickAndMorty>>()
fun merge() {
liveDataMerger.addSource(locationsState) {
liveDataMerger.value = it
}
liveDataMerger.addSource(charactersState) {
liveDataMerger.value = it
}
}
fun processAllRequests(
name: String
) {
viewModelScope.launch {
withContext(Dispatchers.IO) {
getAllCharacters(name, "", "")
}.collect {
_charactersState.value = it as PagingData<RickAndMorty>
}
}
viewModelScope.launch {
withContext(Dispatchers.IO) {
getAllLocations(name, "", "")
}.collect {
_locationsState.value = it as PagingData<RickAndMorty>
}
}
viewModelScope.launch {
withContext(Dispatchers.IO) {
getAllEpisodes(name, "")
}.collect {
_episodesState.value = it as PagingData<RickAndMorty>
}
}
}
}
functions getAllCharacters, getAllLocations, getAllEpisodes are suspend, and all my models are combined into one Sealed class.
example code in Fragment:
#AndroidEntryPoint
class SearchFragment : BaseFragment<FragmentSearchBinding, SearchViewModel>(
R.layout.fragment_search
) {
override val binding by viewBinding(FragmentSearchBinding::bind)
override val viewModel by viewModels<SearchViewModel>()
private val adapter: SearchAdapter = SearchAdapter()
private val args: SearchFragmentArgs by navArgs()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
}
override fun initialize() {
setupRecycler()
}
private fun setupRecycler() = with(binding) {
searchRecycler.layoutManager = LinearLayoutManager(requireContext())
searchRecycler.adapter = adapter.withLoadStateFooter(LoadStateAdapter {
adapter.retry()
})
}
override fun setupRequests() {
viewModel.processAllRequests("")
Log.e("anime", args.filterType)
viewModel.merge()
}
override fun setupObserves() {
viewModel.liveDataMerger.observe(viewLifecycleOwner, {
lifecycleScope.launch {
adapter.submitData(it)
}
})
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
super.onCreateOptionsMenu(menu, inflater)
inflater.inflate(R.menu.toolbar_menu, menu)
val searchItem = menu.findItem(R.id.search)
val searchView = searchItem.actionView as SearchView
searchView.setTools(context)
searchView.submitSearch { viewModel.processAllRequests(it.toString()) }
searchItem.setOnActionExpandListener(searchView) { hideKeyboard() }
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == R.id.filter) {
findNavController().navigate(
SearchFragmentDirections.actionSearchFragmentToFilterDialogFragment(
getString(R.string.search_filter_type)
)
)
}
return super.onOptionsItemSelected(item)
}
}
My Adapter:
class SearchAdapter :
PagingDataAdapter<RickAndMorty, SearchRecyclerViewHolder<ViewBinding>>(
SearchDiffUtil()
) {
class SearchDiffUtil : DiffUtil.ItemCallback<RickAndMorty>() {
override fun areItemsTheSame(oldItem: RickAndMorty, newItem: RickAndMorty): Boolean {
return oldItem == newItem
}
override fun areContentsTheSame(oldItem: RickAndMorty, newItem: RickAndMorty): Boolean {
return oldItem == newItem
}
}
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): SearchRecyclerViewHolder<ViewBinding> {
val inflater = LayoutInflater.from(parent.context)
return when (viewType) {
R.layout.item_characters -> SearchRecyclerViewHolder.CharactersViewHolder(
ItemCharactersBinding.inflate(
inflater,
parent,
false
)
)
R.layout.item_locations -> SearchRecyclerViewHolder.LocationsViewHolder(
ItemLocationsBinding.inflate(
inflater,
parent,
false
)
)
R.layout.item_episodes -> SearchRecyclerViewHolder.EpisodesViewHolder(
ItemEpisodesBinding.inflate(
inflater,
parent,
false
)
)
else -> throw IllegalAccessException("Invalid viewType provided")
}
}
override fun onBindViewHolder(
holder: SearchRecyclerViewHolder<ViewBinding>,
position: Int
) {
when (holder) {
is SearchRecyclerViewHolder.CharactersViewHolder ->
holder.onBind(getItem(position) as RickAndMorty.CharactersItem)
is SearchRecyclerViewHolder.LocationsViewHolder ->
holder.onBind(getItem(position) as RickAndMorty.LocationsItem)
is SearchRecyclerViewHolder.EpisodesViewHolder ->
holder.onBind(getItem(position) as RickAndMorty.EpisodesItem)
}
}
override fun getItemViewType(position: Int): Int {
return when (getItem(position)) {
is RickAndMorty.CharactersItem -> R.layout.item_characters
is RickAndMorty.LocationsItem -> R.layout.item_locations
is RickAndMorty.EpisodesItem -> R.layout.item_episodes
null -> throw IllegalStateException("Unknown view")
}
}
}
Related
I want to navigate 2 different fragments using same adapter according to the type of my model. But my problem is I can't handle onItemClicked with two different models. As you can see:
private val onItemClicked: (WordsWithMeanings) -> Unit
FavoriteFragment
class FavoriteFragment : Fragment(R.layout.fragment_favorite) {
private var _binding: FragmentFavoriteBinding? = null
private val binding get() = _binding!!
private val viewModel: WordViewModel by activityViewModels {
WordViewModelFactory(
(activity?.application as WordApplication).database.wordDao()
)
}
private lateinit var adapter: FavoriteListAdapter
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = FragmentFavoriteBinding.inflate(inflater)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
adapter = FavoriteListAdapter(viewModel) {
val action = FavoriteFragmentDirections.actionFavouriteFragmentToWordDetailFragment(it).
this.findNavController().navigate(action)
}
binding.recyclerView.adapter = adapter
viewModel.allWordsWithMeanings.observe(this.viewLifecycleOwner) { items ->
items.let {
for (i in items) {
if (i.word.isFavorite == true) {
adapter.mData.add(i)
}
}
}
favoriteFragmentFavoriteStatus()
}
viewModel.allProverbs.observe(this.viewLifecycleOwner) { items ->
items.let {
for (i in items) {
if (i.isFavorite == true) {
adapter.mData.add(i)
}
}
}
}
binding.recyclerView.layoutManager = LinearLayoutManager(this.context)
}
}`
```
FavoriteListAdapter
private const val WORD_VIEW_TYPE = 0
private const val PROVERB_VIEW_TYPE = 1
class FavoriteListAdapter(
private val viewModel: WordViewModel,
private val onItemClicked: (WordsWithMeanings) -> Unit
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
val mData = mutableListOf<Any>()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val inflater = LayoutInflater.from(parent.context)
return when (viewType) {
WORD_VIEW_TYPE -> WordItemViewHolder(
FavoriteItemsBinding.inflate(
inflater,
parent,
false
)
)
PROVERB_VIEW_TYPE -> ProverbItemViewHolder(
FavoriteItemsBinding.inflate(
inflater,
parent,
false
)
)
else -> throw IllegalArgumentException("Invalid view type")
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val element = mData[position]
when (getItemViewType(position)) {
WORD_VIEW_TYPE -> {
holder.itemView.setOnClickListener {
onItemClicked(element as WordsWithMeanings)
}
(holder as WordItemViewHolder).bind(
element as WordsWithMeanings,
viewModel
)
}
PROVERB_VIEW_TYPE -> {
(holder as ProverbItemViewHolder).bind(element as Proverb, viewModel)
}
}
}
class WordItemViewHolder(private var binding: FavoriteItemsBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(favoriteWordsWithMeanings: WordsWithMeanings, viewModel: WordViewModel) {
binding.apply {
title.text = favoriteWordsWithMeanings.word.word
content.text = favoriteWordsWithMeanings.means[0].wordMean
favoriteButton.isChecked =
favoriteWordsWithMeanings.word.isFavorite == true
favoriteButton.setOnClickListener {
if (favoriteButton.isChecked) {
favoriteButton.isChecked = true
viewModel.updateFavoriteWord(favoriteWordsWithMeanings, true)
} else {
favoriteButton.isChecked = false
viewModel.updateFavoriteWord(favoriteWordsWithMeanings, false)
}
}
}
}
}
class ProverbItemViewHolder(private var binding: FavoriteItemsBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(favoriteProverb: Proverb, viewModel: WordViewModel) {
binding.apply {
title.text = favoriteProverb.proverb
content.text = favoriteProverb.proverbMean
favoriteButton.isChecked =
favoriteProverb.isFavorite == true
favoriteButton.setOnClickListener {
if (favoriteButton.isChecked) {
favoriteButton.isChecked = true
viewModel.updateFavoriteProverb(favoriteProverb, true)
} else {
favoriteButton.isChecked = false
viewModel.updateFavoriteProverb(favoriteProverb, false)
}
}
}
}
}
override fun getItemCount(): Int {
return mData.size
}
override fun getItemViewType(position: Int): Int {
return when (mData[position]) {
is WordsWithMeanings -> WORD_VIEW_TYPE
is Proverb -> PROVERB_VIEW_TYPE
else -> throw IllegalArgumentException("Unsupported type")
}
}
}
So I only navigate to one fragment. What I want to do is if clicked item is 'WordsWithMeanings' navigate to WordDetailFragment, if clicked item is 'Proverb' navigate to ProverbDetailFragment. Is there proper way to do this?
SOLUTION:
Firstly I changed this
private val onItemClicked: (WordsWithMeanings) -> Unit
to this
private val onItemClicked: (Any) -> Unit
After that using action like this solved my problem.
action = if (it is WordsWithMeanings) {
FavoriteFragmentDirections.actionFavouriteFragmentToWordDetailFragment(it)
} else {
FavoriteFragmentDirections.actionFavouriteFragmentToProverbDetailFragment(it as Proverb)
}
this.findNavController().navigate(action as NavDirections)
You are passing a WordsWithMeaning item in your adapter onItemClicked callback.
You can check if the item passed to your callback is of WordsWithMeaning type or Proverb type and then act accordingly:
adapter = FavoriteListAdapter(viewModel) {
val action = if (it is Proverb) {
// navigate to proverb details
} else {
// navigate to word details
}
}
In my scenario, I have two fragments. In my first Fragment I have a RecyclerView with SelectionTracker. After selecting an item in my first fragment I can navigate to the second fragment. But from my second fragment if I navigate back to the first fragment using back button all my previous selection is lost. How can I prevent this?
I am using Jetpack Navigation Component to handle Fragment navigation.
Here is my fragment
class SelectVideoForCourseFragment : Fragment(R.layout.fragment_select_video_for_course) {
.....
..
private val adapter by lazy { MediaListAdapter() }
private lateinit var selectionTracker: SelectionTracker<String>
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initializeListeners()
initializeRecyclerView(savedInstanceState)
fetchMedias()
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
selectionTracker.onSaveInstanceState(outState)
}
private fun fetchMedias() {
val mediaRepo = MediaRepo()
mediaRepo.getVideos(contentResolver).observe(viewLifecycleOwner) {
adapter.submitItems(it)
}
}
private fun initializeRecyclerView(savedInstanceState: Bundle?) {
binding.mediaList.layoutManager = GridLayoutManager(requireContext(), 3)
binding.mediaList.addItemDecoration(GridSpacingItemDecoration(3, 5.dp, false))
binding.mediaList.adapter = adapter
selectionTracker = SelectionTracker.Builder(
"tracker-select-video-for-course",
binding.mediaList,
MediaItemKeyProvider(adapter),
MediaItemDetailsLookup(binding.mediaList),
StorageStrategy.createStringStorage()
).withSelectionPredicate(
SelectionPredicates.createSelectSingleAnything()
).build()
if (savedInstanceState != null) {
selectionTracker.onRestoreInstanceState(savedInstanceState)
}
adapter.tracker = selectionTracker
}
......
...
}
My RecyclerView Adapter:
class MediaListAdapter: RecyclerView.Adapter<RecyclerView.ViewHolder>() {
val differ = AsyncListDiffer(this, DIFF_CALLBACK_MEDIA)
var tracker: SelectionTracker<String>? = null
fun submitItems(updatedItems: List<Any>) {
differ.submitList(updatedItems)
}
fun getSelections(): List<MediaEntity> {
....
..
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return if (viewType == 1) {
val binding = ListItemImageBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
ViewHolderImage(binding)
} else {
val binding = ListItemVideoBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
ViewHolderVideo(binding)
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
if (holder is ViewHolderImage) {
tracker?.let {
val item = differ.currentList[position] as ImageEntity
holder.bind(item , it.isSelected(item.uri.toString()))
}
} else {
holder as ViewHolderVideo
val item = differ.currentList[position] as VideoEntity
tracker?.let {
holder.bind(item, it.isSelected(item.uri.toString()))
}
}
}
override fun getItemCount(): Int = differ.currentList.size
override fun getItemViewType(position: Int): Int {
return if (differ.currentList[position] is ImageEntity) {
1
} else {
2
}
}
inner class ViewHolderImage(
private val binding: ListItemImageBinding): RecyclerView.ViewHolder(binding.root
) {
fun bind(item: ImageEntity, isSelected: Boolean) {
.....
...
}
fun getItemDetails(): ItemDetailsLookup.ItemDetails<String> =
object : ItemDetailsLookup.ItemDetails<String>() {
override fun getPosition(): Int = absoluteAdapterPosition
override fun getSelectionKey(): String {
val item = differ.currentList[absoluteAdapterPosition] as ImageEntity
return item.uri.toString()
}
override fun inSelectionHotspot(e: MotionEvent): Boolean = true
}
}
inner class ViewHolderVideo(
private val binding: ListItemVideoBinding,
): RecyclerView.ViewHolder(binding.root) {
fun bind(item: VideoEntity, isSelected: Boolean) {
.....
...
}
fun getItemDetails(): ItemDetailsLookup.ItemDetails<String> =
object : ItemDetailsLookup.ItemDetails<String>() {
override fun getPosition(): Int = absoluteAdapterPosition
override fun getSelectionKey(): String {
val item = differ.currentList[absoluteAdapterPosition] as VideoEntity
return item.uri.toString()
}
override fun inSelectionHotspot(e: MotionEvent): Boolean = true
}
}
companion object {
val DIFF_CALLBACK_MEDIA = object: DiffUtil.ItemCallback<Any>() {
.....
...
}
}
}
When I tried running the app, I got this error
I've tried different ways to implement the adapter into the fragment, but it doesn't seem to work
The Adapter
class AsteroidViewAdapter(private val onClickListener: OnClickListener) :
ListAdapter<Asteroid, AsteroidViewAdapter.AsteroidViewHolder>(DiffCallback) {
companion object DiffCallback : DiffUtil.ItemCallback<Asteroid>() {
override fun areItemsTheSame(oldItem: Asteroid, newItem: Asteroid): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: Asteroid, newItem: Asteroid): Boolean {
return oldItem == newItem
}
}
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): AsteroidViewHolder {
return AsteroidViewHolder(
AsteroidListContainerBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
}
override fun onBindViewHolder(holder: AsteroidViewHolder, position: Int) {
holder.bind(getItem(position), onClickListener)
}
class AsteroidViewHolder(private val binding: AsteroidListContainerBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(item: Asteroid, listener: OnClickListener) {
binding.value = item
binding.listener = listener
binding.executePendingBindings()
}
}
class OnClickListener(val clickListener: (asteroid: Asteroid) -> Unit) {
fun onClick(asteroid: Asteroid) = clickListener(asteroid)
}
}
The ViewModel
private const val TAG = "MainViewModel"
class MainViewModel(application: Application) : ViewModel() {
private val database = AsteroidDatabase.getDatabase(application)
private val repository = AsteroidRepository(database.asteroidDao)
private val _pictureOfDay = MutableLiveData<PictureOfDay>()
val pictureOfDay: LiveData<PictureOfDay>
get() = _pictureOfDay
val info: LiveData<List<Asteroid>> = Transformations.map(repository.feeds) {
it
}
private val _showProgress = MutableLiveData(true)
val showProgress: LiveData<Boolean>
get() = _showProgress
private val _navigation: MutableLiveData<Asteroid> = MutableLiveData()
val navigation: LiveData<Asteroid>
get() = _navigation
init {
fetchThePictureOfTheDay()
loadFeeds()
}
private fun loadFeeds() {
_showProgress.value = true
viewModelScope.launch {
try {
repository.fetchFeeds()
}catch (e: Exception) {
Log.e(TAG, e.message, e.cause)
}
}
}
private fun fetchThePictureOfTheDay() {
viewModelScope.launch {
try {
val picture = repository.getPictureOfTheDay()
_pictureOfDay.postValue(picture)
}catch (e: Exception) {
Log.e(TAG, e.message, e.cause)
}
}
}
fun navigateToDetails(asteroid: Asteroid) {
_navigation.value = asteroid
}
fun navigationDone() {
_navigation.value = null
}
fun progress(empty: Boolean) {
_showProgress.value = empty
}
fun filter(filter: Filter) {
viewModelScope.launch(Dispatchers.IO) {
repository.filterFeeds(filter)
}
}
}
The Fragment
class MainFragment : Fragment() {
// private lateinit var manager: RecyclerView.LayoutManager
private lateinit var viewModel: MainViewModel
private lateinit var adapter: AsteroidViewAdapter
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
val mainViewModelFactory = MainViewModelFactory(requireActivity().application)
viewModel = ViewModelProvider(this, mainViewModelFactory).get(MainViewModel::class.java)
val binding = FragmentMainBinding.inflate(inflater)
binding.lifecycleOwner = this
binding.viewModel = viewModel
// val mutableList: MutableList<Asteroid> = ArrayList()
// mutableList.add(Asteroid(1, "fgnuugrhrg", "bihagtyjerwailgubivb", 4.0, 8.0,3.0, 9.0, false))
// mutableList.add(Asteroid(2, "fguk.nuugrhrg", "bidjswjyhagrwailgubivb", 3.0, 90.0,355.0, 9.0, true))
// mutableList.add(Asteroid(3, "fgnssuugrhrg", "bshjtihagrwailgubivb", 4.0, 33.0,33.0, 9.0, false))
// mutableList.add(Asteroid(4, "fgnuw4suugrhrg", "bjsryjihagrwailgubivb", 6.0, 8.0,11.0, 9.0, true))
// mutableList.add(Asteroid(5, "fgnuugrudkdkhrg", "bihjjkkuagrwailgubivb", 4.0, 5.0,77.0, 9.0, false))
// manager = LinearLayoutManager(this.context)
binding.asteroidRecycler.adapter = AsteroidViewAdapter(AsteroidViewAdapter.OnClickListener {
viewModel.navigateToDetails(it)
})
viewModel.info.observe(viewLifecycleOwner, Observer {
viewModel.progress(it.isNullOrEmpty())
binding.asteroidRecycler.smoothScrollToPosition(0)
adapter.submitList(it)
})
viewModel.navigation.observe(viewLifecycleOwner, Observer { asteroid ->
asteroid?.let {
findNavController().navigate(MainFragmentDirections.actionShowDetail(it))
viewModel.navigationDone()
}
})
// binding.asteroidRecycler.adapter = adapter
setHasOptionsMenu(true)
return binding.root
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.main_overflow_menu, menu)
super.onCreateOptionsMenu(menu, inflater)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.menu_show_week -> {
viewModel.filter(Filter.WEEK)
true
}
R.id.menu_show_today -> {
viewModel.filter(Filter.TODAY)
true
}
R.id.menu_show_saved -> {
viewModel.filter(Filter.SAVED)
true
}
else -> false
}
}
}
As per the error message, you've never actually set your adapter to any value - you just directly assign it to binding.asteroidRecycler.adapter.
Since an adapter keeps a hard reference to the RecyclerView itself, you shouldn't hold a reference to it at the fragment level anyways, you should instead create a local variable for it before assigning it to your RecyclerView:
val adapter = binding.asteroidRecycler.adapter = AsteroidViewAdapter(AsteroidViewAdapter.OnClickListener {
viewModel.navigateToDetails(it)
})
binding.asteroidRecycler.adapter = adapter
I have set up a fragment with a recyclerView in it and I fetch data from firestore successfully. What I want to know is that if it is possible to add items at a certain position in recyclerView. Suppose, I want to add an item (from a different collection in Firestore) after every 5 items in a recyclervView. Is it possible to do it in Android using Kotlin?
Thank you.
Edit:
DashboardFragment.kt
class DashboardFragment : BaseFragment() {
var srchProductsList: ArrayList<Product> = ArrayList()
var adList: ArrayList<Ads> = ArrayList()
var srchTempProductsList: ArrayList<Product> = ArrayList()
var newView: String = "ListView"
private lateinit var binding: FragmentDashboardBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentDashboardBinding.inflate(inflater, container, false)
binding.fbDashboard.setImageResource(R.drawable.ic_grid_view)
binding.fbDashboard.setOnClickListener {
if (newView=="ListView"){
newView="GridView"
fb_dashboard.setImageResource(R.drawable.ic_list_view)
}else{
newView="ListView"
fb_dashboard.setImageResource(R.drawable.ic_grid_view)
}
onResume()
}
return binding.root
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
val id = item.itemId
when (id) {
R.id.action_settings -> {
startActivity(Intent(activity, SettingsActivity::class.java))
return true
}
R.id.action_cart -> {
startActivity(Intent(activity, CartListActivity::class.java))
return true
}
}
return super.onOptionsItemSelected(item)
}
override fun onResume() {
super.onResume()
srchProductsList.clear()
srchTempProductsList.clear()
getDashboardItemsList()
}
private fun getDashboardItemsList() {
showProgressDialog(resources.getString(R.string.please_wait))
getDashboardItemsList2()
}
fun successDashboardItemsList(dashboardItemsList: ArrayList<Product>) {
val adsLists =getListOfAds()
hideProgressDialog()
if (dashboardItemsList.size > 0) {
Toast.makeText(
context,
"Total " + dashboardItemsList.size + " products loaded",
Toast.LENGTH_LONG
).show()
rv_dashboard_items.visibility = View.VISIBLE
tv_no_dashboard_items_found.visibility = View.GONE
rv_dashboard_items.layoutManager =
StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL)
rv_dashboard_items.setHasFixedSize(true)
val adapter = DashboardItemsListAdapterTest(requireActivity(), dashboardItemsList,adsLists)
rv_dashboard_items.adapter = adapter
//////// I HAVE PROBLEM WITH THE FOLLOWING BLOCK OF CODE WHICH IS WHY I HAVE COMMENTED IT OUT ONLY TO CHECK IF OTHER PART OF THE CODE IS WORKING. I NEED TO FIX THE ERROR FOR THE BELOW BLOCK OF CODE ALSO
/* adapter.setOnClickListener(object :
DashboardItemsListAdapter.OnClickListener {
override fun onClick(position: Int, product: Product) {
val intent = Intent(context, ProductDetailsActivity::class.java)
intent.putExtra(Constants.EXTRA_PRODUCT_ID, product.product_id)
intent.putExtra(Constants.EXTRA_PRODUCT_OWNER_ID, product.user_id)
startActivity(intent)
}
})*/
} else {
rv_dashboard_items.visibility = View.GONE
tv_no_dashboard_items_found.visibility = View.VISIBLE
}
}
fun successDashboardItemsListListView(dashboardItemsList: ArrayList<Product>) {
val adsLists =getListOfAds()
hideProgressDialog()
if (dashboardItemsList.size > 0) {
Toast.makeText(
context,
"Total " + dashboardItemsList.size + " products loaded",
Toast.LENGTH_LONG
).show()
rv_dashboard_items.visibility = View.VISIBLE
tv_no_dashboard_items_found.visibility = View.GONE
rv_dashboard_items.layoutManager =
LinearLayoutManager(context)
rv_dashboard_items.setHasFixedSize(true)
val adapter = DashboardItemsListAdapterTest(requireActivity(), dashboardItemsList,adsLists)
rv_dashboard_items.adapter = adapter
//////// I HAVE PROBLEM WITH THE FOLLOWING BLOCK OF CODE WHICH IS WHY I HAVE COMMENTED IT OUT ONLY TO CHECK IF OTHER PART OF THE CODE IS WORKING. I NEED TO FIX THE ERROR FOR THE BELOW BLOCK OF CODE ALSO
/* adapter.setOnClickListener(object :
DashboardItemsListAdapter.OnClickListener {
override fun onClick(position: Int, product: Product) {
val intent = Intent(context, ProductDetailsActivity::class.java)
intent.putExtra(Constants.EXTRA_PRODUCT_ID, product.product_id)
intent.putExtra(Constants.EXTRA_PRODUCT_OWNER_ID, product.user_id)
startActivity(intent)
}
})*/
} else {
rv_dashboard_items.visibility = View.GONE
tv_no_dashboard_items_found.visibility = View.VISIBLE
}
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.dashboard_menu, menu)
super.onCreateOptionsMenu(menu, inflater)
val item = menu.findItem(R.id.my_search_bar)
val searchView = item?.actionView as SearchView
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String?): Boolean {
srchTempProductsList.clear()
val searchKey = query
if (searchKey != null) {
if (searchKey.isNotEmpty()) {
srchProductsList.forEach {
if (it.description.toLowerCase(Locale.getDefault())
.contains(searchKey)
) {
srchTempProductsList.add(it)
}
}
rv_dashboard_items.adapter!!.notifyDataSetChanged()
} else {
srchTempProductsList.clear()
srchTempProductsList.addAll(srchProductsList)
rv_dashboard_items.adapter!!.notifyDataSetChanged()
}
}
return false
}
override fun onQueryTextChange(newText: String?): Boolean {
srchTempProductsList.clear()
val searchText = newText!!.toLowerCase(Locale.getDefault())
if (searchText.isNotEmpty()) {
srchProductsList.forEach {
if (it.description.toLowerCase(Locale.getDefault()).contains(searchText)) {
srchTempProductsList.add(it)
}
}
rv_dashboard_items.adapter!!.notifyDataSetChanged()
} else {
srchTempProductsList.clear()
srchTempProductsList.addAll(srchProductsList)
rv_dashboard_items.adapter!!.notifyDataSetChanged()
}
return false
}
})
}
private fun getDashboardItemsList2() {
val mFireStore = FirebaseFirestore.getInstance()
mFireStore.collection(Constants.PRODUCTS)
.get()
.addOnSuccessListener { document ->
for (i in document.documents) {
val product = i.toObject(Product::class.java)!!
product.product_id = i.id
srchProductsList.add(product)
}
srchTempProductsList.addAll(srchProductsList)
if (newView == "ListView") {
successDashboardItemsListListView(srchTempProductsList)
} else {
successDashboardItemsList(srchTempProductsList)
}
}
.addOnFailureListener {
}
}
private fun getListOfAds() : ArrayList<Ads>{
val mFireStore = FirebaseFirestore.getInstance()
mFireStore.collection("ads")
.get()
.addOnSuccessListener { document ->
for (i in document.documents) {
val ad = i.toObject(Ads::class.java)!!
ad.ad_id = i.id
adList.add(ad)
}
}
.addOnFailureListener {
}
return adList
}
}
DashboardItemListAdapterTest.kt
open class DashboardItemsListAdapterTest(
private val context: Context,
private var prodlist: ArrayList<Product>,
private var adslist: ArrayList<Ads>) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
companion object {
const val produc= 1
const val ads= 2
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return if (viewType == produc) {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_dashboard_list_view_layout, parent, false)
Collection1Holder(view)
} else {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_dashboard_ad_view_layout, parent, false)
Collection2Holder(view)
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val promodel = prodlist[position]
val adsmodel = adslist[position]
if(getItemViewType(position) == produc) {
holder.itemView.tv_item_name.text = promodel.title
}else{
holder.itemView.tv_item_name.text = adsmodel.title
}
}
override fun getItemCount(): Int {
return prodlist.size + adslist.size
}
override fun getItemViewType(position: Int): Int {
return if(position%5 == 0) ads else produc
}
inner class Collection1Holder(itemView: View) : RecyclerView.ViewHolder(itemView) {
}
inner class Collection2Holder(itemView: View) : RecyclerView.ViewHolder(itemView) {
}
}
Model class
Ads.kt
data class Ads(
val title: String = "",
var ad_id: String = ""
)
You can use two viewholder for two different collections of data.
Change your adapter class like this.
class YourAdapter() : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
companion object {
const val COLLCTION1= 1
const val COLLCTION2= 2
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return if (viewType == COLLCTION1) {
val view = LayoutInflater.from(viewGroup.context)
.inflate(R.layout.collection1, viewGroup, false)
Collection1Holder(view)
} else {
val view = LayoutInflater.from(viewGroup.context)
.inflate(R.layout.collection2, viewGroup, false)
Collection2Holder(view)
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
if(getItemViewType(position) == COLLCTION1) {
holder.name.text = collection1.text
}else{
holder.name.text = collection2.text
}
}
override fun getItemCount(): Int {
return collection1.size + collection2.size
}
override fun getItemViewType(position: Int): Int {
return if(position%5 == 0) COLLCTION2 else COLLCTION1
}
inner class Collection1Holder(itemView: View) : RecyclerView.ViewHolder(itemView) {
}
inner class Collection2Holder(itemView: View) : RecyclerView.ViewHolder(itemView) {
}
}
I have a list.I am sorting that list based on the selected chip.Here is my sorting code:
SortingUtil
enum class SortingUtil {
DESCBYTOTALCASES,
ASCBYTOTALCASES,
DESCBYDEATHCASES,
ASCBYDEATHCASES,
DESCBYALPHA,
ASCBYALPHA
}
CountriesFragment
#Inject
lateinit var mAdapter: CountriesFragmentAdapter
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initRecycler()
observeViewModel()
initChips()
}
private fun initChips() {
binding.chipGroup.setOnCheckedChangeListener { _, checkedId ->
when (checkedId) {
R.id.totalDesc -> {
mViewModel.rearangeCountries(SortingUtil.DESCBYTOTALCASES)
}
R.id.totalAsc -> {
mViewModel.rearangeCountries(SortingUtil.ASCBYTOTALCASES)
}
R.id.deathDesc -> {
mViewModel.rearangeCountries(SortingUtil.DESCBYDEATHCASES)
}
R.id.deathAsc -> {
mViewModel.rearangeCountries(SortingUtil.ASCBYDEATHCASES)
}
R.id.alphaDesc -> {
mViewModel.rearangeCountries(SortingUtil.DESCBYALPHA)
}
R.id.alphaAsc -> {
mViewModel.rearangeCountries(SortingUtil.ASCBYALPHA)
}
}
}
}
private fun observeViewModel() {
mViewModel.countries.observe(viewLifecycleOwner) { resource ->
when (resource) {
is Resource.Loading -> showProgress()
is Resource.Error -> {
hideProgress()
SnackUtil.showSnackbar(
requireContext(),
requireView(),
resource.message.toString(),
R.color.color_danger
)
}
is Resource.Success -> {
hideProgress()
mViewModel.setCountryList(resource.data!!)
}
}
}
mViewModel.mediatorLiveData.observe(viewLifecycleOwner) { countryList ->
mAdapter.diff.submitList(countryList)
}
}
CountriesFragmentViewModel
#HiltViewModel
class CountriesFragmentViewModel #Inject constructor(
private val repo: Repo
) : ViewModel() {
private val _countries = MutableLiveData<Resource<List<CountryResponse>>>()
val countries: LiveData<Resource<List<CountryResponse>>> get() = _countries
val mediatorLiveData = MediatorLiveData<List<CountryResponse>>()
private val countryList = MutableLiveData<List<CountryResponse>>()
private var currentOrder = SortingUtil.DESCBYTOTALCASES
init {
getCountries()
mediatorLiveData.addSource(countryList) { countries ->
countries.let {
mediatorLiveData.value = sortList(it, currentOrder)
}
}
}
fun rearangeCountries(order: SortingUtil) = countryList.value.let {
mediatorLiveData.value = sortList(it!!, order)
}.also {
currentOrder = order
}
fun getCountries() {
viewModelScope.launch {
_countries.value = repo.getCountries()
}
}
fun setCountryList(list: List<CountryResponse>) {
countryList.value = list
}
private fun sortList(
countryList: List<CountryResponse>,
order: SortingUtil
): List<CountryResponse> {
when (order) {
SortingUtil.DESCBYTOTALCASES -> {
return countryList.sortedByDescending {
it.cases
}
}
SortingUtil.ASCBYTOTALCASES -> {
return countryList.sortedBy {
it.cases
}
}
SortingUtil.DESCBYDEATHCASES -> {
return countryList.sortedByDescending {
it.deaths
}
}
SortingUtil.ASCBYDEATHCASES -> {
return countryList.sortedBy {
it.deaths
}
}
SortingUtil.DESCBYALPHA -> {
return countryList.sortedByDescending {
it.country
}
}
SortingUtil.ASCBYALPHA -> {
return countryList.sortedBy {
it.country
}
}
}
}
}
CountriesFragmentAdapter
#FragmentScoped
class CountriesFragmentAdapter #Inject constructor() :
RecyclerView.Adapter<CountriesFragmentAdapter.ViewHolder>() {
private val differCallback = object : DiffUtil.ItemCallback<CountryResponse>() {
override fun areItemsTheSame(oldItem: CountryResponse, newItem: CountryResponse): Boolean =
oldItem.countryInfo.id == newItem.countryInfo.id
override fun areContentsTheSame(
oldItem: CountryResponse,
newItem: CountryResponse
): Boolean = oldItem == newItem
}
val diff = AsyncListDiffer(this, differCallback)
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): CountriesFragmentAdapter.ViewHolder {
return ViewHolder(
CountryItemLayoutBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
}
override fun onBindViewHolder(holder: CountriesFragmentAdapter.ViewHolder, position: Int) {
val currentItem = diff.currentList[position]
holder.bind(currentItem)
}
override fun getItemCount(): Int = diff.currentList.size
inner class ViewHolder(private val binding: CountryItemLayoutBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(countryResponse: CountryResponse) {
binding.apply {
Glide.with(itemView.context)
.load(countryResponse.countryInfo.flag)
.into(binding.countryFlagIv)
countryNameTv.text = countryResponse.country
totalCasesTv.text = formatStat(countryResponse.cases)
totalDeathsTv.text = formatStat(countryResponse.deaths)
}
}
private fun formatStat(stat: Long): String = "%,d".format(stat)
}
}
My sorting logic is working as expected.My problem is when my items rearranged, my recyclerview is getting just some random position.I want to scroll my recyclerview to the top when items rearranged.I have also tried binding.countryRv.scrollToPosition(0) but it did not work.