Recyclerview Duplicate Values with LiveData onBackPressed in Fragment - android

I have a recyclerview with load more, and I can't store the values in the database. So when I load the data, everything works perfectly. The problem occurs when I navigate to another fragment, and click onBackPressed, the onChanged of the observer is being called again and it is giving me the last values called from the API. Then as you can see in the code below, they are automatically being added to the list and published in UI
productsViewModel.productsObject.observe(viewLifecycleOwner, Observer { result ->
if (result.status != Status.LOADING) {
(activity as MainActivity?)!!.dismissProgressDialog(getView())
if (result.status == Status.SUCCESS && result.data != null) {
val productsResult = parseObject(result.data.asJsonObject)
if (!productsResult.isNullOrEmpty()) {
products.addAll(productsResult)
productAdapter.submitList(products)
progressBar.visibility = View.GONE
numberSearch.text = products.size.toString() + "/" + total + " " + resources.getString(R.string.items_found)
} else if (result.status == Status.ERROR) {
if (result.message.isNullOrBlank())
Utils.showSnackBar(requireContext(), view, getString(R.string.something_wrong_try_again), true)
else
Utils.showSnackBar(requireContext(), view, result.message, true)
}
}
}
})

I think that you are not observing your result correctly.
For example, if the value of result is Status.LOADING what happens? After you enter that block, maybe there are other cases that you are not handling. We don't know because there is no other when or else if block.
Another way more "clean" would be:
productsViewModel.productsObject.observe(viewLifecycleOwner, Observer { result ->
when(result.status) {
Status.LOADING -> manageLoading()
Status.SUCCESS -> manageSuccess()
Status.ERROR -> manageError()
else -> manageBaseCase()
}
}
Thus, it is not all embedded in a unique if block making the management more understandable.
In your Fragment you should manually send an event to "reset" the actual state of the LiveData so that you will be sure that there won't be duplicate items.
Your Fragment may look something like this:
class ProductsFragment : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.product_fragment, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
observe()
productsViewModel.send(Event.Load)
}
}

Related

How to return value from coroutine in viewmodelScope?

I am using Room and I need to return id to Fragment which is returned when insert().
However, But I couldn't return the value from viewModelScope.
I saw other similar questions, but the answer was to return LiveData.
But I don't need LiveData. I just want to return values ​​of type Long.
How can I do it?
Repo
class WorkoutListRepository(private val dao: WorkoutDao) {
#RequiresApi(Build.VERSION_CODES.O)
suspend fun createDailyLog(part: BodyPart) : Long {
...
return dao.insertDailyLog(data)
}
}
ViewModel
class WorkoutListViewModel(
private val repository: WorkoutListRepository
) : ViewModel() {
...
#RequiresApi(Build.VERSION_CODES.O)
fun createDailyLog(part: BodyPart) : Long {
viewModelScope.launch(Dispatchers.IO) {
return#launch repository.createDailyLog(part) // can't return
}
}
}
Fragment
class WorkoutListTabPagerFragment : Fragment(), WorkoutListAdapter.OnItemClickListener {
...
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = FragmentWorkoutListTabPagerBinding.inflate(inflater, container, false)
...
return binding.root
}
#RequiresApi(Build.VERSION_CODES.O)
override fun onItemClick(workout: String) {
when(PageState.curPageState) {
is PageState.startWorkout -> {
val id = vm.createDailyLog(part)
...
}
is PageState.addWorkout -> //TODO:
is PageState.editWorkout -> //TODO:
}
}
}
But I don't need LiveData
You do. You need some kind of observable data holder because the code inside launch is asynchronous. It doesn't run immediately. It is only kind of scheduled for execution. launch function, on the other hand, returns immediately, i.e. your createDailyLog function in ViewModel returns before the call to repository.createDailyLog(part) is made. So you can't return a value synchronously from an asynchronous method.
You could either use LiveData or Kotlin's StateFlow to send this data to the Fragment. Your fragment will observe changes to that state and respond accordingly. I suggest using StateFlow here. The code will look somewhat like this:
// ViewModel
class WorkoutListViewModel(
private val repository: WorkoutListRepository
) : ViewModel() {
private val _logIdFlow = MutableStateFlow<Long?>(null)
val logIdFlow = _logIdFlow.asStateFlow()
...
#RequiresApi(Build.VERSION_CODES.O)
fun createDailyLog(part: BodyPart) : Long {
viewModelScope.launch(Dispatchers.IO) {
_logIdFlow.value = repository.createDailyLog(part)
}
}
}
// Fragment
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = FragmentWorkoutListTabPagerBinding.inflate(inflater, container, false)
...
viewLifecycleOwner.lifecycleScope.launch {
viewModel.logIdFlow.collect { logId ->
if(logId != null) {
// Do whatever you want with the log Id
}
}
}
return binding.root
}
An alternate solution can be to use Kotlin Channel and send data through that Channel.
If you just need a quick, short solution, you can call the repository function from the Fragment's lifecycle scope directly, like this:
// ViewModel
suspend fun createDailyLog(part: BodyPart) : Long {
return repository.createDailyLog(part)
}
//Fragment
override fun onItemClick(workout: String) {
viewLifecycleOwner.lifecycleScope.launch {
when(PageState.curPageState) {
is PageState.startWorkout -> {
val id = vm.createDailyLog(part) // This will now work
...
}
is PageState.addWorkout -> //TODO:
is PageState.editWorkout -> //TODO:
}
}
}
The only problem with this solution is that, now db operation is tied to fragment's lifecycle. So if there is any event which destroy's fragment's lifecycle (like a config change), the operation will be cancelled. This shouldn't be that big of an issue here as your db operation will only take a few milliseconds. But the first option of using a StateFlow or Channel to send data to Fragment/Activity is a more general and recommended way. You can go with whichever option you like.

re call api in fragment android kotlin and perform searchView

the question is similar on Where should I call Rest API in fragment
but i want to discuss about the right on code.
i have call an api from oncreateView()
for example
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
GlobalScope.launch {
showHomeViewModel("someState")
}
}
in function showHomeViewModel() i have defined condition for adapter
private suspend fun showHomeViewModel(state: String) {
withContext(Dispatchers.Main) {
viewModel.liveData().observe(viewLifecycleOwner, observer)
if(adapter.itemCount == 0) callApi() else Log.d(TAG_LOG, "nothing todo")
}
}
so its calling if the data == 0
but i have searchView component , when i navigate to other fragment and back to first fragment , the observer is null...
private val observer =
Observer<MutableList<DataItem>> { item ->
Log.d(TAG_LOG, "observer data $item") // always null if i have ever navigate to other fragment
if (item != null)
adapter.setData(item)
binding.progress.gone()
}
the problem is solve when i remove the condition adapter.itemCount == 0.
but the lifecycle my device always call api when screen or fragment appear.
before i always put the callApi from onResume(), but in my course should i put to the onCreateView and other explain should in onViewCreate() but the main problem is , **its good the code if i use the condition adapter.itemCount == 0 ? to perform searachView , where i defined on viewModel for search (liveData) **
i was tried to from adapter Filterable to perform search but its not work with liveData, so i use my viewModel instead of filterable.

Android MVVM Fragment Button click doesn't work after view pager swipe

I have an activity with three fragments that are navigated with a ViewPager. The starting fragment has a button with a click event. When the fragment first appears the button works but when I swipe to the last fragment and back to the main fragment the button doesn't work. It only does this with the button nothing else... I know it's probably something fairly obvious be gentle folks lol!
button layout
<Button
android:id="#+id/login_button"
android:layout_width="match_parent"
android:textSize="#dimen/body_text_size"
android:layout_height="wrap_content"
android:layout_marginStart="#dimen/layout_margin"
android:layout_marginEnd="#dimen/layout_margin"
android:layout_marginBottom="#dimen/layout_margin"
android:focusable="false"
android:background="#drawable/button_pressed_state"
android:text="#string/login"
android:textColor="#color/white"
android:textStyle="bold" />
Fragment Code that it happens on.
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = DataBindingUtil.inflate(inflater, R.layout.login_fragment, container, false)
viewModel = ViewModelProvider(this).get(LoginViewModel::class.java)
loginView = binding.root
initClickableLinks()
launch {
initButtonClick()
}
return loginView
}
private suspend fun initButtonClick(): String? {
val edittextSize = binding.emailAddressEditText.textSize
val textviewSize = binding.forgetPassTextview.textSize
var message: String? = ""
binding.loginButton.setOnClickListener {
fun onClick(view: View?) {
try {
viewModel.setEmailAddress(binding.emailAddressEditText.text.toString())
viewModel.setPassword(binding.passwordEditText.text.toString())
//if nothing is entered this will do nothing but update text
val invalidString = requireActivity().getString(R.string.invalid_combo)
binding.authTextView.text = ""
if (binding.emailAddressEditText.text.toString()
.isBlank() || binding.passwordEditText.text.toString().isBlank()
) {
binding.authTextView.text = invalidString
//exits listener because authentication failed
// return#setOnClickListener
}
binding.progressBar.visibility = View.VISIBLE
//background thread for IO
GlobalScope.launch(Dispatchers.IO) {
//call api
//UI Thread
withContext(Dispatchers.Main) {
val mess = viewModel.getMessage()
if (mess.equals("Successful")) {
val intent = Intent(activity, MemberActivity::class.java)
val loginfo = viewModel.getLoginResult().toString()
intent.putExtra("loginIno", loginfo)
activity?.startActivity(intent)
} else {
binding.authTextView.text = mess
Toast.makeText(activity!!.applicationContext, mess, Toast.LENGTH_LONG).show()
}
binding.progressBar.visibility = View.GONE
}
}
} catch (ex: Exception) {
println(ex)
}
}
}
return message
}
This only happens with the button
In part I found that the following only gets called once :
initButtonClick()
Removing the launch and changing the Private suspend function to a private function that still asynchronously calls the login service fixed the issue and now gets called multiple times.

Why doesn't RequireView() not return a view onResume()?

I'm parsing data with a Volley Request on the onResume() method of my fragment, to populate the layout with the parsed data I need to get the view of my fragment, that's why I'm calling requireView() onResume() after a successfull parsing.
For the first time now my App crashed on one line which I'm going to mark with a * saying:
java.lang.IllegalStateException: Fragment DataMain{1a53f20}
(4f68cb4d-c05f-4416-932e-26e455fbf106)} did not return a View from
onCreateView() or this was called before onCreateView().
This is the code:
class DataMain : Fragment(), CoroutineScope by MainScope() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.fragment_Data, container, false)
DataBalance = view.findViewById(R.id.Data_balance)
// Firebase User Auth
getUID.requestUidToken()?.addOnSuccessListener {
}?.addOnFailureListener {
val unregisteredView = inflater.inflate(R.layout.fragment_unregistred, container, false)
val registerNow = unregisteredView.findViewById<TextView>(R.id.textview_registernow)
registerNow.setOnClickListener {
val mIntent = Intent(activity, LoginMain::class.java)
requireActivity().startActivity(mIntent)
}
}
view
} else {
val unregisteredView = inflater.inflate(R.layout.fragment_unregistred, container, false)
val registerNow = unregisteredView.findViewById<TextView>(R.id.textview_registernow)
registerNow.setOnClickListener {
val mIntent = Intent(activity, LoginMain::class.java)
requireActivity().startActivity(mIntent)
}
unregisteredView
}
}
private fun populateDataLayout(res: JSONObject, view: View) {
val response = Gson().fromJson(res.toString(), ServerResponse::class.java)
//here I need the view
val lVDataMenu: ListView = view.findViewById(R.id.lV_DataMenu)
lVDataMenu.adapter = lVadapter
else if (response.code == 403) {
}
}
override fun onResume() {
getUID.requestUidToken()?.addOnSuccessListener { getTokenResult ->
Volley(config.DATA_LOADDATA, { res ->
**CRASH**populateDataLayout(res,requireView())
}, {
tv_unverified.visibility = View.VISIBLE
})
}
super.onResume()
}
I could get rid of the use of "view.findViewById(R.id.lV_DataMenu)" in the populate Routine but then I wouldn't understand the problem here.
I wasn't able to reacreate the error, so that's why I'm asking. So why is it possible that the requireView() doesn't return a View? Is it because the user switches to the next fragment while Volley isn't finished with the parsing? And how can I fix that?
override fun onResume() {
getUID.requestUidToken()?.addOnSuccessListener { getTokenResult ->
Volley(config.DATA_LOADDATA, { res ->
**CRASH**populateDataLayout(res,requireView())
}, {
tv_unverified.visibility = View.VISIBLE
})
}
You're not calling requireView() in onResume() here.
You have two asynchronous calls and you're passing them functions (those lambda expressions in {}) as parameters. These functions are invoked later e.g. when the async operation completes. At that time your fragment could have finished its lifecycle.
Two common strategies for handling it:
Cancel your pending async calls when the fragment is finishing up. You seem to fire the calls in onCreateView() and onResume(). The lifecycle counterparts for these would be onDestroyView() and onPause(). How to do this specifically depends on the async APIs you're using.
Fire-and-forget with staleness check: When the async completes, check if the fragment is still alive, otherwise do nothing. For example, change
populateDataLayout(res,requireView())
to something like
view?.let { populateataLayout(res, it) }
where view is the nullable getView() counterpart to nonnull requireView().

ViewPager with unknown or dynamic number of pages

I am trying to develop a Survey application for Android. In the requirements, the client is asking for a swipe effect between the questions. I used a ViewPager with FragmentPagerAdapter and everything works fine.
The problem comes when they require a Tree Decision System. In other words, when the surveyed person select an answer, I should lead him to a question according on which one is defined in that answer.
As you can see in the image, before answer the first question, I can't load the second page because I don't know which will be the question to load.
Also I can't know the number of pages that should I return in the getCount method, only when the user responds, I can know if there's one more page or not, and which should be its content.
I tried many solution posted over there, but the most important, or at least was logic for me. Is to set the count as the known pages number, and when the user select an answer, I tried to change the count and call notifyDataSetChanged method, but all what I get, is to change the number of pages dynamically, but not the content.
The scenario is:
In the first question, I set the count to 1, so the user can't swipe to the next page because it's unknown.
When the user select an answer, I change the count to 2 and load the next question. Everything OK!
If the user back to the first question and change his answer, I tried to destroy or change the content of the second page. But in this case, the notifyDataSetChanged doesn't replace the Fragment.
I know that I am asking a strange and difficult behavior, but I want to know if someone has to do the same thing and could find the right solution.
Sorry for don't post any code, but after so many intents, my code becomes ugly and I do revert in VCS.
You could try a linked list for your data items. Here's a quick example of how that might look.
var viewPager: ViewPager? = null
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_dynamic_view_pager, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewPager = view.findViewById(R.id.viewPager)
viewPager?.apply {
adapter = MyAdapter(childFragmentManager, this).also {
it.initialDataItem = buildDataItems()
}
}
}
fun buildDataItems(): DataItem {
val item0 = DataItem(0).also {
it.title = "What can I help you with today?"
}
val item1 = DataItem(1).also {
it.title = "Technical Problem"
}
val item2 = DataItem(2).also {
it.title = "Ordering Problem"
}
val item3 = DataItem(3).also {
it.title = "Is your power light on?"
}
val item4 = DataItem(4).also {
it.title = "Have you received your order?"
}
val item5 = DataItem(5).also {
it.title = "New content node"
}
val item6 = DataItem(6).also {
it.title = "New content node"
}
item0.yesItem = item1
item0.noItem = item2
item1.yesItem = item3
item2.yesItem = item4
item3.yesItem = item5
item3.noItem = item6
return item0
}
data class DataItem(
var id: Int = 0
) {
var title: String = ""
var yesItem: DataItem? = null
var noItem: DataItem? = null
var answer: Answer = Answer.UNANSWERED
}
enum class Answer {
UNANSWERED,
YES,
NO
}
class MyAdapter(fm: FragmentManager, val viewPager: ViewPager) : FragmentPagerAdapter(fm) {
var initialDataItem: DataItem = DataItem()
override fun getItem(position: Int): Fragment {
var index = 0
var dataItem: DataItem? = initialDataItem
while (index < position) {
when (dataItem?.answer) {
Answer.YES -> dataItem = dataItem.yesItem
Answer.NO -> dataItem = dataItem.noItem
else -> {}
}
index ++
}
return DetailFragment(dataItem) {
dataItem?.answer = if (it) Answer.YES else Answer.NO
notifyDataSetChanged()
viewPager.setCurrentItem(position + 1, true)
}
}
override fun getCount(): Int {
var count = 1
var dataItem: DataItem? = initialDataItem
while (dataItem?.answer != Answer.UNANSWERED) {
when (dataItem?.answer) {
Answer.YES -> dataItem = dataItem.yesItem
Answer.NO -> dataItem = dataItem.noItem
else -> {}
}
count++
}
return count
}
}
class DetailFragment(val dataItem: DataItem?, val listener: ((answeredYes: Boolean) -> Unit)) : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? = inflater.inflate(R.layout.fragment_viewpager_detail, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
view.findViewById<TextView>(R.id.title)?.text = dataItem?.title
view.findViewById<Button>(R.id.yesButton)?.setOnClickListener {
listener(true)
}
view.findViewById<Button>(R.id.noButton)?.setOnClickListener {
listener(false)
}
}
}
Note: You will want to refine this a bit to handle error cases and reaching the end of the tree. Also, you'll want to use a better method for injecting data into the detail fragment - this is just for illustration purposes.

Categories

Resources