I have this fragment, its viewmodel, and its adapter. It can already listen to a click, but all I know to do on this is to show a toast. I want it to go to another fragment which will show its "details", which passes the data of the clicked recyclerview item to that "details" fragment. Hint: on the fragment, there's a //TODO there, and I need the code for that.
Here's the fragment:
class HomeFragment : Fragment(), RecyclerViewClickListener {
private lateinit var factory: HomeViewModelFactory
private lateinit var viewModel: HomeViewModel
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_home, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?)
{
super.onActivityCreated(savedInstanceState)
val api = DormsAPI()
val repository = DormRepository(api)
factory = HomeViewModelFactory(repository)
viewModel = ViewModelProviders.of(this, factory).get(HomeViewModel::class.java)
viewModel.getDorms()
viewModel.dorms.observe(viewLifecycleOwner, Observer { dorms ->
recyclerViewDorms.also{
it.layoutManager = LinearLayoutManager(requireContext())
it.setHasFixedSize(true)
it.adapter = dormAdapter(dorms, this)
}
})
}
override fun onRecyclerViewItemClick(view: View, dorms: Dorms) {
when(view.id){
R.id.button_reserve -> {
// TODO: Go to new account if not signed up, etc...
Toast.makeText(requireContext(), "Reserve button clicked", Toast.LENGTH_LONG).show()
}
R.id.layoutBox -> {
// TODO: Go to Dorm Details
Toast.makeText(requireContext(), "Go to dorm details", Toast.LENGTH_LONG).show()
}
}
}
}
As for the Adapter class:
class dormAdapter(
private val dorms: List<Dorms>,
private val listener: RecyclerViewClickListener
) : RecyclerView.Adapter<dormAdapter.DormViewHolder>() {
override fun getItemCount() = dorms.size
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
DormViewHolder(
DataBindingUtil.inflate(
LayoutInflater.from(parent.context),
R.layout.layout_home,
parent, false
)
)
override fun onBindViewHolder(holder: DormViewHolder, position: Int) {
holder.recyclerviewDormBinding.dorm = dorms[position]
holder.recyclerviewDormBinding.buttonReserve.setOnClickListener {
listener.onRecyclerViewItemClick(holder.recyclerviewDormBinding.buttonReserve, dorms[position])
}
holder.recyclerviewDormBinding.layoutBox.setOnClickListener {
listener.onRecyclerViewItemClick(holder.recyclerviewDormBinding.layoutBox, dorms[position])
}
}
inner class DormViewHolder(
val recyclerviewDormBinding: LayoutHomeBinding
) : RecyclerView.ViewHolder(recyclerviewDormBinding.root)
}
Finally, here's the ViewModel:
class HomeViewModel(private val repository: DormRepository) : ViewModel() {
private lateinit var job: Job
private val _dorms = MutableLiveData<List<Dorms>>()
val dorms: LiveData<List<Dorms>>
get() = _dorms
fun getDorms() {
job = Coroutines.ioThenMain(
{ repository.getDorms() },
{ _dorms.value = it }
)
}
override fun onCleared() {
super.onCleared()
if(::job.isInitialized) job.cancel()
}
}
EDIT: I also have this interface, if needed:
interface RecyclerViewClickListener {
fun onRecyclerViewItemClick(view: View, dorms: Dorms)
}
Since we are adding the HomeFragment from an activity, what we will try to do is create a interface to communicate between activity and fragment.
1. Create an interface
class HomeFragment : Fragment(), RecyclerViewClickListener {
...
...
private var callback : Callback? = null
...
...
override fun onAttach(context: Context) {
...
// Callback instance is initialized
if(context is Callback) callback = context
else throw RuntimeException("$context must implement Callback")
}
...
...
override fun onDetach() {
callback = null
}
...
...
override fun onRecyclerViewItemClick(view: View, dorms: Dorms) {
when(view.id){
R.id.button_reserve -> {
// TODO: Go to new account if not signed up, etc...
Toast.makeText(requireContext(), "Reserve button clicked", Toast.LENGTH_LONG).show()
}
R.id.layoutBox -> {
// Go to Dorm Details
callback?.onShowDormDetail(dorm)
}
}
}
...
...
// This interface will act as mode to communication between
// activity and fragment
interface Callback {
fun onShowDormDetail(dorm: Dorm)
}
}
2. Implement the Callback on the calling activity
class HomeActivity : AppCompatActivity(), HomeFragment.Callback {
...
...
override onShowDormDetail(dorm: Dorm) {
// Add or replace the detail fragment here
}
}
Related
I have an adapter inside a fragment that belongs to an Activity A, from this adapter I open the detail of an element in an Activity B. How can I tell the adapter of Activity A that it has to update the list of the adapter that is in a fragment of Activity B?
This is the method I have inside the activity:
private fun setOnClickFavoriteListener(recipe: RecipesItem?) {
binding?.recipeDetailContainerFavoriteButton?.setOnClickListener {
if (recipe?.isFavorite == true) {
isFavorite = false
viewModel.setFavoriteRecipe(recipe.id, isFavorite)
binding?.recipeDetailImgFavoriteButton?.setColorFilter(
ContextCompat.getColor(
this,
R.color.silver
)
)
} else {
isFavorite = true
viewModel.setFavoriteRecipe(recipe?.id, isFavorite)
binding?.recipeDetailImgFavoriteButton?.setColorFilter(
ContextCompat.getColor(
this,
R.color.sunglo
)
)
}
}
}
And this is my adapter which is not in the same activity, so I have no communication between them:
class FavoritesAdapter(private val favorites: List<RecipesItem>) :
RecyclerView.Adapter<FavoritesViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FavoritesViewHolder {
val binding = FavoritesRowBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
return FavoritesViewHolder(binding)
}
override fun onBindViewHolder(holder: FavoritesViewHolder, position: Int) {
holder.bind(favorites[position])
}
override fun getItemCount(): Int {
return favorites.size
}
}
class FavoritesViewHolder(private val binding: FavoritesRowBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(recipesItem: RecipesItem) {
binding.favoritesRowImgRecipeImage.load(recipesItem.imageURL)
binding.favoritesRowLabelRecipeName.text = recipesItem.name
binding.favoritesRowLabelTime.text = binding.root.context.getString(
R.string.recipe_time,
recipesItem.time
)
binding.favoritesRowLabelDifficult.text = recipesItem.difficult
binding.root.setOnClickListener {
val bundle = bundleOf(RECIPE_BUNDLE to recipesItem)
val intent = Intent(binding.root.context, RecipeDetailActivity::class.java)
intent.putExtras(bundle)
binding.root.context.startActivity(intent)
}
}
}
I tried to put the fetching of the favourites list in the onResume, but it does a strange effect where it briefly shows the empty list image and then shows the updated list:
#AndroidEntryPoint
class FavoritesFragment : Fragment() {
private val viewModel: FavoritesViewModel by viewModels()
private var binding: FragmentFavoritesBinding? = null
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentFavoritesBinding.inflate(layoutInflater, container, false)
return binding?.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
(activity as DashboardActivity).showToolbar(true)
(activity as DashboardActivity).setToolbarTitle(getString(R.string.option_menu_favorite))
}
private fun initObservers() {
viewModel.favoritesLiveData.observe(viewLifecycleOwner) { favorites ->
if (favorites.isNullOrEmpty()) {
binding?.favoritesRecyclerViewRecipes?.hide()
binding?.favoritesGroupEmptyRecipes?.show()
} else {
binding?.favoritesRecyclerViewRecipes?.show()
binding?.favoritesGroupEmptyRecipes?.hide()
val adapter = FavoritesAdapter(favorites)
binding?.favoritesRecyclerViewRecipes?.layoutManager = LinearLayoutManager(
context,
LinearLayoutManager.VERTICAL,
false
)
binding?.favoritesRecyclerViewRecipes?.adapter = adapter
}
}
}
override fun onResume() {
super.onResume()
viewModel.getFavoritesFromDatabase()
initObservers()
}
override fun onDestroy() {
binding = null
super.onDestroy()
}
From The adapter in Fragment of Activity 'A' you send the Item in the intent bundle as you did.
Then from Activity 'B' you handle this bundle and send the item to the Fragment in Activity 'B' then the fragment will render the item in its adapter.
I hope I understand your question right
I'm trying to inflate a custom dialog in my "CreateShoppingListMenuFragment" I've followed android's documentation but seem to be having a problem with the Listener, I know FragmentManager() is deprecated and used both parentFragmentManager & child FragmentManager to no success, maybe it's related?
Here is the error message:
logo1200.shoppinglist, PID: 24194
java.lang.ClassCastException: com.camilogo1200.shoppinglist.presentation.MainActivity#11852bbmust implement ShoppingListNameRequestListener
at com.camilogo1200.shoppinglist.presentation.fragments.ShoppingListNameRequestDialog.onAttach(ShoppinListNameRequestDialog.kt:68)
at androidx.fragment.app.Fragment.performAttach(Fragment.java:2922)
at androidx.fragment.app.FragmentStateManager.attach(FragmentStateManager.java:464)
at androidx.fragment.app.FragmentStateManager.moveToExpectedState(FragmentStateManager.java:275)
at androidx.fragment.app.FragmentManager.executeOpsTogether(FragmentManager.java:2189)
This is my DialogFragment:
class ShoppingListNameRequestDialog : DialogFragment() {
private lateinit var listener: ShoppingListNameRequestListener
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return activity?.let {
val builder = AlertDialog.Builder(it)
val inflater: LayoutInflater = requireActivity().layoutInflater
val requestNameView = inflater.inflate(R.layout.shopping_list_name_request_dialog, null)
val nameInput = requestNameView.findViewById<TextView>(R.id.shopping_list_dialog_input)
var listName = ""
builder.setView(requestNameView)
.setPositiveButton(R.string.save_shopping_list,
DialogInterface.OnClickListener {dialog, id ->
if(nameInput.text.toString() != "")
listName = nameInput.text.toString()
listener.onDialogPositiveClick(this,listName);
})
.setNegativeButton(R.string.cancel,
DialogInterface.OnClickListener{dialog, id ->
listener.onDialogNegativeClick(this)
})
builder.create()
} ?: throw IllegalStateException("Activity cannot be null")
}
interface ShoppingListNameRequestListener {
fun onDialogPositiveClick(dialog: DialogFragment,listName:String)
fun onDialogNegativeClick(dialog: DialogFragment)
}
override fun onAttach(context: Context) {
super.onAttach(context)
try {
listener = context as ShoppingListNameRequestListener
} catch (e: ClassCastException) {
throw ClassCastException((context.toString() +
"must implement ShoppingListNameRequestListener"))
}
}
This is my "CreateShoppingListMenuFragment" (the host fragment where I'm inflating the dialog):
class CreateShoppingListMenuFragment : Fragment(),
ShoppingListNameRequestDialog.ShoppingListNameRequestListener {
private lateinit var binding: FragmentCreateShoppingListMenuBinding
private val viewModel: CreateShoppingListMenuViewModel by activityViewModels()
private val args: CreateShoppingListMenuFragmentArgs by navArgs()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = DataBindingUtil.inflate(
layoutInflater,
R.layout.fragment_create_shopping_list_menu,
container,
false
)
binding.lifecycleOwner = this
viewModel.createItems()
viewModel.viewState.observe(viewLifecycleOwner, ::handleViewState)
val login = args.ownerName
val listId = args.listId
viewModel.setOwnerAndList(login, listId)
binding.createItemButton.setOnClickListener {
val directionToFragment =
CreateShoppingListMenuFragmentDirections.actionCreateShoppingListMenuFragmentToCreateItemMenuFragment(
login,
listId
)
Navigation.findNavController(binding.root).navigate(directionToFragment)
}
binding.completeShoppingListButton.setOnClickListener {
showNoticeDialog()
}
return binding.root
}
private fun showNoticeDialog() {
val dialog = ShoppingListNameRequestDialog()
dialog.show(parentFragmentManager, "ShoppingListNameRequestDialog")
}
override fun onDialogPositiveClick(dialog: DialogFragment,listName: String) {
val result = viewModel.saveShoppingList(listName)
Log.i("shoppingListResult", "$result")
// travel to final fragment sent shoppinglist as arg
}
override fun onDialogNegativeClick(dialog: DialogFragment) {
// User touched the dialog's negative button
}
private fun handleViewState(viewState: CreateShoppingListMenuViewState) {
when (viewState) {
is CreateShoppingListMenuViewState.ErrorViewState -> showError(viewState.exception as ShoppingException)
//is RegisterViewState.SuccessViewState ->showSuccess()
else -> showSuccess(viewState)
}
}
private fun showSuccess(viewState: CreateShoppingListMenuViewState) {
val receivedList = viewState as CreateShoppingListMenuViewState.SuccessViewState
val dataList = receivedList.data
val adapter = ShoppingListMenuAdapter(dataList, viewModel::changeItemCount)
binding.itemListArray.adapter = adapter
}
private fun showError(exception: ShoppingException) {
if (exception.idError as? ItemError == ItemError.NO_ITEMS_CREATED) {
val message = getString(R.string.no_items_created_error_messages)
Toast.makeText(activity, message, Toast.LENGTH_LONG).show()
}
Any help would be greatly appreciated!
The Context in onAttach(Context context) is context Activity fragment does not have its own Context .
The problem here is you are casting context to ShoppingListNameRequestListener for this to work your Activity needs to implement the listener .
To solve this problem there are several ways. if we go with your approach we can pass fragment instance as targetFragment and use it as listener inside the DialogFragment .
private fun showNoticeDialog() {
val dialog = ShoppingListNameRequestDialog()
dialog.setTargetFragment(this)
dialog.show(parentFragmentManager, "ShoppingListNameRequestDialog")
}
Then inside dialog you can do something like this .
class ShoppingListNameRequestDialog:DialogFragment(){
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
targetFragment?.let {
listener = it as ShoppingListNameRequestListener
}
}
}
However setTargetFragment is deprecated now. As a Alternate way you can do the same with a shared ViewModel or with the new API FragmentResultListener.
I am getting exception:
E/MessageQueue-JNI: java.lang.IllegalArgumentException: No view found for id 0x7f0a0554 (ua.com.company.app:id/vpBanners) for fragment BannerItemFragment{30698be} (4c80b228-4303-4c80-b99d-a55b8359b8c2) id=0x7f0a0554}
My hierarchy looks like so:
My adapter for vpHome:
class HomeViewPagerAdapter(fm: FragmentManager) : FragmentStatePagerAdapter(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
private val mFragmentList: MutableList<Fragment> = ArrayList()
private val mFragmentTitleList: MutableList<String> = ArrayList()
override fun getItem(position: Int): Fragment {
return mFragmentList[position]
}
override fun getCount(): Int {
return mFragmentList.size
}
override fun getPageTitle(position: Int): CharSequence? {
return mFragmentTitleList[position]
}
fun addFragment(fragment: Fragment, title: String) {
mFragmentList.add(fragment)
mFragmentTitleList.add(title)
}
}
And I apply it in this way:
private fun setupViewPager(viewPager: DisableSwipeViewPager) {
vpAdapter = HomeViewPagerAdapter(childFragmentManager).apply {
addFragment(ForYouFragment(), "for you")
addFragment(AnotherFragment1(), "a1")
addFragment(AnotherFragment2(), "a2")
}
viewPager.adapter = vpAdapter
}
Next, my SnapBannersCarouselViewHolder to handle items with ViewPager inside:
class SnapBannersCarouselViewHolder(
private val mBinding: HomeFragmentItemSnapBannersCarouselBinding
) : RecyclerView.ViewHolder(mBinding.root) {
companion object {
// ...
fun newInstance(
inflater: LayoutInflater,
parent: ViewGroup,
onMoreInteractionListener: ((infoBlockId: String) -> Unit)?
): SnapBannersCarouselViewHolder {
val binding =
HomeFragmentItemSnapBannersCarouselBinding.inflate(inflater, parent, false)
return SnapBannersCarouselViewHolder(
binding,
onMoreInteractionListener
)
}
}
fun bind(item: SnapBannersItem, fragmentManager: FragmentManager) {
// ...
val adapter = BannerPagesAdapter(fragmentManager, item.banners)
with(mBinding.vpBanners) {
// ...
this.adapter = adapter
offscreenPageLimit = 3
}
}
}
My RecyclerView adapter ForYouContentAdapter:
class ForYouContentAdapter(
var data: List<HomeBaseItem> = emptyList(),
var fragmentManagerRetriever: () -> FragmentManager
) : BaseRecyclerViewAdapter<HomeBaseItem>(data) {
enum class ViewType(val value: Int) {
// ...
SNAP_BANNERS(6)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return when (viewType) {
// ...
ViewType.SNAP_BANNERS.value -> SnapBannersCarouselViewHolder.newInstance(
mInflater!!,
parent,
onBannersMoreInteractionListener // todo possibly change it
)
else -> throw RuntimeException("Can not create view holder for undefined view type")
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (getItemViewType(position)) {
// ...
ViewType.SNAP_BANNERS.value -> {
val vHolder = holder as SnapBannersCarouselViewHolder
vHolder.bind(
getItem(position) as SnapBannersItem,
fragmentManagerRetriever.invoke()
)
}
}
}
}
And my fragmentManagerRetriever implementation in ForYouFragment looks like so:
private val fragmentManagerRetriever: () -> FragmentManager = {
childFragmentManager
}
My BannerItemFragment code:
class BannerItemFragment : Fragment() {
private lateinit var mBinding: HomeFragmentSnapBannerItemBinding
companion object {
// ...
fun newInstance(
item: RectWebViewItem
): BannerItemFragment {
return BannerItemFragment()
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
mBinding = HomeFragmentSnapBannerItemBinding.inflate(inflater, container, false)
return mBinding.root
}
// ...
}
In every place where I need to create fragments inside other fragments I am using childFragmentManager.
And when I open ForYouFragment first time, it works normally. My item with ViewPager works normally. But when I replace fragment in Activity's container (adding to back stack) being on ForYouFragment and then return back (back on HomeFragment because ForYouFragment inside HomeFragment), I am getting error.
To replace fragments I am using this method inside ForYouFragment:
private fun showAnotherFragment() {
ActivityUtils.replaceFragmentToActivity(
requireActivity(),
SomeFragment.newInstance(),
true
)
}
And ActivityUtils code:
object ActivityUtils {
fun replaceFragmentToActivity(
activity: FragmentActivity,
fragment: Fragment,
addToBackStack: Boolean
) {
replaceFragmentToActivity(activity, fragment, addToBackStack, containerId = R.id.fragmentContainer)
}
fun replaceFragmentToActivity(
activity: FragmentActivity,
fragment: Fragment,
addToBackStack: Boolean,
containerId: Int
) {
val fragmentManager = activity.supportFragmentManager
val transaction = fragmentManager.beginTransaction()
transaction.replace(containerId, fragment)
if (addToBackStack) {
transaction.addToBackStack(null)
}
transaction.commitAllowingStateLoss()
}
}
Please, help me understand why I am getting this exception?
UPD
Adapter for ViewPager inside RecyclerView:
class BannerPagesAdapter(
fragmentManager: FragmentManager,
var data: List<RectWebViewItem>
) : FragmentStatePagerAdapter(
fragmentManager,
BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT
) {
private var fragments: MutableList<BannerItemFragment> = mutableListOf()
init {
// initial empty fragments
for (i in data.indices) {
fragments.add(BannerItemFragment())
}
}
override fun getItem(position: Int): Fragment {
return BannerItemFragment.newInstance(data[position])
}
override fun getCount(): Int {
return fragments.size
}
override fun instantiateItem(container: ViewGroup, position: Int): Any {
val f = super.instantiateItem(container, position)
fragments[position] = f as BannerItemFragment
return f
}
}
Solved
The problem was that fragments want to be attached to the ViewPager before the ViewPager is attached to its parent. This question outlined here.
So, to solve this problem, I created custom ViewPager:
/**
* Use this ViewPager when you need to place ViewPager inside RecyclerView.
* [LazyViewPager] allows you to set [PagerAdapter] in lazy way. This prevents IllegalStateException
* in case when the fragments want to be attached to the viewpager before the viewpager is
* attached to its parent
*/
class LazyViewPager
#JvmOverloads
constructor(
context: Context,
attrs: AttributeSet? = null
) : ViewPager(context, attrs) {
private var mPagerAdapter: PagerAdapter? = null
override fun onAttachedToWindow() {
super.onAttachedToWindow()
if (mPagerAdapter != null) {
super.setAdapter(mPagerAdapter)
}
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
super.setAdapter(null)
}
#Deprecated("Do not use this method to set adapter. Use setAdapterLazy() instead.")
override fun setAdapter(adapter: PagerAdapter?) {}
fun setAdapterLazy(adapter: PagerAdapter?) {
mPagerAdapter = adapter
}
}
And then, instead of using setAdapter() I use setAdapterLazy().
Also, it is important to reset adapter to null in onDetachedFromWindow().
I am using live data from a shared ViewModel across multiple fragments. I have a sign-in fragment which takes user's phone number and password and then the user presses sign in button I am calling the API for that, now if the sign-in fails I am showing a toast "Sign In failed", now if the user goes to "ForgotPassword" screen which also uses the same view model as "SignInFragment" and presses back from the forgot password screen, it comes to sign-in fragment, but it again shows the toast "Sign In failed" but the API is not called, it gets data from the previously registered observer, so is there any way to fix this?
SignInFragment.kt
class SignInFragment : Fragment() {
private lateinit var binding: FragmentSignInBinding
//Shared view model across two fragments
private val onBoardViewModel by activityViewModels<OnBoardViewModel>()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = DataBindingUtil.inflate(
inflater,
R.layout.fragment_sign_in,
container,
false
)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
onBoardViewModel.signInResponse.observe(viewLifecycleOwner) { response ->
//This is calling again after coming back from new fragment it.
showToast("Sign In Failed")
}
}
override fun onClick(v: View?) {
when (v?.id!!) {
R.id.forgotPasswordTV -> {
findNavController().navigate(SignInFragmentDirections.actionSignInFragmentToForgotPasswordFragment())
}
R.id.signInTV -> {
val phoneNumber = binding.phoneNumberET.text
val password = binding.passwordET.text
val signInRequestModel = SignInRequestModel(
phoneNumber.toString(),
password.toString(),
""
)
//Calling API for the sign-in
onBoardViewModel.callSignInAPI(signInRequestModel)
}
}
}
}
ForgotPasswordFragment
class ForgotPasswordFragment : Fragment() {
private lateinit var binding: FragmentForgotPasswordBinding
//Shared view model across two fragments
private val onBoardViewModel by activityViewModels<OnBoardViewModel>()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = DataBindingUtil.inflate(
inflater,
R.layout.fragment_forgot_password,
container,
false
)
return binding.root
}
}
OnBoardViewModel
class OnBoardViewModel : ViewModel() {
private var repository: OnBoardRepository = OnBoardRepository.getInstance()
private val signInRequestLiveData = MutableLiveData<SignInRequestModel>()
//Observing this data in sign in fragment
val signInResponse: LiveData<APIResource<SignInResponse>> =
signInRequestLiveData.switchMap {
repository.callSignInAPI(it)
}
//Calling this function from sign in fragment
fun callSignInAPI(signInRequestModel: SignInRequestModel) {
signInRequestLiveData.value = signInRequestModel
}
override fun onCleared() {
super.onCleared()
repository.clearRepo()
}
}
I have tried to move this code inside onActivityCreated but it's still getting called after coming back from new fragment.
onBoardViewModel.signInResponse.observe(viewLifecycleOwner) { response ->
showToast("Sign In Failed")
}
Using SingleLiveEvent class instead of LiveData in OnBoardViewModel class will solve your problem:
val signInResponse: SingleLiveEvent <APIResource<SignInResponse>>.
class SingleLiveEvent<T> : MutableLiveData<T>() {
private val pending = AtomicBoolean(false)
override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
super.observe(owner, Observer<T> { t ->
if (pending.compareAndSet(true, false)) {
observer.onChanged(t)
}
})
}
override fun setValue(t: T?) {
pending.set(true)
super.setValue(t)
}
fun call() {
postValue(null)
}
}
This is a lifecycle-aware observable that sends only new updates after subscription. This LiveData only calls the observable if there's an explicit call to setValue() or call().
I would provide a way to reset your live data. Give it a nullable type. Your observers can ignore it when they get a null value. Call this function when you receive login data, so you also won't be repeating messages on a screen rotation.
class OnBoardViewModel : ViewModel() {
// ...
fun consumeSignInResponse() {
signInRequestLiveData.value = null
}
}
onBoardViewModel.signInResponse.observe(viewLifecycleOwner) { response ->
if (response != null) {
showToast("Sign In Failed")
onBoardViewModel.consumeSignInResponse()
}
}
For Kotlin users #Sergey answer can also be implemented using delegates like below
class SingleLiveEvent<T> : MutableLiveData<T>() {
var curUser: Boolean by Delegates.vetoable(false) { property, oldValue, newValue ->
newValue != oldValue
}
override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
super.observe(owner, Observer<T> { t ->
if (curUser) {
observer.onChanged(t)
curUser = false
}
})
}
override fun setValue(t: T?) {
curUser = true
super.setValue(t)
}
fun call() {
postValue(null)
}
}
I'm trying to show another fragment upon clicking a recyclerview item. I already added an onclicklistener, now there's a todo part on the fragment.kt, which I need to know how to have it navigate to a new fragment
As for the code, here's the adapter:
class dormAdapter(
private val dorms: List<Dorms>,
private val listener: RecyclerViewClickListener
) : RecyclerView.Adapter<dormAdapter.DormViewHolder>() {
override fun getItemCount() = dorms.size
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
DormViewHolder(
DataBindingUtil.inflate(
LayoutInflater.from(parent.context),
R.layout.layout_home,
parent, false
)
)
override fun onBindViewHolder(holder: DormViewHolder, position: Int) {
holder.recyclerviewDormBinding.dorm = dorms[position]
holder.recyclerviewDormBinding.buttonReserve.setOnClickListener {
listener.onRecyclerViewItemClick(holder.recyclerviewDormBinding.buttonReserve, dorms[position])
}
holder.recyclerviewDormBinding.layoutBox.setOnClickListener {
listener.onRecyclerViewItemClick(holder.recyclerviewDormBinding.layoutBox, dorms[position])
}
}
inner class DormViewHolder(
val recyclerviewDormBinding: LayoutHomeBinding
) : RecyclerView.ViewHolder(recyclerviewDormBinding.root)
}
Here's the Fragment on the landing page:
class HomeFragment : Fragment(), RecyclerViewClickListener {
private lateinit var factory: HomeViewModelFactory
private lateinit var viewModel: HomeViewModel
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_home, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
val api = DormsAPI()
val repository = DormRepository(api)
factory = HomeViewModelFactory(repository)
viewModel = ViewModelProviders.of(this, factory).get(HomeViewModel::class.java)
viewModel.getDorms()
viewModel.dorms.observe(viewLifecycleOwner, Observer { dorms ->
recyclerViewDorms.also{
it.layoutManager = LinearLayoutManager(requireContext())
it.setHasFixedSize(true)
it.adapter = dormAdapter(dorms, this)
}
})
}
override fun onRecyclerViewItemClick(view: View, dorms: Dorms) {
when(view.id){
R.id.button_reserve -> {
**// TODO: Go to new account if not signed up, etc...**
Toast.makeText(requireContext(), "Reserve button clicked", Toast.LENGTH_LONG).show()
}
R.id.layoutBox -> {
**// TODO: Go to Dorm Details**
Toast.makeText(requireContext(), "Go to dorm details", Toast.LENGTH_LONG).show()
}
}
}
}
Anything else needed for this question will be followed up for later.
HUGE EDIT: Onclicklistener is now added. Project is now good for MVVM architecture.
1) Declare mOnclicklistener property in Adapter class
private val mOnClickListener: View.OnClickListener
init {
mOnClickListener = View.OnClickListener { v ->
Log.d("onclick of item")
}
}
2) In onBindViewHolder function setOnclick listener
with(holder.mView) {
setOnClickListener(mOnClickListener)
}