How to get activity in compose - android

Is there a way to get current activity in compose function?
#Composable
fun CameraPreviewScreen() {
val context = ContextAmbient.current
if (ActivityCompat.checkSelfPermission(
context,
Manifest.permission.CAMERA
) != PackageManager.PERMISSION_GRANTED
) {
ActivityCompat.requestPermissions(
this, MainActivity.REQUIRED_PERMISSIONS, MainActivity.REQUEST_CODE_PERMISSIONS // get activity for `this`
)
return
}
}

While the previous answer (which is ContextWrapper-aware) is indeed the correct one,
I'd like to provide a more idiomatic implementation to copy-paste.
fun Context.getActivity(): AppCompatActivity? = when (this) {
is AppCompatActivity -> this
is ContextWrapper -> baseContext.getActivity()
else -> null
}
As ContextWrappers can't possibly wrap each other significant number of times, recursion is fine here.

You can get the activity from your composables casting the context (I haven't found a single case where the context wasn't the activity). However, has Jim mentioned, is not a good practice to do so.
val activity = LocalContext.current as Activity
Personally I use it when I'm just playing around some code that requires the activity (permissions is a good example) but once I've got it working, I simply move it to the activity and use parameters/callback.
Edit: As mentioned in the comments, using this in production code can be dangerous, as it can crash because current is a context wrapper, my suggestion is mostly for testing code.

To get the context
val context = LocalContext.current
Then get activity using the context. Create an extension function, and call this extension function with your context like context.getActivity().
fun Context.getActivity(): AppCompatActivity? {
var currentContext = this
while (currentContext is ContextWrapper) {
if (currentContext is AppCompatActivity) {
return currentContext
}
currentContext = currentContext.baseContext
}
return null
}

Rather than casting the Context to an Activity, you can safely use it by creating a LocalActivity.
val LocalActivity = staticCompositionLocalOf<ComponentActivity> {
noLocalProvidedFor("LocalActivity")
}
private fun noLocalProvidedFor(name: String): Nothing {
error("CompositionLocal $name not present")
}
Usage:
CompositionLocalProvider(LocalActivity provides this) {
val activity = LocalActivity.current
// your content
}

For requesting runtime permission in Jetpack Compose use Accompanist library: https://github.com/google/accompanist/tree/main/permissions
Usage example from docs:
#Composable
private fun FeatureThatRequiresCameraPermission(
navigateToSettingsScreen: () -> Unit
) {
// Track if the user doesn't want to see the rationale any more.
var doNotShowRationale by rememberSaveable { mutableStateOf(false) }
val cameraPermissionState = rememberPermissionState(android.Manifest.permission.CAMERA)
PermissionRequired(
permissionState = cameraPermissionState,
permissionNotGrantedContent = {
if (doNotShowRationale) {
Text("Feature not available")
} else {
Column {
Text("The camera is important for this app. Please grant the permission.")
Spacer(modifier = Modifier.height(8.dp))
Row {
Button(onClick = { cameraPermissionState.launchPermissionRequest() }) {
Text("Ok!")
}
Spacer(Modifier.width(8.dp))
Button(onClick = { doNotShowRationale = true }) {
Text("Nope")
}
}
}
}
},
permissionNotAvailableContent = {
Column {
Text(
"Camera permission denied. See this FAQ with information about why we " +
"need this permission. Please, grant us access on the Settings screen."
)
Spacer(modifier = Modifier.height(8.dp))
Button(onClick = navigateToSettingsScreen) {
Text("Open Settings")
}
}
}
) {
Text("Camera permission Granted")
}
}
Also, if you check the source, you will find out, that Google uses same workaround as provided by Rajeev answer, so Jim's answer about bad practice is somewhat disputable.

This extention function allows you to specify activity you want to get:
inline fun <reified Activity : ComponentActivity> Context.getActivity(): Activity? {
return when (this) {
is Activity -> this
else -> {
var context = this
while (context is ContextWrapper) {
context = context.baseContext
if (context is Activity) return context
}
null
}
}
}
Example:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent { HomeScreen() }
}
}
#Composable
fun HomeScreen() {
val activity = LocalContext.current.getActivity<MainActivity>()
}

Getting an activity from within a Composable function is considered a bad practice, as your composables should not be tightly coupled with the rest of your app. Among other things, a tight coupling will prevent you from unit-testing your composable and generally make reuse harder.
Looking at your code, it looks like you are requesting permissions from within the composable. Again, this is not something you want to be doing inside your composable, as composable functions can run as often as every frame, which means you would keep calling that function every frame.
Instead, setup your camera permissions in your activity, and pass down (via parameters) any information that is needed by your composable in order to render pixels.

Below is a slight modification to #Jeffset answer since Compose activities are based off of ComponentActivity and not AppCompatActivity.
fun Context.getActivity(): ComponentActivity? = when (this) {
is ComponentActivity -> this
is ContextWrapper -> baseContext.findActivity()
else -> null
}

I actually found this really cool extension function inside the accompanist
library to do this:
internal fun Context.findActivity(): Activity {
var context = this
while (context is ContextWrapper) {
if (context is Activity) return context
context = context.baseContext
}
throw IllegalStateException("Permissions should be called in the context of an Activity")
}
which gets used inside a composable function like this:
#Composable
fun composableFunc(){
val context = LocalContext.current
val activity = context.findActivity()
}

Related

The best solution for the main thread to obtain data in the coroutine to avoid null values?

I have the following code, in the click event I need to use the class that is queried from the database.
data class Work(...)
fun Compose(){
var work1: Work? = null
var work2: Work? = null
var work3: Work? = null
LaunchedEffect(true){
CoroutineScope(IO).launch {
work1 = workViewModel.getById(1)
work2 = workViewModel.getById(2)
work3 = workViewModel.getById(3)
}
}
Card(
modifier = Modifier
.clickable(onClick = {
val url = "https://www.google.com/"
when{
url.contains(work1?.baseUrl) -> {...}
url.contains(work2?.baseUrl) -> {...}
url.contains(work3?.baseUrl) -> {...}
}
})
){}
}
this creates a problem, work3?.baseUrl found String? type Required CharSequence type.
So far it seems that only the !! operator can successfully run this code. But this code is based on a database query, using the !! operator is very risky.
And if you add a null operator before this, also not working.
requireNotNull(work1)
when{
url.contains(work1.baseUrl) -> {...}
}
Smart cast to 'Work' is impossible, because 'work1' is a local variable that is captured by a changing closure
Can you tell me what is the best solution?
I suggest not having that logic in the Composable. Try to move that to a function in the ViewModel, something like:
private val url = "https://www.google.com/"
fun validateBaseUrl() {
viewModelScope.launch(Dispatchers.IO) {
workViewModel.getById(1)?.let {
if (url.contains(it)) { .. }
}
work2 = workViewModel.getById(2)?.let {
if (url.contains(it)) { .. }
}
work3 = workViewModel.getById(3)?.let {
if (url.contains(it)) { .. }
}
}
}
And then in the Composable would be something like:
fun Compose() {
Card(
modifier = Modifier
.clickable(onClick = { viewModel.validateBaseUrl() })
){}
}
Remember to use the State Hoisting instead of sending the view model through the Composables.
Finally, you would need to send back a State to the Composable, either using a LiveData or a StateFlow.
You need to create a scope for the work properties :
work1?.run { url.contains(baseUrl) } == true -> {...}
Within the run lambda the accessed object is immutable even if the work properties themselves are mutable.
The == true is needed because the left side of the comparison operator can be either a Boolean or null.
You could also define an extension function like this:
fun Work?.isContainedIn(url: String) = this?.run { url.contains(baseUrl) } == true
and then just do:
work1.isContainedIn(url) -> { }
work2.isContainedIn(url) -> { }
work3.isContainedIn(url) -> { }

Android login/logout flow with the new SplashScreen API

I'm using the new SplashScreen API for android 12 but I'm a bit confused on the login flow now. I have one activity and multiple fragments as advised by google , the mainActivity is where the splashScreen launches and the user's supposed to be directed to either login fragment or the homefragment.
my question is how do I implement this work flow with the new SplashAPI? What fragment should be the startDestination? I don't want to use the popTo attribute since it doesn't look pretty to always show the loginfragment and then direct the user to Homefragment.
If someone could explain this to me I'd be grateful.
Homefragment should be the startDestination. Conditionally navigate to loginfragment and pop back to Homefragment after authentication.
Refer to the following video by Ian Lake.
https://www.youtube.com/watch?v=09qjn706ITA
I have a workaround for that you can set the content of the activity after you check if the user is authorized or not and save the user state in a ViewModel or something, then set your content upon this state.
I leverage the power of setKeepOnScreenCondition function from core-splashscreen library.
SplashInstaller.kt
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
SplashInstaller(
activity = this,
beforeHide = {
// Check if logged in or not asyncrounsly
delay(2000)
},
afterHide = {
setContent()
// start with you desired destination up on the check you did in beforeHide
})
}
/**
* #author Mohamed Elshaarawy on Oct 14, 2021.
*/
class SplashInstaller<A : ComponentActivity>(
private val activity: A,
visibilityPredicate: () -> Boolean = { BuildConfig.BUILD_TYPE != "debug" },
private val beforeHide: suspend A.() -> Unit = { delay(2000) },
private val afterHide: A.() -> Unit
) : CoroutineScope by activity.lifecycleScope {
private val isSplashVisibleChannel by lazy { Channel<Boolean>() }
private val isAfterCalled by lazy { Channel<Boolean>(capacity = 1) }
private val splashSuspensionJob = launch(start = CoroutineStart.LAZY) {
activity.beforeHide()
isSplashVisibleChannel.send(false)
}
init {
if (visibilityPredicate()) {
splashSuspensionJob.start()
installSplash()
} else afterSplash()
}
private fun installSplash() {
activity.installSplashScreen().setKeepOnScreenCondition {
val isVisible = isSplashVisibleChannel.tryReceive().getOrNull() ?: true
if (!isVisible) {
afterSplash()
}
isVisible
}
}
private fun afterSplash() {
if (isAfterCalled.tryReceive().getOrNull() != true) {
isAfterCalled.trySend(true)
activity.afterHide()
}
}
}
This solution uses
androidx.core:core-splashscreen:1.0.0-beta01
androidx.lifecycle:lifecycle-runtime-ktx:2.4.0
org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2

Android Permissions Helper Functions

I have an activity that requires camera permission.
this activity can be called from several user configurable places in the app.
The rationale dialog and permission dialog themselves should be shown before the activity opens.
right now I am trying to handle these dialogs in some kind of extension function.
fun handlePermissions(context: Context, required_permissions: Array<String>, activity: FragmentActivity?, fragment: Fragment?): Boolean {
var isGranted = allPermissionsGranted(context, required_permissions)
if (!isGranted) {
//null here is where I used to pass my listener which was the calling fragment previously that implemented an interface
val dialog = DialogPermissionFragment(null, DialogPermissionFragment.PermissionType.QR)
activity?.supportFragmentManager?.let { dialog.show(it, "") }
//get result from dialog? how?
//if accepted launch actual permission request
fragment?.registerForActivityResult(ActivityResultContracts.RequestPermission()) { success ->
isGranted = success
}?.launch(android.Manifest.permission.CAMERA)
}
return isGranted
}
But I am having trouble to get the dialog results back from the rationale/explanation dialog.
My research until now brought me to maybe using a higher order function, to pass a function variable to the dialog fragment that returns a Boolean value to the helper function. But I am absolutely unsure if thats the right thing.
I thought using my own code would be cleaner and less overhead, could I achieve this easier when using a framework like eazy-permissions? (is Dexter still recommendable since its no longer under development)
is that even a viable thing I am trying to achieve here?
One approach that I've implemented and seems viable to use is this:
Class PermissionsHelper
class PermissionsHelper(private val activity: Activity) {
fun requestPermissionsFromDevice(
arrayPermissions: Array<String>, permissionsResultInterface: PermissionsResultInterface
) {
setPermissionResultInterface(permissionsResultInterface)
getMyPermissionRequestActivity().launch(arrayPermissions)
}
fun checkPermissionsFromDevice(permission: String): Boolean {
return ContextCompat.checkSelfPermission(activity, permission) ==
PackageManager.PERMISSION_GRANTED
}
}
Class PermissionsResultInterface to the helper class be able to communicate with the activity.
interface PermissionsResultInterface {
fun onPermissionFinishResult(permissions: MutableMap<String, Boolean>)
}
Usage with this approach to remove all files from app directory:
private fun clearFiles(firstCall: Boolean = false) {
if (verifyStoragePermissions(firstCall)) {
val dir = File(getExternalFilesDir(null).toString())
removeFileOrDirectory(dir)
Toast.makeText(
applicationContext,
resources.getString(R.string.done),
Toast.LENGTH_SHORT
).show()
}
}
private fun verifyStoragePermissions(firstCall: Boolean = false): Boolean {
val arrayListPermissions = arrayOf(
android.Manifest.permission.READ_EXTERNAL_STORAGE,
android.Manifest.permission.WRITE_EXTERNAL_STORAGE
)
for (permission in arrayListPermissions) {
if (!PermissionsHelper(this).checkPermissionsFromDevice(permission)) {
if (firstCall) PermissionsHelper(this)
.requestPermissionsFromDevice(arrayListPermissions, this)
else PermissionsDialogs(this).showPermissionsErrorDialog()
return false
}
}
return true
}
override fun onPermissionFinishResult(permissions: MutableMap<String, Boolean>) {
clearFiles()
}
With this approach you are able to call the permissions helper and using the result interface, after each of the answers from user, decide wether you still need to make a call for permissions or show a dialog to him.
If you need any help don't hesitate to contact me.

End flow/coroutines task before go further null issue

Fragment
private fun makeApiRequest() {
vm.getRandomPicture()
var pictureElement = vm.setRandomPicture()
GlobalScope.launch(Dispatchers.Main) {
// what about internet
if (pictureElement != null && pictureElement!!.fileSizeBytes!! < 400000) {
Glide.with(requireContext()).load(pictureElement!!.url)
.into(layout.ivRandomPicture)
layout.ivRandomPicture.visibility = View.VISIBLE
} else {
getRandomPicture()
}
}
}
viewmodel
fun getRandomPicture() {
viewModelScope.launch {
getRandomPictureItemUseCase.build(Unit).collect {
pictureElement.value = it
Log.d("inspirationquotes", "VIEWMODEL $pictureElement")
Log.d("inspirationquotes", "VIEWMODEL VALUE ${pictureElement.value}")
}
}
}
fun setRandomPicture(): InspirationQuotesDetailsResponse? {
return pictureElement.value
}
Flow UseCase
class GetRandomPictureItemUseCase #Inject constructor(private val api: InspirationQuotesApi): BaseFlowUseCase<Unit, InspirationQuotesDetailsResponse>() {
override fun create(params: Unit): Flow<InspirationQuotesDetailsResponse> {
return flow{
emit(api.getRandomPicture())
}
}
}
My flow task from viewmodel doesn't goes on time. I do not know how to achieve smooth downloading data from Api and provide it further.
I was reading I could use runBlocking, but it is not recommended in production as well.
What do you use in your professional applications to achieve nice app?
Now the effect is that that image doesn't load or I have null error beacause of my Log.d before GlobalScope in Fragment (it is not in code right now).
One more thing is definding null object I do not like it, what do you think?
var pictureElement = MutableStateFlow<InspirationQuotesDetailsResponse?>(null)
EDIT:
Viewmodel
val randomPicture: Flow<InspirationQuotesDetailsResponse> = getRandomPictureItemUseCase.build(Unit)
fragment
private fun makeApiRequest() = lifecycleScope.launch {
vm.randomPicture
.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED)
.collect { response ->
if (response.fileSizeBytes < 600000) {
Log.d("fragment", "itGetsValue")
Glide.with(requireContext()).load(response.url)
.into(layout.ivRandomPicture)
layout.ivRandomPicture.visibility = View.VISIBLE
} else {
onFloatingActionClick()
}
}
}
Edit2 problem on production, another topic:
Link -> What is the substitute for runBlocking Coroutines in fragments and activities?
First of all, don't use GlobalScope to launch a coroutine, it is highly discouraged and prone to bugs. Use provided lifecycleScope in Fragment:
lifecycleScope.launch {...}
Use MutableSharedFlow instead of MutableStateFlow, MutableSharedFlow doesn't require initial value, and you can get rid of nullable generic type:
val pictureElement = MutableSharedFlow<InspirationQuotesDetailsResponse>()
But I guess we can get rid of it.
Method create() in GetRandomPictureItemUseCase returns a Flow that emits only one value, does it really need to be Flow, or it can be just a simple suspend function?
Assuming we stick to Flow in GetRandomPictureItemUseCase class, ViewModel can look something like the following:
val randomPicture: Flow<InspirationQuotesDetailsResponse> = getRandomPictureItemUseCase.build(Unit)
And in the Fragment:
private fun makeApiRequest() = lifecycleScope.launch {
vm.randomPicture
.flowWithLifecycle(lifecycle, State.STARTED)
.collect { response ->
// .. use response
}
}
Dependency to use lifecycleScope:
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.0'

rememberLauncherForActivityResult throws Launcher has not been initialized in Jetpack Compose

When trying to invoke the Firebase Auth UI, using the below code the compiler throws java.lang.IllegalStateException: Launcher has not been initialized. Not sure, why the launcher is not initialized
#Composable
internal fun ProfileUI(profileViewModel: ProfileViewModel) {
val loginLauncher = rememberLauncherForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
if (result != null) {
//do something
}
}
if (profileViewModel.isAnonymousUser) {
loginLauncher.launch(profileViewModel.buildLoginIntent())
} else {
}
}
override fun buildLoginIntent(): Intent {
val authUILayout = AuthMethodPickerLayout.Builder(R.layout.auth_ui)
.setGoogleButtonId(R.id.btn_gmail)
.setEmailButtonId(R.id.btn_email)
.build()
return AuthUI.getInstance().createSignInIntentBuilder()
.setIsSmartLockEnabled(!BuildConfig.DEBUG)
.setAvailableProviders(
listOf(
AuthUI.IdpConfig.EmailBuilder().build(),
AuthUI.IdpConfig.GoogleBuilder().build()
)
)
.enableAnonymousUsersAutoUpgrade()
.setLogo(R.mipmap.ic_launcher)
.setAuthMethodPickerLayout(authUILayout)
.build()
}
java.lang.IllegalStateException: Launcher has not been initialized
at androidx.activity.compose.ActivityResultLauncherHolder.launch(ActivityResultRegistry.kt:153)
at androidx.activity.compose.ManagedActivityResultLauncher.launch(ActivityResultRegistry.kt:142)
at androidx.activity.result.ActivityResultLauncher.launch(ActivityResultLauncher.java:47)
at com.madhu.locationbuddy.profile.ProfileUIKt.ProfileUI(ProfileUI.kt:37)
at com.madhu.locationbuddy.profile.ProfileUIKt.ProfileUI(ProfileUI.kt:15)
Any ideas on how to resolve this issue?
As per the Side-effects in Compose documentation:
Composables should be side-effect free.
Key Term: A side-effect is a change to the state of the app that happens outside the scope of a composable function.
Launching another activity, such as calling launch, is absolutely a side effect and therefore should never be done as part of the composition itself.
Instead, you should put your call to launch within one of the Effect APIs, such as SideEffect (if you want it to run on every composition) or LaunchedEffect (which only runs when the input changes - that would be appropriate if profileViewModel.isAnonymousUser was being driven by a mutableStateOf()).
Therefore your code could be changed to:
internal fun ProfileUI(profileViewModel: ProfileViewModel) {
val loginLauncher = rememberLauncherForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
if (result != null) {
//do something
}
}
if (profileViewModel.isAnonymousUser) {
SideEffect {
loginLauncher.launch(profileViewModel.buildLoginIntent())
}
} else {
// Output your UI, etc.
}
}

Categories

Resources