https://kotlinlang.org logo
#compose
Title
# compose
i

Ian Warwick

02/01/2020, 10:03 AM
is
ContextAmbient
always activity? I need to get at the activity to silently get a
ViewModel
behind the scenes using
ViewModelProvider
Copy code
fun getModel(): MyViewModel {
        val context = ambient(ContextAmbient) as AppCompatActivity
        return ViewModelProvider(context).get(MyViewModel::class.java)
    }
k

Klaas Kabini

02/01/2020, 10:15 AM
If I still recall well, I think of the framework engineers at Google highlighted that is not a full activity context. It is only a lightweight context to allow accessing application resources such as strings, drawables, etc.
i

Ian Warwick

02/01/2020, 11:12 AM
cool thanks @Klaas Kabini it is the activity right now but since its
Context
it could change to anything
m

Manuel Wrage

02/01/2020, 11:29 AM
You could just create your own ViewModelStoreAmbient.
☝️ 3
i

Ian Warwick

02/01/2020, 2:47 PM
Its a good idea but at the moment for my little
Router
API I don't require the user to provide anything to do with view models or the activity and wanted to sneakily grab it behind the scenes 😄 the function I call
startComposing()
is a
@Composable
function and there I do that activity cast from
ContextAmbient
Its not such a crime to require the user to provide the activity really though just prefer one less arg and + 1 more magic. Is a
ActivityAmbient
something we want to avoid? It could be useful in some scenarios but I understand it could also be misused 🤔
a

Adam Powell

02/01/2020, 3:40 PM
just prefer one less arg and + 1 more magic
often you want the reverse of this for scalability, understandability, composability, probably lots of other -abilities 🙃 it helps when each component declares its dependencies explicitly and callers know what's in play and can influence things
i

Ian Warwick

02/01/2020, 4:26 PM
I guess so... though I think API developers break those rules quite often also as a trade off for convenience, Something like
val service = ambient(ServiceAmbient)
is already quite magical! 🙂 even so I do agree with your point completely since its just an extra argument for an object the caller has access to anyway
a

Adam Powell

02/01/2020, 4:35 PM
Every one of those we add is a debate 🙂
i

Ian Warwick

02/01/2020, 4:36 PM
ah! 🙂 I guess its just critical stuff? is
ambient
at risk of being removed?
a

Adam Powell

02/01/2020, 4:45 PM
the
ambient()
function has already been removed, but it was replaced with a
val
extension on the ambient types,
.current
I think? It's sticking around as a concept.
👍 2
it's an abstraction that is highly useful but it's very easy to overuse and make a mess with since it has that, "magic" property
If you're looking for some reading on the pros and cons of the pattern, take a look around the web for material on React's
Context
- many of the same ideas apply to Compose's
Ambient
i

Ian Warwick

02/01/2020, 4:49 PM
yeh defo a trade-off, most things like that as long as they don't make testing things difficult its ok to add magic providing its not to obscure
a

Adam Powell

02/01/2020, 4:49 PM
it's double-edged, and there was definitely a surge in posts and conference talks for a while in the spirit of, "we finally removed all of our Contexts from our app!" in that community after folks got bitten by the pitfalls
😆 1
i

Ian Warwick

02/01/2020, 4:50 PM
ah nice thanks 👍 will check that out I never used those frameworks so this style is totally new to me 🙂
a

Adam Powell

02/01/2020, 4:52 PM
it should be telling that react's official page on context starts with a big, "before you use this, try all of these other things first" section 🙂 https://reactjs.org/docs/context.html
😆 1
i

Ian Warwick

02/01/2020, 4:52 PM
nice was just reading that
a

Adam Powell

02/01/2020, 4:57 PM
in particular, Compose leans very heavily on lexical scoping for things like this. You can
remember {}
something in a
val
in a composable but use it several layers down the UI tree without those layers in between having to understand it or pass it as parameters
👍 2
You only need to think about things like ambients when that sort of structure isn't an option and you need to cross-cut a bit differently
👍 1
i

Ian Warwick

02/01/2020, 4:58 PM
Here is something I am doing in my screen
Copy code
val router = ambient(AmbientRouterContext)

    val cardActions = CardActions(
        editCard = { card ->
            router.goto(MemsetDestination.CardDesigner(card.uuid))
        })

    HomeScreenContent(state, cardActions)
Using this router basically sets a model property above this composable
HomeScreen
causing it to show another
XyzScreen
a

Adam Powell

02/01/2020, 5:00 PM
Right. So can your screens accept a
Router
? Can they accept an
onEditCard: (Uuid) -> Unit
event callback, so that the caller that does have a reference to the router can pass a lambda that makes the actual
router.goto
call?
👍 1
in particular, designing things like the latter suggestion and publishing higher-level event callbacks can make testability a breeze
👍 1
since you don't have to assert that your router did the right thing, you just assert that the composable published an instruction to navigate to the expected place
and if you jumped straight to ambients as the solution you'd miss out on all of those benefits of designing the abstraction a little bit differently
i

Ian Warwick

02/01/2020, 5:04 PM
yeh actually its a bit more complicated like that I had to come up with two themes, I can do this
Copy code
Button("Click Me", onClick = goto(MemsetDestination.HomeScreen))
and that goto function looks like this:
Copy code
@Composable
fun goto(destination: Destination, context: GotoContext.() -> Unit = { go() }) = goto(destination.uri, context)

@Composable
fun goto(uri: String, context: GotoContext.() -> Unit = { go() }): () -> Unit {
    val model = ambient(AmbientRouterContext)
    return {
        context(GotoContext(model, uri))
    }
}
I am going a bit nuts with it but its fun 😆 seriously cool stuff
and then the whole selecting a new screen is
Copy code
AmbientRouterContext.Provider(value = model) {
            state.currentUri.let {
                findMapping(it).invoke(it)
            }
        }
they are across a few classes
but I think this is quite cool as and end user
Copy code
Button("Click Me", onClick = goto(MemsetDestination.HomeScreen))
Another thing I guess from where I am looking at this is my top level is a composable function
Copy code
fun HomeScreen(repository: MemoryCardRepository = get()) {
(
get()
is just some poor mans DI) and I chose that to be the point that gets changed from one screen to the next the only thing retained above this is "what was the last screen the user was on" and when that
state.currentUri
changes it constructs a new screen.
got some 🤔 to do you make a good point if I did pass this Router into the compose function tree then it would be way easy to test 🤔
a

Adam Powell

02/01/2020, 5:15 PM
and even easier if you passed lambdas that receive info that can be used to construct a destination
i

Ian Warwick

02/01/2020, 5:20 PM
by pulling up navigation to top compose? I have been favouring passing down lambdas such as that
Copy code
val cardActions = CardActions(
        editCard = { card ->
            router.goto(MemsetDestination.CardDesigner(card.uuid))
        }

 HomeScreenContent(state, cardActions)
is that what you mean?
a

Adam Powell

02/01/2020, 5:21 PM
sure, that's one way. If you have an object full of those it starts looking like an interface real quick 😄
😆 1
i

Ian Warwick

02/01/2020, 5:21 PM
defo seems a good idea to make composable functions not rely on to many things and have callbacks into complex parts of the UI tree, I was passing down my model all the way before I decided to pass lambdas down instead 🙂
aha true man though the card editor does have a few functions 🙂 I will keep an eye on it
a

Adam Powell

02/01/2020, 5:22 PM
yeah, lambdas and interfaces make good black boxes for carrying logic down
👍 1
i

Ian Warwick

02/01/2020, 5:24 PM
do you prefer individual lambdas passed to a function or an interface full of lambdas? its hard to say specially with default args make it not so much of an issue with Kotlin... I did think about wether
CardActions
was a good idea over just individual lambda per action (only got two actions so far, edit and delete)
a

Adam Powell

02/01/2020, 5:26 PM
depends on how many required callbacks you need. If several semantically-related lambdas keep appearing in the same place and are never separated, that sounds like an interface.
👍 2
7 Views