Modo icon indicating copy to clipboard operation
Modo copied to clipboard

Navigation library based on UDF principles

Modo

Maven Central License: MIT

Modo is navigation library based on UDF principles for developing Single Activity applications.

Power navigation Multibackstack Launch external activities
 Modo                                                  Activity
 +---------------------------------------------+       +----------------------+
 |                                             |       |                      |
 |         +---------------------------------+ |       |                      |
 |         |                                 | |       |                      |
 |         \/          +-----------------+   | |       |                      |
 |  NavigationState--->|                 |   | |       |  +----------------+  |
 |                     |NavigationReducer|---+-|-------|->|NavigationRender|  |
 |             +------>|                 |     |       |  +----------------+  |
 |             |       +-----------------+     |       |                      |
 +---------------------------------------------+       +----------------------+
               |                                                   |
               |       +----------------+                          |
               +-------|NavigationAction|<-------------------------+
                       +----------------+

Main idea

There is NavigationState which can be used for busines logic. You can read and write current navigation stack and UI render has to apply your changes. But here can be some limitations e.g. render based on FragmentManager can hadle changes like this A-B-C -> A-D-C other way you expect. In this case B and C screens will be droped and Fragment manager will add new D and C screens.

Download

plugins {
  //...
  //for serialization screens
  id("kotlin-parcelize")
}

dependencies {
  //...
  //modo core
  implementation("com.github.terrakok:modo:${latest_version}")
  //for navigation based on FragmentManager
  implementation("com.github.terrakok:modo-render-android-fm:${latest_version}")
}

Usage

  1. Init Modo instance:
class App : Application() {
    val modo = Modo(AppReducer(this))
}
  1. Describe your screens:
object Screens {
  @Parcelize
  class Start : AppScreen("Start") {
    override fun create() = StartFragment()
  }

  fun Browser(url: String) = ExternalScreen {
    Intent(Intent.ACTION_VIEW, Uri.parse(url))
  }
}
  1. Setup your application activity:
class MainActivity : AppCompatActivity() {
  private val modo = App.INSTANCE.modo

  //must be lazy otherwise initialization fails with early access to fragment manager
  private val modoRender by lazy { ModoRender(this, R.id.container) }

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    modo.init(savedInstanceState, Screens.Start())
  }

  override fun onResume() {
    super.onResume()
    modo.render = modoRender
  }

  override fun onPause() {
    modo.render = null
    super.onPause()
  }

  override fun onSaveInstanceState(outState: Bundle) {
    super.onSaveInstanceState(outState)
    //add this if you want to restore app after process death
    modo.saveState(outState)
  }
}
  1. Use Modo for navigation: See CommandsFragment.kt for additional examples.
modo.forward(screen) //navigate to next screen
modo.replace(screen) //replace current screen
modo.newStack(screen) //replace current screen stack
modo.backTo(screenId) //back to screen in current stack if exist
modo.back() //back to previous
modo.exit() //exit from activity

Multistack navigation

Modo gives you multistack support out-of-the-box!

  1. Use MultiReducer for initialization
modo = Modo(AppReducer(this, MultiReducer()))
  1. Add multistack screen:
object Screens {
  @Parcelize
  class Start : AppScreen("Start") {
    override fun create() = StartFragment()
  }

  @Parcelize
  class MyScreen : AppScreen("MyScreen") {
    override fun create() = MyFragment()
  }
  //other screens...

  fun MultiStack() = MultiAppScreen(
    "MultiStack", //some id
    listOf(Start(), MyScreen()), //root screens in tabs
    1 //selected tab by default
  )
}
  1. Describe how tab view will be look:
class MyMultiStackFragment : MultiStackFragmentImpl() {
  override fun createTabView(index: Int, parent: LinearLayout): View =
    LayoutInflater.from(context)
      .inflate(R.layout.layout_tab, parent, false)

  // optional
  override fun decorTabContainer(view: LinearLayout) {
    // customize tab container
    // view.elevation = 8.dpToPx()
  }
}
  1. Put it in your render:
private val modoRender by lazy {
  object : ModoRender(this@AppActivity, R.id.container) {
    override fun createMultiStackFragment() = MyMultiStackFragment()
  }
}
  1. Just use new available commands! See TabFragment.kt for additional examples.
modo.externalForward(Screens.Start()) //open new screen above tabs
modo.selectStack(1) //change tab
modo.backToTabRoot() //return on tab root

Debug

You can use LogReducer for logging navigation state changes

Modo(
  if (BuildConfig.DEBUG) LogReducer(AppReducer(this@App))
  else AppReducer(this@App)
)

Logcat (from sample application):

D/Modo: Activity first launch
D/Modo: New action=com.github.terrakok.modo.Forward@9d0f15d
D/Modo: New state=NavigationState(chain=[[1]])

Samples

Base features are showed in sample app: sample-android-fm

Extending ModoRender

private val modoRender by lazy {
  object : ModoRender(this@MainActivity, R.id.container) {
    override fun pop(count: Int) {
      hideKeyboard()
      super.pop(count)
    }

    override fun push(screens: List<Screen>) {
      hideKeyboard()
      super.push(screens)
    }

    override fun setupTransaction(
      fragmentManager: FragmentManager,
      transaction: FragmentTransaction,
      screen: AppScreen,
      newFragment: Fragment
    ) {
      //e.g. setup your animation
    }
  }
}

fun Activity.hideKeyboard() {
  currentFocus?.apply {
    val inputManager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
    inputManager.hideSoftInputFromWindow(windowToken, 0)
  }
}

License

MIT License

Copyright (c) 2021 Konstantin Tskhovrebov (@terrakok)

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.