Nikolas Bozic
Nikolas Bozic

Reputation: 1081

ViewModel onchange gets called multiple times when back from Fragment

I am working with Android architecture components. What i want is when user type "0" in Edittext and click on Button to replace Fragment with new one , and if type anything else post Toast error message. In Problem is when i back from new Fragment(BlankFragment) and click on button again and type "0" again and click, onchange() is called multiple times so Fragment is get created multiple times

FragmentExample.class:

     @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        manager = getActivity().getSupportFragmentManager();
        viewmModel = ViewModelProviders.of(getActivity(), viewModelFactory)
                .get(VModel.class);

        View v = inflater.inflate(R.layout.fragment_list, container, false);   
        b = (Button) v.findViewById(R.id.b);
        et = (EditText) v.findViewById(R.id.et);

        viewmModel.observeData().observe(getActivity(), new Observer<String>() {
            @Override
            public void onChanged(@Nullable String s) {

                if(s.equals("0")) {
                    BlankFragment fragment = (BlankFragment) manager.findFragmentByTag(DETAIL_FRAG);
                    if (fragment == null) {
                        fragment = BlankFragment.newInstance();
                    }
                    addFragmentToActivity(manager,
                            fragment,
                            R.id.root_activity_detail,
                            DETAIL_FRAG
                    );
                } else {
                    Toast.makeText(getContext(), "Wrong text", Toast.LENGTH_SHORT).show();
                }
            }
        });

        b.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                viewmModel.setData(et.getText().toString());
            }
        });
        return v;
    }
    private void addFragmentToActivity(FragmentManager fragmentManager, BlankFragment fragment, int root_activity_detail, String detailFrag) {
        android.support.v4.app.FragmentTransaction transaction = fragmentManager.beginTransaction();
        transaction.replace(root_activity_detail, fragment, detailFrag).addToBackStack(detailFrag);
        transaction.commit();
    }

Repository class:


    public class Repository {
    MutableLiveData<String> dataLive = new MutableLiveData<>();  

    public Repository() {

    }

    public void setListData(String data) {
       dataLive.setValue(data);
    }

    public MutableLiveData<String> getData() {
        return dataLive;
    }
}

BlankFragment.class:

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {

        listItemViewModel = ViewModelProviders.of(this, viewModelFactory)
                .get(VModel.class);
        listItemViewModel.setData("");
        return inflater.inflate(R.layout.fragment_blank, container, false);
    }

Upvotes: 44

Views: 43073

Answers (13)

ladytoky0
ladytoky0

Reputation: 679

to add an observer to a LiveData, you should initialize your observer in onViewCreated (as you did) and attach the viewLifecycleOwner. So, it should look like this:

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        viewModel.yourVariable.observe(viewLifecycleOwner, Observer{ newValue ->
        // todo your function
})

See Kotlin Android Training Livedata for more information!

Upvotes: 2

Andrew
Andrew

Reputation: 2906

Solution for similar issue with Flow

If you`re using Flow instead of LiveData then don't forget to use viewLifecycleOwner.lifecycleScope.launch instead of lifecycleScope.launch:

viewLifecycleOwner.lifecycleScope.launch {
        flow.flowOn(Default).collect {
            requireContext()
        }
    }

Or with extension:

extension:

fun <T> Flow<T>.launchWhenStarted(lifecycleOwner: LifecycleOwner) {
    lifecycleOwner.lifecycleScope.launchWhenStarted {
        [email protected]()
    }
}

in fragment`s onViewCreated:

availableLanguagesFlow
    .onEach {
        //update view
    }.launchWhenStarted(viewLifecycleOwner)

Upvotes: 3

Cheok Yan Cheng
Cheok Yan Cheng

Reputation: 42880

Instead of using getActivity as LifecycleOwner, you should use fragment.

Change

viewModel.observeData().observe(getActivity(), new Observer<String>() {

to

viewModel.observeData().removeObservers(this);
viewModel.observeData().observe(this, new Observer<String>() {

Upvotes: 12

Francois LE FORT
Francois LE FORT

Reputation: 1

I had the same problem, when I created a fragment and declared an observer into onCreate() or onCreateView(), after a rotation of screen, my livedata runned twice times. In order to solve this problem, I tried kotlin extension in order to remove observer just before to create an other one, I tried to remove into onDestroyView(), I tried to change the lifecyclcleOwner when the viewmodel declaration (requiredActivity - this - viewLifecycleOwner) but all tests failed.

But finally I found a solution with coroutines. I don't know if it's a good practice but it work :

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {

  // ......

    // observer
    CoroutineScope(Dispatchers.Main).launch {
        delay(100)
        mMainViewModel.getMailings().observe(viewLifecycleOwner, Observer { showViewModelMailingsList(it) })
    }

    // call
    mMainViewModel.responseMailingList(preferences.userEmailAccount, preferences.deviceToken)

   // ....

}

Upvotes: 0

Gast&#243;n Saill&#233;n
Gast&#243;n Saill&#233;n

Reputation: 13179

To be clear, to instantiate a ViewModel we pass either the context of the Fragment itself, in which this viewmodel will be scoped to, or we pass the activity that holds this fragment as the context.

Is not the same and they have different purposes, if we do this

listItemViewModel = ViewModelProvider(requireActivity(), viewModelFactory)
                .get(VModel.class);

We attach this instance of the viewmodel to the parent activity of the fragment, after this fragment dies , the instance of that viewmodel will be kept in memory holding the reference to the parent activity.

If we do this

// requireContext() or this
listItemViewModel = ViewModelProvider(requireContext(), viewModelFactory)
                .get(VModel.class);

We scope the instance of the viewmodel in the parent itself, so whenever the fragment dies, this viewmodel instance also gets removed.

Now, the observers are the same, we need to specify that we only want to observer to the lifetime span of the Fragment, for example, we want to observer untill this fragment is destroyed and then detach any observer if we are not in this fragment, for this, here it comes viewLyfeCycleOwner which will observe until this fragment dies or pauses to go to antoher fragment, it's important to use it in each fragment:

 viewmModel.observeData().observe(viewLyfeCycleOwner, new Observer<String>() { ... }

if we attach this observer to the activity with

viewmModel.observeData().observe(getActivity(), new Observer<String>() { ... }

It will keep observing until the parent activity holding the fragment dies, and is not a good idea since it will subscribe multiple observers to the same livedata.

Upvotes: 1

Rafael Ruiz Mu&#241;oz
Rafael Ruiz Mu&#241;oz

Reputation: 5472

Adding some useful information after @Samuel-Eminet, it's true that onCreate(Bundle?) is called only once on the Fragment creation and when you press back, the view is recreated but not the fragment (hence why the ViewModel is the same. If you subscribe in any method of the lifecycle that affects the view, it will resubscribe again and again. Observers would have been gone and you won't be able to tell even if you ask for liveData.hasObservers().

The best thing to do is subscribing when onCreate(Bundle?) but many of us are using the binding, and the view isn't created at this time, so this is the best way to do it:

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    lifecycleScope.launchWhenStarted {
        subscribeUI()
    }
}

now you're telling the Fragment's lifecycle to do something when it starts the Fragment and it will call it only once.

Upvotes: -1

Biplob Das
Biplob Das

Reputation: 3114

Here is an example how i solve this problem .[TESTED AND WORKING]

 viewModel.getLoginResponse().observe(getViewLifecycleOwner(), new Observer<String>() {
        @Override
        public void onChanged(String response) {
            if(getViewLifecycleOwner().getLifecycle().getCurrentState()== Lifecycle.State.RESUMED){
                // your code here ...
            }

        }
    });

Upvotes: 28

Aryan Dhankar
Aryan Dhankar

Reputation: 309

here is what you are doing wrong...

  viewmModel.observeData().observe(getActivity(), new Observer<String>() {
    @Override
    public void onChanged(@Nullable String s) {

        if(s.equals("0")) {
            BlankFragment fragment = (BlankFragment) manager.findFragmentByTag(DETAIL_FRAG);
            if (fragment == null) {
                fragment = BlankFragment.newInstance();
            }
            addFragmentToActivity(manager,
                    fragment,
                    R.id.root_activity_detail,
                    DETAIL_FRAG
            );
        } else {
            Toast.makeText(getContext(), "Wrong text", Toast.LENGTH_SHORT).show();
        }
    }
});

in above code instead of "getActivity()" either you can use "this" or "viewLifecycleOwner".

Because as you are passing the getActivity() in observe method, whenever you open your fragment you are attaching the new instance of the observer with the Activity not with the fragment. So observer will keep alive even if you kill your fragment. So when livedata postvalue, it will send data to all the observers, as there are too many observers observing livedata, then all will get notified. Because of this, your observer gets called too many times. so you have to observe live data in fragment something like this.

  viewmModel.observeData().observe(this, new Observer<String>() {
    @Override
    public void onChanged(@Nullable String s) {

        if(s.equals("0")) {
            BlankFragment fragment = (BlankFragment) manager.findFragmentByTag(DETAIL_FRAG);
            if (fragment == null) {
                fragment = BlankFragment.newInstance();
            }
            addFragmentToActivity(manager,
                    fragment,
                    R.id.root_activity_detail,
                    DETAIL_FRAG
            );
        } else {
            Toast.makeText(getContext(), "Wrong text", Toast.LENGTH_SHORT).show();
        }
    }
});  

But still your onchanged method will get called two times.
You can stop this by checking one condition inside your onchanged method..

    dash_viewModel.getDashLiveData().observe(viewLifecycleOwner, object : Observer<AsyncResponse> {
        override fun onChanged(t: AsyncResponse?) {
            if(viewLifecycleOwner.lifecycle.currentState==Lifecycle.State.RESUMED){
                setData(t)
            }

        }

    })

from my research, I have found out that, if fragment using the ViewModel of its corresponding activity, So when even you start observing the livedata, it will first send you the most recently emitted item. even if you didn't call it from your fragment.

so onChange method got called two times

  1. When the fragment is on start state - to receive the most recently emitted item

  2. When the fragment is on Resumed state - to receive the call made by fragment either for api.

so on changed I always check the state of the fragment with the help of viewLifecycleOwner like this

   if(viewLifecycleOwner.lifecycle.currentState==Lifecycle.State.RESUMED){
      // if the fragment in resumed state then only start observing data
        }

viewlifecycleowner is provided by both Fragments and Activity as Google implemented this solution directly in support library 28.0.0 and androidx with getViewLifecycleOwner() method. viewlifecycleowner contains info about the lifecycle of the component.

in java you can use getViewLifecycleOwner() intead of viewlifecycleowner .

Upvotes: 14

emirua
emirua

Reputation: 508

Just declare your Observer as a field variable so you don't create a new observer every time the lifecycle calls that part of your code. ;)

i.e. with kotlin:

YourFragment: Fragment() {

private val dataObserver = Observer<Data> { data ->
      manageData(data)
  }

...

//now you should subscribe your data after you instantiate your viewModel either in onCreate, onCreateView, onViewCreated, depends on your case..

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    viewModel.liveData.observe(this, dataObserver)
}

...

}

Upvotes: 1

VIVEK CHOUDHARY
VIVEK CHOUDHARY

Reputation: 546

Observe the livedata only once in a fragment. For that call the observe method in onCreate() rather than onCreateView(). When we press back button the onCreateView() method is called which makes the viewmodel to observe data again.

 @Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

   mPatientViewModel.getGetCaseDetailLiveData().observe(this, jsonObjectResponse -> parseViewSentResponse(jsonObjectResponse));
}

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    // TODO: inflate a fragment view
    View rootView = super.onCreateView(inflater, container, savedInstanceState);

    return rootView;
}

Upvotes: -2

Julien Neo
Julien Neo

Reputation: 354

The problem here is that when you dettach the fragment from the acitivity, both fragment and its viewmodel are not destroyed. When you come back, you add a new observer to the livedata when the old observer is still there in the same fragment (If you add the observer in onCreateView()). There is an article (Even a SO thread in fact) talking about it (with solution).

The easy way to fix it (also in the article) is that remove any observer from the livedata before you add observer to it.

Update: In the support lib v28, a new LifeCycleOwner called ViewLifeCycleOwner should fix that more info in here

Upvotes: 22

wangqi060934
wangqi060934

Reputation: 1591

viewmModel = ViewModelProviders.of(getActivity(), viewModelFactory)
            .get(VModel.class);

As your viewmModel's LifecycleOwner is activity, so the observer will only be automatically removed when the state of lifecycle is Lifecycle.State.DESTROYED.
In your situation, the observer will not be automatically removed.So you have to remove the previous observer manually or pass the same instance of observer every time.

Upvotes: 0

Samuel Eminet
Samuel Eminet

Reputation: 4737

You shouldn't create your viewmModel in onCreateView but rather in onCreate so you don't add a listener to your data each time view is created.

Upvotes: 10

Related Questions