how to retrieve list of objects from dao via LiveData<> in View Model when there is no change to trigger onChange method in View Model in android

半腔热情 提交于 2019-12-11 14:30:07

问题


I am using android studios to build a dictionary app. Everything has went fairly well with help from the SO community and now I have another question.

I have setup several custom dictionaries so that the user can store and view words in different dictionaries. (e.g. literature, programming, general, etc.)

These will be a FK id in the word entity and filled when the user adds new words.

In the MainActivity, I have an Options menu item for 'Change Dictionary'. This brings up an AlertDialog where the user can change the dictionary and theoretically see a new set of words.

Here is the problem. No changes to the database are happening when the user selects a particular dictionary so the onChange attached to the WordViewModel's getWordsByDictionaryId() method does not trigger. Hence, the LiveData> will not refill.

Code in onCreate() method:

    // get all words from WordDao
    mWordViewModel = ViewModelProviders.of(MainActivity.this).get(WordViewModel.class);
    mWordViewModel.getWordByDictionaryId(dictionaryId).observe(MainActivity.this, new Observer<List<Word>>() {
        @Override
        public void onChanged(@Nullable List<Word> words) {
            mainAdapter.setWords(words);
            mAllWords = words;
            mainAdapter.notifyDataSetChanged();
        }
    });

Code in OnOptionsItemSelected() method:

        AlertDialog.Builder changeDictionaryDialog = new AlertDialog.Builder(MainActivity.this);
        changeDictionaryDialog.setTitle("Select Dictionary:");
        LayoutInflater inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        View view = inflater.inflate(R.layout.alert_dialog_spinner_view, null);

        mDictionaryStringList = new ArrayList<>();
        mDictionaryStringList = convertToList(mDictionaryList);

        final Spinner dictionarySpinner = (Spinner) view.findViewById(R.id.alert_dialog_spinner);

        ArrayAdapter<String> spinnerAdapter = new ArrayAdapter(MainActivity.this, android.R.layout.simple_spinner_dropdown_item, mDictionaryStringList);
        spinnerAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
        dictionarySpinner.setAdapter(spinnerAdapter);

        dictionarySpinner.setSelection(0);

        changeDictionaryDialog.setView(view);
        changeDictionaryDialog.setPositiveButton("OK", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                final String dictionaryValue = dictionarySpinner.getSelectedItem().toString();

                for (Dictionary dictionary : mDictionaryList) {
                    if (dictionary.getDictionaryName().trim().equals(dictionaryValue)) {
                        dictionaryId = dictionary.getDid();
                       LiveData<List<Word>> myNewList = mWordViewModel.getWordByDictionaryId(dictionaryId);
                        List<Word> testList = myNewList.
                    }
                }
            }
        });

        changeDictionaryDialog.setNegativeButton("Cancel", null);
        changeDictionaryDialog.create();
        changeDictionaryDialog.show();

Code in the WordViewModel:

public LiveData<List<Word>> getWordByDictionaryId(long dictionaryId) {
    return mWordRepository.getWordByDictionaryId(dictionaryId);
}

Code in WordDao:

@Query("SELECT * FROM word_table WHERE dictionary_id =:dictionaryId")
public LiveData<List<Word>> getWordsByDictionaryID(long dictionaryId);

Finally, the Word entity:

@Entity(tableName = "word_table",
indices = { @Index(value = "word", unique = true),
@Index("dictionary_id") })
public class Word {
@PrimaryKey(autoGenerate = true)
private long id;
private String word;
@ColumnInfo(name = "dictionary_id")
private long dictionaryId;

@Ignore
public Word(String word, long dictionaryId) {
    this.word = word;
    this.dictionaryId = dictionaryId;
}

public Word(long id, String word, long dictionaryId) {
    this.id = id;
    this.word = word;
    this.dictionaryId = dictionaryId;
}

public long getId() {
    return id;
}

public void setId(long id) {
    this.id = id;
}

public String getWord() {
    return word;
}

public void setWord(String word) {
    this.word = word;
}

public long getDictionaryId() {
    return dictionaryId;
}

public void setDictionaryId(long dictionaryId) {
    this.dictionaryId = dictionaryId;
}
}

I have tried to setup a background thread (AsyncTask), but the one time that I did, the results came after I needed them so the return value was null. Some SO developers have said don't use Asynctask, but I think the issue would still be timing...

I have also read a little about generating a callback. Would that actually work in this situation? It seems to me like it might put me in the same boat as the background thread.

I have been really struggling trying to learn how to get data out of this "wonderfully simple way to create databases" when it comes to getting data out of the SQLite database via Room persistence via View Model via LiveData via ???whatever comes next???. Is there a "Google" way to do this? Or is this where the RxJava and other libraries come into play?

So my question is just this "exactly how can I get a List of entity objects out of the database if there is no onChange event through live data?" Is there a best practices or right way to call the query without the LiveData wrapper? or a way to access the query directly within the LiveData wrapper without needing the data to change to get things done?


回答1:


LiveData solution

@Zohaib Amir's comment is correct. You can call LiveData#observe() on anywhere you want as long as you remember to clear the observers. One drawback with this approach is that you have to keep the reference to these observers and LiveData already has Transformations.switchMap() so that you can avoid that.

ViewModel

// Instance variable that stores the current dictionaryId that user wants to see.
private final MutableLiveData<Long> currentDictionaryId = new MutableLiveData<>();

// Instance variable that stores the current list of words. This will automatically change when currentDictionaryId value changes.
private final LiveData<List<Word>> words = Transformations.switchMap(currentDictionaryId, dictionaryId -> 
    mWordRepository.getWordByDictionaryId(dictionaryId));



// Set new dictionaryId
public void setDictionaryId(long dictionaryId) {
    currentDictionaryId.postValue(dictionaryId);
}

// Get LiveData to listen to
public LiveData<List<Word>> getWords() {
    return words;
}

Activity

// onCreate()
    // get all words from WordDao
    mWordViewModel = ViewModelProviders.of(MainActivity.this).get(WordViewModel.class);
    mWordViewModel.setDictionaryId(dictionaryId);
    mWordViewModel.getWords().observe(MainActivity.this, new Observer<List<Word>>() {
        @Override
        public void onChanged(@Nullable List<Word> words) {
            mainAdapter.setWords(words);
            mAllWords = words;
            mainAdapter.notifyDataSetChanged();
        }
    });

    // note that the order of calling setDictionaryId and getWords doesn't matter. setDictionaryId is supposed to call at anytime.
// OnOptionsItemSelected
    ...
    changeDictionaryDialog.setView(view);
        changeDictionaryDialog.setPositiveButton("OK", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                final String dictionaryValue = dictionarySpinner.getSelectedItem().toString();

                for (Dictionary dictionary : mDictionaryList) {
                    if (dictionary.getDictionaryName().trim().equals(dictionaryValue)) {
                        dictionaryId = dictionary.getDid();
                        mWordViewModel.setDictionaryId(dictionaryId);
                        break;
                    }
                }
            }
        });
    ...

This time I prepared other solutions so that you can compare. Other solutions (almost) do the exact same thing.


Non-reactive solution

You can implement above feature based on callbacks, and this means that you have to manually handle lifecycle changes, notifications, and threading. Also, there is a functional difference that LiveData will automatically notify the observer when there is a change in the db, while this callback design is just one-time look up only.

In this example, we will use Executor to execute tasks in the background thread.

WordDao

@Query("SELECT * FROM word_table WHERE dictionary_id =:dictionaryId")
public List<Word> getWordsByDictionaryID(long dictionaryId); // notice that livedata is gone.

WordRepository Database access must be done in a background thread. So when we access db, we need to switch to a background thread at some point. In this example, we will switch to the background thread in this layer.

// This executor is needed to run Runnables on a background thread. In real application, you may
// create this executor outside of this repository and later inject it to this repository.
private final Executor ioExecutor = Executors.newSingleThreadExecutor();


private void getWordByDictionaryId(long dictionaryId, Consumer<List<Word>> callback) {
    ioExecutor.execute(() -> {
        List<Word> wordList = wordDao.getWordsByDictionaryId(dictionaryId);
        callback.accept(wordList);
    });
}

WordViewModel There isn't much ViewModel does in this example. Just pass the parameters to the repository.

public void getWordByDictionaryId(long dictionaryId, Consumer<List<Word>> callback) {
    mWordRepository.getWordByDictionaryId(dictionaryId, callback);
}

Activity Note that Consumer#accept will run on a background thread. Therefore before you do anything with the ui, you need to switch back to the ui thread.

// onCreate
mWordViewModel = ViewModelProviders.of(MainActivity.this).get(WordViewModel.class);
mWordViewModel.getWordByDictionaryId(dictionaryId, words -> {
    runOnUiThread(() -> {
        if (isFinishing() || isDestroyed()) return; // if the activity is finishing, don't do anything.

        mainAdapter.setWords(words);
        mAllWords = words;
        mainAdapter.notifyDataSetChanged();
    });
});


// onOptionsItemSelected
changeDictionaryDialog.setPositiveButton("OK", new DialogInterface.OnClickListener() {
    @Override
    public void onClick(DialogInterface dialog, int which) {
        final String dictionaryValue = dictionarySpinner.getSelectedItem().toString();

        for (Dictionary dictionary : mDictionaryList) {
            if (dictionary.getDictionaryName().trim().equals(dictionaryValue)) {
                dictionaryId = dictionary.getDid();
                mWordViewModel.getWordByDictionaryId(dictionaryId, words -> {
                    runOnUiThread(() -> {
                        if (isFinishing() || isDestroyed()) return; // if the activity is finishing, don't do anything.

                        mainAdapter.setWords(words);
                        mAllWords = words;
                        mainAdapter.notifyDataSetChanged();
                    });
                });
                break;
            }
        }
    }
});


RxJava

RxJava solution will look much like the LiveData solution. It has many advantages over LiveData: it has lots of observables and operators to work with. These advantages are more apparent in more complex applications, where you need to do conditional or delayed async tasks, periodic tasks, multiple requests chained one after the other, error handlings, etc.

WordDao

@Query("SELECT * FROM word_table WHERE dictionary_id =:dictionaryId")
public Flowable<List<Word>> getWordsByDictionaryID(long dictionaryId);

ViewModel

private final BehaviorProcessor<Long> currentDictionaryId = BehaviorProcessor.create();

// Set new dictionaryId
public void setDictionaryId(long dictionaryId) {
    currentDictionaryId.onNext(dictionaryId);
}

// Get Flowable to listen to
public Flowable<List<Word>> getWords() {
    return currentDictionaryId.switchMap(dictionaryId -> mWordRepository
        .getWordByDictionaryId(dictionaryId)
        // add ".take(1)" if you need one-time look up.
        .subscribeOn(Schedulers.io())); 
}

Activity

private fianl CompositeDisposable disposables = new CompositeDisposable();

// onCreate()
mWordViewModel = ViewModelProviders.of(MainActivity.this).get(WordViewModel.class);
mWordViewModel.setDictionaryId(dictionaryId);
disposables.add(mWordViewModel.getWords()
    .observeOn(AndroidSchedulers.mainThread()))
    .subscribe(words -> {
        mainAdapter.setWords(words);
        mAllWords = words;
        mainAdapter.notifyDataSetChanged();
    });


// onOptionsItemSelected()
 changeDictionaryDialog.setPositiveButton("OK", new DialogInterface.OnClickListener() {
    @Override
    public void onClick(DialogInterface dialog, int which) {
        final String dictionaryValue = dictionarySpinner.getSelectedItem().toString();

        for (Dictionary dictionary : mDictionaryList) {
            if (dictionary.getDictionaryName().trim().equals(dictionaryValue)) {
                dictionaryId = dictionary.getDid();
                mWordViewModel.setDictionaryId(dictionaryId);
                break;
            }
        }
    }
});

// onDestroy()
disposables.clear();


来源:https://stackoverflow.com/questions/57484288/how-to-retrieve-list-of-objects-from-dao-via-livedata-in-view-model-when-there

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!