0

I have an Activity that kicks off three fragments. The first is a loading splash screen where I just want to show the name of the app for a couple seconds. The second, in my current case, is a screen that gathers the users name and language. The third fragment will gather which languages they would like to learn. I'm trying to use an Observable in my ViewModel to hold my data, but the info entered at the first fragment is being lost upon switching to the second fragment.

I think I'm running into two problems (or more) simultaneously:

  1. I don't think I'm sharing my runOnceViewModel correctly across fragments
  2. I don't think I'm using Observables quite right.

After permissions are checked from the splash screen, I fire a function (launchProfile) that loads the next fragment like this:

public void launchProfile(){

        if(model.getLearnerPK() > 0){
            learningScreenFragment fragment = new learningScreenFragment(model.getUser());
            getParentFragmentManager()
                    .beginTransaction()
                    .addToBackStack(null)
                    .replace(R.id.fragment_container, fragment, null)
                    .commit();
        } else {
            runOnceNameAndNativeLangFragment fragment = new runOnceNameAndNativeLangFragment();
            getParentFragmentManager()
                    .beginTransaction()
                    .addToBackStack(null)
                    .replace(R.id.fragment_container, fragment, null)
                    .commit();
        }
    }

Since this is a new user, the primary key will be 0 and so I load the runOnceNameAndNativeLangFragment:

 @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
                             @Nullable Bundle savedInstanceState) {
        Log.d(LOG_TAG, "RUNONCE: Gather Name and Native Language");

        mBinding = DataBindingUtil.inflate(getLayoutInflater(), R.layout.runonce_name_native, container, false);

        TransitionInflater TI = TransitionInflater.from(requireContext());
        setEnterTransition(TI.inflateTransition(R.transition.slide_right));

        return mBinding.getRoot();
    }

    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState){
        runOnceViewModelFactory factory = new runOnceViewModelFactory(requireActivity().getApplication());
        model = new ViewModelProvider(this,factory).get(runOnceViewModel.class);
        //super.onViewCreated(view,savedInstanceState); //remains of a road I tried to go down, but perhaps should have gone farther...
        //model = new ViewModelProvider(requireActivity()).get(runOnceViewModel.class);

        ArrayList<language> spinnerListLangs = model.getLangList();

        nativeSLAdapter = new SpinLangAdapter(m_Context, android.R.layout.simple_spinner_dropdown_item, spinnerListLangs);
        mBinding.spCreateNativeLang.setAdapter(nativeSLAdapter);

        mBinding.spCreateNativeLang.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
            @Override
            public void onItemSelected(AdapterView<?> adapterView, View view, int position, long l) {
                language lang = nativeSLAdapter.getItem(position);
                tmpNativeLangNo = lang.getLangNo();
            }

            @Override
            public void onNothingSelected(AdapterView<?> adapterView) {}
        });

        mBinding.btnNext.setOnClickListener(this::onClick);
    }



    @Override
    public void onClick(View view) {
        if (view.getId() == R.id.btnNext) {
            String tmpName = mBinding.etCreateProfileName.getText().toString();
            if(tmpNativeLangNo != null && !tmpName.equals("")){

                model.getNewUser().observeOn(Schedulers.io()) //I'm doing something dumb here
                        .map(user -> {
                            user.setName(tmpName);
                            user.setNativeLang(tmpNativeLangNo);
                            return user;
                        });

                runOnceSelectTargetLangsFragment fragment = new runOnceSelectTargetLangsFragment();
                getParentFragmentManager()
                        .beginTransaction()
                        .addToBackStack(null)
                        .replace(R.id.fragment_container, fragment, null)
                        .commit();
            } else {
                Toast.makeText(m_Context,"Name and Native Language required.",Toast.LENGTH_SHORT).show();
            }
        }
    }

If I put a breakpoint on "runOnceSelectTargetLangsFragment fragment = new runOnceSelectTargetLangsFragment();" then I can verify that model.getNewUser() returns an Observable with the name and native language of whatever I input on the UI. What's killing me is that once I load the next fragment (runOnceSelectTargetLangsFragment), and try to save the target languages in its click handler, the name and native language info are lost. Ala, I'm not sharing the viewmodel right (or, worse, I'm using an Observable when I shouldn't be -- side note: I chose to use an Observable because it makes handling the thread stuff quite a bit easier and I imagine I'll appreciate its flexibility down the road as the app becomes more complicated. At least, that's what I'm telling myself.)

Select Targets Fragment

@Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
                             @Nullable Bundle savedInstanceState) {
        Log.d(LOG_TAG, "RUNONCE: Gather Target Languages");

        mBinding = DataBindingUtil.inflate(getLayoutInflater(), R.layout.runonce_select_targets, container, false);

        TransitionInflater TI = TransitionInflater.from(requireContext());
        setEnterTransition(TI.inflateTransition(R.transition.slide_right));

        return mBinding.getRoot();
    }

    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState){
        //runOnceViewModelFactory factory = new runOnceViewModelFactory(requireActivity().getApplication());
        //model = new ViewModelProvider(this,factory).get(runOnceViewModel.class); //I think this was on the right track...maybe?
        //super.onViewCreated(view,savedInstanceState);
        model = new ViewModelProvider(requireActivity()).get(runOnceViewModel.class);

        mBinding.btnNext.setOnClickListener(this::onClick);
    }


    @Override
    public void onClick(View view) {
        if (view.getId() == R.id.btnNext) {
            if(hasChecked()){

                model.getNewUser().observeOn(Schedulers.io())
                        .subscribe(user -> {
                            user.setTargetLangs(getTargetLangs());
                        });

               String restHereWearyTraveler = ""; //the inn :)
               //...do more stuff 
    
            } else {
                Toast.makeText(m_Context,"You must select at least one target language.",Toast.LENGTH_SHORT).show();
            }
        }
    }

At the inn, model.getNewUser() returns an Observable with the target languages all set, but the name and native language info are gonezo.

View Model

public class runOnceViewModel extends AndroidViewModel {

    private final profileRepository m_Repository;
    private profileService PCS;
    private ArrayList<language> m_Langs;
    private Observable<User> newUser;

    public runOnceViewModel(Application application, profileRepository newProfileRepo) {
        super(application);
        m_Repository = newProfileRepo;
        m_Langs = m_Repository.getLanguages();
        PCS = profileService.getInstance(m_Repository);
    }

    public Observable<User> getNewUser(){
        if(newUser == null){
            User tmpUser = new User();
            newUser = Observable.just(tmpUser);
        }
        return newUser;
    }
}
sacredfaith
  • 850
  • 1
  • 8
  • 22
  • Why aren't you using `LiveData` instead unless you have a strong reason not to? – che10 Jun 11 '21 at 03:40
  • Great question. I went with an RxJava approach because I saw this: https://stackoverflow.com/questions/46312937/when-to-use-rxjava-in-android-and-when-to-use-livedata-from-android-architectura and was afraid I'd need the extensibility later on. It seems I need a data holder, a task that LiveData is probably better suited for, but then I see people using both and being happy with the results (see the answer by kzotin in the so link, and this link from reddit: https://www.reddit.com/r/androiddev/comments/946oss/livedata_vs_rxjava_2/). I'm just trying to avoid headaches later. – sacredfaith Jun 11 '21 at 09:20
  • 1
    RxJava is not lifecycle aware. `LiveData` was made for this purpose. I am not saying you to abandon it, use it but for observing things I would suggest you would use `LiveData`. Imagine it as a holder. – che10 Jun 11 '21 at 10:04
  • I run into problems when I want to convert from an Observable (from the DAO, done for flexibility) to a LiveData object (in an attempt to use it as a data holder). For better or for worse, I have a database user class (users) and a separate User class I use only at runtime. Ideally, I'd like to have User be passed around as LiveData and keep users (and everything else returned from my DAOs) as Observables (to help solve the problems of tomorrow I'm ignorant of today). – sacredfaith Jun 26 '21 at 03:15

0 Answers0