Skip to content
Merged
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
33 changes: 33 additions & 0 deletions demos/stack-nav/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* Copyright (C) 2025. Uber Technologies
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
plugins {
id("ribs.android.application")
}

android {
namespace = "com.uber.rib.stacknav"

defaultConfig {
applicationId = "com.uber.rib.stacknav"
targetSdk = 36
}
}

dependencies {
implementation(project(":libraries:rib-android"))
implementation(project(":libraries:rib-router-navigator"))
implementation(libs.androidx.appcompat)
}
18 changes: 18 additions & 0 deletions demos/stack-nav/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

<application
android:allowBackup="false"
android:icon="@drawable/ub__ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme">
<activity
android:name="com.uber.rib.stacknav.RootActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* Copyright (C) 2025. Uber Technologies
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.uber.rib.stacknav

import android.view.ViewGroup
import com.uber.rib.core.RibActivity
import com.uber.rib.core.ViewRouter
import com.uber.rib.stacknav.root.RootInteractor
import com.uber.rib.stacknav.root.RootRouter
import com.uber.rib.stacknav.root.RootView

class RootActivity : RibActivity() {

override fun createRouter(parentViewGroup: ViewGroup): ViewRouter<*, *> {
val rootView = RootView(this)
val interactor = RootInteractor()
return RootRouter(rootView, interactor)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* Copyright (C) 2025. Uber Technologies
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.uber.rib.stacknav.root

import com.uber.rib.core.Bundle
import com.uber.rib.core.EmptyPresenter
import com.uber.rib.core.Interactor
import com.uber.rib.stacknav.root.screen.ScreenInteractor

/**
* Orchestrates the navigation stack. Pushes the first screen on start, and reacts to each screen's
* request to push the next one.
*/
class RootInteractor :
Interactor<EmptyPresenter, RootRouter>(EmptyPresenter()), ScreenInteractor.Listener {

override fun didBecomeActive(savedInstanceState: Bundle?) {
super.didBecomeActive(savedInstanceState)
router.pushScreen(1)
}

override fun onPushNextScreen(currentNumber: Int) {
if (currentNumber < MAX_SCREENS) {
router.pushScreen(currentNumber + 1)
}
}

override fun onBackRequested() {
router.popScreen()
}

companion object {
const val MAX_SCREENS = 5
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/*
* Copyright (C) 2025. Uber Technologies
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.uber.rib.stacknav.root

import com.uber.rib.core.BasicViewRouter
import com.uber.rib.core.RouterNavigator
import com.uber.rib.core.StackRouterNavigator
import com.uber.rib.stacknav.root.screen.ScreenInteractor
import com.uber.rib.stacknav.root.screen.ScreenRouter
import com.uber.rib.stacknav.root.screen.ScreenView

/**
* Manages the screen stack using [StackRouterNavigator]. Each call to [pushScreen] pushes a new
* [ScreenRouter] on top; [handleBackPress] pops it.
*/
class RootRouter(
view: RootView,
interactor: RootInteractor,
) : BasicViewRouter<RootView, RootInteractor>(view, interactor) {

private val navigator: RouterNavigator<ScreenState> = StackRouterNavigator(this)

fun pushScreen(number: Int) {
val listener = interactor as ScreenInteractor.Listener
navigator.pushState(
ScreenState(number),
object : RouterNavigator.AttachTransition<ScreenRouter, ScreenState> {
override fun buildRouter(): ScreenRouter {
val screenView = ScreenView(view.context, number, RootInteractor.MAX_SCREENS)
return ScreenRouter(screenView, ScreenInteractor(number, listener))
}

override fun willAttachToHost(
router: ScreenRouter,
previousState: ScreenState?,
newState: ScreenState,
isPush: Boolean,
) {
view.addView(
router.view,
android.view.ViewGroup.LayoutParams(
android.view.ViewGroup.LayoutParams.MATCH_PARENT,
android.view.ViewGroup.LayoutParams.MATCH_PARENT,
),
)
}
},
object : RouterNavigator.DetachTransition<ScreenRouter, ScreenState> {
override fun willDetachFromHost(
router: ScreenRouter,
previousState: ScreenState,
newState: ScreenState?,
isPush: Boolean,
) {
view.removeView(router.view)
}
},
)
}

override fun handleBackPress(): Boolean {
if (navigator.size() > 1) {
navigator.popState()
return true
}
return false
}

fun popScreen() {
navigator.popState()
}

override fun willDetach() {
navigator.hostWillDetach()
super.willDetach()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
* Copyright (C) 2025. Uber Technologies
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.uber.rib.stacknav.root

import android.content.Context
import android.widget.FrameLayout

/** Container that holds whichever screen is currently on top of the stack. */
class RootView(context: Context) : FrameLayout(context)
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* Copyright (C) 2025. Uber Technologies
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.uber.rib.stacknav.root

import com.uber.rib.core.RouterNavigatorState

/** One entry in the navigation stack, identified by screen number. */
data class ScreenState(val number: Int) : RouterNavigatorState {
override fun stateName() = "screen_$number"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* Copyright (C) 2025. Uber Technologies
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.uber.rib.stacknav.root.screen

import com.uber.rib.core.Bundle
import com.uber.rib.core.EmptyPresenter
import com.uber.rib.core.Interactor

/**
* Interactor for a single screen. Wires the view's button to [Listener.onPushNextScreen] and cleans
* up on deactivation to avoid leaks.
*/
class ScreenInteractor(
private val screenNumber: Int,
private val listener: Listener,
) : Interactor<EmptyPresenter, ScreenRouter>(EmptyPresenter()) {

interface Listener {
fun onPushNextScreen(currentNumber: Int)
fun onBackRequested()
}

override fun didBecomeActive(savedInstanceState: Bundle?) {
super.didBecomeActive(savedInstanceState)
router.view.onNextClicked = { listener.onPushNextScreen(screenNumber) }
router.view.onBackClicked = { listener.onBackRequested() }
}

override fun willResignActive() {
router.view.onNextClicked = null
router.view.onBackClicked = null
super.willResignActive()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* Copyright (C) 2025. Uber Technologies
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.uber.rib.stacknav.root.screen

import com.uber.rib.core.BasicViewRouter

/** Leaf router for a single numbered screen. No children. */
class ScreenRouter(
view: ScreenView,
interactor: ScreenInteractor,
) : BasicViewRouter<ScreenView, ScreenInteractor>(view, interactor)
Loading
Loading