Alexander Farber
Alexander Farber

Reputation: 22978

How to use 4 simple ViewModels with the same Fragment?

I have an app, which displays 4 lists of words in a Fragment (reusing the same class!):

Here a screenshot of the navigation drawer (please excuse the non-english language):

Screenshot

Currently my ViewModel stores all 4 lists as LiveData:

public class WordsViewModel extends AndroidViewModel {

    private LiveData<List<Word>> mWords2;
    private LiveData<List<Word>> mWords3;
    private LiveData<List<Word>> mWordsHard;
    private LiveData<List<Word>> mWordsEh;

    public WordsViewModel(Application app) {
        super(app);
        mWords2    = WordsDatabase.getInstance(app).wordsDao().fetchWordsLength(2);
        mWords3    = WordsDatabase.getInstance(app).wordsDao().fetchWordsLength(3);
        mWordsHard = WordsDatabase.getInstance(app).wordsDao().fetchWordsContaining("Ъ");
        mWordsEh   = WordsDatabase.getInstance(app).wordsDao().fetchWordsContaining("Э");
    }

    public LiveData<List<Word>> getWords(int condition) {
        switch (condition) {
            case R.id.navi_drawer_letters_2:
                return mWords2;
            case R.id.navi_drawer_letters_3:
                return mWords3;
            case R.id.navi_drawer_letter_hard:
                return mWordsHard;
            case R.id.navi_drawer_letter_eh:
                return mWordsEh;
        }

        return mWords2;
    }
}

However I am concerned, that fetching all 4 lists at once is suboptimal and might cause a UI delay.

So I have tried splitting the view model into a base class and then 4 inheriting classes -

WordsViewModel (acting now as base class):

public class WordsViewModel extends AndroidViewModel {

    protected LiveData<List<Word>> mWords;

    public WordsViewModel (Application app) {
        super(app);
    }

    public LiveData<List<Word>> getWords() {
        return mWords;
    }
}

And the inheriting classes only differ in the DAO method being called -

TwoViewModel (inheriting class):

public class TwoViewModel extends WordsViewModel {
    public TwoViewModel(Application app) {
        super(app);
        mWords = WordsDatabase.getInstance(app).wordsDao().fetchWordsLength(2);
    }
}

ThreeViewModel (inheriting class):

public class ThreeViewModel extends WordsViewModel {
    public ThreeViewModel(Application app) {
        super(app);
        mWords = WordsDatabase.getInstance(app).wordsDao().fetchWordsLength(3);
    }
}

Finally (and thanks for reading sofar!) here is my Fragment:

public class WordsFragment extends Fragment {
    private final ItemAdapter<WordItem> mItemAdapter = new ItemAdapter<>();
    private final FastAdapter<WordItem> mFastAdapter = FastAdapter.with(mItemAdapter);

    private WordsViewModel mViewModel;

    public static WordsFragment newInstance(int condition) {
        WordsFragment f = new WordsFragment();

        Bundle args = new Bundle();
        args.putInt(KEY_CONDITION, condition);
        f.setArguments(args);

        return f;
    }

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

        int condition = (getArguments() == null ? -1 : getArguments().getInt(KEY_CONDITION));
        switch (condition) {
            case R.id.navi_drawer_letter_eh:
                mViewModel = ViewModelProviders.of(this).get(EhViewModel.class);
            case R.id.navi_drawer_letter_hard:
                mViewModel = ViewModelProviders.of(this).get(HardViewModel.class);
            case R.id.navi_drawer_letters_3:
                mViewModel = ViewModelProviders.of(this).get(ThreeViewModel.class);
            default:
                mViewModel = ViewModelProviders.of(this).get(TwoViewModel.class);
        }

        mViewModel.getWords().observe(this, words -> {
            mItemAdapter.clear();
            for (Word word: words) {
                WordItem item = new WordItem();
                item.word = word.word;
                item.expl = word.expl;
                mItemAdapter.add(item);
            }
        });

Unfortunately, this breaks my app by always displaying the 2-letter words list.

I wonder, why does this happen (because of inheritance?) and how to solve this?

UPDATE:

Here my code for opening the fragment from the main activity using MaterialDrawer and withTag() and I have verified in debugger and logs (and in the Toast which can be seen in the above screenshot), that the condition variable differs:

private final Drawer.OnDrawerItemClickListener mFetchWordsListener = (view, position, drawerItem) -> {
    setTitle(drawerItem);
    WordsFragment f = WordsFragment.newInstance( (Integer)drawerItem.getTag() );
    getSupportFragmentManager()
            .beginTransaction()
            .replace(R.id.root, f)
            .commitAllowingStateLoss();
    return false;
};

mNavigationDrawer.addItems(
    ....
    new SectionDrawerItem().withName(R.string.item_dict),
    new PrimaryDrawerItem().withOnDrawerItemClickListener(mFindWordListener).withName(R.string.item_find_word).withIcon(R.drawable.magnify).withIconTintingEnabled(true).withIdentifier(R.id.navi_drawer_find_word),
    new PrimaryDrawerItem().withOnDrawerItemClickListener(mFetchWordsListener).withName(R.string.item_letters_2).withIcon(R.drawable.letters_2).withIconTintingEnabled(true).withIdentifier(R.id.navi_drawer_letters_2).withTag(R.id.navi_drawer_letters_2),
    new PrimaryDrawerItem().withOnDrawerItemClickListener(mFetchWordsListener).withName(R.string.item_letters_3).withIcon(R.drawable.letters_3).withIconTintingEnabled(true).withIdentifier(R.id.navi_drawer_letters_3).withTag(R.id.navi_drawer_letters_3),
    new PrimaryDrawerItem().withOnDrawerItemClickListener(mFetchWordsListener).withName(R.string.item_letters_2).withIcon(R.drawable.letters_hard).withIconTintingEnabled(true).withIdentifier(R.id.navi_drawer_letters_hard).withTag(R.id.navi_drawer_letters_hard),
    new PrimaryDrawerItem().withOnDrawerItemClickListener(mFetchWordsListener).withName(R.string.item_letters_eh).withIcon(R.drawable.letters_eh).withIconTintingEnabled(true).withIdentifier(R.id.navi_drawer_letters_eh).withTag(R.id.navi_drawer_letters_eh)
);

UPDATE 2:

Here my DAO interface and BTW I've noticed that (mViewModel instanceof TwoViewModel) is always true for some reason?

@Dao
public interface WordsDao {
    @Query("SELECT * FROM table_words WHERE LENGTH(word) = :length")
    LiveData<List<Word>> fetchWordsLength(int length);

    @Query("SELECT * FROM table_words WHERE word LIKE '%' || :letter || '%'")
    LiveData<List<Word>> fetchWordsContaining(String letter);
}

Upvotes: 0

Views: 512

Answers (1)

Ryujin
Ryujin

Reputation: 443

You need to put a 'break' at the end of each case block to escape out of the switch when a case matching expression is found. Without the break statement, control flow will 'fall through' the different case statements after the first matching case is found. In your code, the default case will always be executed, which loads TwoViewModel.

Upvotes: 2

Related Questions