android 中的视图模型中没有更改触发 onChange 方法时,如何通过视图模型中的 LiveData<> 从 dao 检索对象列表
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
我正在使用 android 工作室构建字典应用程序。在 SO 社区的帮助下,一切进展顺利,现在我有另一个问题。
我已经设置了几个自定义词典,以便用户可以在不同的词典中存储和查看单词。 (例如文学、编程、一般等)
这些将是单词实体中的FK id,并在用户添加新单词时填充。
在 MainActivity 中,我有一个 'Change Dictionary' 的选项菜单项。这会弹出一个 AlertDialog,用户可以在其中更改词典,理论上可以看到一组新单词。
问题来了。当用户选择特定词典时,数据库不会发生任何更改,因此不会触发附加到 WordViewModel 的 getWordsByDictionaryId() 方法的 onChange。因此,LiveData> 不会重新填充。
onCreate() 方法中的代码:
// 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();
}
});
OnOptionsItemSelected() 方法中的代码:
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();
WordViewModel 中的代码:
public LiveData<List<Word>> getWordByDictionaryId(long dictionaryId) {
return mWordRepository.getWordByDictionaryId(dictionaryId);
}
WordDao 中的代码:
@Query("SELECT * FROM word_table WHERE dictionary_id =:dictionaryId")
public LiveData<List<Word>> getWordsByDictionaryID(long dictionaryId);
最后,Word实体:
@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;
}
}
我曾尝试设置一个后台线程 (AsyncTask),但有一次我这样做了,结果是在我需要它们之后出现的,所以 return 值为空。一些 SO 开发人员说不要使用 Asynctask,但我认为问题仍然是时间问题...
我还阅读了一些有关生成回调的内容。这在这种情况下真的有效吗?在我看来,它可能会让我与后台线程处于同一条船上。
我一直在努力学习如何从这个 "wonderfully simple way to create databases" 中获取数据,当涉及到通过 Room persistence 通过 View Model 通过 LiveData 通过 ??? 无论发生什么下一个???。有 "Google" 的方法吗?或者这是 RxJava 和其他库发挥作用的地方?
所以我的问题就是这个 "exactly how can I get a List of entity objects out of the database if there is no onChange event through live data?" 是否有最佳实践或正确的方法来在没有 LiveData 包装器的情况下调用查询?或者直接在 LiveData 包装器中访问查询而不需要更改数据来完成事情的方法?
LiveData解决方案
@Zohaib Amir 的评论是正确的。只要记得清除观察者,就可以在任何地方调用 LiveData#observe()
。这种方法的一个缺点是您必须保留对这些观察者的引用,而 LiveData
已经有 Transformations.switchMap()
,这样您就可以避免这种情况。
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;
}
}
}
});
...
这次我准备了其他方案,方便大家对比。其他解决方案(几乎)做同样的事情。
非反应性溶液
您可以基于回调实现上述功能,这意味着您必须手动处理生命周期更改、通知和线程。还有一个功能上的区别是LiveData
会在db有变化的时候自动通知观察者,而这个回调设计只是一次性查找而已。
在这个例子中,我们将使用Executor
在后台线程中执行任务。
WordDao
@Query("SELECT * FROM word_table WHERE dictionary_id =:dictionaryId")
public List<Word> getWordsByDictionaryID(long dictionaryId); // notice that livedata is gone.
WordRepository
数据库访问必须在后台线程中完成。所以我们在访问db的时候,需要在某个时候切换到一个后台线程。在这个例子中,我们将切换到后台
在这一层线程。
// 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
在此示例中,ViewModel 所做的事情并不多。只需将参数传递给存储库即可。
public void getWordByDictionaryId(long dictionaryId, Consumer<List<Word>> callback) {
mWordRepository.getWordByDictionaryId(dictionaryId, callback);
}
Activity
请注意,Consumer#accept
将在后台线程上 运行。因此,在对 ui 执行任何操作之前,您需要切换回 ui 线程。
// 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
解决方案看起来很像 LiveData
解决方案。与 LiveData
相比,它有很多优点:它有很多可观察值和运算符可供使用。这些优势在更复杂的应用程序中更为明显,在这些应用程序中,您需要执行有条件或延迟的异步任务、周期性任务、一个接一个地链接的多个请求、错误处理等。
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();
我正在使用 android 工作室构建字典应用程序。在 SO 社区的帮助下,一切进展顺利,现在我有另一个问题。
我已经设置了几个自定义词典,以便用户可以在不同的词典中存储和查看单词。 (例如文学、编程、一般等)
这些将是单词实体中的FK id,并在用户添加新单词时填充。
在 MainActivity 中,我有一个 'Change Dictionary' 的选项菜单项。这会弹出一个 AlertDialog,用户可以在其中更改词典,理论上可以看到一组新单词。
问题来了。当用户选择特定词典时,数据库不会发生任何更改,因此不会触发附加到 WordViewModel 的 getWordsByDictionaryId() 方法的 onChange。因此,LiveData> 不会重新填充。
onCreate() 方法中的代码:
// 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();
}
});
OnOptionsItemSelected() 方法中的代码:
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();
WordViewModel 中的代码:
public LiveData<List<Word>> getWordByDictionaryId(long dictionaryId) {
return mWordRepository.getWordByDictionaryId(dictionaryId);
}
WordDao 中的代码:
@Query("SELECT * FROM word_table WHERE dictionary_id =:dictionaryId")
public LiveData<List<Word>> getWordsByDictionaryID(long dictionaryId);
最后,Word实体:
@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;
}
}
我曾尝试设置一个后台线程 (AsyncTask),但有一次我这样做了,结果是在我需要它们之后出现的,所以 return 值为空。一些 SO 开发人员说不要使用 Asynctask,但我认为问题仍然是时间问题...
我还阅读了一些有关生成回调的内容。这在这种情况下真的有效吗?在我看来,它可能会让我与后台线程处于同一条船上。
我一直在努力学习如何从这个 "wonderfully simple way to create databases" 中获取数据,当涉及到通过 Room persistence 通过 View Model 通过 LiveData 通过 ??? 无论发生什么下一个???。有 "Google" 的方法吗?或者这是 RxJava 和其他库发挥作用的地方?
所以我的问题就是这个 "exactly how can I get a List of entity objects out of the database if there is no onChange event through live data?" 是否有最佳实践或正确的方法来在没有 LiveData 包装器的情况下调用查询?或者直接在 LiveData 包装器中访问查询而不需要更改数据来完成事情的方法?
LiveData解决方案
@Zohaib Amir 的评论是正确的。只要记得清除观察者,就可以在任何地方调用 LiveData#observe()
。这种方法的一个缺点是您必须保留对这些观察者的引用,而 LiveData
已经有 Transformations.switchMap()
,这样您就可以避免这种情况。
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;
}
}
}
});
...
这次我准备了其他方案,方便大家对比。其他解决方案(几乎)做同样的事情。
非反应性溶液
您可以基于回调实现上述功能,这意味着您必须手动处理生命周期更改、通知和线程。还有一个功能上的区别是LiveData
会在db有变化的时候自动通知观察者,而这个回调设计只是一次性查找而已。
在这个例子中,我们将使用Executor
在后台线程中执行任务。
WordDao
@Query("SELECT * FROM word_table WHERE dictionary_id =:dictionaryId")
public List<Word> getWordsByDictionaryID(long dictionaryId); // notice that livedata is gone.
WordRepository 数据库访问必须在后台线程中完成。所以我们在访问db的时候,需要在某个时候切换到一个后台线程。在这个例子中,我们将切换到后台 在这一层线程。
// 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 在此示例中,ViewModel 所做的事情并不多。只需将参数传递给存储库即可。
public void getWordByDictionaryId(long dictionaryId, Consumer<List<Word>> callback) {
mWordRepository.getWordByDictionaryId(dictionaryId, callback);
}
Activity
请注意,Consumer#accept
将在后台线程上 运行。因此,在对 ui 执行任何操作之前,您需要切换回 ui 线程。
// 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
解决方案看起来很像 LiveData
解决方案。与 LiveData
相比,它有很多优点:它有很多可观察值和运算符可供使用。这些优势在更复杂的应用程序中更为明显,在这些应用程序中,您需要执行有条件或延迟的异步任务、周期性任务、一个接一个地链接的多个请求、错误处理等。
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();