21 Commits

Author SHA1 Message Date
terrakok c3873c6e9d Bump version to "6.6" 2020-11-26 13:31:05 +03:00
terrakok f44410fcfe Fix problem with loss of result listener.
Issue #130
2020-11-26 13:30:22 +03:00
Konstantin 608a32c3be Update README.md 2020-11-18 22:34:28 +03:00
Konstantin 17428f0189 Update README.md 2020-11-18 22:08:21 +03:00
terrakok 1701bc4f36 Fixed launching Activity via no concrete Intent. 2020-11-18 22:02:18 +03:00
terrakok 9c9c56bc9b Bump version to "6.4" 2020-11-05 17:24:22 +03:00
terrakok 3da8fe52ca Set fragmentReorderingAllowed=TRUE by default. 2020-11-05 17:21:01 +03:00
terrakok a943aab08e Move ResultListener to another file. 2020-11-05 16:15:28 +03:00
Konstantin 86ac421fb8 Merge pull request #128 from aradxxx/feature/java-interop
java interop
2020-11-05 16:06:50 +03:00
aradxxx f7009f53e8 java interop 2020-11-03 05:13:13 +04:00
Konstantin 994c497147 Update README.md 2020-11-02 20:55:34 +03:00
Konstantin bb13ace1c9 Update README.md 2020-11-02 15:29:24 +03:00
Konstantin dbe065275d Update README.md 2020-11-02 15:15:24 +03:00
Konstantin 2ebbc98a99 Update README.md 2020-11-02 15:10:17 +03:00
Konstantin ba7eea60f5 Update README.md 2020-11-02 10:10:17 +03:00
Konstantin 6347fe7d95 Update README.md 2020-11-01 21:30:28 +03:00
Konstantin 2b6e2ee874 Update README.md 2020-11-01 18:34:03 +03:00
terrakok f50a4d8106 Bump version to "6.3" 2020-11-01 17:07:08 +03:00
terrakok 3535f491f5 Refactor Screen class as interface. 2020-11-01 17:07:04 +03:00
Konstantin 1c5b89d2ec Update README.md 2020-11-01 14:54:59 +03:00
Konstantin 127cce1fe4 Update README.md 2020-11-01 14:06:53 +03:00
19 changed files with 137 additions and 63 deletions
+47 -15
View File
@@ -6,15 +6,39 @@
[![Android Weekly](https://img.shields.io/badge/Android%20Weekly-250-green.svg)](http://androidweekly.net/issues/issue-250)
[![Android Weekly](https://img.shields.io/badge/Android%20Weekly-271-green.svg)](http://androidweekly.net/issues/issue-271)
![](https://habrastorage.org/files/644/32e/9eb/64432e9eb3664723b3ee438449dab3b0.png)
<table>
<tr>
<td>
<img src="https://github.com/terrakok/Cicerone/raw/master/media/navigation.gif" width="256"/>
</td>
<td>
<img src="https://github.com/terrakok/Cicerone/raw/master/media/insta_tabs.gif" width="256"/>
</td>
<td>
<img src="https://github.com/terrakok/Cicerone/raw/master/media/animations.gif" width="256"/>
</td>
</tr>
<tr>
<td>
Power navigation
</td>
<td>
Multibackstack
</td>
<td>
Result listeners
</td>
</tr>
</table>
Cicerone (a guide, one who conducts sightseers) is a lightweight library that makes the navigation in an Android app easy.
Cicerone (means - a guide, one who conducts sightseers) is a lightweight library that makes the navigation in an Android app easy.
It was designed to be used with the MVP/MVVM/MVI patterns but will work great with any architecture.
## Main advantages
+ is not tied to Fragments
+ not a framework
+ not a framework (very lightweight)
+ short navigation calls (no builders)
+ static typed checks for screen parameters!
+ lifecycle-safe!
+ functionality is simple to extend
+ suitable for Unit Testing
@@ -109,13 +133,13 @@ override fun onPause() {
## Navigation commands
This commands set will fulfill the needs of the most applications. But if you need something special - just add it!
+ Forward - Opens new screen
![](https://github.com/terrakok/Cicerone/raw/develop/media/forward_img.png)
![](https://github.com/terrakok/Cicerone/raw/master/media/forward_img.png)
+ Back - Rolls back the last transition
![](https://github.com/terrakok/Cicerone/raw/develop/media/back_img.png)
![](https://github.com/terrakok/Cicerone/raw/master/media/back_img.png)
+ BackTo - Rolls back to the needed screen in the screens chain
![](https://github.com/terrakok/Cicerone/raw/develop/media/backTo_img.png)
![](https://github.com/terrakok/Cicerone/raw/master/media/backTo_img.png)
+ Replace - Replaces the current screen
![](https://github.com/terrakok/Cicerone/raw/develop/media/replace_img.png)
![](https://github.com/terrakok/Cicerone/raw/master/media/replace_img.png)
## Predefined navigator
The library provides predefined navigator for _Fragments_ and _Activity_.
@@ -147,21 +171,22 @@ private val navigator = object : AppNavigator(this, R.id.container) {
Describe your screens as you like e.g. create Kotlin `object` with all application screens:
```kotlin
object Screens {
val Main = FragmentScreen("MainFragment") { MainFragment() }
val AddressSearch = FragmentScreen("AddressSearchFragment") { AddressSearchFragment() }
fun Profile(userId: Long) = FragmentScreen("ProfileFragment") { ProfileFragment(userId) }
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)) }
}
```
Additional you can use `FragmentFactory` for creating your screens:
```kotlin
val SomeScreen = FragmentScreen("SomeScreenId") { factory: FragmentFactory -> ... }
fun SomeScreen() = FragmentScreen { factory: FragmentFactory -> ... }
```
## Screen parameters and result listener
```kotlin
//you have to specify screen parameters via new FragmentScreen creation
fun SelectPhoto(resultKey: String) = FragmentScreen("SelectPhoto") {
fun SelectPhoto(resultKey: String) = FragmentScreen {
SelectPhotoFragment.getNewInstance(resultKey)
}
```
@@ -188,9 +213,16 @@ To see how to add, initialize and use the library and predefined navigators see
For more complex use case check out the [GitFox (Android GitLab client)](https://gitlab.com/terrakok/gitlab-client)
![](https://github.com/terrakok/Cicerone/raw/develop/media/navigation.gif)
![](https://github.com/terrakok/Cicerone/raw/develop/media/insta_tabs.gif)
![](https://github.com/terrakok/Cicerone/raw/develop/media/animations.gif)
## Applications with Cicerone inside
<a href="https://play.google.com/store/apps/details?id=ru.foodfox.client"><img src="https://play-lh.googleusercontent.com/gWYedIqy8QujCQOn0kzEIBEkGLBSpuKvFm-fMcfkWnJ1Oirtv847xAE4OyhAaohdcp5V=s360" width="64" /> Яндекс.Еда — доставка еды/продуктов. Food delivery</a><br>
<a href="https://play.google.com/store/apps/details?id=com.foodient.whisk"><img src="https://play-lh.googleusercontent.com/eKotZjJcZOU2_L9t2l34EEY7aGl5zhvKVuEbF0Kc4MRs_pAC2SJgOnWMkMTFjR_e9EY=s360" width="64" /> Whisk: Recipe Saver, Meal Planner & Grocery List</a><br>
<a href="https://play.google.com/store/apps/details?id=kz.beeline.odp"><img src="https://play-lh.googleusercontent.com/hzgjpQQpy6Z-Byye0aVKSv9P7h8yx58i6pVkQtiM6jB99iWFXjYfKeaPqJ3wm6Rtb38=s360" width="64" /> Мой Beeline (Казахстан)</a><br>
<a href="https://play.google.com/store/apps/details?id=com.mercuryo.app"><img src="https://play-lh.googleusercontent.com/FKulXdc15r5PWX6hTZi2i3iaJjcQHwd9xParp6YPiQ2KiBqza7jwEt_b_tqLwXpyEHg=s360" width="64" /> Mercuryo Bitcoin Cryptowallet</a><br>
<a href="https://play.google.com/store/apps/details?id=com.warefly.checkscan"><img src="https://play-lh.googleusercontent.com/2c2uuiSl2vwGgp-vdI-VArQEMdSSXk1neUK5A-Udc0WANPcvp5kBJFEugrFiXnxUc7k=s360" width="64" /> ЧекСкан - кэшбэк за чеки, цены и акции в магазинах</a><br>
<a href="https://github.com/eduard1abdulmanov123/News"><img src="https://raw.githubusercontent.com/eduard1abdulmanov123/News/dev/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png" width="64" /> RSS Reader для Вести.Ru</a><br>
<a href="https://play.google.com/store/apps/details?id=com.epam.connect.android"><img src="https://play-lh.googleusercontent.com/aN7R6BiR7yt7b3oEoBI30pVwzsdzaWe3TWpw8c9igqoOj79Pm2xVh4_C4qwjSKwjVio=s360" width="64" /> EPAM Connect</a><br>
<a href="https://play.google.com/store/apps/details?id=org.consumerreports.ratings"><img src="https://play-lh.googleusercontent.com/dEdOwZOjXAdamytxY1TgY8LS-Hc9FKCcit5HP1RyaKqRAWjDJEyFSQS1XlqQPpeY5UI=s360" width="64" /> Consumer Reports: Product Reviews & Ratings</a><br>
<a href="https://play.google.com/store/apps/details?id=ru.zakaz.android"><img src="https://play-lh.googleusercontent.com/jj18yK2dB2MHZ_QdO21aXyznGXteIF2q4mgxY4ubLhFv9gwZqHVDeu1i2FmanS-0Furm=s360" width="64" /> Zakaz.ru</a><br>
## Participants
+ idea and code - Konstantin Tskhovrebov (@terrakok)
+1 -1
View File
@@ -18,7 +18,7 @@ ext {
bintrayName = 'cicerone-kotlin'
publishedGroupId = 'com.github.terrakok'
artifact = 'cicerone'
libraryVersion = '6.2'
libraryVersion = '6.6'
gitUrl = 'https://github.com/terrakok/Cicerone'
allLicenses = ['MIT']
}
@@ -10,12 +10,16 @@ abstract class BaseRouter {
private val resultWire = ResultWire()
/**
* Sets data listener with given key.
* Sets data listener with given key
* and returns [ResultListenerHandler] for availability to dispose subscription.
*
* After first call listener will be removed.
*/
fun setResultListener(key: String, listener: (data: Any) -> Unit) {
resultWire.setResultListener(key, listener)
fun setResultListener(
key: String,
listener: ResultListener
): ResultListenerHandler {
return resultWire.setResultListener(key, listener)
}
/**
@@ -32,6 +36,5 @@ abstract class BaseRouter {
*/
protected fun executeCommands(vararg commands: Command) {
commandBuffer.executeCommands(commands)
resultWire.flush()
}
}
@@ -17,12 +17,14 @@ class Cicerone<T : BaseRouter> private constructor(val router: T) {
/**
* Creates the Cicerone instance with the default [Router]
*/
@JvmStatic
fun create() = create(Router())
/**
* Creates the Cicerone instance with the custom router.
* @param customRouter the custom router extending [BaseRouter]
*/
@JvmStatic
fun <T : BaseRouter> create(customRouter: T) = Cicerone(customRouter)
}
}
@@ -1,23 +1,30 @@
package com.github.terrakok.cicerone
import java.lang.ref.WeakReference
/**
* Interface definition for a result callback.
*/
fun interface ResultListener {
fun onResult(data: Any)
}
/**
* Handler for manual delete subscription and avoid leak
*/
fun interface ResultListenerHandler {
fun dispose()
}
internal class ResultWire {
private val listeners = mutableMapOf<String, WeakReference<(Any) -> Unit>>()
private val listeners = mutableMapOf<String, ResultListener>()
fun setResultListener(key: String, listener: (data: Any) -> Unit) {
listeners[key] = WeakReference(listener)
}
fun sendResult(key: String, data: Any) {
listeners.remove(key)?.get()?.let { listener ->
listener(data)
fun setResultListener(key: String, listener: ResultListener): ResultListenerHandler {
listeners[key] = listener
return ResultListenerHandler {
listeners.remove(key)
}
}
fun flush() {
listeners.entries
.filter { it.value.get() == null }
.forEach { listeners.remove(it.key) }
fun sendResult(key: String, data: Any) {
listeners.remove(key)?.onResult(data)
}
}
@@ -14,6 +14,7 @@ open class Router : BaseRouter() {
* @param screen screen
* @param clearContainer if FALSE then new screen shows over previous
*/
@JvmOverloads
fun navigateTo(screen: Screen, clearContainer: Boolean = true) {
executeCommands(Forward(screen, clearContainer))
}
@@ -58,6 +59,7 @@ open class Router : BaseRouter() {
* @param screens
* @param showOnlyTopScreenView if FALSE then all screen views show together
*/
@JvmOverloads
fun newChain(vararg screens: Screen, showOnlyTopScreenView: Boolean = true) {
val commands = screens.map { Forward(it, showOnlyTopScreenView) }
executeCommands(*commands.toTypedArray())
@@ -69,6 +71,7 @@ open class Router : BaseRouter() {
* @param screens
* @param showOnlyTopScreenView if FALSE then all screen views show together
*/
@JvmOverloads
fun newRootChain(vararg screens: Screen, showOnlyTopScreenView: Boolean = true) {
val commands = screens.mapIndexed { index, screen ->
if (index == 0)
@@ -1,8 +1,8 @@
package com.github.terrakok.cicerone
/**
* Screen is class for description application screen.
* Screen is interface for description application screen.
*/
abstract class Screen {
open val screenKey: String = this::class.qualifiedName!!
interface Screen {
val screenKey: String get() = this::class.java.name
}
@@ -1,5 +1,6 @@
package com.github.terrakok.cicerone.androidx
import android.content.ActivityNotFoundException
import android.content.Intent
import androidx.fragment.app.*
import com.github.terrakok.cicerone.*
@@ -13,11 +14,11 @@ import com.github.terrakok.cicerone.androidx.TransactionInfo.Type.REPLACE
*
* Recommendation: most useful for Single-Activity application.
*/
open class AppNavigator constructor(
open class AppNavigator @JvmOverloads constructor(
protected val activity: FragmentActivity,
protected val containerId: Int,
protected val fragmentManager: FragmentManager = activity.supportFragmentManager,
protected val fragmentFactory: FragmentFactory = FragmentFactory()
protected val fragmentFactory: FragmentFactory = fragmentManager.fragmentFactory
) : Navigator {
protected val localStackCopy = mutableListOf<TransactionInfo>()
@@ -109,6 +110,7 @@ open class AppNavigator constructor(
) {
val fragment = screen.createFragment(fragmentFactory)
val transaction = fragmentManager.beginTransaction()
transaction.setReorderingAllowed(true)
setupFragmentTransaction(
transaction,
fragmentManager.findFragmentById(containerId),
@@ -170,9 +172,9 @@ open class AppNavigator constructor(
private fun checkAndStartActivity(screen: ActivityScreen) {
// Check if we can start activity
val activityIntent = screen.createIntent(activity)
if (activityIntent.resolveActivity(activity.packageManager) != null) {
try {
activity.startActivity(activityIntent, screen.startActivityOptions)
} else {
} catch (e: ActivityNotFoundException) {
unexistingActivity(screen, activityIntent)
}
}
@@ -7,16 +7,25 @@ import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentFactory
import com.github.terrakok.cicerone.Screen
sealed class AppScreen : Screen()
sealed class AppScreen : Screen
open class FragmentScreen(
override val screenKey: String,
val createFragment: (FragmentFactory) -> Fragment
) : AppScreen()
fun interface Creator<A, R> {
fun create(argument: A): R
}
open class ActivityScreen(
override val screenKey: String,
val createIntent: (context: Context) -> Intent
open class FragmentScreen @JvmOverloads constructor(
private val key: String? = null,
private val fragmentCreator: Creator<FragmentFactory, Fragment>
) : AppScreen() {
override val screenKey: String get() = key ?: super.screenKey
fun createFragment(factory: FragmentFactory) = fragmentCreator.create(factory)
}
open class ActivityScreen @JvmOverloads constructor(
private val key: String? = null,
private val intentCreator: Creator<Context, Intent>
) : AppScreen() {
override val screenKey: String get() = key ?: super.screenKey
open val startActivityOptions: Bundle? = null
fun createIntent(context: Context) = intentCreator.create(context)
}
@@ -12,6 +12,7 @@ data class TransactionInfo(
override fun toString() = screenKey + type.symbol
companion object {
@JvmStatic
fun fromString(str: String) = TransactionInfo(
str.dropLast(1),
if (str.last() == Type.ADD.symbol) Type.ADD else Type.REPLACE
@@ -36,6 +36,10 @@ public class FragmentManager {
throw new RuntimeException("Stub!");
}
public FragmentFactory getFragmentFactory() {
throw new RuntimeException("Stub!");
}
public interface BackStackEntry {
int getId();
@@ -6,6 +6,10 @@ package androidx.fragment.app;
*/
public class FragmentTransaction {
public FragmentTransaction setReorderingAllowed(boolean reorderingAllowed) {
throw new RuntimeException("Stub!");
}
public FragmentTransaction add(int containerViewId, Fragment fragment, String tag) {
throw new RuntimeException("Stub!");
}
@@ -24,39 +24,39 @@ object Screens {
SampleFragment.getNewInstance(number)
}
fun Start() = ActivityScreen("Start") {
fun Start() = ActivityScreen {
Intent(it, StartActivity::class.java)
}
fun Main() = ActivityScreen("Main") {
fun Main() = ActivityScreen {
Intent(it, MainActivity::class.java)
}
fun BottomNavigation() = ActivityScreen("BottomNavigation") {
fun BottomNavigation() = ActivityScreen {
Intent(it, BottomNavigationActivity::class.java)
}
fun Tab(tabName: String) = FragmentScreen("Tab") {
fun Tab(tabName: String) = FragmentScreen {
TabContainerFragment.getNewInstance(tabName)
}
fun Forward(containerName: String, number: Int) = FragmentScreen("Forward") {
fun Forward(containerName: String, number: Int) = FragmentScreen {
ForwardFragment.getNewInstance(containerName, number)
}
fun Github() = ActivityScreen("Github") {
fun Github() = ActivityScreen {
Intent(Intent.ACTION_VIEW, Uri.parse("https://github.com/terrakok/Cicerone"))
}
fun Profile() = ActivityScreen("Profile") {
fun Profile() = ActivityScreen {
Intent(it, ProfileActivity::class.java)
}
fun ProfileInfo() = FragmentScreen("ProfileInfo") {
fun ProfileInfo() = FragmentScreen {
ProfileFragment()
}
fun SelectPhoto(resultKey: String) = FragmentScreen("SelectPhoto") {
fun SelectPhoto(resultKey: String) = FragmentScreen {
SelectPhotoFragment.getNewInstance(resultKey)
}
}
@@ -1,5 +1,6 @@
package com.github.terrakok.cicerone.sample.mvp.animation.profile
import com.github.terrakok.cicerone.ResultListenerHandler
import com.github.terrakok.cicerone.Router
import com.github.terrakok.cicerone.sample.R
import com.github.terrakok.cicerone.sample.Screens.SelectPhoto
@@ -13,6 +14,7 @@ import moxy.MvpPresenter
class ProfilePresenter(
private val router: Router
) : MvpPresenter<ProfileView>() {
private var resultListenerHandler: ResultListenerHandler? = null
companion object {
private const val RESULT_KEY = "photo_result"
@@ -25,12 +27,17 @@ class ProfilePresenter(
}
fun onPhotoClicked() {
router.setResultListener(RESULT_KEY) { data ->
resultListenerHandler = router.setResultListener(RESULT_KEY) { data ->
viewState!!.showPhoto(data as Int)
}
router.navigateTo(SelectPhoto(RESULT_KEY))
}
override fun onDestroy() {
resultListenerHandler?.dispose()
super.onDestroy()
}
fun onBackPressed() {
router.exit()
}
@@ -46,7 +46,7 @@ class ProfileActivity : AppCompatActivity() {
super.onPause()
}
private val navigator: Navigator = object : AppNavigator(this, R.id.container, supportFragmentManager, supportFragmentManager.fragmentFactory) {
private val navigator: Navigator = object : AppNavigator(this, R.id.container) {
override fun setupFragmentTransaction(fragmentTransaction: FragmentTransaction, currentFragment: Fragment?, nextFragment: Fragment?) {
if (currentFragment is ProfileFragment
@@ -87,7 +87,7 @@ class BottomNavigationActivity : MvpAppCompatActivity(), BottomNavigationView, R
if (newFragment == null) {
transaction.add(
R.id.ab_container,
Tab(tab).createFragment.invoke(fm.fragmentFactory), tab
Tab(tab).createFragment(fm.fragmentFactory), tab
)
}
if (currentFragment != null) {
@@ -23,7 +23,7 @@ import javax.inject.Inject
class TabContainerFragment : Fragment(), RouterProvider, BackButtonListener {
private val navigator: Navigator by lazy {
AppNavigator(activity!!, R.id.ftc_container, childFragmentManager, childFragmentManager.fragmentFactory)
AppNavigator(activity!!, R.id.ftc_container, childFragmentManager)
}
@Inject
@@ -31,7 +31,7 @@ class MainActivity : MvpAppCompatActivity(), ChainHolder {
@Inject
lateinit var navigatorHolder: NavigatorHolder
private val navigator: Navigator = object : AppNavigator(this, R.id.main_container, supportFragmentManager, supportFragmentManager.fragmentFactory) {
private val navigator: Navigator = object : AppNavigator(this, R.id.main_container) {
override fun applyCommands(commands: Array<out Command>) {
super.applyCommands(commands)
@@ -29,7 +29,7 @@ class StartActivity : MvpAppCompatActivity(), StartActivityView {
@InjectPresenter
lateinit var presenter: StartActivityPresenter
private val navigator: Navigator = AppNavigator(this, -1, supportFragmentManager, supportFragmentManager.fragmentFactory)
private val navigator: Navigator = AppNavigator(this, -1)
@ProvidePresenter
fun createStartActivityPresenter() = StartActivityPresenter(router)