-
Notifications
You must be signed in to change notification settings - Fork 4.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[e-m-c][Android] Create ActivityResultsRegistry
#17572
[e-m-c][Android] Create ActivityResultsRegistry
#17572
Conversation
074dbea
to
a091ccd
Compare
4525a53
to
0605c91
Compare
0605c91
to
52e337a
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks very solid 🔥🔥🔥Great job 🏅
requireNotNull(currentActivity) { | ||
"Current Activity is not available at this moment" | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That would lead to a crash when the current Activity isn't available here. I'm not sure, but maybe we should handle that differently to prevent the app from crashing 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This would actually never crash, because whenever any of lifecycle method is called (except for OnCreate
) the current Activity
must be available, because it is actually propagating these events 😉
I went with requireNotNull
to get rid of nullability
(I could have gone with !!
, but we don't want that, do we? 👀).
I can go with some other message, like: "This would never happen, but if it does, something is broken very much"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you can leave it as it is.
packages/expo-modules-core/android/src/main/java/expo/modules/kotlin/AppContext.kt
Outdated
Show resolved
Hide resolved
@MainThread | ||
override suspend fun <I, O, P : Serializable> registerForActivityResult( | ||
contract: ActivityResultContract<I, O>, | ||
fallbackCallback: AppContextActivityResultFallbackCallback<O, P> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we make a fallbackCallback optional?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder about this 🤔
I would rather say that we should not. Why? Because not providing this callback means you do not care about the scenario when Android kills the main Activity
and I wanted to have a mechanism that works in every case. If library author decides to omit such case, then it should be done in the library scope by providing noop callback 🤔
What I can do though is to make this additional parameter optional, because it is not at this moment. No matter the conclusion around this topic I'd like to do it in separate PR, do you mind me doing that? 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, you convinced me ;)
...-core/android/src/main/java/expo/modules/kotlin/activityaware/OnActivityAvailableListener.kt
Show resolved
Hide resolved
...core/android/src/main/java/expo/modules/kotlin/activityaware/AppCompatActivityAwareHelper.kt
Outdated
Show resolved
Hide resolved
.../expo-modules-core/android/src/main/java/expo/modules/kotlin/activityresult/DataPersistor.kt
Outdated
Show resolved
Hide resolved
.../expo-modules-core/android/src/main/java/expo/modules/kotlin/activityresult/DataPersistor.kt
Outdated
Show resolved
Hide resolved
.../expo-modules-core/android/src/main/java/expo/modules/kotlin/activityresult/DataPersistor.kt
Outdated
Show resolved
Hide resolved
.../expo-modules-core/android/src/main/java/expo/modules/kotlin/activityresult/DataPersistor.kt
Outdated
Show resolved
Hide resolved
packages/expo/android/src/main/java/expo/modules/ReactActivityDelegateWrapper.kt
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
just leaving a nit, other than that the pr looks good to me.
val a = activity.get() | ||
if (a != null) { | ||
listener.onActivityAvailable(a) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is it intentional to emit the event in the adding method? if yes with this side effect, please add some comments.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, it is intentional. I've adapted this class from https://android.googlesource.com/platform/frameworks/support/+/HEAD/activity/activity/src/main/java/androidx/activity/contextaware/ContextAwareHelper.java and in AndroidX
they do it that way, but yeah, I'll add some comment 😉
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've added a comment to the AppCompatActivityAware
interface 😉
...e/android/src/main/java/expo/modules/kotlin/activityresult/AppContextActivityResultCaller.kt
Outdated
Show resolved
Hide resolved
if (delegate.reactInstanceManager.currentReactContext == null) { | ||
val reactContextListener = object : ReactInstanceEventListener { | ||
override fun onReactContextInitialized(context: ReactContext?) { | ||
delegate.reactInstanceManager.removeReactInstanceEventListener(this) | ||
delegate.onActivityResult(requestCode, resultCode, data) | ||
} | ||
} | ||
return delegate.reactInstanceManager.addReactInstanceEventListener(reactContextListener) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should I add this as a CHANGELOG entry in packages/expo
? 🤔 I'm not sure what is our approach towards expo
package, cc @tsapeta
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it's worth to add a CHANGELOG because it fixes a real bug IMO.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree with Kudo 👍
`ReactNative` apps that uses `emc` cannot hook propoerly into `Activity` lifecycle and thus we cannot cannot directly use all fancy stuff from `AndroidX`. I'm proposing similar solution for registering for results coming from 3rd party Activities as AndroidX's https://developer.android.com/training/basics/intents/result. - Create `AppContextActivityResultCallback` that mirrors https://developer.android.com/reference/androidx/activity/result/ActivityResultCallback and has additional parameter `launchingActivityHasBeenKilled` - Create `AppContextActivityResultCaller` that mirrors https://developer.android.com/reference/androidx/activity/result/ActivityResultCaller to accepts `AppContextActivityResultCallback` - Create `AppContextActivityResultRegistry` that mirros https://developer.android.com/reference/androidx/activity/result/ActivityResultRegistry, but does not throw upon too late initialization. The original class forces registration in `onCreate` lifecycle method and when using `ReactNative` we are not able to hook into that method properly. Moreover the original class is coupled with Activity's lifecycle and we need something that would outlive Activity (and is hooked into `AppContext` lifecycle) - Create `ActivityResultsManager` that is encapsulating all `Activity`-related-for-result-registration outside `AppContext`
…ityResult` via `AppContext`
…xtActivityResultCaller`
…Activity reacreation - create `AppCompatActivityAware` mechanism for detecting propoerly moment when `Activity` is firstly created (similar to `onCreate` method) - convert `registerForActivityResult` API from single-callback based to two-callback based - main callback is straghtforward callback that is used when the Activity outlives the process of launching other 3rd party Activity - secodnary (fallback) callback is stripped from RN context (no promise, etc) and only handles the scanerio when the Activity is recreated, it preserves the optional options though - create initial API for serialization mechanism that would help preserve additional options during Activity recreation - adapt `AppContext` to these changes - postpone registering modules to the very end of `AppContext` creation as the previous approach would crash now
This is an interface that would allow persisting data in between Activity destruction and recreation
A helper class that is responsince for storing and retrieving data using `SharedPrefernces`
…nd Activity recreation - cleanup, unify and comment data structures - cleanup both code flows - lifecycle-based flow - `onActivityResult` (aka `dispatch`) flow - adapt `DataPersistor` - remove unused code
Implement pairs of methods (setter and getter) for each supported data kind
c501756
to
55dc9c2
Compare
It was happening because listener was added to the listeners pool adter it was called with the value. If it tires to unregister itself in its body it was happening before it was even added to the registered listeners pool.
Why
Extracted part of #16251
Depends on #17571
Android is proposing a new way for registering for activity results - using
ComponentActivity.registerForActivityResult
.The old way of using
startActivityForResult
and listening for resultsonActivityResult
is deprecated.Even though #17571 would enforce the underlying Activity to support this new mechanism we cannot use it directly on Activity.
react-native
's application lifecycle is not mirroring the Android lifecycle (and thus Activity's lifecycle) completely. The moment theComponentActivity
expects the registration to happen is not available for us neither inexpo-modules-core
neither inreact-native
's application lifecycle. What is more these two lifecycle lives independently to each other.In order to be able to use modern Android libraries (e.g. https://github.com/CanHub/Android-Image-Cropper) we need to support this new mechanism somehow.
I've copied and adjusted the original
AndroidX
files in order to couple the lifecycle of registered contracts not withActivity
, but withAppContext
(and thusbridge
) lifecycle.How
I've copied
ActivityResultsManager
,ActivityResultRegistry
andActivityResultCallback
fromandroidx
libraries and adjusted them to matchAppContext
lifecycle:AppContext
is alive as long as the application is alive, we don't have to save data inonSaveInstanceState
and retrieve it fromonCreate
(rn-screens
andreact-navigation
are even strictly forcing us to mark the saved state asnull
whenonCreate
is called in order to work properly)registerForActivityResults
mechanism is based on two different callbacks:main
callback that is passed whenlaunch
is called onActivityResultLauncher
- this one could be called stateful callback, because it has access to ReactNative objects, likePromise
, etcfallback
callback that is registered at the very beginning of the module's lifecycle. It handles all the data that is returned to the application, but only when theActivity
has been killed and recreated by the system. When this happensreact-native
recreates the whole application from scratch, so each module has to store possible orphaned data and provide a way to retrieve them.Additionally I had to modify a bit how ReactNative propagates
onActivityResult
- see 150c5e7. This change has to be discussed and properly applied to all projects usingexpo-modules-core
. I'm not sure howReactNativeDelegateWrapper
is applied to every project, so I'm asking for a bit of help with this aspect 🙏 👀Test Plan
To be testable with #17671
Images
andVideos
Activity
is being recreated by Android - there's a flag inDeveloper Options
available on the device that simulates such scenario (Don't keep Activities
)