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|<-------------------------+
+----------------+
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}")
}
- Init Modo instance:
class App : Application() {
val modo = Modo(AppReducer(this))
}
- 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))
}
}
- 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)
}
}
- 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
Modo gives you multistack support out-of-the-box!
- Use
MultiReducer
for initialization
modo = Modo(AppReducer(this, MultiReducer()))
- 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
)
}
- 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)
}
- Put it in your render:
private val modoRender by lazy {
object : ModoRender(this@AppActivity, R.id.container) {
override fun createMultiStackFragment() = MyMultiStackFragment()
}
}
- 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
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]])
Base features are showed in sample app: sample-android-fm
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)
}
}
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.