voyager icon indicating copy to clipboard operation
voyager copied to clipboard

Adds support for result passing between screens

Open Kashif-E opened this issue 1 year ago • 40 comments

Closes #110 Adds support to pass results between screen for Navigator and BottomSheetNavigator.

using Navigator.

Passing the result

    //you can also pass your own key or key of the screen will be the default result key
     navigator.popWithResult("This is result")

     navigator.popUntilWithResult(
     { screen -> screen is ProductDetailsScreen },
     data)

Getting the result

 destinationsNavigator
        .getResult<Boolean>(LocalNavigator.currentOrThrow.lastItem.key)
        .value
        ?.let { result -> onAddedToCart(result) }

using BottomSheetNavigator:

Passing the result


 bsNavigator.hideWithResult(Pair(item.id, item.data), "my-key")

Getting the result:

   bottomSheetNavigator.getResult<Pair>(screenKey = "my-key").value?.let {result->
        //do something with the result here
    }

Kashif-E avatar Apr 12 '23 04:04 Kashif-E

@adrielcafe @DevSrSouza

Kashif-E avatar Apr 14 '23 22:04 Kashif-E

@Kashif-E HI, we will have a look on this soon (testing and trying on our samples), don't know if it will be for 1.0 that will be soon released. Btw thanks for the contribution!

I will let you know soon!

DevSrSouza avatar Apr 24 '23 02:04 DevSrSouza

@DevSrSouza @adrielcafe

Kashif-E avatar Jun 18 '23 18:06 Kashif-E

Hey guys, any updates on this?

FunkyMuse avatar Jun 23 '23 15:06 FunkyMuse

@DevSrSouza any update ?

Kashif-E avatar Jul 15 '23 16:07 Kashif-E

@DevSrSouza @adrielcafe this is something we really need can you please help here?

Kashif-E avatar Jul 31 '23 05:07 Kashif-E

image

@Kashif-E Have you given up on this? :(

jakoss avatar Aug 13 '23 18:08 jakoss

@jakoss i hate to admit this but yes, but, i am thinking about making my fork a separate library.

Kashif-E avatar Aug 13 '23 18:08 Kashif-E

@jakoss i hate to admit this but yes, but, i am thinking about making my fork a separate library.

More fragmentation for navigation libraries for compose, but i understand the reason. This thread is too ignored at this point

jakoss avatar Aug 13 '23 18:08 jakoss

@Kashif-E Can we get back here? Forgive us for inactivity, this feature is very important and we want it on Voyager

Please let me know if you can continue here, I will take care of reviewing it and ensuring it is merged, if not possible I will continue it myself. Thank you in advance for your time and contribution!! 🙏🏽

DevNatan avatar Oct 04 '23 16:10 DevNatan

I don't feel like this implementation is ideal for Voyager, I would prefer something like the one displayed here would be better. https://www.droidcon.com/2022/08/02/painless-typesafe-jetpack-compose-navigation-with-voyager/

I feel it clutters the navigator when it could just be a module added on-top of it

Syer10 avatar Oct 04 '23 18:10 Syer10

1 issue I find with this is that it uses String keys, I find that with Kotlin I would prefer to have a TypeSafe key to Value object that allows managing types inherently. Something like this

interface TypedKey<T>

object DataId : TypedKey<Long>

private val typedKeyMap: Map<TypedKey<*>, Any>

fun <T> putInTypedKeyMap(typedKey: TypedKey<T>, value: T): T {
    typedKeyMap[typedKey] = value
}

fun <T> getFromTypedKeyMap(typedKey: TypedKey<T>): T {
    return typedKeyMap[typedKey] as T
}

fun inputMyData() {
    putInTypedKeyMap(DataId, 100L)
}

fun getMyData() {
    val myDataId: Long = getFromTypedKeyMap(DataId)
}

Syer10 avatar Oct 04 '23 18:10 Syer10

@Syer10 Great! What we want here is the "possibility of passing results between screens" the way, api, imp in which this will be done has not yet been defined. Also agree that such a feature should be something "more" and not tied to the navigator. What you said will be considered, thanks!

DevNatan avatar Oct 04 '23 18:10 DevNatan

@Syer10 what you said is perfectly right. However, there are a few things that should be considered here

  1. Strings are chosen over type-safe keys because screen keys are inherently strings, eliminating the need for custom keys for each result.
  2. Binding results to navigators enhances developer ergonomics, simplifying result access and aligning with screen-based mental models.

Kashif-E avatar Oct 05 '23 05:10 Kashif-E

Both are valid and reasonable, do you guys @Kashif-E @Syer10 have any example code (using Voyager) so that we can reach a consensus on what will be best for everyone? I'm also considering how this will work for Web platform and Multi-module feature (ScreenProvider)

DevNatan avatar Oct 06 '23 20:10 DevNatan

@DevNatan I have been using this code for our main app for more than 6 months now and it's been working great.

Kashif-E avatar Oct 09 '23 08:10 Kashif-E

I have baked your API without requiring to change any internal voyager APIs, could you try it out? If it works for you, we could provide this a result API examples.

val Navigator.navigationResult: VoyagerResultExtension
    @Composable get() = remember {
        NavigatorLifecycleStore.register(this) {
            VoyagerResultExtension(this)
        }
    }

// OR

@Composable
public fun rememberNavigationResultExtension(): VoyagerResultExtension {
    val navigator = LocalNavigator.currentOrThrow

    return remember {
        NavigatorLifecycleStore.get(navigator) {
            VoyagerResultExtension(navigator)
        }
    }
}

class VoyagerResultExtension(
    private val navigator: Navigator
) : NavigatorDisposable {
    private val results = mutableStateMapOf<String, Any?>()

    override fun onDispose(navigator: Navigator) {
        // not used
    }

    public fun setResult(screenKey: String, result: Any?) {
        results[screenKey] = result
    }

    public fun popWithResult(result: Any? = null) {
        val currentScreen = navigator.lastItem
        results[currentScreen.key] = result
        navigator.pop()
    }

    public fun clearResults() {
        results.clear()
    }


    public fun popUntilWithResult(predicate: (Screen) -> Boolean, result: Any? = null) {
        val currentScreen = navigator.lastItem
        results[currentScreen.key] = result
        navigator.popUntil(predicate)
    }

    @Composable
    public fun <T> getResult(screenKey: String): State<T?> {
        val result = results[screenKey] as? T
        val resultState = remember(screenKey, result) {
            derivedStateOf {
                results.remove(screenKey)
                result
            }
        }
        return resultState
    }
}

DevSrSouza avatar Oct 14 '23 16:10 DevSrSouza

+1 for @DevSrSouza's implementation

Syer10 avatar Oct 14 '23 17:10 Syer10

this looks great @DevSrSouza. ill test this in our app and will let you know how this works

Kashif-E avatar Oct 16 '23 08:10 Kashif-E

This could get into the documentation IMHO @DevSrSouza

osrl avatar Oct 23 '23 08:10 osrl

Did it work for you @Kashif-E ?

@osrl we are planning to create a documentation page with "community extension and third party libraries"

DevSrSouza avatar Nov 05 '23 19:11 DevSrSouza

Is it possible to use this snippet with a BottomSheetNavigator? @DevSrSouza

osrl avatar Nov 06 '23 17:11 osrl

@DevSrSouza it works, 🎆🎆 @osrl both mine and what @DevSrSouza proposed can be used with bottomsheet navigator

Kashif-E avatar Nov 07 '23 06:11 Kashif-E

NavigatorLifecycleStore.register takes only Navigator so I couldn't find a way to use @DevSrSouza's code.

osrl avatar Nov 07 '23 07:11 osrl

@osrl you can use these extensions, remove the actual keyword




/**
 * Navigator
 */
private val results = mutableStateMapOf<String, Any?>()


actual fun Navigator.popWithResult(key: String, result: Any?) {
    results[key] = result
    pop()
}
actual fun Navigator.pushX(screen: Screen) {
    val existingScreen = items.firstOrNull { it.key == screen.key }
    if (existingScreen == null) {
        push(screen)
    }
}

actual fun Navigator.replaceAllX(screen: Screen) {
    if (items.last().key != screen.key && items.last().uniqueScreenKey != screen.uniqueScreenKey) {
        replaceAll(screen)
    }
}

actual fun Navigator.popUntilWithResult(predicate: (Screen) -> Boolean, result: Any?) {
    val currentScreen = lastItem
    results[currentScreen.key] = result
    popUntil(predicate)
}

actual fun Navigator.clearResults() {
    results.clear()
}

@Composable
actual fun <T> Navigator.getResult(screenKey: String): State<T?> {
    val result = results[screenKey] as? T
    val resultState = remember(screenKey, result) {
        derivedStateOf {
            results.remove(screenKey)
            result
        }
    }
    return resultState
}

/**
 * bottom sheet navigator
 */


@OptIn(ExperimentalMaterialApi::class)
actual fun BottomSheetNavigator.hideWithResult(result: Any?, key: String) {
    results[key] = result
    this.hide()

}

@Composable
actual fun <T> BottomSheetNavigator.getResult(screenKey: String): State<T?> {
    val result = results[screenKey] as? T
    val resultState = remember(screenKey, result) {
        derivedStateOf {
            results -= screenKey
            result
        }
    }
    return resultState
}

Kashif-E avatar Nov 09 '23 07:11 Kashif-E

Hello, is it going to be supported by the library, or you prefer to keep it as extension function and everybody just copy paste it?

@DevSrSouza This works, it's great, but it seems a little cumbersome to use, maybe I'm using it the wrong way? If you want to get the result of the second page on the first page, you need to get the Key of the second page.

However, due to life cycle issues, the first page can only get the current value by directly using Navigator.lastItem.key. The Key of the page, not the Key of the second page,so you need to use the following method to obtain the result on the first page:

var lastItemKey: String? by rememberSaveable {
    mutableStateOf(null)
}
Box(modifier = Modifier.clickable {
    navigator.push(SecondPageScreen())
    lastItemKey = navigator.lastItem.key
})
if (lastItemKey != null) {
    val result by navigator.navigationResult
        .getResult<String>(lastItemKey!!)
}

0xZhangKe avatar Jan 09 '24 12:01 0xZhangKe

@0xZhangKe it does not matter what you use as key for example you use "papi chulo" as key when passing back result and use it on first screen

Kashif-E avatar Jan 09 '24 12:01 Kashif-E

@0xZhangKe it does not matter what you use as key for example you use "papi chulo" as key when passing back result and use it on first screen

Kashif-E avatar Jan 09 '24 12:01 Kashif-E

@Kashif-E This means that the first page must clearly know the Key of the second page. If the two Screens are in different modules and cannot reference each other, then this will become a bit troublesome. Generally speaking, this situation is It doesn't exist, but I'm currently encountering this problem. My two pages belong to two modules. The first page discovers the second page through routing, so I can only do this.

0xZhangKe avatar Jan 09 '24 12:01 0xZhangKe