androidfacebookandroid-jetpack-compose

Facebook login with Jetpack Compose


I started working on a new project that is 100% written with Jetpack compose, meaning we don't have any fragments and we're also following the Single activity pattern.

Now I have to implement the Facebook login but I'm stuck since they're still using the deprecated onActivityResult instead of the new contract api.

Here's the documentation that I'm trying to follow, any help would be greatly appreciated.

Thank you all,


Solution

  • You have to wait this issue to be resolved.

    For now you can pass callbackManager down from your activity to the Compose tree using CompositionLocalProvider, like this:

    val LocalFacebookCallbackManager =
        staticCompositionLocalOf<CallbackManager> { error("No CallbackManager provided") }
    
    class MainActivity : FragmentActivity() {
        private var callbackManager = CallbackManager.Factory.create();
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContent {
                Theme {
                    CompositionLocalProvider(
                        LocalFacebookCallbackManager provides callbackManager
                    ) {
                        LoginScreen()
                    }
                }
            }
        }
    
        override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
            callbackManager.onActivityResult(requestCode, resultCode, data)
            super.onActivityResult(requestCode, resultCode, data)
        }
    }
    
    @Composable
    fun LoginScreen() {
        val callbackManager = LocalFacebookCallbackManager.current
        DisposableEffect(Unit) {
            LoginManager.getInstance().registerCallback(
                callbackManager,
                object : FacebookCallback<LoginResult> {
                    override fun onSuccess(loginResult: LoginResult) {
                        println("onSuccess $loginResult")
                    }
    
                    override fun onCancel() {
                        println("onCancel")
                    }
    
                    override fun onError(exception: FacebookException) {
                        println("onError $exception")
                    }
                }
            )
            onDispose {
                LoginManager.getInstance().unregisterCallback(callbackManager)
            }
        }
        val context = LocalContext.current
        Button(onClick = {
            LoginManager.getInstance()
                .logInWithReadPermissions(context.findActivity(), Arrays.asList("public_profile"));
        }) {
            Text("FB Login")
        }
    }
    
    fun Context.findActivity(): Activity? = when (this) {
        is Activity -> this
        is ContextWrapper -> baseContext.findActivity()
        else -> null
    }
    

    More general solution is moving facebook logic into a view mode, and passing, then you have to create your own callback manager, something like this:

    ActivityResultCallbackManager.kt

    val LocalActivityResultCallbackManager =
        staticCompositionLocalOf<ActivityResultCallbackManager> { error("No ActivityResultCallbackManager provided") }
    
    interface ActivityResultCallbackI {
        fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean
    }
    
    class ActivityResultCallbackManager {
    
        private val listeners = mutableListOf<ActivityResultCallbackI>()
    
        fun addListener(listener : ActivityResultCallbackI) {
            listeners.add(listener)
        }
    
        fun removeListener(listener : ActivityResultCallbackI) {
            listeners.remove(listener)
        }
    
        fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) : Boolean =
            listeners.any { it.onActivityResult(requestCode, resultCode, data) }
    }
    

    MainActivity.kt

    class MainActivity : AppCompatActivity() {
        private var callbackManager = ActivityResultCallbackManager()
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
    
            WindowCompat.setDecorFitsSystemWindows(window, false)
            setContent {
                Theme {
                    CompositionLocalProvider(
                        LocalActivityResultCallbackManager provides callbackManager
                    ) {
                        LoginScreen()
                    }
                }
            }
        }
    
        override fun onActivityResult(
            requestCode: Int,
            resultCode: Int,
            data: Intent?
        ) {
            if (!callbackManager.onActivityResult(requestCode, resultCode, data)) {
                super.onActivityResult(requestCode, resultCode, data)
            }
        }
    }
    

    FacebookLoginViewModel.kt

    class FacebookLoginViewModel : ViewModel(), ActivityResultCallbackI {
        sealed class LoginState {
            object Initial: LoginState()
            object Processing: LoginState()
            data class Success(val loginResult: LoginResult): LoginState()
            data class Error(val exception: FacebookException): LoginState()
        }
    
        private var callbackManager = CallbackManager.Factory.create()
        var state by mutableStateOf<LoginState>(LoginState.Initial)
            private set
    
        init {
            LoginManager.getInstance().registerCallback(
                callbackManager,
                object : FacebookCallback<LoginResult> {
                    override fun onSuccess(loginResult: LoginResult) {
                        state = LoginState.Success(loginResult)
                    }
    
                    override fun onCancel() {
                        state = LoginState.Initial
                    }
    
                    override fun onError(exception: FacebookException) {
                        state = LoginState.Error(exception)
                    }
                }
            )
        }
    
        override fun onCleared() {
            super.onCleared()
            LoginManager.getInstance().unregisterCallback(callbackManager)
        }
    
        override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean =
            callbackManager.onActivityResult(requestCode, resultCode, data)
    
        fun login(context: Context) {
            state = LoginState.Processing
            LoginManager.getInstance()
                .logInWithReadPermissions(context.findActivity(), Arrays.asList("public_profile"));
        }
    }
    

    LoginScreen.kt

    @Composable
    fun LoginScreen() {
        val viewModel: FacebookLoginViewModel = viewModel()
        val callbackManager = LocalActivityResultCallbackManager.current
        DisposableEffect(Unit) {
            callbackManager.addListener(viewModel)
            onDispose {
                callbackManager.removeListener(viewModel)
            }
        }
        val context = LocalContext.current
        Column {
            Text(viewModel.state.toString())
            Button(onClick = {
                viewModel.login(context)
            }) {
                Text("FB Login")
            }
        }
    }
    

    Also you can try building this fork, it contains changes from this pull request. It adds support of contract api, and is not yet accepted. Check out changes carefully, it's not official!