In my app, I have a long registration flow with several different screens. I have a RegisterPage
that shows a non-scrollable carousel page view with the different registration screens.
Currently, I have a massive RegisterViewModel
that does logic and validation for all the screens at once. This is problematic for maintainability reasons, and I would like to split it into several smaller view models.
The file structure, just for clarity’s sake, looks like this currently:
regisration/
- register.dart
- register_vm.dart
- pages/
- account_type.dart
- user_credentials.dart
And it will need to end up looking like this:
registration/
- register.dart
- main_register_vm.dart
- pages/
- account_type/
- account_type.dart
- account_type_vm.dart
- credentials/
- user_credentials.dart
- user_credentials_vm.dart
Each screen, when the user inputs into it, should send data back to the RegisterViewModel
‘s registration data, where it will be stored until the user completes the register flow where it is then sent to the database.
Each screen has different validation logic, but for all the registration screens they share these functionalities:
- Enable/disable the “Continue” button at the bottom of each page
- Move to the next page in the carousel
- Save inputted data into a
RegistrationData
class.
I have been using get_it
to save an instance of the RegisterViewModel
using registerLazySingleton
to make sure that each registration has the most up to date information. Some of the UI on these screens depend on the data found in RegistrationData
, which is why it is super important to have 1 instance of the view model.
My problem is that I can’t seem to design a decent architecture that ensures that all data is synced across each registration screen. Currently, the file with my registration view model has over 700 lines of code – it needs to be simplified.
I wrote this new view model:
class RegistrationViewModel extends BaseViewModel
with
RegistrationPageHandling,
RegistrationDataHandling,
ContinueActionHandling,
RegisterActionHandling {
@override
void nextPage() {
super.nextPage();
notifyListeners();
}
@override
void previousPage() {
super.previousPage();
notifyListeners();
}
/// Enables the continue button at the bottom of the current registration page.
@override
void enableContinue({bool rebuild = true}) {
super.enableContinue();
if (rebuild) notifyListeners();
}
/// Disables the continue button at the bottom of the current registration page.
@override
void disableContinue({bool rebuild = true}) {
super.disableContinue();
if (rebuild) notifyListeners();
}
}
}
mixin ContinueActionHandling {
bool isContinueActionDisabled = true;
void enableContinue() {
isContinueActionDisabled = false;
}
void disableContinue() {
isContinueActionDisabled = true;
}
}
mixin RegistrationDataHandling {
AccountType? accountType;
RegistrationData registrationData = RegistrationData.blank() // create default values
set registrationData(RegistrationData newData) => registrationData = newData;
}
mixin RegistrationPageHandling {
int pageIndex = 0;
PageController pageController = PageController();
void nextPage() {
pageIndex++;
pageController.animateToPage(pageIndex,
duration: const Duration(milliseconds: 300), curve: Curves.easeIn);
}
void previousPage() {
pageIndex--;
pageController.animateToPage(pageIndex,
duration: const Duration(milliseconds: 300), curve: Curves.easeIn);
}
}
mixin RegisterActionHandling {
Future<void> register() async {} // send registration data to database to register the user
}
This was supposed to be my overarching, “super view model” if you will. Functionality here will be shared across different registration screens.
Option 1:
I tried the following for a registration page where the user sets their account type:
class AccountTypeViewModel extends RegistrationViewModel {
void setAccountType(AccountType type) {
accountType = type;
registrationData.accountType = type;
enableContinue();
}
}
Something like this would be ideal due to how easy it is to use, but there’s a problem.
As far as I understand, instantiating AccountTypeViewModel
will also instantiate a new RegistrationViewModel
. For the other registration screens, when I instantiate their respective sub view models, they will also create a new RegistrationViewModel
. As a result, registrationData
will hold different values for each screen, when it needs to accumulate ALL of the data that has been entered so far.
Option 2:
So I came up with something else to try and fix this problem, and it looks like this:
abstract class SubViewModelRegistration extends BaseViewModel {
// no longer extends RegistrationViewModel
final RegistrationViewModel parentViewModel;
// must point to the same instance of parent viewmodel!
SubViewModelRegistration(this.parentViewModel);
}
class AccountTypeViewModel extends SubViewModelRegistration {
AccountTypeViewModel(super.parentViewModel);
void setAccountType(AccountType type) {
parentViewModel.accountType = type;
parentViewModel.registrationData.accountType = type;
parentViewModel.enableContinue();
}
}
This seems to work fine, I guess. I am using stacked architecture’s ViewModelBuilder.reactive
to listen to state changes from a viewmodel, and I could have the view subscribe to a view model of type AccountTypeViewModel(locator<RegistrationViewModel>())
. But I think this is annoying to use. typing parentViewModel
over and over again is not fun. Especially when this app has about 12 different registration screens. And each of those screens have really complicated validation requirements, where each one has to check and compare data saved in previous registration steps.
What would be a good way to create all of these sub-viewmodels in such a way that adheres to SOLID principles? They need to all share a common data source, a single source of truth, if you will. I also really hate having to type parentViewModel
in front of every use of a variable.
I like Option 1 much much better than Option 2. Is there a way to inherit from a specific instance of a super class? That is, all sub viewmodels, when created, all extend from 1 instance of the overarching viewmodel? That would fix the issue of data not being synced across registration screens.
What’s a good course of action here? Is there an Option 3 I haven’t thought of?