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