Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions navigation/RELEASE_NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@
* ANDDEP-1154 Fixed crashes "You must specify unique tag"
* ANDDEP-1153 Fixed wrong behavior RemoveLast
* ANDDEP-1100 Added command StartForResult to open system activities for getting result
* ANDDEP-1103 Added sequential async and sync command execution
* ANDDEP-1148 code updated for target sdk 30
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,5 @@ import java.io.Serializable
class StartForResult<T : Serializable, R>(
override val route: R,
override val animations: Animations = DefaultAnimations.activity,
val activityOptions: Bundle? = null
override val activityOptions: Bundle? = null
) : ActivityNavigationCommand where R : ActivityWithResultRoute<T>
49 changes: 48 additions & 1 deletion navigation/lib-navigation/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ ACTIVITY_NAVIGATION_TAG.
* [RemoveUntil][removeuntilcom] - удаление фрагментов вплоть до указанного в команде.
* [RemoveAll][removeallcom] - очистка бекстека.
Возможна очистка всех фрагментов с сохранением последнего: необходимо явно указать параметр shouldRemoveLast=false.
Чтобы выполнить эту команду для [TabFragmentNavigator][tfnav], необходимо явно указать параметр isTab=true.
В случае с [TabFragmentNavigator][tfnav], команда [RemoveAll][removeallcom] очистит стек текущего активного таба.
- [DialogNavigationCommand][dcom]
* [Show][showcom] - показ диалога
* [Dismiss][dismisscom] - скрытие диалога
Expand Down Expand Up @@ -198,6 +198,51 @@ noBackupFilesDir.
1. Вызвать метод NavigationCommandExecutor.execute и передать в него команду Add(routeA, sourceTag),
где routeA - route, созданный на первом шаге, sourceTag - тег экрана, созданный на 3 шаге.

### Цепочное выполнение команд
[AppCommandExecutor][appexec] поддерживает выполнение цепочек, состоящих как из синхронных, так и
асинхронных команд.
В зависимости от того какие команды есть в цепочке логика работы [AppCommandExecutor][appexec] будет
немного отличаться. Но основной принцип - выполняется асинхронная команда, выполнение команд идущих
после нее откладывается до момента, когда будет доступен новый [ActivityNavigationHolder][anavholdercom].
И как только холдер станет доступен - срабатывает та же логика.
Исключения из этого:
* Выполнение цепочки команд [Start][startcom], открывающее несколько активити сразу.
* Выполнение асинхронных команд Replace и Start для активити после выполнения команд Finish и FinishAffinity.
Replace и Start выполняются сразу после команд Finish и FinishAffinity в том же [ActivityNavigationHolder][anavholdercom],
так как после выполнения команд закрывающих активити или стек активити в стеке может не остаться
активити и последующие команды не выполнятся.

1. Можно запускать несколько активити за один раз, передавая список из команд Start, например
`listOf(Start(ActivityRoute1()), Start(ActivityRoute2()), Start(ActivityRoute3()))`.
У [ActivityNavigator][anav] для выполнения этой цепочки команд будет вызван метод `Context.startActivities`.
1. Можно асинхронно запустить активити-контейнер фрагментов и сразу добавить в него несколько фрагментов,
[AppCommandExecutor][appexec] отложит выполнение всех команд, идущих после команды старта активити,
дождется пока активити запутится и последовательно выполнит все синхронные команды, идущие после
команды запуска активити.
1. Можно закрыть несколько активити (без использования [FinishAffinity][finishacom]). Для этого нужно
передать список комманд [Finish][finishcom]. Первая команда [Finish][finishcom] из этого списка
выполнится для текущей активити, последующие команды будут выполняться по мере того как будут
становиться доступны холдеры [ActivityNavigationHolder][anavholdercom] ранее запущенных активити.
1. Можно выполнять запуск нескольких активити с добавлением в них фрагментов.
listOf(
Start(FirstActivityRoute()),
Replace(FirstFragmentRoute()),
Replace(SecondFragmentRoute()),
Start(SecondActivityRoute()),
Start(ThirdActivityRoute()),
Replace(FirstFragmentRoute()),
Replace(SecondFragmentRoute())
)
1. Закрываем несколько активити из стека с последующим открытием новых активити.
В этом случае нужно быть очень внимательным, так как если в стеке всего одна активити, а переданы
две команды [Finish][finishcom] и после них есть еще какие-то команды - выполнится только первая
команда, которая закроет активити и вместе с этим все приложение и все последующие команды не будут
выполнены.
listOf(
Finish(),
Finish(),
Start(SomeActivityRoute())
)

[route]: src/main/java/ru/surfstudio/android/navigation/route/Route.kt
[baseroute]: src/main/java/ru/surfstudio/android/navigation/route/BaseRoute.kt
Expand Down Expand Up @@ -250,3 +295,5 @@ noBackupFilesDir.
[dcom]: src/main/java/ru/surfstudio/android/navigation/command/dialog/base/DialogNavigationCommand.kt
[showcom]: src/main/java/ru/surfstudio/android/navigation/command/dialog/Show.kt
[dismisscom]: src/main/java/ru/surfstudio/android/navigation/command/dialog/Dismiss.kt

[anavholdercom]: src/main/java/ru/surfstudio/android/navigation/provider/holder/ActivityNavigationHolder.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package ru.surfstudio.android.navigation.command.activity

import android.os.Bundle
import ru.surfstudio.android.navigation.animation.Animations
import ru.surfstudio.android.navigation.animation.DefaultAnimations
import ru.surfstudio.android.navigation.command.activity.base.ActivityNavigationCommand
Expand All @@ -14,4 +15,5 @@ data class Finish(
) : ActivityNavigationCommand {

override val route: ActivityRoute = StubActivityRoute
override val activityOptions: Bundle? = null
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package ru.surfstudio.android.navigation.command.activity

import android.os.Bundle
import ru.surfstudio.android.navigation.animation.Animations
import ru.surfstudio.android.navigation.animation.DefaultAnimations
import ru.surfstudio.android.navigation.command.activity.base.ActivityNavigationCommand
Expand All @@ -14,4 +15,5 @@ data class FinishAffinity(
) : ActivityNavigationCommand {

override val route: ActivityRoute = StubActivityRoute
override val activityOptions: Bundle? = null
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,5 @@ import ru.surfstudio.android.navigation.route.activity.ActivityRoute
data class Replace(
override val route: ActivityRoute,
override val animations: Animations = DefaultAnimations.activity,
val activityOptions: Bundle? = null
override val activityOptions: Bundle? = null
) : ActivityNavigationCommand
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,5 @@ import ru.surfstudio.android.navigation.route.activity.ActivityRoute
data class Start(
override val route: ActivityRoute,
override val animations: Animations = DefaultAnimations.activity,
val activityOptions: Bundle? = null
override val activityOptions: Bundle? = null
) : ActivityNavigationCommand
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package ru.surfstudio.android.navigation.command.activity.base

import android.os.Bundle
import ru.surfstudio.android.navigation.animation.Animations
import ru.surfstudio.android.navigation.command.NavigationCommand
import ru.surfstudio.android.navigation.route.activity.ActivityRoute

Expand All @@ -8,4 +10,6 @@ import ru.surfstudio.android.navigation.route.activity.ActivityRoute
*/
interface ActivityNavigationCommand: NavigationCommand {
override val route: ActivityRoute
override val animations: Animations
val activityOptions: Bundle?
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
package ru.surfstudio.android.navigation.executor

import android.os.Handler
import android.os.Looper
import ru.surfstudio.android.navigation.command.NavigationCommand
import ru.surfstudio.android.navigation.command.activity.Finish
import ru.surfstudio.android.navigation.command.activity.FinishAffinity
import ru.surfstudio.android.navigation.command.activity.Start
import ru.surfstudio.android.navigation.command.activity.base.ActivityNavigationCommand
import ru.surfstudio.android.navigation.command.dialog.base.DialogNavigationCommand
import ru.surfstudio.android.navigation.command.fragment.base.FragmentNavigationCommand
Expand All @@ -23,6 +27,7 @@ open class AppCommandExecutor(
protected val dialogCommandExecutor: DialogCommandExecutor = DialogCommandExecutor(activityNavigationProvider)
) : NavigationCommandExecutor {

protected val handler = Handler(Looper.getMainLooper())
protected val buffer = mutableListOf<NavigationCommand>()
protected val commandQueue: Queue<NavigationCommand> = LinkedList()

Expand All @@ -43,8 +48,19 @@ open class AppCommandExecutor(
*/
protected open fun safeExecuteWithBuffer(commands: List<NavigationCommand>) {
if (activityNavigationProvider.hasCurrentHolder()) {
divideExecution(commands)
splitExecutionIntoChunks(commands)
} else {
postponeExecution(commands)
}
}


/**
* Postpones command execution to the end of the Message Queue
* and executes [commands] with the next available navigator
*/
protected fun postponeExecution(commands: List<NavigationCommand>) {
handler.post {
buffer.addAll(commands)
activityNavigationProvider.setOnHolderActiveListenerSingle { utilizeBuffer() }
}
Expand All @@ -63,40 +79,76 @@ open class AppCommandExecutor(
* and then call this method again with remaining commands (returning to step 1)
*
* TODO: Dispatch commands in batches, to, for example,
* TODO: execute multiple commands in one FragmentTransaction,
* TODO: or use Context.startActivities(Intent, Intent...) to launch multiple activities.
* TODO: execute multiple commands in one FragmentTransaction
*
*/
protected open fun divideExecution(commands: List<NavigationCommand>) {
protected open fun splitExecutionIntoChunks(commands: List<NavigationCommand>) {
if (commands.isEmpty()) return

val isStartingWithAsyncCommand = checkCommandAsync(commands.first())
val asyncCommands: List<NavigationCommand>
val syncCommands: List<NavigationCommand>
val firstCommand = commands.first()
val isStartingWithAsyncCommand = checkCommandAsync(firstCommand)

if (isStartingWithAsyncCommand) {
asyncCommands = commands.takeWhile(::checkCommandAsync)
syncCommands = commands.takeLast(commands.size - asyncCommands.size)

val hasAsyncCommands = asyncCommands.isNotEmpty()
val hasSyncCommands = syncCommands.isNotEmpty()
splitStartingFromAsync(commands, firstCommand)
} else {
splitStartingFromSync(commands)
}
}

if (hasAsyncCommands) queueCommands(asyncCommands)
private fun splitStartingFromSync(commands: List<NavigationCommand>) {
val firstAsyncCommandIndex = commands.indexOfFirst { checkCommandAsync(it) }
if (firstAsyncCommandIndex >= 1) {
queueCommands(commands.take(firstAsyncCommandIndex))
safeExecuteWithBuffer(commands.subList(firstAsyncCommandIndex, commands.size))
} else {
queueCommands(commands)
}
}

when {
hasAsyncCommands && hasSyncCommands -> postponeExecution(syncCommands)
hasSyncCommands -> divideExecution(syncCommands)
private fun splitStartingFromAsync(commands: List<NavigationCommand>, firstCommand: NavigationCommand) {
val firstNotStartIndex = commands.indexOfFirst { command -> command !is Start }
when {
/** We have only Start commands, just execute them */
firstNotStartIndex == -1 -> {
startSeveralActivities(commands)
}
} else {
syncCommands = commands.takeWhile { !checkCommandAsync(it) }
asyncCommands = commands.takeLast(commands.size - syncCommands.size)
/**
* We have several Start commands and commands of other types right after them, so
* we start activities first and postpone the execution of the remaining commands until
* new navigation holder become available
*/
firstNotStartIndex > 1 -> {
startSeveralActivities(commands.take(firstNotStartIndex))
postponeExecution(commands.subList(firstNotStartIndex, commands.size))
}
/**
* Here are other cases, where we just execute first command.
* If we have to Finish activity or affinity we need to check the next command after
* it. If it's async but not Finish/FinishAffinity command we can immediately execute it
* with current navigation holder. Otherwise we postpone execution until new navigation
* holder becomes available.
*/
else -> {
dispatchCommand(firstCommand)
val restCommands = commands.drop(1)
val canExecuteImmediately: Boolean = restCommands.isNotEmpty()
&& checkCommandAsync(restCommands[0])
&& !isFinishCommand(restCommands[0])
if (canExecuteImmediately) {
splitExecutionIntoChunks(restCommands)
} else {
postponeExecution(restCommands)
}
}
}
}

val hasAsyncCommands = asyncCommands.isNotEmpty()
val hasSyncCommands = syncCommands.isNotEmpty()
private fun isFinishCommand(command: NavigationCommand): Boolean {
return command is Finish || command is FinishAffinity
}

if (hasSyncCommands) queueCommands(syncCommands)
if (hasAsyncCommands) safeExecuteWithBuffer(asyncCommands)
}
private fun startSeveralActivities(commands: List<NavigationCommand>) {
activityCommandExecutor.execute(commands.map { it as ActivityNavigationCommand })
}

/**
Expand Down Expand Up @@ -139,13 +191,6 @@ open class AppCommandExecutor(
}
}

/**
* Postpones command execution to the end of the Message Queue.
*/
protected open fun postponeExecution(commands: List<NavigationCommand>) {
Handler().post { safeExecuteWithBuffer(commands) }
}

/**
* Checks whether the effect of this command will be shown immediately after execution,
* or it will take some time to appear.
Expand All @@ -157,7 +202,10 @@ open class AppCommandExecutor(
* Executes everything in buffer and cleans it.
*/
protected fun utilizeBuffer() {
execute(buffer)
val commands = ArrayList(buffer)
buffer.clear()
handler.post {
execute(commands)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import ru.surfstudio.android.navigation.command.activity.*
import ru.surfstudio.android.navigation.command.activity.base.ActivityNavigationCommand
import ru.surfstudio.android.navigation.provider.ActivityNavigationProvider
import ru.surfstudio.android.navigation.executor.CommandExecutor
import ru.surfstudio.android.navigation.navigator.activity.ActivityNavigatorInterface

/**
* Command executor for [ActivityNavigationCommand]s.
Expand All @@ -14,8 +15,10 @@ open class ActivityCommandExecutor(
private val activityNavigationProvider: ActivityNavigationProvider
) : CommandExecutor<ActivityNavigationCommand> {

private val navigator: ActivityNavigatorInterface
get() = activityNavigationProvider.provide().activityNavigator

override fun execute(command: ActivityNavigationCommand) {
val navigator = activityNavigationProvider.provide().activityNavigator
when (command) {
is Start -> navigator.start(command.route, command.animations, command.activityOptions)
is Replace -> navigator.replace(command.route, command.animations, command.activityOptions)
Expand All @@ -25,6 +28,13 @@ open class ActivityCommandExecutor(
}

override fun execute(commands: List<ActivityNavigationCommand>) {
TODO("Activity navigation command list execution")
if (commands.isEmpty()) return

val lastCommand = commands.first() as Start
navigator.start(
routes = commands.map { it.route },
animations = lastCommand.animations,
activityOptions = lastCommand.activityOptions
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,16 @@ open class ActivityNavigator(val activity: AppCompatActivity) : ActivityNavigato

protected open val animationSupplier = ActivityAnimationSupplier()

override fun start(routes: List<ActivityRoute>, animations: Animations, activityOptions: Bundle?) {
val intents = Array(routes.size) { index: Int ->
val route = routes[index]
route.createIntent(activity)
}
val optionsWithAnimations: Bundle? =
animationSupplier.supplyWithAnimations(activity, activityOptions, animations)
activity.startActivities(intents, optionsWithAnimations)
}

override fun start(route: ActivityRoute, animations: Animations, activityOptions: Bundle?) {
val optionsWithAnimations =
animationSupplier.supplyWithAnimations(activity, activityOptions, animations)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import ru.surfstudio.android.navigation.route.activity.ActivityRoute
*/
interface ActivityNavigatorInterface {

fun start(routes: List<ActivityRoute>, animations: Animations, activityOptions: Bundle?)

fun start(route: ActivityRoute, animations: Animations, activityOptions: Bundle?)

fun replace(route: ActivityRoute, animations: Animations, activityOptions: Bundle?)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ open class ActivityNavigationProviderCallbacks(
// So, we're just waiting until it executes all actions, and then updating holder status.
handler.post { updateCurrentHolder(id) }
} else {
updateCurrentHolder(id)
handler.post { updateCurrentHolder(id) }
}
}
}
Expand Down
Loading