registerForActivityResult() outside onCreate() with Compose - android

So I am trying to launch the intent Intent.ACTION_OPEN_DOCUMENT. I first tried with startActivityForResult but I noticed it was depreciated so I tried to find another way to do this. So I found the registerForActivityResult method but it turns out it must run after onCreate() has finished :
Note: While it is safe to call registerForActivityResult() before your fragment or activity is created, you cannot launch the ActivityResultLauncher until the fragment or activity's Lifecycle has reached CREATED.
Since I am using Jetpack Compose and setContent is in onCreate() my Activity has actually never finished creating because all my Composables functions are run in the setContent of my MainActivity
So how can I achieve this ?

Using the latest version of activity-compose you can use rememberLauncherForActivityResult() to register a request to Activity#startActivityForResult.
Something like:
val result = remember { mutableStateOf<Uri?>(null) }
val launcher = rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) {
result.value = it
}
Button(onClick = { launcher.launch(arrayOf("application/pdf")) }) {
Text(text = "Open Document")
}
result.value?.let {
//...
}

Related

How to handle callbacks in Jetpack compose?

I am migrating my multiple activity app to single activity app.
In the activity I am observing a live data from view model. When the observable triggers, I start a payment activity from a third party SDK as shown below.
onCreate() {
viewmodel.orderCreation.observe {
thirdpartysdk.startPaymentWithThisOrder(context)
}
}
onActivityResult() {
// use payment result
}
As I will be using a Composable now,
#Composable
fun PaymentScreen(onOrderCreated: () -> Unit) {
val orderCreation by viewmodel.orderCreation.observeAsState()
// How to use order creation once here to call onOrderCreated here only once as composable is called again and again
}
Here's my suggestion:
In your viewmodel, create a function to reset your orderCreation. And another field + function to store the payment result.
Something like:
fun resetOrderCreation() {
_orderCreation.value = null
}
fun paymentResult(value: SomeType) {
_paymentResult.value = value
}
Now, in your composable, you can do the following:
#Composable
fun PaymentScreen(onOrderCreated: () -> Unit) {
// 1
val orderCreation by viewmodel.orderCreation.observeAsState()
var paymentResult by viewmodel.paymentResult.observeAsState()
// 2
val launcher = rememberLauncherForActivityResult(
PaymentActivityResultContract()
) { result ->
viewModel.paymentResult(result)
}
...
// 3
LaunchedEffect(orderCreation) {
if (orderCreation != null) {
launcher.launch()
viewModel.resetOrderCreation()
}
}
// 4
if (paymentStatus != null) {
// Show some UI showing the payment status
}
}
Explaining the code:
I'm assuming that you're using LiveData. But I really suggest you move to StateFlow instead. See more here.
You will probably need to write a ActivityResultContact to your third party lib. I wrote a post about (it's in Portuguese, but I think you can get the idea translating it to English).
As soon the orderCreation has changed, the LaunchedEffect block will run, then you can start the third party activity using launcher.launch() (the parameters for this call are defined in your ActivityResultContract).
Finally, when the payment status changed, you can show something different to the user.

How to Pass finish() from Activity class as a function parameter (So the code can be reused in Kotlin)?

I have to use this peice of code twice in two different places in two different activites. No good programmer would willingly want to use same code in multiple places without reusing it.
//when back key is pressed
override fun onBackPressed() {
dialog.setContentView(twoBtnDialog.root)
twoBtnDialog.title.text = getString(R.string.warning)
twoBtnDialog.msgDialog.text = getString(R.string.backPressWarning)
twoBtnDialog.ok.text = getString(R.string.exit)
twoBtnDialog.cancel.text = getString(R.string.cancel)
twoBtnDialog.ok.setOnClickListener {
//do nav back
finish()
dialog.dismiss()
}
twoBtnDialog.cancel.setOnClickListener {
dialog.dismiss() //just do nothing
}
dialog.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
dialog.show()
}
I can move it to one place, but the problem is I have to pass in the finish() function from Activity Class to close the calling activity.
My simple question is how can I resue it ? Or How can I pass this function (finish()) to a different class (which is in some other file).
Take a function type parameter in your method.
fun doBackPress(finish: () -> Unit) {
// you need to invoke the finish method when necessary
finish.invoke()
}
Then you need to call the method and have to pass the finish() method from any other activity or fragment method like bellow.
override fun onBackPressed() {
doBackPress { finish() }
}
You could make an interface and extension function, which I think is less messy than trying to pass everything you need as parameters to a function, because it communicates intent better and makes it harder to do something wrong.
interface MyDialogOwner {
val dialog: Dialog
val twoBtnDialog: MyDialogBinding
fun Activity.handleBackPress() {
//the exact same content you have in your function now.
}
}
// In Activity:
override fun onBackPressed() = handleBackPress()
Your Activities should implement the interface, using your existing properties for dialog and twoBtnDialog (just add override in front of their declarations).
I'm assuming twoBtnDialog is a view binding.

Could registerForActivityResult be used in a coroutine?

The problem with this as I see it is that you have to guarantee that registerForActivityResult() is called before your own activity's OnCreate() completes. OnCreate() is obviously not a suspending function, so I can't wrap registerForActivityResult() and ActivityResultLauncher.launch() in a suspendCoroutine{} to wait for the callback, as I can't launch the suspendCoroutine from OnCreate and wait for it to finish before letting OnCreate complete...
...which I did think I might be able to do using runBlocking{}, but I have found that invoking runBlocking inside OnCreate causes the app to hang forever without ever running the code inside the runBlocking{} block.
So my question is whether runBlocking{} is the correct answer but I am using it wrong, or whether there is some other way to use registerForActivityResult() in a coroutine, or whether it is simply not possible at all.
You can do something like this.
Please refer to the implementation below.
class RequestPermission(activity: ComponentActivity) {
private var requestPermissionContinuation: CancellableContinuation<Boolean>? = null
#SuppressLint("MissingPermission")
private val requestFineLocationPermissionLauncher =
activity.registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
requestPermissionContinuation?.resumeWith(Result.success(isGranted))
}
suspend operator fun invoke(permission: String) = suspendCancellableCoroutine<Boolean> { continuation ->
requestPermissionContinuation = continuation
requestFineLocationPermissionLauncher.launch(permission)
continuation.invokeOnCancellation {
requestPermissionContinuation = null
}
}
}
Make sure you initialize this class before onStart of the activity. registerForActivityResult API should be called before onStart of the activity. Refer to the sample below
class SampleActivity : AppCompatActivity() {
val requestPermission: RequestPermission = RequestPermission(this)
override fun onResume() {
super.onResume()
lifecycleScope.launch {
val isGranted = requestPermission(Manifest.permission.ACCESS_FINE_LOCATION)
//Do your actions here
}
}
}

How to start an activity from flutter plugin using an API

So I am making a Flutter plugin and I am attempting to run Kotlin code on Android. The problem is, this code runs a method which attempts to start an activity without the FLAG_ACTIVITY_NEW_TASK flag on the intent. The problem with this is that it also does NOT have a way to give it an intent instance as it attempts to instantiate an instance inside the method itself. The method expects to be called from a button or other method that is stored on the UI and called from it. However, since it is called from the onMethodCall method in the Flutter plugin, it does not seem to work. I have attempted many workarounds such as adding a method inside the Activity and running the code inside while calling it from the flutter plugin class. I have also tried using the UIThread and no luck either. Any workarounds?
Note: I have not provided any code due to keeping this API hidden. It should only be known that I am running the code from the onMethodCall event.
Error: Calling startActivity() from outside of an Activity context requires the FLAG_ACTIVITY_NEW_TASK flag. Is this really what you want?
You can extend your plugin to implement ActivityAware in your plugin class, when you implement it, you get a couple of callbacks that gives you the current activity. Like this :
lateinit activity: Activity? = null
override fun onDetachedFromActivity() {
activity = null
}
override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
activity = binding.activity
}
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
activity = binding.activity
}
override fun onDetachedFromActivityForConfigChanges() {
activity = null
}
After that you can just startActivity from the assigned activity variable.
Let me know if you need further help.
As you mentioned, For Flutter plugin any platform-dependent logics should be kept in the subclass of FlutterActivity which was used to show flutter module/screens inside a native module. Now you can launch intent from that subclass without any additional flags.
#note - Subclass of FlutterActvity should be kept in the native module.
class FlutterResponseActivity : FlutterActivity() {
private var methodResult: Result? = null
override fun provideFlutterEngine(context: Context): FlutterEngine? {
return MyApplication.mContext.flutterEngine //Pre-warmed flutter engine
}
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
GeneratedPluginRegistrant.registerWith(flutterEngine)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result ->
when (call.method) {
"startMainActivity" -> {
startMainActivity()
result.success(true)
}
else -> result.notImplemented()
}
}
}
private fun startMainActivity() {
val intent = Intent(this, MainActivity::class.java)
startActivity(intent)
}
}

How to close activity from class with context usage?

I created another class where I have function of logout:
fun logOut(context: Context) {
context.stopService(Intent(context, CheckNewMessages::class.java))
val intent = Intent(context, LoginScr::class.java)
intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP and
Intent.FLAG_ACTIVITY_NEW_TASK and
Intent.FLAG_ACTIVITY_NO_ANIMATION
context.startActivity(intent)
(context as Activity).finish()
}
and as you can see I use this line for finishing activity:
(context as Activity).finish()
But it is still alive and as a result I have two or more same activities at my system. I tried a lot of ways like creating static variable at first activity and using this variable at the second one for closing. But my activity stays alive. I also tried to use lauchmode at manifest and some other ways. Maybe someone knows where I did a mistake?
UPDATE
Two places from which I call logOut(). 1st is interface between RV adapter and fragment:
override fun finish() {
APICallRequests.logOut(context!!)
activity!!.finishAffinity()
}
and 2nd at Interceptor for requests:
private fun updateAccessToken(context: Context) {
val sp = context.getSharedPreferences(Constants.SHARED_PREFS_STORAGE, 0)
synchronized(this) {
val tokensCall = accessTokenApi()
.getNewToken(ReqAccessToken(sp.getString("refresh_token", "")!!))
.execute()
if (tokensCall.isSuccessful) {
} else {
when (tokensCall.code()) {
500 -> {
val thread = object : Thread() {
override fun run() {
Looper.prepare()
Toast.makeText(cont, cont.getString(R.string.server_error_500), Toast.LENGTH_SHORT).show()
Looper.loop()
}
}
thread.start()
}
401 -> {
APICallRequests.logOut(context)
}
}
}
}
}
That's not the way it works. What happens is this. When you do:
context.startActivity(intent)
This doesn't start the new Activity immediately. It just requests that Android starts the new Activity when it gets control back. Then you do this:
(context as Activity).finish()
This call just finishes the current Activity. When you eventually return control to the Android framework, it will launch the new Activity as you requested in your call to startActivity().
If you want your app to exit (ie: all activities finished), you can just do:
(context as Activity).finishAffinity()
This call will finish the current Activity and all other activities in the task that belong to the same app.
NOTE: This only works if all activities in your app share the same affinity, which is the default case.
try to pass Activity instead of Context in inner param fun logOut(activity: Activity), this should help you if you are calling this function from activity. If you calling it from fragment you can use requareActivity.finish()

Categories

Resources