How to update all injected objects with new live data

Viewed 93

I am using Dependency injection, Coordinators & the MVP design pattern.

Once a user logs into my app, I get their account object from Firestore in my AppCoordinator. This account object and other services are injected into my 'TabBarCoodinator' which injects them into all child coordinators and they do the same with presenters, etc.

init(_ navigationController: UINavigationController,
     _ firestoreRepository: FirestoreRepositoryProtocol,
     _ firebaseCloudStorageService: FirebaseCloudStorageServiceProtocol,
     _ firebaseCrashlyticsService: FirebaseCrashlyticsServiceProtocol,
     _ notificationService: NotificationServiceProtocol,
     _ account: FIRAccount,
     _ alertHandlerService: AlertHandlerServiceProtocol) {

    self.navigationController = navigationController

    super.init()

    self.coord1 = Coord1(
        firestoreRepository,
        firebaseCloudStorageService,
        geoLocationService,
        alertHandlerService,
        account,
        delegate: self)

    self.coord2 = Coord2(
        firestoreRepository,
        notificationService,
        alertHandlerService,
        account)

    self.coord3 = Coord3(
        firestoreRepository,
        firebaseCloudStorageService,
        firebaseCrashlyticsService,
        geoLocationService,
        alertHandlerService,
        account,
        delegate: self)
}

I have set up a listener in the AppCoordinator to listen for changes to the account object. When changes occur, I want this to propagate down into all other classes that the account is injected into so no objects have an outdated account object.

The problem I am facing is how to propagate account object changes to all children classes.

Each part of the app, such as the 'EditProfilePresenter', plays a role in editing a piece of the account object. An edit is made to the account object in a particular presenter and changes are pushed to Firestore.

This results in a huge issue, all account objects that have been injected into other coordinators and presenters from the AppCoordinator are now out of date. The only up-to-date object is the 'EditProfilePresenter' account object and the one held in the AppCoordinator due to the listener.

How can I either propagate the updated information to all objects using the account object or, how should I set up my objects to have a single source of truth for a user account object? Ideally, I want the injected account to reference the original stored on the AppCoordinator, not a copy or clone of it.

Things I have considered:

  1. Set up three individual listeners for the three main parts of the app. Each listener would manage that slice of the apps account object. However, this results in 3 reads for a single update which is not great for scalability at all.

  2. Store the account object on the FirestoreRepositoryService. Most classes requiring this service use the account object and it is instantiated in the app coordinator so should be shared throughout all classes. However, this sounds bad as it breaks the responsibility of the service simply performing Firestore related requests.

  3. Use a singleton to manage the account state which is shared and used by all classes needing it. This would mean the account object is always kept updated in a single place with a single listener. However, this would drastically reduce testability & readability due to the account object no longer being injected as well as creating tight coupling throughout the whole app on a single object that whose state is shared everywhere.

  4. Look into restructuring the account object where information is split into specific sections of the account object. Ideally, one tab should only deal with its assigned slice of the information and would not have the whole account object. Rather, it would have a sub-collection within the account object. However, shared information that is commonly used everywhere, such as a name, would either be stored on the account object or in a sub-collection resulting in even more reads required to maintain data and a few other drawbacks.

  5. Setup a notification to be sent out every time the app coordinator receives an account update and register for this notification in every object that uses the account object. However, this again feels like a really poor way of dealing with the issue due to the overhead of needing to ensure every object is registered to this notification or the data will be inconsistent.

I am really not sure what to do, any help on how to deal with this would be appreciated!

1 Answers

I could not find an answer to this, so instead, I decided to go the Singleton route and everything is working perfectly.

My main concern with using a singleton was around the testability of the code, however, I have circumvented this through exposing an extension method on the singleton enabling me to set up mock data for the user object in my unit tests.

Sure, this means theoretically I could accidentally call it in the live code, but an aptly named function indicating it is for creating mock data is enough for me.

Furthermore, I could unit test the code on the singleton in one place and not have to repeat the same test in multiple places which is a huge plus.

Here is the code. If anyone does come up with a solution to updating injected object data that would be great.

class CurrentUserService: CurrentUserServiceProtocol {

// Vars so I can override them in unit tests
var firebaseCrashlytics: FirebaseCrashlyticsServiceProtocol
var firestoreRepository: FirestoreRepositoryProtocol

private (set) var userListener: ListenerRegistration?
private (set) var user: FIRAccount?

static let shared = CurrentUserService()

init() {
    self.firebaseCrashlytics = FirebaseCrashlyticsService()
    self.firestoreRepository = FirestoreRepository()
}

func setUser(user: FIRAccount, ref: CollecitonRef) throws {
    self.user = user
    setupAccountListener(docId: user.id, ref)
}

func setupAccountListener(docId: String, _ ref: CollectionReference) {

    userListener?.remove()

// Setup listener however you need to do it in your code
    userListener = self.firestoreRepository.readById(
        docId: docId,
        from: ref,
        returning: FIRAccount.self,
        withSnapshotListener: true) { (result, account) in

            switch result.succeeded {
            case false:
// Log error
            case true:
                if let account = account {
                    self.user = account
                } else {
                    // Log error
                }
            }
    }
}

func updateUser(_ account: FIRAccount) {
    self.user = account
}

func logoutCurrentUser() {
    self.user = nil

    userListener?.remove()
    self.userListener = nil
}


}

extension CurrentUserService {
    func setMockUserData(_ account: FIRAccount) {
        self.user = account
    }
}
Related