Skip to content

Commit

Permalink
fix: async view removal from onDetachedFromWindow (#244)
Browse files Browse the repository at this point in the history
## 📜 Description

Remove `eventView` asynchronously.

## 💡 Motivation and Context

It seems that before opening a universal link and before
`onDetachedFromWindow` Android takes a snapshot of view hierarchy. Then
in `onDetachedFromWindow` we are removing a View and later Android calls
`dispatchDetachedFromWindow` for each view from snapshot. But since
we've already removed a view it tries to call a method on `null` object
reference and we are getting an expected NPE.

In this PR I've reworked an approach (to one that was suggested in the
thread of the issue). And I remove a view asynchronously. It shouldn't
add more new issues, but it'll prevent NPE in the case described above,
because we'll remove a view after `dispatchDetachedFromWindow` has been
called.

Also I've added deep links/universal links to both examples app (paper,
fabric). In order to open an app from a link I had to:
- long press on app icon
- press App Info
- press on "Open by default"
- add a link (should be suggested automatically)

Later I'll try to cover this scenario by e2e tests.

Closes
#242

## 📢 Changelog

### Android
- call `this.removeKeyboardCallbacks()` as
`Handler(Looper.getMainLooper()).post { this.removeKeyboardCallbacks()
}`

## 🤔 How Has This Been Tested?

Tested on Pixel 7 Pro.

## 📸 Screenshots (if appropriate):


https://github.com/kirillzyusko/react-native-keyboard-controller/assets/22820318/e1bbbe48-74f9-4010-a817-fa79354966b6

## 📝 Checklist

- [x] CI successfully passed
  • Loading branch information
kirillzyusko committed Sep 21, 2023
1 parent ef0f625 commit 9430657
Show file tree
Hide file tree
Showing 5 changed files with 65 additions and 9 deletions.
11 changes: 9 additions & 2 deletions FabricExample/android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,16 @@
android:launchMode="singleTask"
android:windowSoftInputMode="adjustPan"
android:exported="true">
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="rnkcfabricexample"/>
<data android:scheme="https" android:host="rnkcfabricexample.com" />
</intent-filter>
</activity>
</application>
Expand Down
22 changes: 20 additions & 2 deletions FabricExample/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import 'react-native-gesture-handler';

import * as React from 'react';
import { StyleSheet } from 'react-native';
import { ActivityIndicator, StyleSheet } from 'react-native';
import { NavigationContainer } from '@react-navigation/native';
import { KeyboardProvider } from 'react-native-keyboard-controller';
import {
Expand All @@ -17,12 +17,30 @@ const styles = StyleSheet.create({
},
});

const linking = {
prefixes: ['https://rnkcfabricexample.com', 'rnkcfabricexample://'],
config: {
initialRouteName: 'EXAMPLES_STACK' as const,
screens: {
EXAMPLES_STACK: {
path: 'examples',
screens: {
ANIMATED_EXAMPLE: {
path: 'animated',
},
},
},
},
},
};
const spinner = <ActivityIndicator color="blue" size="large" />;

export default function App() {
return (
<SafeAreaProvider initialMetrics={initialWindowMetrics}>
<GestureHandlerRootView style={styles.root}>
<KeyboardProvider statusBarTranslucent>
<NavigationContainer>
<NavigationContainer linking={linking} fallback={spinner}>
<RootStack />
</NavigationContainer>
</KeyboardProvider>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.reactnativekeyboardcontroller.views

import android.annotation.SuppressLint
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.widget.FrameLayout
import androidx.appcompat.widget.FitWindowsLinearLayout
Expand Down Expand Up @@ -45,7 +47,11 @@ class EdgeToEdgeReactViewGroup(private val reactContext: ThemedReactContext) : R
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()

this.removeKeyboardCallbacks()
// we need to remove view asynchronously from `onDetachedFromWindow` method
// otherwise we may face NPE when app is getting opened via universal link
// see https://github.com/kirillzyusko/react-native-keyboard-controller/issues/242
// for more details
Handler(Looper.getMainLooper()).post { this.removeKeyboardCallbacks() }
}
// endregion

Expand Down
11 changes: 9 additions & 2 deletions example/android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,16 @@
android:launchMode="singleTask"
android:windowSoftInputMode="adjustPan"
android:exported="true">
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="rnkcexample"/>
<data android:scheme="https" android:host="rnkcexample.com" />
</intent-filter>
</activity>
</application>
Expand Down
22 changes: 20 additions & 2 deletions example/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import 'react-native-gesture-handler';

import * as React from 'react';
import { StyleSheet } from 'react-native';
import { ActivityIndicator, StyleSheet } from 'react-native';
import { NavigationContainer } from '@react-navigation/native';
import { KeyboardProvider } from 'react-native-keyboard-controller';
import {
Expand All @@ -17,12 +17,30 @@ const styles = StyleSheet.create({
},
});

const linking = {
prefixes: ['https://rnkcexample.com', 'rnkcexample://'],
config: {
initialRouteName: 'EXAMPLES_STACK' as const,
screens: {
EXAMPLES_STACK: {
path: 'examples',
screens: {
ANIMATED_EXAMPLE: {
path: 'animated',
},
},
},
},
},
};
const spinner = <ActivityIndicator color="blue" size="large" />;

export default function App() {
return (
<SafeAreaProvider initialMetrics={initialWindowMetrics}>
<GestureHandlerRootView style={styles.root}>
<KeyboardProvider statusBarTranslucent>
<NavigationContainer>
<NavigationContainer linking={linking} fallback={spinner}>
<RootStack />
</NavigationContainer>
</KeyboardProvider>
Expand Down

0 comments on commit 9430657

Please sign in to comment.