Навигация внутри 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.
Так как не хотел чтобы в статье было много кода я ограничусь этим примером. Вот ссылка на репозиторий для подробного изучения библиотеки.