5 Commits

Author SHA1 Message Date
terrakok 8b2654ee2b WIP: add default destination. 2020-11-28 17:08:24 +03:00
terrakok 8c8521d2c5 WIP: chain builder as function. 2020-11-28 17:08:24 +03:00
terrakok 14b94927e4 WIP: add jumps. 2020-11-28 17:08:24 +03:00
terrakok 1e40fb29bb WIP: add link between vertex. 2020-11-28 17:08:24 +03:00
terrakok dbc8c3363f WIP: navigation graph. 2020-11-28 17:08:24 +03:00
18 changed files with 724 additions and 0 deletions
@@ -0,0 +1,134 @@
package com.github.terrakok.cicerone.graph
import com.github.terrakok.cicerone.*
class GraphRouter(
private val root: Root
) : BaseRouter() {
private val vertexes: Map<String, Vertex>
private val currentPath: MutableList<String>
val currentVertex get() = vertexes.getValue(currentPath.last())
init {
val allVertexes = mutableMapOf<String, Vertex>()
val links = mutableSetOf<VertexLink>()
val jumps = mutableListOf<List<String>>()
validateGraph(root.vertex, allVertexes, links, jumps)
val linkAndJumpIds = links.map { it.id } + jumps.flatten()
linkAndJumpIds.forEach { id ->
require(allVertexes.containsKey(id)) { "Not found vertex for id=$id" }
}
vertexes = allVertexes
currentPath = mutableListOf(root.vertex.id)
jumps.forEach {
require(validateJump(it)) { "Invalid jump path=$it" }
}
if (root.defaultDestination != null) {
require(root.vertex.edges.any { it.id == root.defaultDestination }) { "Not found default destination" }
navigateTo(root.defaultDestination)
}
}
private fun validateGraph(
vertex: Vertex,
allVertexes: MutableMap<String, Vertex>,
links: MutableSet<VertexLink>,
jumps: MutableList<List<String>>
) {
if (allVertexes.containsKey(vertex.id)) error("Graph contains duplicate id ${vertex.id}")
allVertexes[vertex.id] = vertex
links.addAll(vertex.edges.filterIsInstance<VertexLink>())
jumps.addAll(vertex.jumps.map { listOf(it.backTo ?: vertex.id, *it.chain.toTypedArray()) })
vertex.edges.forEach {
if (it !is VertexLink) validateGraph(it, allVertexes, links, jumps)
}
}
private fun validateJump(path: List<String>): Boolean {
if (path.isEmpty()) return false
if (path.size == 1) return true
val v = vertexes.getValue(path[0])
v.edges.firstOrNull { it.id == path[1] } ?: return false
return validateJump(path.subList(1, path.size))
}
fun navigateTo(
vertexId: String,
screenFactory: (vertexId: String) -> Screen? = { null }
) {
val destination = currentVertex.edges.first { it.id == vertexId }
val screen = createScreen(destination.id, screenFactory)
val command =
if (currentVertex.id == Root.ID) Replace(screen)
else Forward(screen, destination.destroyPreviousView)
currentPath.add(destination.id)
executeCommands(command)
}
fun jumpTo(
jumpId: String,
screenFactory: (vertexId: String) -> Screen? = { null }
) {
val jump = currentVertex.jumps.first { it.id == jumpId }
if (jump.backTo == Root.ID && jump.chain.isEmpty()) {
finish()
return
}
val commands = mutableListOf<Command>().apply {
if (jump.backTo != null) {
val id = jump.backTo
val index = currentPath.indexOfFirst { it == id }
if (index == -1) error("Current path doesn't contain vertex $id")
currentPath.subList(index + 1, currentPath.size).clear()
if (jump.backTo == Root.ID) add(BackTo(null))
else add(BackTo(Key(id)))
}
jump.chain.forEachIndexed { index, vertexId ->
val screen = createScreen(vertexId, screenFactory)
currentPath.add(vertexId)
if (index == 0 && jump.backTo == Root.ID) {
add(Replace(screen))
} else {
add(Forward(screen, vertexes.getValue(vertexId).destroyPreviousView))
}
}
}
executeCommands(*commands.toTypedArray())
}
fun exit() {
currentPath.removeLast()
executeCommands(Back())
}
private fun finish() {
currentPath.clear()
currentPath.add(Root.ID)
executeCommands(BackTo(null), Back())
}
private fun createScreen(
vertexId: String,
screenFactory: (vertexId: String) -> Screen?
): Screen {
val screen = screenFactory(vertexId)
?: vertexes.getValue(vertexId).screenFactory(vertexId)
?: error("Unknown screen for vertex $vertexId")
require(screen.screenKey == vertexId) { "Screen key must be equal vertex id!" }
return screen
}
private class Key(
override val screenKey: String
) : Screen
}
@@ -0,0 +1,134 @@
package com.github.terrakok.cicerone.graph
import com.github.terrakok.cicerone.*
open class Vertex internal constructor(
val id: String,
val edges: Set<Vertex>,
val jumps: Set<Jump>,
val destroyPreviousView: Boolean = true,
val screenFactory: (id: String) -> Screen? = { null }
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Vertex
if (id != other.id) return false
return true
}
override fun hashCode(): Int {
return id.hashCode()
}
}
internal class VertexLink(
id: String,
destroyPreviousView: Boolean = true
): Vertex(id, emptySet(), emptySet(), destroyPreviousView)
class Jump(
val id: String,
val backTo: String?,
val chain: List<String>
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Jump
if (id != other.id) return false
return true
}
override fun hashCode(): Int {
return id.hashCode()
}
}
//Graph DSL
class GraphInfo(
var edges: MutableSet<Vertex>.() -> Unit = {},
var jumps: MutableSet<Jump>.() -> Unit = {}
)
class Root(
val vertex: Vertex,
val defaultDestination: String?
) {
companion object {
const val ID = "graph-root"
}
}
fun graph(
defaultDestination: String? = null,
setup: GraphInfo.() -> Unit
): Root {
val info = GraphInfo().apply(setup)
val v = Vertex(
Root.ID,
mutableSetOf<Vertex>().apply(info.edges),
mutableSetOf<Jump>().apply(info.jumps)
)
return Root(v, defaultDestination)
}
class VertexInfo(
var edges: MutableSet<Vertex>.() -> Unit = {},
var jumps: MutableSet<Jump>.() -> Unit = {},
var screen: (id: String) -> Screen? = { null }
)
fun MutableSet<Vertex>.dest(
id: String,
destroyPreviousView: Boolean = true,
setup: VertexInfo.() -> Unit = {}
) {
val info = VertexInfo().apply(setup)
add(Vertex(
id,
mutableSetOf<Vertex>().apply(info.edges),
mutableSetOf<Jump>().apply(info.jumps),
destroyPreviousView,
info.screen
))
}
fun MutableSet<Vertex>.edge(
id: String,
destroyPreviousView: Boolean = true
) {
add(VertexLink(id, destroyPreviousView))
}
class JumpInfo(
var backTo: String? = null,
internal var chain: List<String> = emptyList()
) {
fun chain(vararg id: String) {
chain = id.toList()
}
}
fun MutableSet<Jump>.jump(
id: String,
setup: JumpInfo.() -> Unit
) {
val info = JumpInfo().apply(setup)
add(Jump(
id,
info.backTo,
info.chain
))
}
fun MutableSet<Jump>.finish(id: String) {
add(Jump(id, Root.ID, emptyList()))
}
+1
View File
@@ -21,5 +21,6 @@
<activity android:name=".ui.main.MainActivity"/>
<activity android:name=".ui.bottom.BottomNavigationActivity"/>
<activity android:name=".ui.animations.ProfileActivity"/>
<activity android:name=".ui.graph.GraphActivity"/>
</application>
</manifest>
@@ -0,0 +1,75 @@
package com.github.terrakok.cicerone.sample
import com.github.terrakok.cicerone.androidx.FragmentScreen
import com.github.terrakok.cicerone.graph.*
import com.github.terrakok.cicerone.sample.ui.graph.ForkFragment
import com.github.terrakok.cicerone.sample.ui.graph.RoadFragment
private val RoadScreen = { id: String -> FragmentScreen(id) { RoadFragment.getNewInstance(id) } }
private val ForkScreen = { id: String -> FragmentScreen(id) { ForkFragment.getNewInstance(id) } }
fun Graph() = graph("8") {
edges = {
dest("1") {
screen = RoadScreen
edges = {
dest("2") {
screen = ForkScreen
edges = {
dest("3") {
screen = RoadScreen
edges = {
dest("4") {
screen = RoadScreen
edges = {
edge("5")
}
}
}
}
dest("5") {
screen = ForkScreen
edges = {
dest("6") {
screen = RoadScreen
}
dest("7") {
screen = RoadScreen
jumps = {
finish("1")
}
}
}
}
}
}
}
}
dest("8") {
screen = ForkScreen
edges = {
dest("9") {
screen = RoadScreen
edges = {
edge("1")
}
jumps = {
jump("1") {
backTo = Root.ID
chain("1", "2", "3", "4", "5", "7")
}
}
}
dest("10") {
screen = ForkScreen
edges = {
dest("11") {
screen = RoadScreen
}
edge("10")
}
}
}
}
}
}
@@ -10,6 +10,7 @@ import com.github.terrakok.cicerone.sample.ui.animations.profile.ProfileFragment
import com.github.terrakok.cicerone.sample.ui.bottom.BottomNavigationActivity
import com.github.terrakok.cicerone.sample.ui.bottom.ForwardFragment
import com.github.terrakok.cicerone.sample.ui.bottom.TabContainerFragment
import com.github.terrakok.cicerone.sample.ui.graph.GraphActivity
import com.github.terrakok.cicerone.sample.ui.main.MainActivity
import com.github.terrakok.cicerone.sample.ui.main.SampleFragment
import com.github.terrakok.cicerone.sample.ui.start.StartActivity
@@ -59,4 +60,8 @@ object Screens {
fun SelectPhoto(resultKey: String) = FragmentScreen {
SelectPhotoFragment.getNewInstance(resultKey)
}
fun Graph() = ActivityScreen {
Intent(it, GraphActivity::class.java)
}
}
@@ -7,6 +7,9 @@ import com.github.terrakok.cicerone.sample.ui.animations.photos.SelectPhotoFragm
import com.github.terrakok.cicerone.sample.ui.animations.profile.ProfileFragment
import com.github.terrakok.cicerone.sample.ui.bottom.BottomNavigationActivity
import com.github.terrakok.cicerone.sample.ui.bottom.TabContainerFragment
import com.github.terrakok.cicerone.sample.ui.graph.ForkFragment
import com.github.terrakok.cicerone.sample.ui.graph.GraphActivity
import com.github.terrakok.cicerone.sample.ui.graph.RoadFragment
import com.github.terrakok.cicerone.sample.ui.main.MainActivity
import com.github.terrakok.cicerone.sample.ui.main.SampleFragment
import com.github.terrakok.cicerone.sample.ui.start.StartActivity
@@ -37,4 +40,10 @@ interface AppComponent {
fun inject(fragment: SelectPhotoFragment)
fun inject(activity: ProfileActivity)
fun inject(activity: GraphActivity)
fun inject(fragment: RoadFragment)
fun inject(fragment: ForkFragment)
}
@@ -4,8 +4,11 @@ import com.github.terrakok.cicerone.Cicerone
import com.github.terrakok.cicerone.Cicerone.Companion.create
import com.github.terrakok.cicerone.NavigatorHolder
import com.github.terrakok.cicerone.Router
import com.github.terrakok.cicerone.graph.GraphRouter
import com.github.terrakok.cicerone.sample.Graph
import dagger.Module
import dagger.Provides
import javax.inject.Named
import javax.inject.Singleton
/**
@@ -14,6 +17,7 @@ import javax.inject.Singleton
@Module
class NavigationModule {
private val cicerone: Cicerone<Router> = create()
private val graphCicerone: Cicerone<GraphRouter> = create(GraphRouter(Graph()))
@Provides
@Singleton
@@ -26,4 +30,17 @@ class NavigationModule {
fun provideNavigatorHolder(): NavigatorHolder {
return cicerone.getNavigatorHolder()
}
@Provides
@Singleton
fun provideGraphRouter(): GraphRouter {
return graphCicerone.router
}
@Provides
@Singleton
@Named("graph")
fun provideGraphNavigatorHolder(): NavigatorHolder {
return graphCicerone.getNavigatorHolder()
}
}
@@ -0,0 +1,28 @@
package com.github.terrakok.cicerone.sample.mvp.graph
import com.github.terrakok.cicerone.graph.GraphRouter
import moxy.InjectViewState
import moxy.MvpPresenter
import moxy.MvpView
@InjectViewState
class ForkPresenter(
private val graphRouter: GraphRouter
): MvpPresenter<MvpView>() {
fun onTopButtonClick() {
graphRouter.currentVertex.edges.firstOrNull()?.id?.let { id ->
graphRouter.navigateTo(id)
}
}
fun onBottomButtonClick() {
graphRouter.currentVertex.edges.lastOrNull()?.id?.let { id ->
graphRouter.navigateTo(id)
}
}
fun onBackPressed() {
graphRouter.exit()
}
}
@@ -0,0 +1,28 @@
package com.github.terrakok.cicerone.sample.mvp.graph
import com.github.terrakok.cicerone.graph.GraphRouter
import moxy.InjectViewState
import moxy.MvpPresenter
import moxy.MvpView
@InjectViewState
class RoadPresenter(
private val graphRouter: GraphRouter
): MvpPresenter<MvpView>() {
fun onButtonClick() {
graphRouter.currentVertex.edges.firstOrNull()?.id?.let { id ->
graphRouter.navigateTo(id)
}
}
fun onJumpClick() {
graphRouter.currentVertex.jumps.firstOrNull()?.id?.let { id ->
graphRouter.jumpTo(id)
}
}
fun onBackPressed() {
graphRouter.exit()
}
}
@@ -2,6 +2,7 @@ package com.github.terrakok.cicerone.sample.mvp.start
import com.github.terrakok.cicerone.Router
import com.github.terrakok.cicerone.sample.Screens.BottomNavigation
import com.github.terrakok.cicerone.sample.Screens.Graph
import com.github.terrakok.cicerone.sample.Screens.Main
import com.github.terrakok.cicerone.sample.Screens.Profile
import moxy.MvpPresenter
@@ -23,6 +24,10 @@ class StartActivityPresenter(private val router: Router) : MvpPresenter<StartAct
router.navigateTo(Profile())
}
fun onGraphPressed() {
router.navigateTo(Graph())
}
fun onBackPressed() {
router.exit()
}
@@ -0,0 +1,65 @@
package com.github.terrakok.cicerone.sample.ui.graph
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.github.terrakok.cicerone.graph.GraphRouter
import com.github.terrakok.cicerone.sample.SampleApplication
import com.github.terrakok.cicerone.sample.databinding.FragmentForkBinding
import com.github.terrakok.cicerone.sample.databinding.FragmentRoadBinding
import com.github.terrakok.cicerone.sample.mvp.graph.ForkPresenter
import com.github.terrakok.cicerone.sample.mvp.graph.RoadPresenter
import com.github.terrakok.cicerone.sample.ui.common.BackButtonListener
import moxy.MvpAppCompatFragment
import moxy.MvpView
import moxy.presenter.InjectPresenter
import moxy.presenter.ProvidePresenter
import javax.inject.Inject
class ForkFragment : MvpAppCompatFragment(), MvpView, BackButtonListener {
lateinit var binding: FragmentForkBinding
@Inject
lateinit var graphRouter: GraphRouter
@InjectPresenter
lateinit var presenter: ForkPresenter
@ProvidePresenter
fun provideRoadPresenter(): ForkPresenter {
return ForkPresenter(graphRouter)
}
override fun onCreate(savedInstanceState: Bundle?) {
SampleApplication.INSTANCE.appComponent.inject(this)
super.onCreate(savedInstanceState)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
binding = FragmentForkBinding.inflate(inflater)
return binding.root
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
binding.textView.text = arguments?.getString(EXTRA_NUMBER)
binding.topButton.setOnClickListener { presenter.onTopButtonClick() }
binding.bottomButton.setOnClickListener { presenter.onBottomButtonClick()}
}
override fun onBackPressed(): Boolean {
presenter.onBackPressed()
return true
}
companion object {
private const val EXTRA_NUMBER = "extra_number"
fun getNewInstance(number: String) = ForkFragment().apply {
arguments = Bundle().apply {
putString(EXTRA_NUMBER, number)
}
}
}
}
@@ -0,0 +1,63 @@
package com.github.terrakok.cicerone.sample.ui.graph
import android.os.Bundle
import android.transition.ChangeBounds
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentTransaction
import com.github.terrakok.cicerone.Command
import com.github.terrakok.cicerone.Navigator
import com.github.terrakok.cicerone.NavigatorHolder
import com.github.terrakok.cicerone.Replace
import com.github.terrakok.cicerone.androidx.AppNavigator
import com.github.terrakok.cicerone.graph.GraphRouter
import com.github.terrakok.cicerone.sample.R
import com.github.terrakok.cicerone.sample.SampleApplication
import com.github.terrakok.cicerone.sample.Screens.ProfileInfo
import com.github.terrakok.cicerone.sample.ui.animations.photos.SelectPhotoFragment
import com.github.terrakok.cicerone.sample.ui.animations.profile.ProfileFragment
import com.github.terrakok.cicerone.sample.ui.common.BackButtonListener
import javax.inject.Inject
import javax.inject.Named
/**
* Created by Konstantin Tskhovrebov (aka @terrakok) on 14.07.17.
*/
class GraphActivity : AppCompatActivity() {
@Inject
@Named("graph")
lateinit var navigatorHolder: NavigatorHolder
@Inject
lateinit var graphRouter: GraphRouter
private val navigator = AppNavigator(this, R.id.container)
override fun onCreate(savedInstanceState: Bundle?) {
SampleApplication.INSTANCE.appComponent.inject(this)
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_container)
}
override fun onResumeFragments() {
super.onResumeFragments()
navigatorHolder.setNavigator(navigator)
}
override fun onPause() {
navigatorHolder.removeNavigator()
super.onPause()
}
override fun onBackPressed() {
val fragment = supportFragmentManager.findFragmentById(R.id.container)
if (fragment != null && fragment is BackButtonListener
&& (fragment as BackButtonListener).onBackPressed()) {
return
} else {
super.onBackPressed()
}
}
}
@@ -0,0 +1,63 @@
package com.github.terrakok.cicerone.sample.ui.graph
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.github.terrakok.cicerone.graph.GraphRouter
import com.github.terrakok.cicerone.sample.SampleApplication
import com.github.terrakok.cicerone.sample.databinding.FragmentRoadBinding
import com.github.terrakok.cicerone.sample.mvp.graph.RoadPresenter
import com.github.terrakok.cicerone.sample.ui.common.BackButtonListener
import moxy.MvpAppCompatFragment
import moxy.MvpView
import moxy.presenter.InjectPresenter
import moxy.presenter.ProvidePresenter
import javax.inject.Inject
class RoadFragment : MvpAppCompatFragment(), MvpView, BackButtonListener {
lateinit var binding: FragmentRoadBinding
@Inject
lateinit var graphRouter: GraphRouter
@InjectPresenter
lateinit var presenter: RoadPresenter
@ProvidePresenter
fun provideRoadPresenter(): RoadPresenter {
return RoadPresenter(graphRouter)
}
override fun onCreate(savedInstanceState: Bundle?) {
SampleApplication.INSTANCE.appComponent.inject(this)
super.onCreate(savedInstanceState)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
binding = FragmentRoadBinding.inflate(inflater)
return binding.root
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
binding.textView.text = arguments?.getString(EXTRA_NUMBER)
binding.forwardButton.setOnClickListener { presenter.onButtonClick() }
binding.jumpButton.setOnClickListener { presenter.onJumpClick() }
}
override fun onBackPressed(): Boolean {
presenter.onBackPressed()
return true
}
companion object {
private const val EXTRA_NUMBER = "extra_number"
fun getNewInstance(number: String) = RoadFragment().apply {
arguments = Bundle().apply {
putString(EXTRA_NUMBER, number)
}
}
}
}
@@ -52,6 +52,9 @@ class StartActivity : MvpAppCompatActivity(), StartActivityView {
findViewById<View>(R.id.result_and_anim_button).setOnClickListener {
presenter.onResultWithAnimationPressed()
}
findViewById<View>(R.id.graph_button).setOnClickListener {
presenter.onGraphPressed()
}
}
override fun onResume() {
@@ -43,4 +43,11 @@
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:text="@string/result_and_anim_nav"/>
<Button
android:id="@+id/graph_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:text="@string/graph_nav"/>
</LinearLayout>
@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<Space
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_weight="1"/>
<TextView
android:id="@+id/textView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:padding="16dp"
android:textColor="@android:color/white"
android:textSize="13sp"/>
<Button
android:id="@+id/top_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:minWidth="100dp"
android:text="➔"
android:textSize="20sp"/>
<Button
android:id="@+id/bottom_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:minWidth="100dp"
android:text="➔"
android:textSize="20sp"/>
<Space
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_weight="1"/>
</LinearLayout>
@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<Space
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_weight="1"/>
<TextView
android:id="@+id/textView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:padding="16dp"
android:textColor="@android:color/white"
android:textSize="13sp"/>
<Button
android:id="@+id/forward_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:minWidth="100dp"
android:text="➔"
android:textSize="20sp"/>
<Button
android:id="@+id/jump_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:minWidth="100dp"
android:text="JUMP"
android:textSize="20sp"/>
<Space
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_weight="1"/>
</LinearLayout>
+1
View File
@@ -4,6 +4,7 @@
<string name="ordinary_nav">Ordinary navigation</string>
<string name="multi_nav">Multi navigation</string>
<string name="result_and_anim_nav">Resulting and animation sample</string>
<string name="graph_nav">Navigation graph</string>
<string name="tab_android">Android</string>
<string name="tab_bug">Bug</string>