Functional Route Util for Android
This is a routing library for Android, use Kotlin coroutines
and functional library Arrow as the core, and it is built based on the combinator-oriented idea of the functional paradigm.
The goal is to build a flexible, simple structure, static type routing library
Features:
- The core is simplified to only the composite type
YRoute
and the runnerCoreEngine
- Static type
- Based on combinator-oriented programming, there is no complicated inheritance structure, hierarchical concept, only the combination of
YRoute
- State and logic are separated, state protected by the core, so logic can be combined safely and flexibly
- Unlike
Redux
orFlux
which separate side effects through theMiddleware
structure, YRoute uses the coroutinesuspend
to separate side effects, which is more flexible, composable, and contagious - The core type
YRoute
is a monad and has no side effects, so it can be combined and created arbitrarily - It does not restrict the use of
CoreEngine
in the global singleton mode, and you can also create a sub-Core or a completely independent Core
- Activity lifecycle management
- Start Activity in Rx mode
- Activity of multi-stack Fragment
- The same startFragment and finishFragment operation methods as Activity
- startFragmentForResult (onFragmentResult callback method)
- startFragmentForRx and startActivityForRx
- Fragment and Activity start and exit animation control
- onShow and onHide callback methods
- StackActivity is divided into Single and Table, supporting different modes of single stack and multi stack
- Support global Activity life cycle management
- Support to start Route from Uri
- You can choose to construct two routes: ordinary
YRoute
without parameters andLazyYRoute
with parameters - Fragment and Activity do not need to inherit a certain basic class (but need to implement some basic interfaces)
- The new parameter transfer interface can support parameter transfer in ways other than Intent, and can transfer any type of object (including non-serializable types)
- You can get the result of Route running, failure or success, or even get the new Activity started after
startActivity
Step1: Add in root build.gradle:
allprojects {
repositories {
jcenter()
maven { url "https://jitpack.io" }
}
}
Step2: Add dependencies in target module:
dependencies {
implementation 'com.github.Yumenokanata:YRoute:x.y.z'
}
// Config at App
class App : Application() {
lateinit var core: MainCoreEngine<ActivitiesState>
override fun onCreate() {
super.onCreate()
MainCoreEngine.apply {
core = create(this@App, ActivitiesState(emptyList())).unsafeRunSync()
core.start().catchSubscribe()
core.bindApp().catchSubscribe()
}
}
}
// Use
launch {
StackRoute
.startStackFragActivity(ActivityBuilder(FragmentStackActivity::class.java))
.start(core)
}
launch {
val result = ActivitiesRoute.run {
createActivityIntent<BaseActivity, ActivitiesState>(ActivityBuilder(OtherActivity::class.java))
.flatMapR { startActivityForResult(it, 1) }
}
.start(core)
Logger.d("MainActivity", result.toString())
}
launch {
try {
val result = StackRoute.run {
routeStartFragmentForRx(FragmentBuilder(FragmentOther::class.java)
.withParam(OtherParam("This is param from FragmentPage1."))
) runAtF this@FragmentPage1
}.startLazy(core).flattenForYRoute()
withContext(Dispatchers.Main) {
Toast.makeText(activity,
"YResult from Other fragment: \nresultCode=${it.a}, data=${it.b?.getString("msg")}",
Toast.LENGTH_LONG).show()
}
} catch (e: Throwable) {
e.printStackTrace()
}
}
FragmentManagerAlternative use of this library
Sample code, the main alternative class is:
- BaseFragmentManagerActivity -> indi.yume.tools.yroute.fragmentmanager.BaseTableActivity and BaseSingleActivity
- BaseManagerFragment -> indi.yume.tools.yroute.fragmentmanager.BaseManagerFragment
The library has two core types: YRoute<S, R>
and CoreEngine
. The steps to use are
- Build
YRoute
- Put into
CoreEngine
to run
The two paradigms of YRoute<S, R>
are: the State data type corresponding to S
, and the return value after running the R
route
The YRoute<S, R>
type can be regarded as a pure function:
suspend (S, Cxt) -> Pair<S, YResult<R>>
The meaning is: input the current state S
and the context Cxt
, and output a new state and the running result YResult<R>
There are currently some functional routes developed in the library:
The corresponding State is ActivitiesState
, which saves the state of all Activities
The routes are:
object ActivitiesRoute {
fun routeStartActivityByIntent(intent: Intent): YRoute<ActivitiesState, Activity>
fun <A : Activity> routeStartActivity(builder: ActivityBuilder<A>): YRoute<ActivitiesState, A>
fun routeStartActivityForResult(builder: ActivityBuilder<Activity>, requestCode: Int): YRoute<ActivitiesState, Activity>
fun routeStartActivityForRx(builder: ActivityBuilder): YRoute<ActivitiesState, Maybe<Tuple2<Int, Bundle?>>>
val routeFinishTop: YRoute<ActivitiesState, Unit>
fun routeFinish(activity: Activity): YRoute<ActivitiesState, Unit>
}
This is a route similar to the FragmentManager library that manages the Fragment stack in the Activity, but compared to the FragmentManager library, it can choose a single stack or multi-stack switching, and it can be in the Activity without limitation, and it can also manage the Fragment nested in the ParentFragment mode; and Not limited to having to inherit the base class.
Activity or Fragment as a container needs to inherit StackHost<F, out Type: StackType<F>>
:
abstract class FragmentTableActivity : FragmentActivity(), StackHost<BaseFragment, StackType.Table<BaseFragment> {
override val fragmentId: Int = R.id.fragment_layout
override var controller: StackController = StackController.defaultController()
override val initStack: StackType.Table<BaseFragment> =
StackType.Table.create(
defaultMap = mapOf(
"page1" to FragmentPage1::class.java,
"page2" to FragmentPage2::class.java,
"page3" to FragmentPage3::class.java
)
)
...
}
As a managed sub-Fragment, you need to implement the StackFragment
interface:
class FragmentPage1 : Fragment(), StackFragment {
override var controller: FragController = FragController.defaultController()
...
}
The managed Fragment can choose to implement the FragmentParam<T>
interface:
class FragmentPage1 : Fragment(), StackFragment, FragmentParam<ParamModel> {
override val injector: Subject<OtherParam> = FragmentParam.defaultInjecter()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
injector.subscribe {
Toast.makeText(activity, it.toString(), Toast.LENGTH_LONG).show()
}
}
...
}
This way, when building a FragmentBuilder
that implements the FragmentParam<T>
interface, a method withParam()
will be added:
FragmentBuilder(FragmentPage1::class.java)
.withParam(OtherParam("This is param.")
In this way, any type of parameter can be injected.
After the above work is completed, StackRoute can be used. The routing functions are:
object StackRoute {
fun <F, T, A> routeStartStackActivity(builder: ActivityBuilder<A>)
: YRoute<ActivitiesState, A>
where F : Fragment, T : StackType<F>, A : FragmentActivity, A : StackHost<F, T>
fun <F, T, A> routeGetStackFromActivity(activity: A)
: YRoute<ActivitiesState, Lens<ActivitiesState, StackFragState<F, T>?>>
where F : Fragment, T : StackType<F>, A : FragmentActivity, A : StackHost<F, T>
fun <F, T> routeGetStackActivityFromFrag(frag: Fragment)
: YRoute<ActivitiesState, Lens<ActivitiesState, StackFragState<F, T>?>>
where F : Fragment, T : StackType<F>
fun <F, R> routeRunAtFrag(frag: Fragment, route: YRoute<StackFragState<F, StackType<F>>, R>): YRoute<ActivitiesState, R>
where F : Fragment
infix fun <F, R> YRoute<StackFragState<F, StackType<F>>, R>.runAtF(frag: Fragment): YRoute<ActivitiesState, R>
where F : Fragment
fun <F, A, T, R> routeRunAtAct(act: A, route: YRoute<StackFragState<F, T>, R>): YRoute<ActivitiesState, R>
where F : Fragment, T : StackType<F>, A : FragmentActivity, A : StackHost<F, T>
infix fun <F, A, T, R> YRoute<StackFragState<F, T>, R>.runAtA(act: A): YRoute<ActivitiesState, R>
where F : Fragment, T : StackType<F>, A : FragmentActivity, A : StackHost<F, T>
fun <F> routeStartFragmentAtSingle(builder: FragmentBuilder<F>): YRoute<StackFragState<F, StackType.Single<F>>, F>
where F : Fragment, F : StackFragment
fun <F> routeStartFragmentAtTable(builder: FragmentBuilder<F>): YRoute<StackFragState<F, StackType.Table<F>>, F>
where F : Fragment, F : StackFragment
fun <F> routeStartFragment(builder: FragmentBuilder<F>): YRoute<StackFragState<F, StackType<F>>, F>
where F : Fragment, F : StackFragment
fun <F> routeStartFragmentForResultAtSingle(builder: FragmentBuilder<F>, requestCode: Int): YRoute<StackFragState<F, StackType.Single<F>>, F>
where F : Fragment, F : StackFragment
fun <F> routeStartFragmentForResultAtTable(builder: FragmentBuilder<F>, requestCode: Int): YRoute<StackFragState<F, StackType.Table<F>>, F>
where F : Fragment, F : StackFragment
fun <F> routeStartFragmentForResult(builder: FragmentBuilder<F>, requestCode: Int): YRoute<StackFragState<F, StackType<F>>, F>
where F : Fragment, F : StackFragment
fun <F> routeStartFragmentForRxAtSingle(builder: FragmentBuilder<F>): YRoute<StackFragState<F, StackType.Single<F>>, Maybe<Tuple2<Int, Bundle?>>>
where F : Fragment, F : StackFragment, F : FragmentLifecycleOwner
fun <F> routeStartFragmentForRxAtTable(builder: FragmentBuilder<F>): YRoute<StackFragState<F, StackType.Table<F>>, Maybe<Tuple2<Int, Bundle?>>>
where F : Fragment, F : StackFragment, F : FragmentLifecycleOwner
fun <F> routeStartFragmentForRx(builder: FragmentBuilder<F>): YRoute<StackFragState<F, StackType<F>>, Maybe<Tuple2<Int, Bundle?>>>
where F : Fragment, F : StackFragment, F : FragmentLifecycleOwner
fun <F> routeSwitchTag(tag: TableTag): YRoute<StackFragState<F, StackType.Table<F>>, F?> where F : Fragment
fun <F : Fragment> routeFinishFragmentAtSingle(target: StackFragment?): YRoute<StackFragState<F, StackType.Single<F>>, Tuple2<SingleTarget<F>?, FinishResult>>
fun <F : Fragment> routeFinishFragmentAtTable(target: StackFragment?): YRoute<StackFragState<F, StackType.Table<F>>, Tuple2<TableTarget<F>?, FinishResult>>
fun <F : Fragment> routeFinishFragment(target: StackFragment?): YRoute<StackFragState<F, StackType<F>>, Tuple2<Either<SingleTarget<F>, TableTarget<F>>, FinishResult>>
fun <F, A> routeStartFragAtNewSingleActivity(activityBuilder: ActivityBuilder<A>,
fragBuilder: FragmentBuilder<F>)
: YRoute<ActivitiesState, Tuple2<A, F>>
where F : Fragment, F : StackFragment, A : FragmentActivity, A : StackHost<F, StackType.Single<F>>
fun <F, A> routeStartFragAtNewTableActivity(activityBuilder: ActivityBuilder<A>,
fragBuilder: FragmentBuilder<F>)
: YRoute<ActivitiesState, Tuple2<A, F>>
where F : Fragment, F : StackFragment, A : FragmentActivity, A : StackHost<F, StackType.Table<F>>
}
Route to jump by Uri string:
val routeNavi = UriRoute.build("main") {
put("/test/other",
routeStartStackActivity(ActivityBuilder(FragmentStackActivity::class.java)))
put("/test/page1",
routeStartFragAtNewSingleActivity(
ActivityBuilder(SingleStackActivity::class.java),
FragmentBuilder(FragmentOther::class.java).withParam(OtherParam("Msg from MainActivity."))
))
}
routeNavi.withParam("route://main/test/other").start(core) // return IO<Result<Any?>>
YRoute is an Action
that transforms according to the current context and state. It needs to be actually executed in CoreEngine
CoreEngine
is an interface that describes the usual method of running YRoute:
interface CoreEngine<S> {
val routeCxt: RouteCxt
@CheckResult
fun runIO(io: IO<*>): IO<Unit>
@CheckResult
fun putStream(stream: Completable): IO<Unit>
@CheckResult
fun <R> runAsync(route: YRoute<S, R>, callback: (YResult<R>) -> Unit): IO<Unit>
@CheckResult
fun <R> run(route: YRoute<S, R>): IO<YResult<R>>
}
You can implement this interface yourself to provide different operating modes. By default, a MainCoreEngine
is implemented, and the run queue is managed through Rx, running serially:
MainCoreEngine.apply {
core = create(this@App, ActivitiesState(emptyList())).unsafeRunSync()
core.start().catchSubscribe()
}
After the Core is created, you can run YRoute:
routeStartStackActivity(ActivityBuilder(FragmentStackActivity::class.java)) // YRoute
.start(core) //suspend Result<R>
lazyR1(routeStartStackActivity(ActivityBuilder(FragmentStackActivity::class.java)))
.withParams("param")// LazyYRoute
.start(core) //suspend Result<R>
If you don't want to execute the route immediately, you can use the startLazy
method:
routeStartStackActivity(ActivityBuilder(FragmentStackActivity::class.java)) // YRoute
.startLazy(core) //SuspendP<Result<R>>
lazyR1(routeStartStackActivity(ActivityBuilder(FragmentStackActivity::class.java)))
.withParams("param")// LazyYRoute
.startLazy(core) //SuspendP<Result<R>>
SuspendP
can be converted to Rx stream and then started with Rx:
suspendP.asSingle()
After the actual executed, you can get a value of type YResult
, which has two optional types, Success
and Fail
, representing whether the result is success or failure
sealed class YResult<out T> : YResultOf<T>
data class Success<T>(val t: T) : YResult<T>()
data class Fail(val message: String, val error: Throwable? = null) : YResult<Nothing>()
Routes can be freely combined to achieve more complex custom functions:
// Use Monad to combine:
StackRoute.run { YRoute.monadError<StackFragState<BaseFragment, StackType.Table<BaseFragment>>>().fx.monad {
val currentStackTab = !routeFromState<StackFragState<BaseFragment, StackType.Table<BaseFragment>>, String?> { it.stack.current?.first }
if (currentStackTab == tab) {
// If the Tab to be switched is the current Tab
val currentStackSize = !routeFromState<StackFragState<BaseFragment, StackType.Table<BaseFragment>>, Int> {
it.stack.table[it.stack.current?.first]?.size ?: 0
}
if (currentStackSize > 1)
// If the current Tab is not at the top level, return to the top level Fragment
!routeBackToTopForTable<BaseFragment>()
else
// If you are currently at the top Fragment, restart the top Fragment
!routeClearCurrentStackForTable<BaseFragment>(true)
} else {
// Switch Tab directly
!routeSwitchTag<BaseFragment>(tag)
}
Unit
}.fix() runAtA this@MainBaseActivity }
// Use Route constructor to combine:
routeF<StackFragState<BaseFragment, StackType.Table<BaseFragment>>, Unit> { state, routeCxt ->
val currentStackTab = state.stack.current?.first
val (newState, _) = if (currentStackTab == tab) {
// If the Tab to be switched is the current Tab
val currentStackSize = state.stack.table[state.stack.current?.first]?.size ?: 0
if (currentStackSize > 1)
// If the current Tab is not at the top level, return to the top level Fragment
StackRoute.routeBackToTopForTable<BaseFragment>()
.runRoute(state, routeCxt)
else
// If you are currently at the top Fragment, restart the top Fragment
StackRoute.routeClearCurrentStackForTable<BaseFragment>(true)
.runRoute(state, routeCxt)
} else {
// Switch Tab directly
StackRoute.routeSwitchTag<BaseFragment>(tag)
.runRoute(state, routeCxt)
}
newState toT YResult.success(Unit)
}
Provide routing support for Jetpack Compose
Yumenokanata: Segmentfault
Copyright 2019 Yumenokanata 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.