Home > Net >  Problem Implement LiveData : Observe always called when resume fragment (backstack from another frag
Problem Implement LiveData : Observe always called when resume fragment (backstack from another frag

Time:06-17

I on step learn to implement LiveData in My Apps. My App have one MainActivity with 2 fragment with navigate listner. ListFragment and DetailListFragment.

I call function to get data from server on onCreateView ListFragment by viewModel, and observe this to populate data in RecyclerView when success. Then I click one item to show detail in DetailListFragment.

The Problem is when back from DetailListFragment, the observe viewModel re-called but i want not it

Bellow my code

ListFragment

class ListFragment : BaseFragment(), ListClickListener {
    private lateinit var _observeListViewModel: Observer<BaseViewModel.State>
    lateinit var listViewModel: ListViewModel
    private lateinit var adapter: ListAdapter
    private var _binding: ListBinding? = null
    private val binding get() = _binding!!

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
        listViewModel.state.removeObserver(_observeListViewModel)
    }


    private var itemsData = ArrayList<ListResponseDtoListModel>()

    @SuppressLint("PrivateResource")
    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        // Inflate the layout for this fragment
        _binding = ListBinding.inflate(inflater, container, false)
       
        listViewModel =
            ViewModelProvider(this).get(ListViewModel::class.java)

        adapter = ListAdapter(itemsData, this)
        val llm = LinearLayoutManager(requireActivity())
        binding.rv.setHasFixedSize(true)
        binding.rv.layoutManager = llm
        binding.rv.adapter = adapter

        //get List
        _observeListViewModel =
            Observer<BaseViewModel.State> { observeListViewModel(it) }
        listViewModel.state.observe(viewLifecycleOwner, _observeListViewModel)
        listViewModel.getList(requireContext())

        return binding.root
    }

    private fun observeListViewModel(state: BaseViewModel.State?) {
        when (state) {
            BaseViewModel.State.Loading -> {
                loadingState()
            }
            is BaseViewModel.State.Error -> {
                errorState()
            }
            is BaseViewModel.State.Success -> {
                val data = state.data as ListModel
                if (data.status == KopraMobile().SUCCESS) {
                    if (data.content!!.listResponseDtoList.size == 0) {
                        nodataState()
                    } else data.content?.listResponseDtoList.let {
                        successState(it)
                    }
                } else
                    errorState()
            }
            is BaseViewModel.State.SessionTimeout -> {
                errorState()
                (parentFragment as BaseFragment).logOut()
            }
            is BaseViewModel.State.ErrorResponse -> {
                errorState()
            }

            else -> {}
        }
    }

    private fun successState(it: Any) {
        ....
    }

    private fun loadingState() {
        ....
    }

    private fun nodataState() {
        ....
    }

    private fun errorState() {
        ....
    }


    override fun onItemClicked(dashboardItem: ListResponseDtoListModel?) {
        findNavController().navigate(R.id.action_listFragment_to_detailListFragment)
    }

}

DetailFragment

class DetailListFragment : BaseFragment(){
    private var _binding: DetailListBinding? = null
    private val binding get() = _binding!!

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }


    private var itemsData = ArrayList<DetailListResponseDtoListModel>()

    @SuppressLint("PrivateResource")
    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        // Inflate the layout for this fragment
        _binding = DetailListBinding.inflate(inflater, container, false)
      
        binding.incToolbar1.header.text = "Detail"
        binding.incToolbar1.back.setImageResource(com.google.android.material.R.drawable.material_ic_keyboard_arrow_previous_black_24dp)
        binding.incToolbar1.back.setOnClickListener {
            findNavController().popBackStack()
        }


        return binding.root
    }
}

ListViewModel

class ListViewModel : BaseViewModel() {
    fun getList(context: Context)
    {
        _state.postValue(State.Loading)
        job = CoroutineScope(Dispatchers.IO   exceptionHandler).launch {
            try {
                val response =
                    NetworkApi().getListApi().getList( BuildConfig.APPLICATION_ID,  Prefs.getPublicAuthorization(context)
                    )
                withContext(Dispatchers.Main) {
                    _state.postValue(
                        if (response.isSuccessful) {
                            State.Success( response.headers(), response.body()   )
                        } else {
                            State.ErrorResponse( response.headers(), response.errorBody()  ) }
                    )
                }
            } catch (throwble: Throwable) {
                _state.postValue(
                    State.Error("Error : ${throwble.message.toString()} ")
                )
            }
        }
    }
}

BaseViewModel

open class BaseViewModel : ViewModel() {

    sealed class State {
        object Loading : State()
        data class Success(val headers: Headers, val data: Any?) : State()
        data class ErrorResponse(val headers: Headers, val errorResponse: ResponseErrorModel) :
            State()
        data class Error(val message: String?) : State()
        data class SessionTimeout(val sessionTimeout: String?) : State()
    }

    var job: Job? = null
    val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
        _state.postValue(State.Error("Exception handled: ${throwable.localizedMessage}"))
    }

    val _state = MutableLiveData<State>()
    val state: LiveData<State> get() = _state
    
    override fun onCleared() {
        super.onCleared()
        job?.cancel()
    }
}

I hope someone can help me to solve the problem. thanks, sorry for my English.

CodePudding user response:

you can use this subclass of LiveData it will only emit once

public class SingleLiveEvent<T> extends MutableLiveData<T> {

private static final String TAG = "SingleLiveEvent";

private final AtomicBoolean mPending = new AtomicBoolean(false);

@MainThread
public void observe(@NonNull LifecycleOwner owner, @NonNull final Observer<? super T> observer) {

    if (hasActiveObservers()) {
        Log.w(TAG, "Multiple observers registered but only one will be notified of changes.");
    }

    // Observe the internal MutableLiveData
    super.observe(owner, new Observer<T>() {
        @Override
        public void onChanged(@Nullable T t) {
            if (mPending.compareAndSet(true, false)) {
                observer.onChanged(t);
            }
        }
    });
}

@MainThread
public void setValue(@Nullable T t) {
    mPending.set(true);
    super.setValue(t);
}

/**
 * Used for cases where T is Void, to make calls cleaner.
 */
@MainThread
public void call() {
    setValue(null);
 }
}

CodePudding user response:

I think you should use SingleLiveEvent for your case.

The LiveData always observers by lifeCycleOwner. So when you back to ListFragment, the viewLifeCycleOwner is re-created, and the LiveData is trigger again. That's the design of LiveData.

If you don't want it, you must using some library or custom your LiveData. I recommend you to using the Event like Google is using.

import androidx.lifecycle.Observer

/**
 * Used as a wrapper for data that is exposed via a LiveData that represents an event.
 */
open class Event<out T>(private val content: T) {

    var hasBeenHandled = false
        private set // Allow external read but not write

    /**
     * Returns the content and prevents its use again.
     */
    fun getContentIfNotHandled(): T? {
        return if (hasBeenHandled) {
            null
        } else {
            hasBeenHandled = true
            content
        }
    }

    /**
     * Returns the content, even if it's already been handled.
     */
    fun peekContent(): T = content
}

And how to use Event and apply to your project?

Firstly, in your BaseViewModel.kt, you should create a new live data for event, like below:

private val _event = MutableLiveData<Event<Any>>()
val event: LiveData<Event<Any>> = _event

And you set the value for _event like this:

_event.value = Event(Any())
_event.postValue(Any())

Finally, in your Activity or Fragment, you observe the event livedata like this:

viewModel.event.observe(viewLifecycleOwner) {
    it?.getContentIfNotHandled()?.let { value ->
        // Update the UI or anything you want.
    }
}

More Detail:

ANOTHER ISSUE

You call the listViewModel.getList() in ListFragment.onCreateView(). When you backed from DetailListFragment to ListFragment, the onCreateView() called again. You must move the getList in ViewModel to init block of ViewModel, like the below code:

class ListViewModel : BaseViewModel() {
    init {
        getList()
    }

    fun getList() { // Remove the context in your `getList`. You don't need pass Fragment context to your `ViewModel`. If you want to using `context`, you must using `AndroidViewModel` or pass the `context` to your viewmodel constructor by using `Hilt`.
        ...
    }
}

And in the ListFragment, remove the listViewModel.getList().

observeListViewModel =
            Observer<BaseViewModel.State> { observeListViewModel(it) }
        listViewModel.state.observe(viewLifecycleOwner, _observeListViewModel)
        listViewModel.getList(requireContext()) // remove this line
  • Related