Навигация внутри Android приложения

    Введение


    При Андроид разработке мы используем разные архитектурные решения(паттерны). Например Mvp, Mvvm, Mvi и т.д… Каждый из этих паттернов решает несколько важных задач и поскольку они не идеальны они нам оставляют кое-какие нерешенные задачи. К примеру этих задач относятся навигация внутри приложения(routing), передача информации с экрана на экран(говоря экран я имею ввиду Activity, Fragment или View), Сохранение состояний приложения при смене конфигурации(configuration change).


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


    Задача


    В нашей компании мы используем Mvp архитектуру. Чтобы иметь максимальную гибкость при показе, смене и передачи данных между экранами мы стараемся следовать принципу называемому Single-responsibility principle. Принцип гласит о том что каждый модуль должен решать конкретную задачу. В нашем случае экран должен быть изолирован от глобальной задачи и должен решать свою конкретную задачу показывать/принимать информацию. Он не должен знать о других экранах вообще. Так мы можем достичь максимальной гибкости. Ниже пример настройки и использования библиотеки.


    Flowzard


    Создание flow


    class MainFlow(flowManager: FlowManager) : Flow(flowManager) {
        // Вызывается при создании или восстановлении flow 
        override fun onCreate(savedInstance: DataBunch?, data: DataBunch?) {
            super.onCreate(savedInstance, data)
        }
    }

    Создание flow navigator


    Навигаторы выполняют две функции: Создают flow контейнеры(Activity, Fragment, View) для переходов между flow и экраны для переходов внутри flow.


    class DefaultFlowNavigator(activity: AppCompatActivity) : SimpleFlowNavigator(activity){
        // Вызывается при связывании flow с Activity 
        override fun getActivityIntent(id: String, data: Any?): Intent {
            return when (id) {
                Flows.SIGN_UP -> Intent(activity, SignupActivity::class.java)
                else -> throw RuntimeException("Cannot find activity for id=$id")
            }
        }
    }

    Привязывание к Activity


    Чтобы связать Activity с Flow мы наследуем FlowActivity и предоставляем Navigator, в нашем случае DefaultFlowNavigator.


    class MainActivity : FlowActivity() {
        override val navigator: Navigator
            get() = DefaultFlowNavigator(this)
    }

    Создание FlowManager


    class DefaultFlowManager : FlowManager() {
        // Вызывается при создании главного(main) flow
        override fun createMainFlow(): Flow {
            return MainFlow(this)
        }
    
        // Вызывается при создании flow
        override fun createFlow(id: String): Flow {
            return when (id) {
                Flows.SIGN_UP -> SignupFlow(this)
                else -> throw RuntimeException("Cannot find flow for id=$id")
            }
        }
    }
    
    // Привязываем наш FlowManager к Application
    class App : Application(), FlowManagerProvider {
        private val flowManager = DefaultFlowManager()
        override fun getProvideManager(): FlowManager {
            return flowManager
        }
    }

    Передача сообщений между flow и экраном


    При нажатии кнопки login активити отправляет сообщение в main flow. Flow создает SIGN_UP flow и ожидает ответа от него. При удачном логине SIGN_UP flow отправляет результат в main flow и вызывается onFlowResult:MainFlow с кодом и объектом результата. Main flow проверяет, если результат правильный то отправляет сообщение обратно в активити, что пользователь удачно залогинился.


    class MainFlow(flowManager: FlowManager) : Flow(flowManager) {
        companion object {
            const val LOGIN_REQUEST_CODE = 1
        }
    
        // вызывается при получении сообщений
        override fun onMessage(code: String, message: Any) {
            super.onMessage(code, message)
            if (code == "main" && message == "login") {
                newFlow(Flows.SIGN_UP, LOGIN_REQUEST_CODE)
            }
        }
    
        // вызывается при получении результата от другого flow
        override fun onFlowResult(requestCode: Int, result: Result) {
            super.onFlowResult(requestCode, result)
            if (requestCode == LOGIN_REQUEST_CODE && result is Result.SUCCESS) {
                sendMessageFromFlow("main", true)
            }
        }
    }
    
    class MainActivity : FlowActivity() {
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
    
            loginButton.setOnClickListener {
                // отправляет сообщение “login” с кодом “main” 
                flow.sendMessage("main", "login")
            }
    
            // слушает сообщение с кодом “main”
            setMessageListener("main") {
                if (it is Boolean && it) {
                    statusTextView.text = "Logined"
                }
            }
        }
    }

    Сохранение состоянии при смене конфигурации или при остановке процесса операционной системой


    Так как Андроид сохраняет стеки Activity и Fragment то созданные flow с этими контейнерами будут сохранять и восстанавливать свое состояние. С View контейнером нужно будет писать свой кастомный FlowManager так как библиотека пока еще не имеет такой менеджер. В следующем обновлении будет фича для сохранении промежуточных данных из flow.


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

    • +11
    • 5,1k
    • 3
    Поделиться публикацией

    Похожие публикации

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

      0
      К сожалению вы переписали уже существующие решения: Cicerone или же Conductor
        0
        Извините но Conductor работает только с View. Cicerone хороший механизм для навигации но не решает вопрос изоляции логики переключения между экранами от логики экранов. Я упомянул про Cicerone в README файле на репозитории проекта Flowzard что использовал их код для решения вопроса навигации.
          0
          В Cicerone есть возможность изолирования логики навигации путём создания своего навигатор класса. По поводу кондуктора согласен т.к. он является фремворком.

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

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