Опыт использования «координаторов» в реальном «iOS»-проекте

    Мир современного программирования богат на тренды, а для мира программирования «iOS»-приложений это справедливо вдвойне. Надеюсь, я не сильно ошибусь, утверждая, что одним из самых «модных» архитектурных шаблонов последних лет является «координатор». Вот и наша команда какое-то время назад осознала непреодолимое желание попробовать на себе этот прием. Тем более, что подвернулся очень удачный случай – значительное изменение логики и тотальная перепланировка навигации в приложении.

    Проблема


    Зачастую так получается, что контроллеры начинают брать на себя слишком многое: «отдавать команды» напрямую владеющим его UINavigationController, «общаться» с родными «братьями»-контроллерами (даже инициализировать их и передавать в навигационный стек) – в общем делать много того, о чем им не положено даже подозревать.

    Одним из возможных способов этого избежать как раз и является «координатор». Причем, как оказалось, довольно удобным в работе и очень гибким: шаблон способен управлять навигационными событиями как небольших модулей (представляющих собой, возможно, лишь один-единственный экран), так и всего приложения (запуская свой «flow», условно говоря, прямо из UIApplicationDelegate).

    История


    Мартин Фаулер в своей книге «Patterns of Enterprise Application Architecture» назвал этот шаблон «Application Controller». А первым его популяризатором в среде «iOS» считается Соруш Ханлу: все началось с его доклада на «NSSpain» в 2015 году. Затем появилась обзорная статья на его сайте, которая имела несколько продолжений (например это).

    А затем последовали множество обзоров (запрос «ios coordinators» выдает десятки результатов разного качества и степени подробности), в том числе даже руководство на Ray Wenderlich и статья от Пола Хадсона на его «Hacking with Swift» в рамках серии материалов о путях избавления от проблемы «массивного» контроллера.

    Забегая вперед, самая заметная тема для обсуждений – это проблема кнопки возврата в UINavigationController, нажатие на которую обрабатывается не нашим кодом, а мы лишь можем получить обратный вызов.

    Собственно, почему это проблема? Координаторы, как и любые объекты, чтобы существовать в памяти, нуждаются в том, чтобы какой-то другой объект ими «владел». Как правило, при выстраивании системы навигации с помощью координаторов, одни координаторы порождают другие и хранят на них сильную ссылку. При «выходе из зоны ответственности» порожденного координатора, контроль возвращается к порождающему координатору, и память, которую занимал порождаемый, должна быть освобождена.

    Соруш имеет свое видение решения этой проблемы, а также отмечает еще пару достойных подходов. Но мы к этому еще вернемся.

    Первое приближение


    Перед тем как начать показывать реальный код, я бы хотел пояснить, что хотя принципы полностью соответствуют тем, к которым мы пришли в проекте, отрывки из кода и примеры его использования упрощены и сокращены везде, где это не мешает их восприятию.

    Когда мы в команде впервые взялись экспериментировать с координаторами, у нас не было для этого очень много времени и свободы действий: необходимо было считаться с существующими принципами и устройством навигации. Первый вариант реализации координаторов базировался на общем «роутере», который владеет и управляет UINavigationController. Он умеет делать с экземплярами UIViewController все, что нужно касаемо навигации – «push»/«pop», «present»/«dismiss» плюс манипуляции с «root»-контроллером. Пример интерфейса такого роутера:

    import UIKit
    
    protocol Router {
        func present(_ module: UIViewController, animated: Bool)
        func dismissModule(animated: Bool, completion: (() -> Void)?)
        func push(_ module: UIViewController, animated: Bool, completion: (() -> Void)?)
        func popModule(animated: Bool)    
        func setAsRoot(_ module: UIViewController)
        func popToRootModule(animated: Bool)
    }

    Конкретная реализация инициализируется с экземпляром UINavigationController и ничего особенно хитрого в себе не содержит. Единственное ограничение: в качестве значений аргументов методов интерфейса нельзя передавать другие экземпляры UINavigationController (по понятным причинам: UINavigationController не может содержать UINavigationController в своем стеке – это ограничение UIKit).

    Координатору, как и любому объекту, необходим владелец – другой объект, который будет хранить в себе ссылку на него. Ссылку на корневой может хранить порождающий его объект, но каждый координатор также может порождать другие координаторы. Поэтому базовым интерфейсом был написан класс, обеспечивающий механизм менеджмента порождаемых координаторов:

    class Coordinator {
        
        private var childCoordinators = [Coordinator]()
    
        func add(dependency coordinator: Coordinator) {
            // ...
        }
        
        func remove(dependency coordinator: Coordinator) {
            // ...
        }
    
    }

    Одно из подразумеваемых достоинств координаторов – это инкапсуляция знаний о конкретных подклассах UIViewController. Чтобы обеспечить взаимодействие роутера и координаторов мы ввели следующий интерфейс:

    protocol Presentable {
        func presented() -> UIViewController
    }

    Тогда каждый конкретный координатор должен наследоваться от Coordinator и реализовывать интерфейс Presentable, а интерфейс роутера – принять следующий вид:

    protocol Router {
        func present(_ module: Presentable, animated: Bool)
        func dismissModule(animated: Bool, completion: (() -> Void)?)
        func push(_ module: Presentable, animated: Bool, completion: (() -> Void)?)
        func popModule(animated: Bool)    
        func setAsRoot(_ module: Presentable)
        func popToRootModule(animated: Bool)
    }

    (Подход с Presentable также позволяет использовать координаторы внутри модулей, которые написаны для взаимодействия напрямую с экземплярами UIViewController, не подвергая их (модули) кардинальной переработке.)

    Краткий пример этого всего в деле:

    final class FirstCoordinator: Coordinator, Presentable {
        
        func presented() -> UIViewController {
            return UIViewController()
        }
        
    }
    
    final class SecondCoordinator: Coordinator, Presentable {
        
        func presented() -> UIViewController {
            return UIViewController()
        }
        
    }
    
    let nc = UINavigationController()
    let router = RouterImpl(navigationController: nc) // Router implementation.
    router.setAsRoot(FirstCoordinator())
    
    router.push(SecondCoordinator(), animated: true, completion: nil)
    router.popToRootModule(animated: true)

    Следующее приближение


    И вот однажды наступил момент тотальной переделки навигации и абсолютной свободы самовыражения! Момент, когда ничто не мешало попробовать реализовать навигацию на координаторах с использованием заветного метода start() – версии, которая и пленила изначально своей простотой и лаконичностью.

    Упомянутые выше возможности Coordinator, очевидно, останутся не лишними. Но к общему интерфейсу необходимо прибавить тот самый метод:

    protocol Coordinator {
        
        func add(dependency coordinator: Coordinator)
        func remove(dependency coordinator: Coordinator)
        func start()
        
    }
    
    class BaseCoordinator: Coordinator {
        
        private var childCoordinators = [Coordinator]()
        
        func add(dependency coordinator: Coordinator) {
            // ...
        }
        func remove(dependency coordinator: Coordinator) {
            // ...
        }
        func start() { }
        
    }

    «Swift» не предлагает возможность объявлять абстрактные классы (т.к. в большей степени он ориентирован на протокольно-ориентированный подход, нежели на более классический, объектно-ориентированный), поэтому метод start() можно как оставить с пустой реализацией, так и засунуть туда что-нибудь вроде fatalError(_:file:line:) (принуждая переопределять этот метод наследниками). Лично мне первый вариант больше по душе.

    Но у «Swift» есть замечательная возможность добавлять протокольным методам реализации по умолчанию, поэтому первой мыслью, конечно, была не объявлять базовый класс, а сделать что-нибудь вроде этого:

    extension Coordinator {
        
        func add(dependency coordinator: Coordinator) {
            // ...
        }
        func remove(dependency coordinator: Coordinator) {
            // ...
        }
        
    }

    Но расширения протоколов не могут объявлять хранимые поля, а реализации этих двух методов, очевидно, должна основываться на приватном хранимом свойстве типа.

    Основа любого конкретного координатора таким образом будет выглядеть так:

    final class SomeCoordinator: BaseCoordinator {
        
        override func start() {
            // ...
        }
        
    }

    В инициализатор могут быть добавлены какие угодно зависимости, которые необходимы для функционирования координатора. Как типовой случай – экземпляр UINavigationController.

    Если это корневой координатор, ответственность которого заключается в отображении корневого UIViewController, координатор может, например, принимать новый экземпляр UINavigationController с пустым стеком.

    Внутри себя координатор при обработке событий (об этом – далее) может передавать этот UINavigationController дальше, другим координаторам, которые он порождает. А те могут также делать с текущим состоянием навигации то, что им необходимо: «push», «present», да хоть бы и весь навигационный стек подменять.

    Возможные улучшения интерфейса


    Как выяснилось позднее, далеко не каждый координатор будет порождать другие координаторы, поэтому не все они должны зависеть от такого базового класса. Поэтому один из коллег из смежной команды предложил избавиться от наследования и ввести интерфейс менеджера зависимостей в качестве внешей зависимости:

    protocol CoordinatorDependencies {
        
        func add(dependency coordinator: Coordinator)
        func remove(dependency coordinator: Coordinator)
        
    }
    
    final class DefaultCoordinatorDependencies: CoordinatorDependencies {
        
        private let dependencies = [Coordinator]()
        
        func add(dependency coordinator: Coordinator) {
            // ...
        }
        func remove(dependency coordinator: Coordinator) {
            // ...
        }
        
    }
    
    final class SomeCoordinator: Coordinator {
        
        private let dependencies: CoordinatorDependencies
        
        init(dependenciesManager: CoordinatorDependencies = DefaultCoordinatorDependencies()) {
            dependencies = dependenciesManager
        }
        
        func start() {
            // ...
        }
        
    }

    Обработка событий, порождаемых пользователем


    Хорошо, координатор создал и каким-то образом инициировал новое отображение. Скорее всего, пользователь смотрит на экран и видит некий набор визуальных элементов, с которыми он может взаимодействовать: кнопки, текстовые поля и пр. Часть из них провоцируют навигационные события, и ими должен управлять координатор, породивший данный контроллер. Для решения этой задачи мы используем традиционное делегирование.

    Предположим, имеется некий подкласс UIViewController:

    final class SomeViewController: UIViewController { }

    И координатор, который добавляет его в стек:

    final class SomeCoordinator: Coordinator {
        
        private let dependencies: CoordinatorDependencies
        private weak var navigationController: UINavigationController?
        
        init(navigationController: UINavigationController,
             dependenciesManager: CoordinatorDependencies = DefaultCoordinatorDependencies()) {
            self.navigationController = navigationController
            dependencies = dependenciesManager
        }
        
        func start() {
            let vc = SomeViewController()
            navigationController?.pushViewController(vc, animated: true)
        }
        
    }

    Этому же координатору мы делегируем обработку соответствующих событий контроллера. Тут, собственно, используется классическая схема:

    protocol SomeViewControllerRoute: class {
        func onSomeEvent()
    }
    
    final class SomeViewController: UIViewController {
        
        private weak var route: SomeViewControllerRoute?
        
        init(route: SomeViewControllerRoute) {
            self.route = route
            super.init(nibName: nil, bundle: nil)
        }
        
        required init?(coder aDecoder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        }
        
        @IBAction
        private func buttonAction() {
            route?.onSomeEvent()
        }
        
    }
    
    final class SomeCoordinator: Coordinator {
        
        private let dependencies: CoordinatorDependencies
        private weak var navigationController: UINavigationController?
        
        init(navigationController: UINavigationController,
             dependenciesManager: CoordinatorDependencies = DefaultCoordinatorDependencies()) {
            self.navigationController = navigationController
            dependencies = dependenciesManager
        }
        
        func start() {
            let vc = SomeViewController(route: self)
            navigationController?.pushViewController(vc, animated: true)
        }
        
    }
    
    extension SomeCoordinator: SomeViewControllerRoute { 
        func onSomeEvent() {
            // ...
        }    
    }

    Обработка нажатия на кнопку возврата


    Еще один неплохой обзор обсуждаемого архитектурного шаблона был опубликован Полом Хадсоном на его сайте «Hacking with Swift», можно даже сказать, руководство. В нем же содержится простое, без обиняков, объяснение одного их возможных решений упомянутой выше проблемы кнопки возврата: координатор (если это необходимо) объявляет себя делегатом передаваемого ему экземпляра UINavigationController и отслеживает интересующее нас событие.

    У этого подхода есть небольшой недостаток: делегатом UINavigationController может быть только наследник NSObject.

    Итак, имеется координатор, который порождает другой координатор. Этот, другой, по вызову start() добавляет в стек UINavigationController какой-то свой UIViewController. По нажатию на кнопку возврата назад на UINavigationBar все, что нужно сделать – это дать знать порождающему координатору, что порожденный координатор закончил свою работу («флоу»). Для этого мы ввели еще один инструмент делегирования: каждому порождаемому координатору выделяется делегат, интерфейс которого реализует порождающий координатор:

    protocol CoordinatorFlowListener: class {
        func onFlowFinished(coordinator: Coordinator)
    }
    
    final class MainCoordinator: NSObject, Coordinator {
        
        private let dependencies: CoordinatorDependencies
        private let navigationController: UINavigationController
        
        init(navigationController: UINavigationController,
             dependenciesManager: CoordinatorDependencies = DefaultCoordinatorDependencies()) {
            self.navigationController = navigationController
            dependencies = dependenciesManager
            super.init()
        }
        
        func start() {
            let someCoordinator = SomeCoordinator(navigationController: navigationController, flowListener: self)
            dependencies.add(someCoordinator)
            someCoordinator.start()
        }
        
    }
    
    extension MainCoordinator: CoordinatorFlowListener {
        
        func onFlowFinished(coordinator: Coordinator) {
            dependencies.remove(coordinator)
            // ...
        }
        
    }
    
    final class SomeCoordinator: NSObject, Coordinator {
        
        private weak var flowListener: CoordinatorFlowListener?
        private weak var navigationController: UINavigationController?
        
        init(navigationController: UINavigationController,
             flowListener: CoordinatorFlowListener) {
            self.navigationController = navigationController
            self.flowListener = flowListener
        }
        
        func start() {
            // ...
        }
        
    }
    
    extension SomeCoordinator: UINavigationControllerDelegate {
        
        func navigationController(_ navigationController: UINavigationController,
                                  didShow viewController: UIViewController,
                                  animated: Bool) {
            guard let fromVC = navigationController.transitionCoordinator?.viewController(forKey: .from) else { return }
            if navigationController.viewControllers.contains(fromVC) { return }
            
            if fromVC is SomeViewController {
                flowListener?.onFlowFinished(coordinator: self)
            }
        }
        
    }

    В примере выше MainCoordinator, ничего не делает: просто запускает «флоу» другого координатора – в реальной жизни это, конечно, бесполезно. В нашем приложении MainCoordinator получает извне данные, по которым он определяет, в каком состоянии находится приложение – авторизованном, не авторизованном и т.д. – и какой именно экран необходимо показать. В зависимости от этого, он запускает «флоу» соответствующего координатора. Если порожденный координатор закончил свою работу, главный координатор получает об этом сигнал через CoordinatorFlowListener и, скажем, запускает «флоу» другого координатора.

    Заключение


    Прижившееся решение, конечно, обладает рядом недостатков (как и любое решение любой проблемы).

    Да, приходится использовать много делегирования, но оно простое и имеет единое направление: от порождаемого к порождающему (от контроллера к координатору, от порождаемого координатора к порождающему).

    Да, чтобы спастись от утечек памяти, приходится в каждый координатор добавлять метод делегата UINavigationController с практически идентичной реализацией. (Первый подход этого недостатка лишен, но вместо этого более «щедро» делится внутренними знаниями о назначении конкретного координатора.)

    Но самый большой недочет такого подхода – это то, что в реальной жизни координаторы, к сожалению, будут знать об окружающем их мире чуть больше, чем хотелось бы. Точнее, в них придется добавлять элементы логики, зависящие от внешних условий, о которых координатор не осведомлен напрямую. В основном, это, собственно, то, что происходит по вызову метода start() или по обратному вызову onFlowFinished(coordinator:). А происходить в этих местах может что угодно, и это всегда будет «hardcoded»-поведение: добавление контроллера в стек, подмена стека, возврат к корневому контроллеру – что угодно. И это все зависит не от компетенций текущего контроллера, а от внешних условий.

    Тем не менее, код получается «симпатичный» и лаконичный, работать с ним действительно приятно, а навигация прослеживается по коду гораздо проще. Нам показалось, с упомянутыми недостатками, будучи о них осведомленными, вполне можно существовать.
    Спасибо, что дочитали до этого места! Надеюсь, узнали что-нибудь для себя полезное. А если вдруг захочется «больше меня», то вот ссылка на мой Twitter.
    • +12
    • 2,7k
    • 6
    Поделиться публикацией

    Комментарии 6

      0
      Спасибо за статью!
      Я не очень в теме паттерна координатора и не совсем понимаю плюсов от его использования.
      Я вижу, что в статье упор сделан на навигацию с помощью UINavigationController. Но навигация бывает разной: есть UITabBarController, есть UISplitViewController, есть «простые» методы открытия новых экранов с помощью present, есть и кастомные реализации навигации, например, Drawer'ы, могут быть модули не владеющие UIViewController, например, ячейки коллекций и некоторые другие ситуации.
      У меня нескольно вопросов:
      1) Насколько гибкими могут быть координаторы и как сильно инкапсулировать в себе логику различных видов навигации и их сочетания?
      2) Насколько удобно будет восстанавливать состояние навигации при перезапуске приложения?
      3) Какие именно удобства предоставляет координатор? Почему логику навигации не оставить в Router (я его понимаю в контексте VIPER модуля)?
        0
        Спасибо за хорошие вопросы! Постараюсь прокомментировать максимально подробно и по порядку пока хотя бы все, что смогу.

        «не совсем понимаю плюсов от его использования» / «Какие именно удобства предоставляет координатор?»
        Лично мне координаторы приглянулись своей линейностью: один координатор – один шаг навигации. Шаг вперед – инициализация и start(), шаг назад – это, условно, onFlowFinished().

        «Почему логику навигации не оставить в Router (я его понимаю в контексте VIPER модуля)?»
        В данном приложении у нас до этого все было на роутерах, и все довольно быстро превратилось в клубок (понятно, что тут не столько роутеры виноваты, сколько люди их писавшие, но все же). Но VIPER-ом у нас не пахло: были MVC-/MVP- и MVVM-модули с навигацией по ним на роутерах.

        «есть UITabBarController» / «Насколько удобно будет восстанавливать состояние навигации при перезапуске приложения?»
        У нас основной режим приложения – это экран с таб-баром как раз. Мы написали MainCoordinator, который, условно, внутри start() определяет состояние приложения и запускает тот или иной координатор. Есть некий AuthorizedStateCoordinator, который порождается MainCoordinator и открывает таб-бар с контроллерами. С этого момента у нас пока действуют в основном старые принципы навигации, поэтому я не смогу поделиться опытом, можно ли и удобно ли использовать координаторы для навигации по вкладкам. Но многие составляющие модули, которые дописывались в последнее время, «запечатаны» внутри координаторов (например, у нас есть вкладка с меню – там сам бог велел).
        Это, кстати, был один из пропагандируемых плюсов координаторов: начинать их использовать можно с любого места. Хотя бы чтобы просто понять, нравится или нет.

        «Насколько гибкими могут быть координаторы и как сильно инкапсулировать в себе логику различных видов навигации и их сочетания? „
        Все внутри и все hardcoded. Например, в start() может быть что-нибудь вроде navigationController.push(...), а в onFlowFinished() – очистка стека и передача управления другому координатору, который добавит свой контроллер в стек, и тот станет новым root. Грубо говоря, что угодно, но, к сожалению, только заранее определенно.
        Сочетания навигаций, как я себе вижу, можно как-то осуществлять с помощью сочетаний координаторов. По изначальной идее, координатор инициализируется под определенный способ навигации (с экземпляром UINavigationController, как у меня тут, например). Но модули (UIViewController или, скажем, какие-то группы UITabBarController) могут и переиспользоваться в разных координаторах – они прямо точно не должны зависеть от навигационных принципов.

        “могут быть модули не владеющие UIViewController, например, ячейки коллекций»
        Это дело у нас все в «представлении» и управляется контроллером. Но у контроллеров есть некий навигационный делегат – то, что у меня названо Route – интерфейс, который реализуется порождающим координатором и получает обратные вызовы по событиям от контроллера (нажата кнопка, выбрана ячейка и т.п.) По выбору ячейки, например, может быть порожден другой, дочерний, координатор, в который будет передан принцип навигации (например, наш UINavigationController) и по start() произойдет push соответствующего контроллера.

        В общем, мысли возникают какие-то такие. Но я буду стараться делиться и реальным опытом, когда что-то из этого буду пробовать на себе!
        0
        Мы поигрались с координаторами, но в итоге вернулись к подходу который использовали раньше в предыдущих проектах (swift и objc) и пока всем довольны. С координаторами начинается головная боль когда приходится решать вопросы диплинков и т.д. и задачи типа, а что если пользователь уже на этом экране, а что если пользователь в этот момент что то покупал, осталось только на кнопку нажать а тут он тапнул на пуш, и т.д. То есть поначалу казавшийся довольно изящным паттерн начинал обрастать странными иф/елсами, какие то вещи должны были быть весьма асинхронными и становились все более сложными в отладке. В итоге вернулись к старому подходу, и долизали его до библиотеки. Примерно описано тут:
        habr.com/ru/post/421097
        А делегаты живут абсолютно отдельно. Кому надо — подписываться и становится делегатом.
          0
          Большое спасибо, что поделились опытом – интересно!

          Не могли бы вы чуть подробней рассказать о проблемах, с которыми столкнулись при реализации диплинков при участии координаторов? Дело в том, что у нас в приложении они успешно (кажется) сосуществуют. Но я не исключаю, что что-то могло быть упущено!
            0

            Там есть несколько причин, сейчас уже все подробности не вспомню, попробую по пунктам:


            1. Классический пример координатора привносит знание во вью контроллер что будет происходить дальше (и у вас в начале статьи он есть). И это делает сложным A/B тестирование — например следующий контроллер будет показан пушем в текущий навигейшен контроллер или презентован модально. Та же проблема возникает с возвратом обратно


              func push(_ module: UIViewController, animated: Bool, completion: (() -> Void)?)
              func popModule(animated: Bool)    

              Конечно это решается более высоким уровнем абстракции как у вас или в других либах, но тем не менее.


            2. Различные асинхронные диплинки: Допустим у нас в URL в письме может быть продукт код, а показать контроллер мы можем только по продукт айди, то есть сперва надо сделать запрос на сервер, перевести продукт код в продукт айди и потом показывать уже соответствующий контроллер.


            3. На координатор начинают падать не свойственные ему функции значительной трассровки стека вью котнтроллеров на предмет — пользователь тапнул на пуш что бы показать ему продукт с таким то айди, а он уже находится в во вью контроллере ему его показывающим, пушить еще раз? Опеределить где он щас?


            4. Прочие сложности похожие на вариант 1, стилист шарит продукт в аймеседж приложении (доступный не всем пользователям), пользователь кликает, но он не в программе-лояльности, ему нужно показать весь флоу (5 контроллеров) подключения к программе, и потом показать ему продукт если он удачно подписался.


            5. Во время прохождения флоу на подключение к программе лояльности пользователь может забыть пароль от своего аккаунта, тогда ему придет письмо востановления пароля, где он тапнет в линк и внутри флоу будет еще один флоу восстановления пароля. После которого он может при желании продолжить флоу лояльности.


            6. Пользователь на онбоардинг скринах (тупо картинки), тапает на пуш, показывать продукт нужно другим совершенно способом.


            7. Куча логин составляющий и ограничений доступа, из серии что бы куда то попасть пользователь должен быть не только залогинен, но быть еще залогинен в программу доставки от другого вендора.


            8. Все даже не упомню. Из серии анимация навигации в одно место еще не отиграла, а пользователь уже кликнул еще на какой то внешний енгейджмент.



            Что было в голом остатке, сами координаторы являясь как бы архитектурным упрощающим паттерном требовали декомпозиция внутри, иначе они сами начинали выглядеть как спагетти. Туда начинала кроме того проваливаться бизнес логика, или они начинали обрастать координаторами координаторов. Координаторы сложно юнит тестить так как они завязаны жестко завязаны на UIKit.


            Поскольку мы уже использовали подобный подход успешно прежде, решено было отказаться от координаторов и формализовать старый подход использованный в других приложениях разрабатывавшихся у нас, который показал себя хорошо решая те же задачи.


            Используется формализованная библиотека уже более года — полет нормальный. Но конфигурация требует изучения. Примерно как автолайаут команды. Но щас все девелоперы с ней знакомы и ни у кого не возникает проблем.


            Каждый этап разделен на сущности, которые можно тестировать отдельно.


            Задачи имеют доступ к некоторому объекту мы его зовем wireframe где простые методы — goToProductArray(productArrayId: String), goToProduct(product: String), goToAccount() еоторые просто содержат внутри развернутую конфигурацию, которую проигрывает роутер библиотеки. Что и как ни одна из задач не знает и все работает на ура.

              0
              Спасибо большое за развернутый ответ!
              Пока что прочитал по-быстрому, некоторые пункты обязательно надо обдумать!

        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

        Самое читаемое