Cicerone Android Navigation Library

repository·master·Indexed 25 days ago

https://github.com/terrakok/cicerone

A lightweight Android navigation library for MVP, MVVM, and MVI architectures. It decouples navigation logic from UI components using a Router/Navigator pattern with a CommandBuffer to ensure lifecycle safety. Supports FragmentScreen and ActivityScreen definitions, parameter passing, result listeners, and custom routing via BaseRouter.

Tokens
2K
Snippets
10
Records
15
Agent score
83%

What's inside Cicerone

  1. How Cicerone works: Router, CommandBuffer, and Navigator

    master

    Cicerone follows a specific flow to ensure lifecycle-safe navigation:

    1. Presenter calls a navigation method on the Router (e.g., router.navigateTo(SomeScreen())).
    2. Router converts the call into one or more Command objects and sends them to the CommandBuffer.
    3. CommandBuffer checks for an active Navigator:
      • If a Navigator is active, it passes the commands to it immediately.
      • If no Navigator is active, it queues the commands and applies them once a new Navigator becomes active.
    4. Navigator processes the commands to perform the actual UI transition (e.g., Fragment transactions or Activity starts).
  2. Initialize Cicerone in your Application class

    master

    To make Cicerone accessible throughout your app, initialize it in your Application class. You should expose the Router and NavigatorHolder so they can be used by presenters and activities.

    class App : Application() {
        private val cicerone = Cicerone.create()
        val router get() = cicerone.router
        val navigatorHolder get() = cicerone.getNavigatorHolder()
    
        override fun onCreate() {
            super.onCreate()
            INSTANCE = this
        }
    
        companion object {
            internal lateinit var INSTANCE: App
                private set
        }
    }
  3. Register a Navigator in an Activity

    master

    A Navigator is typically implemented as an anonymous class inside an Activity. You must provide the Navigator to the NavigatorHolder during onResume and remove it during onPause to ensure lifecycle safety.

    Note: If using FragmentActivity, use onResumeFragments() instead of onResume().

    private val navigator = AppNavigator(this, R.id.container)
    
    override fun onResumeFragments() {
        super.onResumeFragments()
        navigatorHolder.setNavigator(navigator)
    }
    
    override fun onPause() {
        navigatorHolder.removeNavigator()
        super.onPause()
    }
  4. Use Navigation Commands to control screen transitions

    master

    Cicerone uses Command objects to describe screen transitions. These commands are processed by a Navigator to execute navigation logic. The primary commands are:

    • Forward(screen): Opens a new screen.
    • Replace(screen): Replaces the current screen with a new one.
    • Back(): Rolls back the last transition in the screen chain.
    • BackTo(screen): Rolls back to a specific screen in the chain. If the specified screen is not found, behavior depends on the Navigator implementation, but returning to the root is recommended.
  5. Customize the AppNavigator

    master

    You can extend AppNavigator to customize fragment transactions (e.g., for animations) or to perform actions like hiding the keyboard before navigation.

    private val navigator = object : AppNavigator(this, R.id.container) {
        override fun setupFragmentTransaction(
            screen: FragmentScreen,
            fragmentTransaction: FragmentTransaction,
            currentFragment: Fragment?,
            nextFragment: Fragment
        ) {
            //setup your animation
        }
    
        override fun applyCommands(commands: Array<out Command>) {
            hideKeyboard()
            super.applyCommands(commands)
        }
    }
  6. Handle screen parameters and result listeners

    master

    Passing parameters

    Pass parameters to a screen by including them in the constructor or factory method of your FragmentScreen definition.

    fun SelectPhoto(resultKey: String) = FragmentScreen {
        SelectPhotoFragment.getNewInstance(resultKey)
    }

    Listening for results

    Use router.setResultListener(resultKey) to listen for data returned from a screen. Use router.navigateTo(...) to start the screen.

    // Listen for result
    fun onSelectPhotoClicked() {
        router.setResultListener(RESULT_KEY) { data ->
            view.showPhoto(data as Bitmap)
        }
        router.navigateTo(SelectPhoto(RESULT_KEY))
    }

    Sending results

    Use router.sendResult(resultKey, data) to send data back to the listener, followed by router.exit() to close the current screen.

    // Send result
    fun onPhotoClick(photo: Bitmap) {
        router.sendResult(resultKey, photoRes)
        router.exit()
    }
  7. Define application screens using FragmentScreen and ActivityScreen

    master

    Screens are defined as objects that return a Screen instance. You can use FragmentScreen for fragment-based navigation or ActivityScreen for starting new activities via Intent.

    object Screens {
        fun Main() = FragmentScreen { MainFragment() }
        fun AddressSearch() = FragmentScreen { AddressSearchFragment() }
        fun Profile(userId: Long) = FragmentScreen("Profile_$userId") { ProfileFragment(userId) }
        fun Browser(url: String) = ActivityScreen { Intent(Intent.ACTION_VIEW, Uri.parse(url))  }
    }