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

galex

07/23/2020, 2:58 PM
What about Hilt ? 😄 (dislaimer: didn’t try it yet, will do soon)
a

Adam Powell

07/23/2020, 3:05 PM
Try it with the new compiler in dev15, we had some kapt incompatibilities before but I don't think we've done extensive testing with hilt/dagger on dev15 yet
👍 1
g

galex

07/23/2020, 3:10 PM
Can I inject something as parameter of a Composable?
a

Adam Powell

07/23/2020, 3:39 PM
No, but you can inject a class that has composable methods.
z

Zach Klippenstein (he/him) [MOD]

07/23/2020, 3:45 PM
There's a discussion about this every couple weeks in this channel. Here was a recent one: https://kotlinlang.slack.com/archives/CJLTWPH7S/p1594338382091800?thread_ts=1594338382.091800&cid=CJLTWPH7S
🙏 1
g

galex

07/23/2020, 5:08 PM
Interesting read, thanks @Zach Klippenstein (he/him) [MOD]. I’ve read the Ambiant doc and it seems to be what I need. Access to general information without having to pass it by parameter for each level of the tree. Sweet!
z

Zac Sweers

07/23/2020, 5:09 PM
😂 2
1
g

galex

07/23/2020, 5:09 PM
Sorry about that @Zac Sweers 😂
a

Adam Powell

07/23/2020, 5:21 PM
Ambients are also one of those, "use with caution" kinds of APIs. It's very, very easy to go overboard with them and in a great many cases you don't need them thanks to lexical scoping giving you access to things across the tree in ways that aren't possible with view xml, so it's not always the first thing that springs to mind.
😨 1
r

Ricardo C.

07/23/2020, 5:43 PM
Ambients just feel like the service locator approach. And passing stuff through params like regular constructor dependencies. But there’s a point that some composable will have to act as a bridge to the rest of the app. I’m thinking in some sort of screen composable that depends on a presenter. But idk, I’m afraid that I’m too blinded by the current approaches.
g

galex

07/23/2020, 7:53 PM
For example, I would like to use Google Sign In, which is really tied to an activity or a Fragment. We are in a Single Activity App so everything goes into the Activity. I am providing already 2 ambiants so that I can show a button and click in it in our of my “screens”, a composable function because I want there to start the flow:
Copy code
override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val gso = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
            .requestIdToken(FIREBASE_CLIENT_ID)
            .requestEmail()
            .requestProfile()
            .requestId()
            .build()

        googleSignInClient = GoogleSignIn.getClient(this, gso)
        val account = GoogleSignIn.getLastSignedInAccount(this)
        Log.d(TAG, "account = $account")

        auth = Firebase.auth

        val googleSignIn: ActivityResultLauncher<GoogleSignInClient> = registerForActivityResult(GoogleSignInContract()) { googleSignInAccount ->
            googleSignInAccount?.let { firebaseAuthWithGoogle(it) }
        }

        setContent {
            Providers(GoogleSignInAmbient provides googleSignIn, GoogleSignInClientAmbient provides googleSignInClient) {
                MainScreen(navigationViewModel)
            }
        }
    }
Here is the Composable using those ambients:
Copy code
@Composable
fun ProfileScreen() {
    
    val googleSignInResultLauncher = GoogleSignInAmbient.current
    val googleSignInClient = GoogleSignInClientAmbient.current
    
    Column {
        val auth = Firebase.auth
        Text("See all your stuff here, dear ${auth.currentUser?.displayName ?: "John Doe"}")
        TextButton(onClick = { googleSignInResultLauncher.launch(googleSignInClient) }) {
            Text("Sign In with Google!")
        }
    }
}
Is that what I am supposed to do for this? Feels somewhat wrong 😕 I don’t want to pass those objects through all the hierarchy, who knows what else I’ll have to pass afterwards
And to be fair, I have a concern that the Activity will turn in a God Object if I have to add more frameworks after this one
r

Ricardo C.

07/23/2020, 8:03 PM
Copy code
interface Controller {
    fun onActive() {}
    fun onDispose() {}
}

@Composable
fun <C : Controller> Screen(
    controller: C,
    children: @Composable (C) -> Unit
) {
    onActive {
        controller.onActive()
    }
    onDispose {
        controller.onDispose()
    }
    children(controller)
}

class BrowseController @Inject constructor(
    private val getSubredditPosts: GetSubredditPosts
) : Controller {
    private val _subreddit = MutableStateFlow("androiddev")
    val subreddit: StateFlow<String>
        get() = _subreddit

    val posts: Flow<List<Post>>
        get() = subreddit.map {
            val params = GetSubredditPosts.Params(subreddit = it)
            getSubredditPosts(params).data
        }
}

@Composable
fun BrowseScreen(controller: BrowseController) {
    Screen(controller) {
        val subreddit = controller.subreddit.collectAsState()
        val posts = controller.posts.collectAsState(emptyList())

        Column {
            Text(
                text = "Posts for ${subreddit.value}",
                modifier = Modifier.fillMaxWidth()
                    .drawBackground(purple200)
                    .gravity(Alignment.CenterHorizontally)
                    .padding(8.dp)
            )
            ScrollableColumn {
                posts.value.forEach { post ->
                    Post(post)
                }
            }
        }
    }
}
I'm trying something alone those lines. I inject the controller and pass it to the screen composable. I'm not expecting the smaller composables to have access to more than data. Only the root one for the screen
👌 1
z

Zach Klippenstein (he/him) [MOD]

07/23/2020, 8:18 PM
Copy code
fun <C : Controller> Screen(
    controller: C,
    children: @Composable (C) -> Unit
) {
Why does
children
take
C
as an argument? The caller, who’s already passing the controller to this function, can just reference the same object in the lambda it also passes.
r

Ricardo C.

07/23/2020, 8:19 PM
That's true. I'm still messing around with this to see what would be something clean to use 😄
z

Zach Klippenstein (he/him) [MOD]

07/23/2020, 8:19 PM
Copy code
onActive {
        controller.onActive()
    }
    onDispose {
        controller.onDispose()
    }
If your
Controller
class implements
CompositionLifecycleObserver
, then these lifecycle methods will automatically be called from wherever the
Controller
is `remember`ed, if you’re using
remember
to store it. E.g.
Copy code
class Controller : CompositionLifecycleObserver {
  override fun onEnter { … }
  override fun onLeave { … }
}

@Composable fun Main() {
  val controller = remember { Controller() }
  …
}
But if the point of
Controller
is to be provided by an
Activity
, then this wouldn’t be useful.
r

Ricardo C.

07/23/2020, 8:24 PM
Not sure if I would like that tbh. I think it would make more sense to keep the controller not dependant on compose. It should always come from outside and be injected. Right now I'm injecting in the activity but a provider + multibinds with the current backstack screen could work
👍 1
I want to make everything as detached as possible where compose is just the way we render the state. Still trying to figure everything out though...
g

galex

07/24/2020, 2:14 PM
The issue here with Google Sign In is that I am tied to the Results API of the Activity, no way around this sadly
a

Adam Powell

07/24/2020, 2:24 PM
We've got a few snippets and gists floating around that hook into the new activity result jetpack library from a composable, that might help out here
😱 1
g

galex

07/24/2020, 3:04 PM
Sounds great 😮
@Adam Powell Here’s a gist adapting your code to GoogleSignIn API https://gist.github.com/galex/8690cf881ae0f1d291df91b0f1fae3e8, WDYT? It also contains a question, as I am not sure how to integrate code with callbacks into Composables functions, do you know what I should do in those cases?
a

Adam Powell

07/24/2020, 3:45 PM
A lot of the standard coroutines guidelines apply here, especially around scoping of operations
if you do the usual conversion of the callback to a suspend function by way of
suspendCancellableCoroutine
, you can use the scope returned by
rememberCoroutineScope()
to launch a call to it in a user action event handler
and it'll cancel if the
rememberCoroutineScope
call leaves the composition
if you have something more actor-like then
launchInComposition
might be a more appropriate place to do things
if you want the login attempt in progress to survive beyond the user navigating away, then you'll want to
async
the login attempt on a different, wider scope and
await
the result in the composition scope somewhere
but some of this is business logic decisions that determine which best practice to use
g

galex

07/24/2020, 4:17 PM
Thank you, I'll check all that
Wow! It works! It does mean we’ll need a lot of that kind of boilerplate for using 3rd party libraries that are not themselves “converted” to Compose 😮
Copy code
package il.co.galex.alexpizzapp.utils

import android.content.Context
import android.content.ContextWrapper
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.ActivityResultRegistry
import androidx.activity.result.contract.ActivityResultContract
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.*
import androidx.ui.core.ContextAmbient
import com.google.android.gms.auth.api.signin.GoogleSignInAccount
import com.google.android.gms.auth.api.signin.GoogleSignInClient
import com.google.firebase.auth.GoogleAuthProvider
import com.google.firebase.auth.ktx.auth
import com.google.firebase.ktx.Firebase
import il.co.galex.alexpizzapp.feature.user.UserRepository
import il.co.galex.alexpizzapp.model.User
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine

@OptIn(ExperimentalComposeApi::class)
@Composable
fun <I, O> ActivityResultRegistry.activityResultLauncher(
    requestContract: ActivityResultContract<I, O>,
    onResult: (O) -> Unit
): ActivityResultLauncher<I> {

    val key = currentComposer.currentCompoundKeyHash.toString()
    val launcher = remember(requestContract, onResult) {
        register(key, requestContract, onResult)
    }
    onDispose {
        launcher.unregister()
    }
    return launcher
}

fun Context.findActivity(): AppCompatActivity? {
    var context = this
    while (context is ContextWrapper) {
        if (context is AppCompatActivity) return context
        context = context.baseContext
    }
    return null
}

class GoogleSignInState(
    private val googleSignInClient: GoogleSignInClient,
    val signInState: State<User?>,
    private val launcher: ActivityResultLauncher<GoogleSignInClient>
) {

    fun launchSignInRequest() = launcher.launch(googleSignInClient)
}

@Composable
fun ActivityResultRegistry.googleSignIn(googleSignInClient: GoogleSignInClient): GoogleSignInState {
    val context = ContextAmbient.current
    //val activity = context.findActivity()
    val signInState = mutableStateOf<User?>(null)

    val launcher = activityResultLauncher(GoogleSignInContract()) {
        GlobalScope.launch {
            signInState.value = firebaseAuthWithGoogle(it!!)
        }
    }

    return remember(launcher) {
        GoogleSignInState(googleSignInClient, signInState, launcher)
    }
}

suspend fun firebaseAuthWithGoogle(account: GoogleSignInAccount): User? {

    val credential = GoogleAuthProvider.getCredential(account.idToken, null)

    return suspendCoroutine { continuation ->

        Firebase.auth.signInWithCredential(credential).addOnCompleteListener { task ->

            if (task.isSuccessful) {
                // Sign in success, update UI with the signed-in user's information
                val firebaseUser = Firebase.auth.currentUser

                val repo = UserRepository()
                account.toUser(firebaseUser!!.uid).let { user ->
                    repo.add(user) {
                        // TODO something nice to show to the user when he is officially in the Firebase User table
                        continuation.resume(user)
                    }
                }

            } else {
                // If sign in fails, display a message to the user.
                continuation.resume(null)
            }

        }
    }
}
I’ll improve this code later, but that seems like a great start !! Thanks so much @Adam Powell :))
i

Ian Lake

08/09/2020, 12:59 AM
You might look at the Coroutine extensions on Play Service's Task API to offer a suspending `await`: https://github.com/Kotlin/kotlinx.coroutines/tree/master/integration/kotlinx-coroutines-play-services
😱 3
🔥 2
That would remove the entire
addOnCompletionListener
necessity
b

brandonmcansh

08/09/2020, 1:00 AM
Oh nifty. I'll def check this out. Thanks @Ian Lake!