Skip to content
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

Android side-effect issue with KeyboardProvider #584

Closed
hyochan opened this issue Sep 13, 2024 · 15 comments
Closed

Android side-effect issue with KeyboardProvider #584

hyochan opened this issue Sep 13, 2024 · 15 comments
Assignees
Labels
KeyboardAvoidingView 🧪 Anything related to KeyboardAvoidingView component question You wanted to clarify something about the usage of the library or have a question about something

Comments

@hyochan
Copy link

hyochan commented Sep 13, 2024

Describe the bug
Hello, thank you for creating such a great library. I've been using KeyboardAvoidingView in React Native for a while, but recently needed the keyboard functionality from react-native-keyboard-controller, which allowed me to implement the keyboard behavior I wanted. I really appreciate your work.

However, I encountered an issue where KeyboardAvoidingView from the core react-native package only causes a side-effect on Android. Specifically, when I wrap my app layout with KeyboardProvider, the KeyboardAvoidingView in react-native package stops working, but this issue is only happening on Android.

It seems like the issue was resolved by setting padding instead of leaving the behavior as undefined, and also by providing a keyboardVerticalOffset, but the exact cause and behavior remain unclear, which leaves me feeling a bit uncertain. I would like to better understand how this change impacts screens where KeyboardAvoidingView is already being used. Specifically, I’d like to know what effects this adjustment may have on the existing views, especially in terms of layout shifts or keyboard interaction, and whether this might introduce any potential side effects. Could you explain how this configuration influences the layout and behavior in detail?

Code snippet
RootProvider.tsx

function RootProvider({initialThemeType, children}: Props): JSX.Element {
  return (
    <KeyboardProvider>
      <RecoilRoot>
        <DoobooProvider
          themeConfig={{
            initialThemeType: initialThemeType ?? undefined,
            customTheme: theme,
          }}
        >
          <ErrorBoundary
            FallbackComponent={FallbackComponent}
            onError={handleErrorConsole}
          >
            <ActionSheetProvider>{children}</ActionSheetProvider>
          </ErrorBoundary>
        </DoobooProvider>
      </RecoilRoot>
    </KeyboardProvider>
  );
}

_layout.tsx

    <GestureHandlerRootView
      style={css`
        flex: 1;
      `}
    >
      <RootProvider initialThemeType={localThemeType as ColorSchemeName}>
        <>
          <StatusBarBrightness />
          <Layout />
        </>
      </RootProvider>
    </GestureHandlerRootView>

index.tsx

import styled, {css} from '@emotion/native';
import {EditText, IconButton, Typography, useDooboo} from 'dooboo-ui';
import {Stack} from 'expo-router';

import {t} from '../src/STRINGS';
import {KeyboardAvoidingView, Platform, View} from 'react-native';
import {useState} from 'react';

const Container = styled.View`
  background-color: ${({theme}) => theme.bg.basic};

  flex: 1;
  align-self: stretch;
  justify-content: center;
  align-items: center;
`;

export default function Index(): JSX.Element {
  const {theme} = useDooboo();
  const [text, setText] = useState('');

  return (
    <Container>
      <Stack.Screen
        options={{
          title: t('HOME'),
        }}
      />
      <KeyboardAvoidingView
        style={css`
          flex: 1;
          width: 100%;
        `}
        behavior={Platform.OS === 'ios' ? 'padding' : undefined}
      >
        <View
          style={css`
            flex: 1;
            background-color: ${theme.bg.paper};

            justify-content: center;
            align-items: center;
          `}
        >
          <Typography.Heading5>Hi there!</Typography.Heading5>
        </View>
        <EditText
          onChangeText={setText}
          value={text}
          decoration="boxed"
          style={css`
            background-color: ${theme.bg.basic};
          `}
          endElement={<IconButton icon="Bird" onPress={() => {}} size={24} />}
        />
      </KeyboardAvoidingView>
    </Container>
  );
}

Repo for reproducing
I would be highly appreciate if you can provide repository for reproducing your issue. It can significantly reduce the time for discovering and fixing the problem.

To Reproduce
Steps to reproduce the behavior:

  1. Go and clone react-native-keyboard-controller-issue repository.
  2. Run bun install or use your package manager.
  3. Checkout initial commit git checkout 16a313a4d8ae1d4a5fb1cdd07529f440527c3dcb.
  4. Run the app and see it if works.
  5. Checkout commit 41f812e9b6edd1b27cf787f74de7600ae20360fe which installed react-native-keyboard-controller and wrapped with KeyboardProvider and test that keyboard is not showing this time.
  6. Checkout commit 61bc6f35ffc30048df04c4a78a8a2a1d29de2d90 and test that it works again with workaround codes.
  7. Also tested using custom hook, useKeyboardAnimation with various options.

Expected behavior
Even when using react-native-keyboard-controller and wrapping the app with KeyboardProvider, the existing KeyboardAvoidingView from react-native should continue to function without issues on Android.

Screenshots

Video Previews

1. Initial Commit

16a313a.mp4

2. Install and wrap with KeyboardProvider

41f812e.mp4

3. Naive workaround

61bc6f3.mp4

4. Test other options with useKeyboardAnimation

dd8d452.mp4

Smartphone (please complete the following information):

  • Device: All Android Phones
  • OS: Android
  • RN version: 0.74.5
  • RN architecture: old
  • JS engine: Hermes
  • Library version: 1.13.4

Additional context
N/A

@kirillzyusko
Copy link
Owner

kirillzyusko commented Sep 13, 2024

Hello @hyochan

When you wrap the app in KeyboardProvider it automatically moves the app in edge-to-edge mode and with adjustResize mode the automatic window resize will not work.

So in your case:

  • the first video relies on default Android keyboard handling mechanism
  • second video - you wrapped app with KeyboardProvider and thus automatic resize stopped to work (edge-to-edge mode + adjustResize)
  • third video - you explicitly specified that you want KeyboardAvoidingView from RN to work on Android (on first video KeyboardAvoidingView wasn't working and the resize that happened was controlled by Android OS). keyboardOffset is needed to specify because useWindowDimensions hook from RN that used inside KeyboardAvoidingView is buggy when it comes to edge-to-edge mode. keyboardOffset is needed because you have a view above the KeyboardAvoidingView (i. e. header) and you need to add it to offset so that layout of KeyboardAvoidingView matches to window layout.
  • 4th video - you changed mode to adjustPan - in this case you force Android OS to handle the keyboard on OS level. In this case adjustPan simply does transform: [{ translateY: keyboardHeight }] on OS level (and it's not highly desired, because your navigation header will be also translated - you can see it on your video).

The ways how you can solve your problem:

  • start to use KeyboardAvoidingView from react-native-keyboard-controller package (in this case you will have smooth animations on both iOS and Android app), you don't need to write platform-conditional code , don't need to specify keyboardOffset since I use internal hook for window dimensions and everything is handled properly;
  • if you want to gradually adopt the functionality of this package in your app then you can use useKeyboardController hook. You can disable module by default <KeyboardProvider enabled={false}> and enable it using useKeyboardController on screens where you want to use them.

Let me know if you want me to explain any aspect in more details 🙏

Even when using react-native-keyboard-controller and wrapping the app with KeyboardProvider, the existing KeyboardAvoidingView from react-native should continue to function without issues on Android.

Unfortunately it's not achievable in a current reality. The best thing that you can do is to use KeyboardAvoidingView from react-native-keyboard-controller.

@kirillzyusko kirillzyusko added the question You wanted to clarify something about the usage of the library or have a question about something label Sep 13, 2024
hyochan referenced this issue in hyochan/react-native-keyboard-controller-issue Sep 13, 2024
@hyochan
Copy link
Author

hyochan commented Sep 13, 2024

@kirillzyusko Thank you so much for the detailed and excellent response. Thanks to your explanation, I have gained a better understanding of your library.

Screenshot 2024-09-14 at 12 54 51 AM

As per your suggestion, I removed the platform-specific code and used the KeyboardAvoidingView from react-native-keyboard-controller in this commit: hyochan/react-native-keyboard-controller-issue@01eb988. I also removed keyboardOffset. However, as shown in the video below, the input does not rise above the keyboard like it does when using the native React Native KeyboardAvoidingView. This also needs to be explained.

keyboard.mp4

Additionally, if you have more references regarding the statement "the useWindowDimensions hook from React Native, used inside KeyboardAvoidingView, is buggy when it comes to edge-to-edge," it would be very helpful to me.

Thank you.

@kirillzyusko
Copy link
Owner

Additionally, if you have more references regarding the statement "the useWindowDimensions hook from React Native, used inside KeyboardAvoidingView, is buggy when it comes to edge-to-edge," it would be very helpful to me.

You can read about that for example here - facebook/react-native#41918

As per your suggestion, I removed the platform-specific code and used the KeyboardAvoidingView from react-native-keyboard-controller in this commit: hyochan/react-native-keyboard-controller-issue@01eb988. I also removed keyboardOffset. However, as shown in the video below, the input does not rise above the keyboard like it does when using the native React Native KeyboardAvoidingView. This also needs to be explained.

I'll check why it happens in nearest time!

@kirillzyusko kirillzyusko added the KeyboardAvoidingView 🧪 Anything related to KeyboardAvoidingView component label Sep 13, 2024
@kirillzyusko
Copy link
Owner

Okay @hyochan

Sorry for initial misleading - in your case you should have verticalKeyboardOffset. Let me slightly dive in the implementation of KeyboardAvoidingView:

image
  • the red rectangle represent the window returned by useWindowDimensions;
  • the blue rectangle is where your KeyboardAvoidingView lives and what it measure.

So as you can see you had a difference which is equal to headerHeight + statusBarHeight (depends on whether it's translucent or not).

And this space is roughly equal to the size of your text input - that's why it gets hidden behind the keyboard. In your case you should artificially increase the area of a blue rectangle and you can do that by adding verticalKeyboardOffset. You can get headerSize using useHeaderHeight hook. If this space is not enough then you can add + StatusBar.currentHeight

I hope I explained why it doesn't work as you expect 😅

Thank you for providing such a detailed and good quality reproduction example ❤️ Let me know if you have any additional questions - I'll be happy to answer on all of them 😊

@beshoo
Copy link

beshoo commented Sep 13, 2024

@kirillzyusko

Thank you for your efforts. Unfortunately, we are still encountering the same issue on Android. The field animates to the top of the upper side of the keyboard, meaning it remains covered.

We have tested all your solutions, but none of them have worked.

I am quite surprised that this bug exists in React Native. I am considering migrating to Flutter, although it is not an easy decision. Some bugs force you to consider alternatives.

If you have any solution that can be applied with clear steps, we would be very grateful. Eg demo which we can understand your sulution since your last reply is very complicated. Demo code is appreciate.

All our app forms are covered by this bug, and the React Native team remains silent.

@hyochan
Copy link
Author

hyochan commented Sep 14, 2024

@kirillzyusko Thanks for the great explanation 👍👍

No Header With Header
No Header With Header

However, I am still curious why react-native-keyboard-controller's KeyboardAvoidingView needs extra headerSize to keyboardOffset. If you could spot the suspicious sections of the code, I would also like to take a look.

This issue needs to be clearly understood to prevent side effects in other views that use KeyboardAvoidingView. This could be critical for large applications, as it would require testing every view before integrating react-native-keyboard-controller. (I understand that we can safely adopt this library by disabling it with <KeyboardProvider enabled={false}> and selectively enabling it where needed. However, I’m trying to understand the root issue. 🤔)

@beshoo I am not getting your statement.

The field animates to the top of the upper side of the keyboard, meaning it remains covered.

I think this should be explained in more details. Providing some code examples and recordings would be very helpful.

@kirillzyusko
Copy link
Owner

Eg demo which we can understand your sulution since your last reply is very complicated. Demo code is appreciate.

@beshoo you can clone the example app https://github.com/kirillzyusko/react-native-keyboard-controller/tree/main/example and see how everything is handled there. This app has a plenty of use cases and covers almost all basic interactions with the keyboard. If you run the code in example app and it works there, but the same code produces different output in your app - it means that something is misconfigured, and it's easier to fix a misconfiguration rather than switching to a new framework/technology 🤷‍♂️

If you can replace the code in example app with your own code and the bug persist in example application, then feel to free to open a new issue and I'll be glad to help to resolve the problem.

The field animates to the top of the upper side of the keyboard, meaning it remains covered.

It's very hard to say what exactly causes the problem, because I don't see the code, don't see the output (including view hierarchy) and can not test/interact with this code. If you think it's a problem in this library - feel free to open a new issue with reproduction example and I'll try to do all my best to help you.

@kirillzyusko
Copy link
Owner

However, I am still curious why react-native-keyboard-controller's KeyboardAvoidingView needs extra headerSize to keyboardOffset. If you could spot the suspicious sections of the code, I would also like to take a look.

It's all handled in

const keyboardY =
screenHeight - keyboard.heightWhenOpened.value - keyboardVerticalOffset;

This issue needs to be clearly understood to prevent side effects in other views that use KeyboardAvoidingView. This could be critical for large applications, as it would require testing every view before integrating react-native-keyboard-controller. (I understand that we can safely adopt this library by disabling it with and selectively enabling it where needed. However, I’m trying to understand the root issue. 🤔)

Well, feel free to correct me here, but I think that verticalOffset is needed for KeyboardAvoidingView from RN as well. Basically how KeyboardAvoidingView works - it measures its dimensions (width/height), then when keyboard is open it understands which part of view is covered by keyboard and changes padding to the same size, so that all the content is visible.

But if some elements are placed above the KeyboardAvoidingView, then dimensions will be gathered incorrectly and it may lead to a situation, when part of the content is obscured by the keyboard.

In the end I implemented test example and covered it by e2e tests to assure that KeyboardAvoidingView satisfies to default implementation:

Default KAV KAV from RNKC

On iOS if you don't specify keyboardVerticalOffset (i. e. header size) you also will end up in the same situation, when bottom elements will be overlapped by keyboard:

RN RNKC
image image

So I think that a component from react-native-keyboard-controller works in the same manner as a default KeyboardAvoidingView on iOS (and on Android it just re-uses all the logic).

I didn't find time to run your project on iOS - but if you run it with keyboardVerticalOffset={0} will the input be overlapped by keyboard?


If you want to fully match default keyboard handling from Android, then you can wrap your entire app in KAV:

    <GestureHandlerRootView
      style={css`
        flex: 1;
      `}
    >
      <RootProvider initialThemeType={localThemeType as ColorSchemeName}>
        <KeyboardAvoidingView
          behavior="padding"
          style={{flex: 1, width: '100%'}}
        >
          <StatusBarBrightness />
          <Layout />
        </KeyboardAvoidingView>
      </RootProvider>
    </GestureHandlerRootView>

Then layout will be measured correctly and you don't need to think about headers and other elements (because all of them will be rendered inside KAV - KAV will match window dimensions).

But sometimes having just KeyboardAvoidingView is not always enough to handle the keyboard in all scenarios, but when you have something on a global level it may be hard to disable that (or it can make your code slightly more complicated). Anyway - feel free to chose what exactly best suitable for your project, but I think wrapping an app in KeyboardAvoidingView is kind of anti-pattern.

Let me know if you have any other questions - will be happy to answer on them as well! 😊 🙌

@hyochan
Copy link
Author

hyochan commented Sep 19, 2024

@kirillzyusko, I feel that wrapping the entire view with KeyboardAvoidingView might not be ideal. My concern is that it could lead to performance issues, as it handles the onLayoutWorklet every time the child component re-renders (this is just my assumption).

Your detailed explanation has made things much clearer, but I wanted to highlight a few concerns I encountered during implementation:

  1. When I dynamically toggle the navigation header using headerShown set to true and false, the KeyboardAvoidingView doesn’t always adjust its height correctly unless I reload the app. This happens intermittently, so I'm unsure what’s happening internally.

  2. I'm still curious why react-native-keyboard-controller's KeyboardProvider affects the default KeyboardAvoidingView behavior in Android. When I change the behavior to padding, it works, but it doesn't work with other behavior values like height or undefined. Interestingly, the issue resolves when I remove KeyboardProvider. I’d just like to clarify this point before closing the issue 🥹.

@kirillzyusko
Copy link
Owner

I feel that wrapping the entire view with KeyboardAvoidingView might not be ideal. My concern is that it could lead to performance issues, as it handles the onLayoutWorklet every time the child component re-renders (this is just my assumption).

@hyochan if re-render doesn't cause a layout re-calculation then onLayout will not be fired. And since you wrap entire app, the most likely there will be a children in hierarchy with styles { flex: 1 } which will take all available width/height and will not change its size over time, so most likely keeping KeyboardAvoidingView at the top of view hierarchy will not cause any problems in terms of performance.

When I dynamically toggle the navigation header using headerShown set to true and false, the KeyboardAvoidingView doesn’t always adjust its height correctly unless I reload the app. This happens intermittently, so I'm unsure what’s happening internally.

Interesting. It should just call onLayout and update a shared value 🤷‍♂️ Maybe update is not happening inside KeyboardAvoidingView? Anyway it's pretty straightforward to debug - you can log values in relativeKeyboardHeight() and compare values between run, i. e. when it works correctly and when it's not.

I'm still curious why react-native-keyboard-controller's KeyboardProvider affects the default KeyboardAvoidingView behavior in Android. When I change the behavior to padding, it works, but it doesn't work with other behavior values like height or undefined. Interestingly, the issue resolves when I remove KeyboardProvider. I’d just like to clarify this point before closing the issue 🥹.

This is a very good question and honestly I don't know the answer. This component heavily relies on LayoutAnimations from react-native and maybe some updates are getting ignored. Did you enable them BTW?

But it's actually very strange - if it works with one mode (i. e. padding), then it means that bottomHeight is calculated properly but styles somehow can not be applied (which is very strange).

I honestly don't know why KeyboardAvoidingView works only with specific mode. When you remove KeyboardProvider, then you delegate resizing of window to Android OS. So that would be very logical to suppose, that if you had behavior as height or undefined, then KeyboardAvoidingView wasn't working and resize of the window was handled by OS. But when you added KeyboardProvider it stopped automatic window resizes and now you can see that KeyboardAvoidingView is not working? What is the behavior of behavior="padding" without KeyboardProvider? Does it apply double keyboard height as padding? 👀

@hyochan
Copy link
Author

hyochan commented Sep 22, 2024

What is the behavior of behavior="padding" without KeyboardProvider? Does it apply double keyboard height as padding? 👀

Below is KeyboardAvoidingView from react-native with behavior=height without KeyboardProvider.

keyboard.with.height.mp4

Below is KeyboardAvoidingView from react-native with behavior=padding without KeyboardProvider.

keyboard.with.padding.mp4

Would this information be more helpful in identifying the cause of the issue?

@kirillzyusko
Copy link
Owner

@hyochan well, that's what I thought. In case of padding the KeyboardAvoidingView applies double padding:

image

Later on it resizes back to proper size, but the key thing is that at some point of time it applies double space (i. e. KeyboardAvoidingView works).

In case of height - everything works fine and I think KeyboardAvoidingView doesn't work at all here. You can try to remove KeyboardAvoidingView and I bet that you will see the identical behavior.

So it makes me thinking that KeyboardAvoidingView with behavior="height" doesn't work on Android and you were relying on default Android resize behavior rather than KeyboardAvoidingView 🤷‍♂️

@hyochan
Copy link
Author

hyochan commented Sep 24, 2024

@kirillzyusko
Thank you for your response! 😊 This is indeed quite challenging! I think it would be a good idea to explain the side effect that only occurs on Android to the users in the documentation. While the root cause isn’t fully clear yet, it would be helpful to open a discussion and investigate it gradually.

If it's alright with you, I’d like to write and upload a document about this issue. Would that be okay?

@kirillzyusko
Copy link
Owner

If it's alright with you, I’d like to write and upload a document about this issue. Would that be okay?

Yes, sure! I think I attempted to clarify it in https://github.com/kirillzyusko/react-native-keyboard-controller/pull/381but if you can write a better documentation that will explain everything in more details then I'm happy to merge it in docs 😊

@kirillzyusko
Copy link
Owner

I'm going to close that issue since I believe I gave an answer on the question. If you want to make adjustments to the documentation - feel free to do that (it's highly appreciated)!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
KeyboardAvoidingView 🧪 Anything related to KeyboardAvoidingView component question You wanted to clarify something about the usage of the library or have a question about something
Projects
None yet
Development

No branches or pull requests

3 participants