'MVVM: ViewModel logic needs info from database, but how to wait for the data?
I am new to MVVM and am trying to figure out how to organize my app. I made a simplified login app to help figure things out.
I separated the app into layers: Activity -> ViewModel -> Repository -> Database.
I have a few Activities, each with their specific ViewModel (i.e. LoginActivity accesses LoginViewModel.) There is one single repository for accessing the Room database through a Dao interface. All ViewModels contain a LocalRepository variable.
I understand the Activity should only handle User Interface jobs. So the LoginActivity gets a username from the user through the EditText Views, displays success and failure Toasts, starts the next activity, etc. But to actually login the user, loginViewModel.login(username) is called so the ViewModel can handle all the business logic.
The LoginViewModel needs to check if the user already exists in the database. But how should the ViewModel wait for the database results?
- Observing LiveData doesn't work in a view model (
currentUser.observe(LoginActivity.this, new Observer<User>()
returns an error fromLoginActivity.this
: "'LoginActivity' is not an enclosing class"). - Creating a callback seems inelegant.
- Adding a wait() line is just dumb.
- I could move the logic to the LoginActivity which would allow me to wait for the user to come back from the database. But aren't I now performing logic in the UI layer?
myViewModel.getUserByName(username).observe(this, new Observer<User>() {
@Override
public void onChanged(User userFromDatabase) {
// If userFromDatabase is not null, login as this user
}
});
Perhaps I'm missing something with how the architecture is structured.
Or is there a technique for accessing the database from the ViewModel?
Or I read about adding another "model" layer? Or a service?
So many techniques and options, hopefully someone can straighten things out.
Cheers. Here's the code:
LoginActivity:
public void loginButtonClicked(View v){
// TODO: Stub. Login user
String msg = myViewModel.loginUser(splashUserInput.getText().toString());
if(msg.isEmpty()){
Intent intent = new Intent(this, DEFAULT_FIRST_ACTIVITY);
startActivity(intent);
} else {
Toast.makeText(this, msg, Toast.LENGTH_SHORT).show();
}
}
LoginViewModel: newUser is returned null even though it DOES exist in the database.
public String loginUser(String username) {
if(username.isEmpty()) {
// If empty user input
return "Username field missing";
} else if(currentUser.getValue() != null && username.equals(currentUser.getValue().getUsername())) {
// If user is already logged on
return null; // No errors
} else {
// Else if logging on to a different user
User oldUser = currentUser.getValue();
User newUser;
newUser = localRepository.getUserByName(username).getValue();
// newUser is null since the repository's background thread is still accessing the database.
if(newUser == null) {
return "Login Failed. Username does not exist.";
} else {
updateCurrentUser(oldUser, newUser);
return null; // No errors
}
}
}
Repository:
public LiveData<User> getUserByName(String name) {
return myDao.getUserByName(name);
}
MyDao:
@Query("SELECT * FROM user_table WHERE username = :username")
LiveData<User> getUserByName(String username);
Edit 4/22/2022: Here are some options I've tried, but still no solution:
In repository:
// getAllMarks returns LiveData<List<Mark>> from myDao
// Just grabbing the value returns null since the background thread is still grabbing the data
List<Mark> localMarks = repositoryLocal.getAllMarks.getValue(); // null
// So I tried to wait for the data, but I can't find the LifecycleOwner from within the repository.
// "this" should be the activity that called the viewModel that called the repository. (Required type: LifecycleOwner)
// Is it appropriate to just pass Activity down the line through viewModel to repository?
repositoryLocal.getAllMarks.observe(this, new Observer<List<Mark>>() {
@Override
public void onChanged(List<Mark> marks) {
localMarks = marks;
}
});
// getAllMarksValue returns List<Mark> from myDao
// IllegalStateException: will lock main thread
List<Mark> localMarks = repositoryLocal.getAllMarksValue();
Solution 1:[1]
There's no need of the LiveData for your use case, you can simply return User from Dao. Like, MyDao:
@Query("SELECT * FROM user_table WHERE username = :username")
User getUserByName(String username);
Repository:
public User getUserByName(String name) {
return myDao.getUserByName(name);
}
LiveData should only be used when the underlying data changes frequently and we want to do some changes accordingly.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | Vaibhav Goyal |