Skip to content

[Woo POS] Avoid initial view model unwanted recreation #12799

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
May 23, 2024

Conversation

iamgabrielma
Copy link
Contributor

This PR addresses the issue discussed in slack, here: p1715934814114829-slack-C025A8VV728 . #12761 should be merged first

Description

The initial view model that drives the pos dashboard is currently created inside the PointOfSaleEntryPointView, which is recreated multiple times, triggering a view model recreation with it.

This PR addresses this problem by extracting the view model initialization a few levels up the chain, to the HubMenuViewController, to assure it's only created once through the app's lifecycle, and then we inject it into the proper view.

Question

I'm not quite sure if we should compose the view model and its dependencies on a different manner. Doing so in the HubMenuViewController doesn't feel quite right, but does follow the same pattern we already use for creating the HubMenuViewModel.

Perhaps moving its composition to the HubMenuCoordinator or the MainTabBarController would make sense? Despite POS not being a "tab", it is intended to be a different mode.

Testing instructions

  • In the POS view, tap on the Filter button and observe that the modal is not automatically dismissed anymore.
  • Alternatively, add a debugprint on view model init and observe that is only created once.

@iamgabrielma iamgabrielma added type: task An internally driven task. feature: POS labels May 21, 2024
@dangermattic
Copy link
Collaborator

dangermattic commented May 21, 2024

2 Warnings
⚠️ View files have been modified, but no screenshot or video is included in the pull request. Consider adding some for clarity.
⚠️ This PR is assigned to the milestone 18.8. This milestone is due in less than 2 days.
Please make sure to get it merged by then or assign it to a milestone with a later deadline.

Generated by 🚫 Danger

@iamgabrielma iamgabrielma added this to the 18.8 milestone May 21, 2024
@wpmobilebot
Copy link
Collaborator

wpmobilebot commented May 21, 2024

WooCommerce iOS📲 You can test the changes from this Pull Request in WooCommerce iOS by scanning the QR code below to install the corresponding build.

App NameWooCommerce iOS WooCommerce iOS
Build Numberpr12799-df0879b
Version18.7
Bundle IDcom.automattic.alpha.woocommerce
Commitdf0879b
App Center BuildWooCommerce - Prototype Builds #9211
Automatticians: You can use our internal self-serve MC tool to give yourself access to App Center if needed.

@iamgabrielma iamgabrielma requested review from jaclync and joshheald May 21, 2024 06:14
@jaclync jaclync self-assigned this May 22, 2024
Copy link
Contributor

@jaclync jaclync left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left a comment but would like to hear other opinions as well!


return viewModel
}()
@ObservedObject private var viewModel: PointOfSaleDashboardViewModel
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just wondering, would @StateObject here fix the issue? Personally, I feel like owning the view model within the POS boundary (as if an invisible framework) would be preferable, and this seems to be the goal of StateObject in the doc. The dependencies can be DI'ed through the view's initializer, and the view just does a simple initialization of the view model with the depenencies.

@ObservedObject view model works in the HubMenu because it's being held by the UIKit hosting controller which doesn't get recreated like in a SwiftUI view.

If @StateObject doesn't work out and the POS view model needs to be DI'ed to the POS view, maybe having HubMenuViewModel own it would be more consistent with the existing setup like InPersonPaymentsMenuViewModel.

Copy link
Contributor Author

@iamgabrielma iamgabrielma May 22, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just wondering, would @StateObject here fix the issue? Personally, I feel like owning the view model within the POS boundary (as if an invisible framework) would be preferable, and this seems to be the goal of StateObject in the doc. The dependencies can be DI'ed through the view's initializer, and the view just does a simple initialization of the view model with the depenencies.

You mean the View declaring its own dependencies via a @StateObject rather than DI it from outside? So:

Suggested change
@ObservedObject private var viewModel: PointOfSaleDashboardViewModel
@StateObject private var viewModel = PointOfSaleDashboardViewModel(products: POSProductFactory.makeFakeProducts(),
cardReaderConnectionViewModel: .init(state: .connectingToReader))

It does seem to fix the problem as well, I'm guessing because of:

SwiftUI creates a new instance of the model object only once during the lifetime of the container that declares the state object. For example, SwiftUI doesn’t create a new instance if a view’s inputs change, but does create a new instance if the identity of a view changes

I like the idea of setting a POS boundary via using StateObject, and the benefits of dependency injection for testing purposes are not lost in this case since is just the initial POS state. I don't really know if there's any other trade-offs we should be aware of?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup could be just @StateObject private var viewModel: PointOfSaleDashboardViewModel and setting the view model in the view init given the dependencies.

I don't really know if there's any other trade-offs we should be aware of?

Good point, I can look more into this tomorrow. We've been using @StateObject in the code base in views that own a view model throughout the UI's lifetime.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After some more reading on the official doc and other posts, StateObject feels to me like the most appropriate place for the view to own the view model that's only created once. With a few notes:

  • The view model's dependencies shouldn't change, which I think is the case for POS - site ID, some data layer service implementation, analytics implementation (singleton in the app), etc.

Explicit state object initialization works well when the external data that the object depends on doesn’t change for a given instance of the object’s container.

If we do want the view model to be recreated again when some dependency changes, it looks like we can achieve this by binding the identity of the view that owns the StateObject view model to one or more parameters.

  • StateObject should be private in all cases, and devs need to be aware not to re-initialize it again after the first initialization because any following inits wouldn't work silently and could result in unexpected behavior

Lemme know what you think!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good to me! Updated on 4a5ded6 . Now the Hub doesn't know anything about the pos viewmodel, but is entirely driven internally by the point of entry.

Base automatically changed from task/12761-pos-quantity-mechanism to trunk May 22, 2024 07:47
This is not needed to be explicitely public anymore since we’ve removed the pos framework.
@iamgabrielma iamgabrielma requested a review from jaclync May 23, 2024 03:59
@iamgabrielma
Copy link
Contributor Author

For some reason it failed CI in the appcenter_upload phase with Internal Service Error, please try again later pointing to a certificate expiration issue. I re-ran it and now passes normally ✅

Copy link
Contributor

@jaclync jaclync left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for trying different things and making the changes! Tests well and LGTM 🚀

}()
@StateObject private var viewModel: PointOfSaleDashboardViewModel = PointOfSaleDashboardViewModel(
products: POSProductFactory.makeFakeProducts(),
cardReaderConnectionViewModel: .init(state: .connectingToReader)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

super nit: is the change of the cardReaderConnectionViewModel initial state from scanningForReader to connectingToReader intentional? really just a minor non-blocking observation as it's just a simulated flow, feel free to leave it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, not intentional at all, just a random connecting initial state :D

@iamgabrielma iamgabrielma merged commit 1533976 into trunk May 23, 2024
22 checks passed
@iamgabrielma iamgabrielma deleted the task/pos-compose-viewmodel-outside-view branch May 23, 2024 16:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature: POS type: task An internally driven task.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants