0

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.

Amay Diam
  • 2,561
  • 7
  • 33
  • 57

2 Answers2

0

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);
 }
}
Abhinav Chauhan
  • 1,304
  • 1
  • 7
  • 24
  • Yes , i have tried this, but still not work :( , observe still called from state loading. May be, want you see my code in github? – Amay Diam Jun 17 '22 at 07:29
0

The problem you are facing is this one:

"Proper configuration changes If an activity or fragment is recreated due to a configuration change, like device rotation, it immediately receives the latest available data."

  • I've seen navigation or app state change also causes the same behavior.
  • It might look like a bug, but it is actually the intended purpose.

My recommendation:

Use a Broadcaster/Receiver instead of the observer. I strongly discourage the handling of Network status at your observer function. At best use an okhttp interceptor that has access to a global Application Context instead, otherwise you will be re-writing the same code every time you make another request.

Also, try not to pass around the context to the ViewModels, this classes shouldn't need to know anything about the views.

WilliamX
  • 357
  • 4
  • 17