如何依次处理两个LiveData?基于Room与ViewModel的注册流程实现
Alright, let's break down how to solve this step by step—since we're dealing with LiveData's asynchronous nature, we need to handle the email check properly and pass results back through the ViewModel to the UI.
We'll create a type-safe result wrapper for registration, implement the asynchronous email check in the registration service, update the ViewModel to relay results to the UI, and finally handle navigation/error displays in the RegisterActivity.
Step 1: Define the RegistrationResponse Class
First, let's make a sealed class to represent registration success or failure—this makes handling results type-safe and clean:
public sealed class RegistrationResponse { public static class Success extends RegistrationResponse { public final User user; public Success(User user) { this.user = user; } } public static class Error extends RegistrationResponse { public final String message; public Error(String message) { this.message = message; } } }
Step 2: Implement performRegistration in UsersRegistrationService
Since getUserByEmail returns a LiveData<User>, we'll use a MediatorLiveData to observe the query result, then either create a new user or return an error. We'll also handle edge cases where the email might get taken between our query and insert:
public class UsersRegistrationService { private final UsersRepository usersRepository; private final UsersDAO usersDAO; public UsersRegistrationService(UsersRepository usersRepository, UsersDAO usersDAO) { this.usersRepository = usersRepository; this.usersDAO = usersDAO; } public LiveData<RegistrationResponse> performRegistration(String name, String email, String password) { MediatorLiveData<RegistrationResponse> resultLiveData = new MediatorLiveData<>(); LiveData<User> existingUserLiveData = usersRepository.findUserByEmail(email); resultLiveData.addSource(existingUserLiveData, existingUser -> { // Stop observing once we get a result to avoid duplicate triggers resultLiveData.removeSource(existingUserLiveData); if (existingUser != null) { resultLiveData.setValue(new RegistrationResponse.Error("Email is taken")); return; } // Create and insert the new user User newUser = new User(name, email, password); try { usersDAO.insert(newUser); resultLiveData.setValue(new RegistrationResponse.Success(newUser)); } catch (SQLiteConstraintException e) { // Handle rare concurrency case where email was taken after our query resultLiveData.setValue(new RegistrationResponse.Error("Email is taken")); } }); return resultLiveData; } }
Don't forget to add the insert method to your UsersDAO:
@Dao abstract class UsersDAO { @Query("SELECT * FROM users WHERE email = :email LIMIT 1") abstract LiveData<User> getUserByEmail(String email); @Insert abstract void insert(User user); }
And update your User entity to enforce unique emails at the database level:
@Entity(tableName = "users") public class User implements Serializable { // Implement Serializable to pass between activities @PrimaryKey(autoGenerate = true) private long id; private String name; @ColumnInfo(unique = true) // Ensures no duplicate emails in the database private String email; private String password; // Constructor, getters, and setters public User(String name, String email, String password) { this.name = name; this.email = email; this.password = password; } // Add getters and setters for all fields here }
Step 3: Update RegisterViewModel to Relay Results
Modify the ViewModel to observe the registration service's result and expose it to the Activity via a LiveData<RegistrationResponse>:
public class RegisterViewModel extends ViewModel { private final UsersRegistrationService registrationService; public final MutableLiveData<String> name = new MutableLiveData<>(); public final MutableLiveData<String> email = new MutableLiveData<>(); public final MutableLiveData<String> password = new MutableLiveData<>(); public final MutableLiveData<String> validationErrorMessage = new MutableLiveData<>(); public final MutableLiveData<RegistrationResponse> registrationResult = new MutableLiveData<>(); public RegisterViewModel(UsersRegistrationService registrationService) { this.registrationService = registrationService; } public void register() { validationErrorMessage.setValue(null); if (!validateInput()) return; LiveData<RegistrationResponse> responseLiveData = registrationService.performRegistration( name.getValue(), email.getValue(), password.getValue() ); // Observe the result once, then clean up the observer to avoid memory leaks responseLiveData.observeForever(response -> { responseLiveData.removeObserver(r -> {}); registrationResult.setValue(response); }); } private boolean validateInput() { String nameStr = name.getValue(); String emailStr = email.getValue(); String passwordStr = password.getValue(); if (nameStr == null || nameStr.trim().isEmpty()) { validationErrorMessage.setValue("Name cannot be empty"); return false; } if (emailStr == null || !android.util.Patterns.EMAIL_ADDRESS.matcher(emailStr).matches()) { validationErrorMessage.setValue("Invalid email format"); return false; } if (passwordStr == null || passwordStr.length() < 6) { validationErrorMessage.setValue("Password must be at least 6 characters"); return false; } return true; } }
Step 4: Update RegisterActivity to Handle UI and Navigation
In the Activity, observe the ViewModel's live data to show errors or navigate to MainActivity when registration succeeds:
public class RegisterActivity extends AppCompatActivity { private RegisterViewModel viewModel; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_register); // Initialize ViewModel (use dependency injection like Hilt in production apps) UsersDAO usersDAO = AppDatabase.getInstance(getApplicationContext()).usersDAO(); UsersRepository repository = new UsersRepository(usersDAO); UsersRegistrationService service = new UsersRegistrationService(repository, usersDAO); viewModel = new ViewModelProvider(this, new ViewModelProvider.Factory() { @NonNull @Override public <T extends ViewModel> T create(@NonNull Class<T> modelClass) { return (T) new RegisterViewModel(service); } }).get(RegisterViewModel.class); // Bind UI elements EditText etName = findViewById(R.id.et_name); EditText etEmail = findViewById(R.id.et_email); EditText etPassword = findViewById(R.id.et_password); Button btnRegister = findViewById(R.id.btn_register); TextView tvError = findViewById(R.id.tv_error); // Link input fields to ViewModel etName.addTextChangedListener(new TextWatcher() { @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) {} @Override public void onTextChanged(CharSequence s, int start, int before, int count) { viewModel.name.setValue(s.toString()); } @Override public void afterTextChanged(Editable s) {} }); // Repeat similar listeners for etEmail and etPassword... // Observe validation errors viewModel.validationErrorMessage.observe(this, errorMsg -> { if (errorMsg != null) { tvError.setText(errorMsg); tvError.setVisibility(View.VISIBLE); } else { tvError.setVisibility(View.GONE); } }); // Observe registration results viewModel.registrationResult.observe(this, response -> { if (response instanceof RegistrationResponse.Error) { String errorMsg = ((RegistrationResponse.Error) response).message; tvError.setText(errorMsg); tvError.setVisibility(View.VISIBLE); } else if (response instanceof RegistrationResponse.Success) { // Navigate to MainActivity and close registration screen Intent intent = new Intent(this, MainActivity.class); intent.putExtra("REGISTERED_USER", ((RegistrationResponse.Success) response).user); startActivity(intent); finish(); } }); // Register button click handler btnRegister.setOnClickListener(v -> viewModel.register()); } }
Key Notes
- LiveData Cleanup: We use
observeForeverin the ViewModel (since ViewModels don't have a lifecycle) but immediately remove the observer after getting a result to prevent memory leaks. - Concurrency Safety: The Room unique constraint plus exception handling covers edge cases where another thread inserts the same email between our query and insert.
- Navigation: Calling
finish()after startingMainActivityensures users can't navigate back to the registration screen via the back button.
内容的提问来源于stack exchange,提问作者clzola

