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

Integration issues with react-native-gesture-handler on Android #264

Closed
arunmenon1975 opened this issue Jul 12, 2019 · 35 comments · Fixed by #266
Closed

Integration issues with react-native-gesture-handler on Android #264

arunmenon1975 opened this issue Jul 12, 2019 · 35 comments · Fixed by #266

Comments

@arunmenon1975
Copy link
Contributor

arunmenon1975 commented Jul 12, 2019

I am attempting to use Navigation React Native as the routing solution for my native app and am liking it quite a bit: performant, simple, intuitive and quite flexible. However, In my early integration attempts (using some of the other core libraries i would be using in my redux-driven app) i find that I have a couple of Android-specific issues when i attempt integration with react-native-gesture-handler .

Main Issue

The SideDrawer seems to work only in the initial screen when an attempt is made to trigger it via a gesture (swipe left/right from the edges, depending on the configuration). When i navigate away to another screen that also has the SideDrawer, it does not work. It does work everywhere when i try to programmatically open/close, regardless of which screen i am on, however.

Secondary Issue

Additionally a minor issue was noticed as well. Running back actions, both via an app button and via the hardware back button, seem to flash the side drawer for about a second before navigation. It seems like it is navigating 2 steps instead of 1 with the flashing drawer seeming to visually appear as an intermediary screen.

Note: Everything works perfectly fine in IoS however.

I use the SideDrawer as an HOC, wrapping screens that would require the SideDrawer but have attempted using it normally as well.

The installation docs for react-native-gesture-handler has a section dedicated to installation for android apps that use native navigation libraries. Essentially they require wrapping each screen with a dedicated HOC shipped with the library.

As far i understand Navigation has a single ReactRoot(a recent change) and so, if wrapping is necessary, it can be done at the topmost level. I tried that but It didn't work. I also attempted wrapping individual screens but with no success. Ive used the gesture handler library in other apps, both JS-driven(react-navigation, no change was necessary) and native (react-native-navigation, with integration of the specific changes required as was documented) and they work fine. My guess is i might perhaps be needing the wrapping of the dedicated HOC shipped with react-native-gesture-handler since, as per the installation docs for Navigation on React Native, there is specific mention of the use of native APIs for navigation:

The Navigation router for React Native uses the underlying native APIs to provide faithful navigation on Android and iOS.

My current minimal package.json:

{
  "name": "menuappnative",
  "version": "0.0.1",
  "private": true,
  "scripts": {
    "start": "node node_modules/react-native/local-cli/cli.js start",
    "test": "jest"
  },
  "dependencies": {
    "@react-native-community/async-storage": "^1.5.1",
    "@react-native-community/netinfo": "^4.0.0",
    "navigation": "^5.2.0",
    "navigation-react": "^4.1.1",
    "navigation-react-native": "^5.3.0",
    "prop-types": "^15.7.2",
    "react": "16.8.3",
    "react-native": "0.59.10",
    "react-native-device-info": "^2.2.2",
    "react-native-firebase": "^5.5.5",
    "react-native-gesture-handler": "^1.3.0",
    "react-native-vector-icons": "^6.6.0",
    "react-redux": "^7.1.0",
    "redux": "^4.0.3",
    "redux-thunk": "^2.3.0"
  },
  "devDependencies": {
    "@babel/core": "^7.5.4",
    "@babel/runtime": "^7.5.4",
    "babel-jest": "^24.8.0",
    "jest": "^24.8.0",
    "metro-react-native-babel-preset": "^0.55.0",
    "react-test-renderer": "16.8.3"
  },
  "jest": {
    "preset": "react-native"
  }
}

The App.js:

import React from 'react';
import { StateNavigator } from 'navigation';
import { NavigationHandler } from 'navigation-react';
import { NavigationStack } from 'navigation-react-native';
import { Alert, BackHandler } from 'react-native';
import { Provider } from 'react-redux';
import Menu from './Menu';
import store from './src/redux/store';
import Welcome from './Welcome';

const stateNavigator = new StateNavigator([
  {key: 'welcome', trackCrumbTrail: true},
  {key: 'menu', trackCrumbTrail: true},
]);

const {welcome,menu} = stateNavigator.states;
welcome.renderScene = () => <Welcome />;
menu.renderScene = () => <Menu />;
stateNavigator.navigate('welcome');

class App extends React.Component{
  componentDidMount(){
    BackHandler.addEventListener("hardwareBackPress",this._handleHardwareBackPress)
  }
  componentWillUnmount(){
    BackHandler.removeEventListener("hardwareBackPress",this._handleHardwareBackPress)
  }
  _handleHardwareBackPress=(args)=>{
      if(!stateNavigator.canNavigateBack(1)){
        Alert.alert("Show a prompt with OK/CANCEL")
        return true
      }
  }
  render(){
    return(
      <Provider store={store}>
          <NavigationHandler stateNavigator={stateNavigator}>
            <NavigationStack />
          </NavigationHandler>
      </Provider>
    );
  }
}
export default App;

The Welcome screen:

import React, { Component } from 'react';
import { NavigationContext } from 'navigation-react';
import { Button, SafeAreaView, StyleSheet, Text } from 'react-native';
import { connect } from 'react-redux';
//import SideDrawer from './src/modules/SideDrawer/SideDrawer';
import withSideDrawer from './src/helpers/hoc/withSideDrawer';

class Welcome extends Component {
  _onPress = (stateNavigator) => {
    stateNavigator.navigate('menu');
  }
  render() {
    return (
      <NavigationContext.Consumer>
        {({stateNavigator}) => (
          <SafeAreaView style={styles.container}>
            <Text>
              Welcome
            </Text>
              <Button title="To Menu Screen" onPress={() => this._onPress(stateNavigator)}/>
          </SafeAreaView>
        )}
      </NavigationContext.Consumer>
    );
  }
}
const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#E91E63'
  },
});

const mapStateToProps = (state) => {
  return {common: state.commonReducer, 
    nav: state.navigationReducer}
}
export default connect(mapStateToProps)(withSideDrawer(Welcome));

The Menu Screen

import { NavigationContext } from 'navigation-react';
import React, { Component } from 'react';
import { Button, SafeAreaView, StyleSheet, Text } from 'react-native';
import { connect } from 'react-redux';
import withSideDrawer from './src/helpers/hoc/withSideDrawer';

class Menu extends Component {
  _onPress = (stateNavigator) => {
    stateNavigator.navigateBack(1);
  }
  render() {
    return (
      <NavigationContext.Consumer>
        {({stateNavigator}) => (
          <SafeAreaView style={styles.container}>
            <Text>
              Menu
            </Text>
              <Button title="Back" onPress={() => this._onPress(stateNavigator)}/>
          </SafeAreaView>
        )}
      </NavigationContext.Consumer>
    );
  }
}
const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#E91E63'
  },
});

const mapStateToProps = (state) => {
  return {common: state.commonReducer, nav: state.navigationReducer}
}
export default connect(mapStateToProps)(withSideDrawer(Menu));
@grahammendick
Copy link
Owner

Thanks, I’m really pleased you like the Navigation router. It’s great that you’re integrating the Drawer Layout. I planned to give it a go but never got around to it.

Let’s tackle the main issue first and see if the secondary one still exists after that.

When installing react-native-gesture-handler don’t follow the React Native Navigation guide. You don’t need to wrap every screen using gestureHandlerRootHOC. Although the Navigation router does use the native APIs, it’s very different to React Native Navigation. You should follow the plain Android guide and add the createReactActivityDelegate to MainActivity.

I think it’s not working on subsequent screens because they use different Activities. They use the SceneActivity class. So you need to add createReactActivityDelegate to this SceneActivity class, too.

@grahammendick
Copy link
Owner

I just had another read through the gesture handler guide and I think that wrapping each screen in the HOC should also work. Can you give this a try as well and let me know how you get on?

const {welcome,menu} = stateNavigator.states;
welcome.renderScene = () => gestureHandlerRootHOC(<Welcome />);
menu.renderScene = () => gestureHandlerRootHOC(<Menu />);

@arunmenon1975
Copy link
Contributor Author

arunmenon1975 commented Jul 12, 2019

I tried the exact same and got a blank screen with a warning

Functions are not valid as a React child. This may happen if you return a Component instead of <Component /> from render. Or maybe you meant to call this function rather than return it.
    in NVScene (created by Scene)
    in Scene (created by Context.Consumer)
    in Scene$1 (created by NavigationStack)
    in NVNavigationStack (created by NavigationStack)
    in NavigationStack (created by Context.Consumer)
    in NavigationStack$1 (at App.js:52)
    in NavigationHandler (at App.js:51)
    in Provider (at App.js:50)
    in App (at renderApplication.js:35)
    in RCTView (at View.js:45)
    in View (at AppContainer.js:98)
    in RCTView (at View.js:45)
    in View (at AppContainer.js:115)
    in AppContainer (at renderApplication.js:34)

Since the initial screen doesn't load i simplified my App.js to just a single route:

import React from 'react';
import { StateNavigator } from 'navigation';
import { NavigationHandler } from 'navigation-react';
import { NavigationStack } from 'navigation-react-native';
import { Alert, BackHandler } from 'react-native';
import { gestureHandlerRootHOC } from 'react-native-gesture-handler';
import { Provider } from 'react-redux';
//import Menu from './Menu';
import store from './src/redux/store';
import Welcome from './Welcome';


/*const Welcome=(props)=>{
  return(
    <View> 
    
    <Text> Welcome </Text>
    
    </View>
  )
}*/

const stateNavigator = new StateNavigator([
  {key: 'welcome', trackCrumbTrail: true},
 // {key: 'menu', trackCrumbTrail: true},
]);

const {welcome} = stateNavigator.states;
welcome.renderScene = () => gestureHandlerRootHOC(<Welcome />);
//menu.renderScene = () => gestureHandlerRootHOC(<Menu />);


stateNavigator.navigate('welcome'); 

class App extends React.Component{
  componentDidMount(){
    BackHandler.addEventListener("hardwareBackPress",this._handleHardwareBackPress)
  }
  componentWillUnmount(){
    BackHandler.removeEventListener("hardwareBackPress",this._handleHardwareBackPress)
  }
  _handleHardwareBackPress=(args)=>{
      if(!stateNavigator.canNavigateBack(1)){
        Alert.alert("Show a prompt with OK/CANCEL")
        return true
      }
  }
  render(){
    return(
      <Provider store={store}>
          <NavigationHandler stateNavigator={stateNavigator}>
            <NavigationStack />
          </NavigationHandler>
      </Provider>
    );
  }
}
export default App;

I also tried welcome.renderScene = () => gestureHandlerRootHOC(Welcome); calling the functional component defined in the same file(and without a call to the <DrawerLayout> component for simplicity) but the same warning and blank screen is returned.

Edit:

I just noticed the comment regarding the installation. I will try that and report the outcome back here.

@grahammendick
Copy link
Owner

Sorry, this should work

const {welcome} = stateNavigator.states; 
const WelcomeScene = gestureHandlerRootHOC(Welcome);
welcome.renderScene = () => <WelcomeScene />; 

@arunmenon1975
Copy link
Contributor Author

I had followed the normal(non-native navigation libraries) installation procedure already so didn't have to change anything in MainApplication.java. In other words all my attempts prior to raising the issue were made with this already in place.

These were my 2 broad attempts and outcomes.

Approach 1:

  • Rollback the additions in MainApplication.java and treat as a native installation and follow the RNN section. [This is just to rule-out this approach if it doesn't work]
  • Add the HOC in my files as suggested. In App.js or individually in each screen that will be rendered

Outcome: Crashes without opening when i attempt navigating to the inner screen i.e Welcome opens but cannot navigate to Menu from the button link. The app crashes. I don't have my own HOC and instead call the DrawerLayout component directly inline to simplify things.

Approach 2

  • Follow the normal non-native installation procedure outlined in the react-native-gesture-handler documentation
  • Follow your suggestions above about addition to SceneActivity.java
  • Try with the HOC
  • Try without the HOC

Outcome: My not upto-the-mark(an understatement) java skills has me stuck at integrating the changes you had outlined. In the node_modules directory i navigated to the SceneActivity.java file and made changes so that the file now reads:

package com.navigation.reactnative;

import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;

import com.facebook.react.ReactActivity;
import com.facebook.react.bridge.GuardedRunnable;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler;
import com.facebook.react.uimanager.JSTouchDispatcher;
import com.facebook.react.uimanager.RootView;
import com.facebook.react.uimanager.UIManagerModule;
import com.facebook.react.uimanager.events.EventDispatcher;
import com.facebook.react.views.view.ReactViewGroup;


 import com.facebook.react.ReactActivityDelegate;
 import com.facebook.react.ReactRootView;
 import com.swmansion.gesturehandler.react.RNGestureHandlerEnabledRootView;


import java.util.HashSet;

public class SceneActivity extends ReactActivity implements DefaultHardwareBackBtnHandler {
    public static final String CRUMB = "Navigation.CRUMB";
    public static final String SHARED_ELEMENTS = "Navigation.SHARED_ELEMENTS";
    private SceneRootViewGroup rootView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        int crumb = getIntent().getIntExtra(CRUMB, 0);
        rootView = new SceneRootViewGroup(getReactNativeHost().getReactInstanceManager().getCurrentReactContext());
        if (crumb < NavigationStackView.sceneItems.size()) {
            View view = NavigationStackView.sceneItems.get(crumb).view;
            if (view.getParent() != null)
                ((ViewGroup) view.getParent()).removeView(view);
            rootView.addView(view);
        }
        setContentView(rootView);
        @SuppressWarnings("unchecked")
        HashSet<String> sharedElements = (HashSet<String>) getIntent().getSerializableExtra(SHARED_ELEMENTS);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && sharedElements != null ) {
            this.postponeEnterTransition();
            SharedElementTransitioner transitioner = new SharedElementTransitioner(this, sharedElements);
            findViewById(android.R.id.content).getRootView().setTag(R.id.sharedElementTransitioner, transitioner);
        }
    }

    @Override
    public void onNewIntent(Intent intent) {
        super.onNewIntent(intent);
        int crumb = intent.getIntExtra(CRUMB, 0);
        if (rootView.getChildCount() > 0)
            rootView.removeViewAt(0);
        if (crumb < NavigationStackView.sceneItems.size()) {
            View view = NavigationStackView.sceneItems.get(crumb).view;
            if (view.getParent() != null)
                ((ViewGroup) view.getParent()).removeView(view);
            rootView.addView(view);
        }
    }
    @Override
    protected String getMainComponentName() {
        return "navigation";
    }

     @Override
      protected ReactActivityDelegate createReactActivityDelegate() {
        return new ReactActivityDelegate(this, getMainComponentName()) {
          @Override
          protected ReactRootView createRootView() {
           return new RNGestureHandlerEnabledRootView(SceneActivity.this);
          }
        };
      }


    static class SceneRootViewGroup extends ReactViewGroup implements RootView {
        private boolean hasAdjustedSize = false;
        private int viewWidth;
        private int viewHeight;

        private final JSTouchDispatcher mJSTouchDispatcher = new JSTouchDispatcher(this);

        public SceneRootViewGroup(Context context) {
            super(context);
        }

        @Override
        protected void onSizeChanged(final int w, final int h, int oldw, int oldh) {
            super.onSizeChanged(w, h, oldw, oldh);
            viewWidth = w;
            viewHeight = h;
            updateFirstChildView();
        }

        private void updateFirstChildView() {
            if (getChildCount() > 0) {
                hasAdjustedSize = false;
                final int viewTag = getChildAt(0).getId();
                ReactContext reactContext = getReactContext();
                reactContext.runOnNativeModulesQueueThread(
                    new GuardedRunnable(reactContext) {
                        @Override
                        public void runGuarded() {
                            (getReactContext()).getNativeModule(UIManagerModule.class)
                                .updateNodeSize(viewTag, viewWidth, viewHeight);
                        }
                    });
            } else {
                hasAdjustedSize = true;
            }
        }

        @Override
        public void addView(View child, int index, LayoutParams params) {
            super.addView(child, index, params);
            if (hasAdjustedSize) {
                updateFirstChildView();
            }
        }

        @Override
        public void handleException(Throwable t) {
            getReactContext().handleException(new RuntimeException(t));
        }

        private ReactContext getReactContext() {
            return (ReactContext) getContext();
        }

        @Override
        public boolean onInterceptTouchEvent(MotionEvent event) {
            mJSTouchDispatcher.handleTouchEvent(event, getEventDispatcher());
            return super.onInterceptTouchEvent(event);
        }

        @Override
        public boolean onTouchEvent(MotionEvent event) {
            mJSTouchDispatcher.handleTouchEvent(event, getEventDispatcher());
            super.onTouchEvent(event);
            return true;
        }

        @Override
        public void onChildStartedNativeGesture(MotionEvent androidEvent) {
            mJSTouchDispatcher.onChildStartedNativeGesture(androidEvent, getEventDispatcher());
        }

        @Override
        public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
        }

        private EventDispatcher getEventDispatcher() {
            ReactContext reactContext = getReactContext();
            return reactContext.getNativeModule(UIManagerModule.class).getEventDispatcher();
        }
    }

    public static class Crumb1 extends SceneActivity {} public static class Crumb2 extends SceneActivity {}
    public static class Crumb3 extends SceneActivity {} public static class Crumb4 extends SceneActivity {}
    public static class Crumb5 extends SceneActivity {} public static class Crumb6 extends SceneActivity {}
    public static class Crumb7 extends SceneActivity {} public static class Crumb8 extends SceneActivity {}
    public static class Crumb9 extends SceneActivity {} public static class Crumb10 extends SceneActivity {}
    public static class Crumb11 extends SceneActivity {} public static class Crumb12 extends SceneActivity {}
    public static class Crumb13 extends SceneActivity {} public static class Crumb14 extends SceneActivity {}
    public static class Crumb15 extends SceneActivity {} public static class Crumb16 extends SceneActivity {}
    public static class Crumb17 extends SceneActivity {} public static class Crumb18 extends SceneActivity {}
    public static class Crumb19 extends SceneActivity {} public static class Crumb20 extends SceneActivity {}

    public static Class getActivity(int crumb) {
        try {
            return Class.forName("com.navigation.reactnative.SceneActivity$Crumb" + crumb);
        } catch (ClassNotFoundException e) {
            return SceneActivity.class;
        }
    }

}

I also added the following lines to settings.gradle file:

include ':react-native-gesture-handler'
project(':react-native-gesture-handler').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-gesture-handler/android')

include ':app'

I get the following build error

> Task :navigation-react-native:compileDebugJavaWithJavac FAILED
/Users/arunmenon/Sites/reactnative/menuappnative/node_modules/navigation-react-native/android/app/src/main/java/com/navigation/reactnative/SceneActivity.java:24: error: package com.swmansion.gesturehandler.react does not exist
 import com.swmansion.gesturehandler.react.RNGestureHandlerEnabledRootView;
                                          ^
/Users/arunmenon/Sites/reactnative/menuappnative/node_modules/navigation-react-native/android/app/src/main/java/com/navigation/reactnative/SceneActivity.java:78: error: cannot find symbol
           return new RNGestureHandlerEnabledRootView(SceneActivity.this);
                      ^
  symbol: class RNGestureHandlerEnabledRootView
2 errors

FAILURE: Build failed with an exception.

I am guessing i will need to call/instantiate the equivalent of new RNGestureHandlerPackage() somewhere.

Im not sure how to proceed to completing the build for this approach to check if it works. My understanding is that this approach is akin to integrating react-native-gesture-handler into the Navigation project versus integrating into my project and extending where necessary. I am fine either way since I think theNavigator library for the native builds aims to provide for only native UI where available and doesn't actually have a wrapper that bridges functionality for UI elements for both the OSs as mentioned in this comment where you say

The Navigation router will never support as many features as RNN because it has a much narrower scope. The Navigation router concentrates on navigation only so will never include a Modal or Drawer component. Also, the Navigation router only exposes the navigation features that are native to the platform. It doesn’t try to erase the platform differences.

[My personal opinion though is that a Drawer component is part of the navigation solution since in almost every solution it resides to purely help navigate. And it is recognisable enough to warrant a second-thought about possibilities of integration in OSs that may not necessarily have the design pattern as standard. Not very unlike a NavigationBarIOS for instance where there are provisions to navigate if necessary. A Modal though is more utility than a pure navigation-focussed UI pattern and anyway React does have a competent, if not robust, built-in modal solution ].

@grahammendick
Copy link
Owner

We should be able to get Approach 1 working. I'll give it a try and see what the problem is

@arunmenon1975
Copy link
Contributor Author

UPDATE:

I tried the following approach which is similar to approach 1 i outlined above:

  • Follow the normal non-native installation procedure outlined in the react-native-gesture-handler documentation
  • Add the HOC in my files as suggested. To get rid of the Functions are not valid as a React child... error i changed the code to:
const {welcome,menu} = stateNavigator.states;
const NewWelcome = gestureHandlerRootHOC(Welcome ) 
const NewMenu = gestureHandlerRootHOC(Menu ) 
welcome.renderScene = () => <NewWelcome />;
menu.renderScene = () => <NewMenu />;
stateNavigator.navigate('welcome');  
  • ensure that i don't call the HoC anywhere inside (the Welcome or Menu components)
  • install react-native-exception-handler to catch the crash and get some idea why it does so

Outcome
The initial screen opens. The minute i tap anywhere on the screen the app crashes. The exception handler kicks in and i get the following messages logged:

java.lang.IllegalStateException: Already prepared or hasn't been reset
	at com.swmansion.gesturehandler.GestureHandler.prepare(GestureHandler.java:178)
	at com.swmansion.gesturehandler.GestureHandlerOrchestrator.recordHandlerIfNotPresent(GestureHandlerOrchestrator.java:379)
	at com.swmansion.gesturehandler.GestureHandlerOrchestrator.recordViewHandlersForPointer(GestureHandlerOrchestrator.java:389)
	at com.swmansion.gesturehandler.GestureHandlerOrchestrator.traverseWithPointerEvents(GestureHandlerOrchestrator.java:466)
	at com.swmansion.gesturehandler.GestureHandlerOrchestrator.extractGestureHandlers(GestureHandlerOrchestrator.java:422)
	at com.swmansion.gesturehandler.GestureHandlerOrchestrator.traverseWithPointerEvents(GestureHandlerOrchestrator.java:464)
	at com.swmansion.gesturehandler.GestureHandlerOrchestrator.extractGestureHandlers(GestureHandlerOrchestrator.java:403)
	at com.swmansion.gesturehandler.GestureHandlerOrchestrator.onTouchEvent(GestureHandlerOrchestrator.java:97)
	at com.swmansion.gesturehandler.react.RNGestureHandlerRootHelper.dispatchTouchEvent(RNGestureHandlerRootHelper.java:121)
	at com.swmansion.gesturehandler.react.RNGestureHandlerRootView.dispatchTouchEvent(RNGestureHandlerRootView.java:36)
	at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3030)
	at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2662)
	at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3030)
	at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2662)
	at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3030)
	at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2662)
	at com.swmansion.gesturehandler.react.RNGestureHandlerEnabledRootView.dispatchTouchEvent(RNGestureHandlerEnabledRootView.java:34)
	at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3030)
	at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2662)
	at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3030)
	at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2662)
	at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3030)
	at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2662)
	at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3030)
	at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2662)
	at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3030)
	at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2662)
	at com.android.internal.policy.DecorView.superDispatchTouchEvent(DecorView.java:440)
	at com.android.internal.policy.PhoneWindow.superDispatchTouchEvent(PhoneWindow.java:1830)
	at android.app.Activity.dispatchTouchEvent(Activity.java:3400)
	at android.support.v7.view.WindowCallbackWrapper.dispatchTouchEvent(WindowCallbackWrapper.java:69)
	at com.android.internal.policy.DecorView.dispatchTouchEvent(DecorView.java:398)
	at android.view.View.dispatchPointerEvent(View.java:12752)
	at android.view.ViewRootImpl$ViewPostImeInputStage.processPointerEvent(ViewRootImpl.java:5106)
	at android.view.ViewRootImpl$ViewPostImeInputStage.onProcess(ViewRootImpl.java:4909)
	at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:4426)
	at android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:4479)
	at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:4445)
	at android.view.ViewRootImpl$AsyncInputStage.forward(ViewRootImpl.java:4585)
	at android.view.ViewRootImpl$InputStage.apply(ViewRootImpl.java:4453)
	at android.view.ViewRootImpl$AsyncInputStage.apply(ViewRootImpl.java:4642)
	at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:4426)
	at android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:4479)
	at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:4445)
	at android.view.ViewRootImpl$InputStage.apply(ViewRootImpl.java:4453)
	at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:4426)
	at android.view.ViewRootImpl.deliverInputEvent(ViewRootImpl.java:7092)
	at android.view.ViewRootImpl.doProcessInputEvents(ViewRootImpl.java:7061)
	at android.view.ViewRootImpl.enqueueInputEvent(ViewRootImpl.java:7022)
	at android.view.ViewRootImpl$WindowInputEventReceiver.onInputEvent(ViewRootImpl.java:7195)
	at android.view.InputEventReceiver.dispatchInputEvent(InputEventReceiver.java:186)
	at android.os.MessageQueue.nativePollOnce(Native Method)
	at android.os.MessageQueue.next(MessageQueue.java:326)
	at android.os.Looper.loop(Looper.java:160)
	at android.app.ActivityThread.main(ActivityThread.java:6669)
	at java.lang.reflect.Method.invoke(Native Method)
	at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:493)
	at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:858)

Essentially what i do get is that the HoC approach done this way doesn't seem to work.

@arunmenon1975
Copy link
Contributor Author

We should be able to get Approach 1 working. I'll give it a try and see what the problem is

Thank you. I am looking forward to using Navigator since it does tick to most of what i am looking for. Unfortunately a Drawer is one of the primary requirements in my current project. Will not be a showstopper though since i may opt for a JS variant if integration is cumbersome. The Gesture Handler library is quite useful beyond just the DrawerLayout and i suspect is used quite a bit all around.

@grahammendick
Copy link
Owner

I agree. It's v important that the Navigation router works with the Drawer and the gesture handler. Leave it with me

@grahammendick
Copy link
Owner

I've caught up with your efforts. Thanks for trying hard to work out the problem. I've done some investigation myself and there is a problem with using the gesture handler and Navigation router on Android. The gesture handler expects to be inside a ReactRooView but only the first Screen is. The later screens aren't. Even if we get it working I think we'd still have the secondary issue of the drawer flash when you navigate back.

So a different solution (I've tried it and it works) is to use the DrawerLayoutAndroid component. It wraps the native DrawerLayout so is probably a better choice for Android anyway. For iOS you can keep using the DrawerLayout component from gesture handler.

The props are identical so you could create a Drawer component that returns the right one depending on the Platform. Then import the Drawer component in your scenes.

import {Platform, DrawerLayoutAndroid} from 'react-native';
import DrawerLayout from 'react-native-gesture-handler/DrawerLayout';

var Drawer = Platform.OS === 'ios' ? DrawerLayout : DrawerLayoutAndroid;

Don't forget to remove all the Android changes for the gesture handler installation (HOC's and MainActivity)

@arunmenon1975
Copy link
Contributor Author

Ok. I had this in mind as a possible solution for Android, though i was thinking of attempting a Swift/Obj-C integration for IoS using something like whats mentioned here or here, albeit with a risk of several bugs since i have zero experience in Swift/Obj-C and and integration of external libraries not written for React Native into the core code. Maybe for another day. A second option was a JS-only drawer for IoS but then i think it would be better to have 'native' solutions for both platforms as the better compromise especially if they also share similar props.

So, for now i will be going by your suggestion and retaining the gesture handler in IoS and falling back to the shipped DrawerLayoutAndroid for Android. During dev, i am actually retaining gesture handler in Android and opening/closing the drawer programatically via buttons and links(the only issue besides the flashing screen during the back action is that the transparent overlay cannot be interacted with as a point to dismiss the SideDrawer; only explicit buttons with the close method can be used which in this case would mean i would need a close button in the drawer).

Gesture handler though would have been a nice addition since i was planning on using it for static bottom tabs (JS) elsewhere in my app and attempting a swipe-to-change tab interaction. Maybe PanResponder and similar modules shipped with React Native could be used instead.

Thanks for looking into it. I am closing the issue since there is a logical closure to the queries.

@grahammendick
Copy link
Owner

Thanks again for your hard work on this issue and I’m glad you’re happy with the solution.

I agree that there should be a 'native' drawer on iOS too. The iOS platform doesn’t have a native drawer, but there should be one for React Native that uses a UIViewController. Until that happens, the DrawerLayout is good enough.

Do you see why I don’t think a Drawer should be part of a navigation library? It’s a UI component just like a Button. Because it’s part of the main screen, you can use props to pass data to it and respond to taps.

Yes, you can use the PanResponder that’s baked into React Native instead of gesture handler.

I’m not sure if I should raise an issue about using the Navigation router and gesture handler together. I don’t want to because I think it’s a gesture handler bug, not a Navigation router one. You see, I copied the React Native’s Modal code to build the native screens on Android but it turns out that gesture handler doesn’t work with the Modal component on Android.

@arunmenon1975
Copy link
Contributor Author

IoS should have a native drawer, definitely yes. From what i recall from a cursory reading of the guidelines, they are not keen on having primary elements that are 'hidden' away. They re-direct any feature requests for a Drawer to Tab-based solutions as a viable option or suggest restructuring the solution ground-up to not have any such interact-able hidden elements. Since it was a while ago i am sure the guidelines would have been amended here and there to keep up with the times. React Native does an interesting merge of two varying platforms with varying guidelines for quite a few elements, components and pattens but i guess they can only go so much before the possibilities of bugs to appear increase exponentially.

Still not fully convinced regarding the separation of the obvious correlation of a Drawer and a navigation solution. But this is from a zoomed-out perspective and admittedly also from practical usage as seen today. I guess i'll have to agree with you when taken as individualised components in a navigation solution. They do not have a firm presence and theoretically can easily be used for just about anything that has nothing to do with navigation in general. It depends on the perspective ultimately.

Very interesting to know about the gesture handler bug you have linked to. I had no clue. In fact this comment is the exact same fix - open a new screen and have a modal popup in it - i accidentally stumbled upon in a recent project(the same project that i am currently doing actually, but using react-navigation with gesture handler) where i had the same perplexing issue in a modal where i had a small wizard-like flow for getting user input. I have subscribed to the issue to see if any fix appears that may directly or indirectly help with the integration here in this library sometime in the future. Till then its DrawerLayoutAndroid for Android and PanResponder for both with gesture-handler included only in IoS and only for the DrawerLayout utility.

@grahammendick
Copy link
Owner

I don't think iOS should have a native drawer because, like you say, it's not the recommended UX on the platform. Android and iOS have different primitives. The Navigation router only wraps the underlying native APIs. That's why it only has a TabBar on iOS. However I do think there should be a React Native community Drawer component that uses a UIViewController.

@arunmenon1975
Copy link
Contributor Author

arunmenon1975 commented Jul 13, 2019

I’m not sure if I should raise an issue about using the Navigation router and gesture handler together. I don’t want to because I think it’s a gesture handler bug, not a Navigation router one. You see, I copied the React Native’s Modal code to build the native screens on Android but it turns out that gesture handler doesn’t work with the Modal component on Android.

I think that is a fair comment, come to think of it. Technically they have mentioned installation procedures for broadly two classifications of Android apps:

  • those using native navigation libraries like RNN, and by that definition Navigator as well
  • other regular Android apps

It may interest the author(s) and maintainers that there is a scenario where it doesn't seem to work and perhaps they may wish to make changes to accommodate navigator solutions such as these. Given some time and a deeper dive into gesture handler/Navigator(so that not all content go over my head in the event of questions or comments ) i may consider opening an issue in the gesture-handler repo sometime in the future. I cannot commit but i see the benefit for sure.

A couple of takeaways(from a technical standpoint) that i get is so far is

The gesture handler expects to be inside a ReactRooView but only the first Screen is. The later screens aren't.

[essentially gesture handler may need to factor in that certain screens in a native Navigation library may have views that are not necessarily inside aReactRootView. Not very unlike the Modal issue currently tracked in issue # 139 in their repo]

Even if we get it working I think we'd still have the secondary issue of the drawer flash when you navigate back.

[Back handlers may react differently depending on the library. How is this to be considered]

@grahammendick
Copy link
Owner

It'd be good if you raised the issue on gesture handler. If you don't feel like creating a new issue you could explain the new scenario in the existing Modal issue.

@arunmenon1975
Copy link
Contributor Author

Ive opened a new issue at the RNGH repo as well (issue 688):

Unable to fully integrate react-natve-gesture-handler with the Navigation library.

I guess, depending on the analysis, it may or may not be merged with issue 139:
[Android] Doesn't work in a modal

@grahammendick
Copy link
Owner

Thanks, good job!

@arunmenon1975
Copy link
Contributor Author

Well, after resolutely refusing to give up due to some slow internet connectivity over the weekend, i was finally able to start out a new React Native project with only the Navigation libraries as dependencies.

This was because i was getting some issues with the openDrawer and closeDrawer methods of DrawerLayoutAndroid not opening the SideDrawer as expected in the inner screens in my existing project (that also had some other dependencies like firebase etc). Only the transparent overlay was getting displayed. The gestures worked fine however.

The fresh new install confirmed that these are reproducible issues and not because of any conflicts with other packages.

I just wanted to know if it indeed worked for you in the inner screens as you mentioned in one of the comments:

So a different solution (I've tried it and it works) is to use the DrawerLayoutAndroid component. It wraps the native DrawerLayout so is probably a better choice for Android anyway. For iOS you can keep using the DrawerLayout component from gesture handler.

My package.json entries:

{
  "name": "menuapp",
  "version": "0.0.1",
  "private": true,
  "scripts": {
    "start": "node node_modules/react-native/local-cli/cli.js start",
    "test": "jest"
  },
  "dependencies": {
    "navigation": "^5.2.0",
    "navigation-react": "^4.1.1",
    "navigation-react-native": "^5.3.0",
    "react": "16.8.3",
    "react-native": "0.59.10"
  },
  "devDependencies": {
    "@babel/core": "^7.5.4",
    "@babel/runtime": "^7.5.4",
    "babel-jest": "^24.8.0",
    "jest": "^24.8.0",
    "metro-react-native-babel-preset": "^0.55.0",
    "react-test-renderer": "16.8.3"
  },
  "jest": {
    "preset": "react-native"
  }
}

Welcome.js:

import React, { Component } from 'react';
import { NavigationContext } from 'navigation-react';
import { Button, SafeAreaView, StyleSheet, Text, View, DrawerLayoutAndroid } from 'react-native';

class Welcome extends Component {
  _openDrawer=()=>{
    this.drawer.openDrawer();
  }
  _closeDrawer=()=>{
    this.drawer.closeDrawer();
  }
  renderDrawer = () => {
    return ( <View style={{flex:1}}>
        <Button title="Close Opened SideDrawer" onPress={this._closeDrawer}/>
      </View>);
  };
  render() {
    return (
      
      <NavigationContext.Consumer>
        {({stateNavigator}) => (
          <DrawerLayoutAndroid
            ref={drawer => {
              this.drawer = drawer;
            }}
          drawerWidth={200}
          drawerPosition={DrawerLayoutAndroid.positions.Right}
          drawerType='front'
          useNativeAnimations={true}
          drawerBackgroundColor="#ddd"
          renderNavigationView={this.renderDrawer}>
          <SafeAreaView style={styles.container}>
            <Text>
              Welcome
            </Text>
            <Button title="To Menu Screen" onPress={() => stateNavigator.navigate('menu')}/>
            <Button title="Open SideDrawer" onPress={this._openDrawer}/>
          </SafeAreaView>
          </DrawerLayoutAndroid>
        )}
      </NavigationContext.Consumer>
    );
  }
}
const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#E91E63'
  },
});

export default Welcome;

Menu.js:

import React, { Component } from 'react';
import { NavigationContext } from 'navigation-react';
import { Button, SafeAreaView, StyleSheet, Text, View, DrawerLayoutAndroid } from 'react-native';

class Menu extends Component {
  _openDrawer=()=>{
    this.drawer.openDrawer();
  }
  _closeDrawer=()=>{
    this.drawer.closeDrawer();
  }
  renderDrawer = () => {
    return ( <View style={{flex:1}}>
        <Button title="Close Opened SideDrawer" onPress={this._closeDrawer}/>
      </View>);
  };
  render() {
    return (
      <NavigationContext.Consumer>
        {({stateNavigator}) => (
          <DrawerLayoutAndroid
            ref={drawer => {
              this.drawer = drawer;
            }}
          drawerWidth={200}
          drawerPosition={DrawerLayoutAndroid.positions.Right}
          drawerType='front'
          useNativeAnimations={true}
          drawerBackgroundColor="#ddd"
          renderNavigationView={this.renderDrawer}>
          <SafeAreaView style={styles.container}>
            <Text>
              Menu
            </Text>
            <Button title="To Welcome Screen" onPress={() => stateNavigator.navigate('welcome')}/>
            <Button title="Open SideDrawer" onPress={this._openDrawer}/>
          </SafeAreaView>
          </DrawerLayoutAndroid>
        )}
      </NavigationContext.Consumer>
    );
  }
}
const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#E91E63'
  }
});

export default Menu;

Do note that it does work on the initial screen as expected: both the gesture and the tap functionalities.

While gestures are important, i intend to have a hamburger menu that would always be the primary navigation artefact intended to be used by people who may not be aware of the gesture capability that also exists as an additional aid. This would require using the openDrawer and closeDrawer methods.

if the issue is reproducible can it because of something that needs to be done here in the Navigation library? It does sound like an issue that may need to reside at the react-native repo since these methods are exposed by the DrawerLayoutAndroid module of react-native. i just wanted to check before proceeding.

@grahammendick
Copy link
Owner

Thanks for the update. It worked for me on the inner screens but I only tried it with the gesture. I didn't try calling openDrawer and closeDrawer. I'll reopen this issue here while I investigate what's wrong.

@grahammendick grahammendick reopened this Jul 15, 2019
@grahammendick
Copy link
Owner

The Android DrawerLayout expects to be attached to a window before the layout runs. If it's attached after then it thinks the layout hasn't run and doesn't open (when you call openDrawer). If we weren't using React Native then Android would ensure the correct sequencing automatically.

The way I'm using React Native is that the new screens are rendered in React first and then I start an Activity and set the screen content. So React Native does the layout before I attach the screen to the window. That's why the drawer doesn't open on the second screen.

I'm working on a solution inspired by the ViewPager component. It does a second layout pass after it's attached to a window. I've copied this 're-layout' code into the SceneView and the Drawer opens on the Menu screen!

I need to do some more testing but I wanted to update you with progress. Here's the new code for the SceneView if you want to try it out. You can see that I'm relying on DrawerLayourAndroid being the first child of the scene. Is that a valid assumption?

public class SceneView extends ViewGroup {

    public SceneView(ThemedReactContext context) {
        super(context);
    }

    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        getChildAt(0).requestLayout();
        post(measureAndLayout);
    }

    private final Runnable measureAndLayout =
        new Runnable() {
            @Override
            public void run() {
                getChildAt(0).measure(
                        MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.EXACTLY),
                        MeasureSpec.makeMeasureSpec(getHeight(), MeasureSpec.EXACTLY));
                getChildAt(0).layout(getLeft(), getTop(), getRight(), getBottom());
            }
        };

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
    }
}

@arunmenon1975
Copy link
Contributor Author

Thanks for clarifying on why the core issue happens. And for re-opening the issue. The core context (RNGH related) for the issue was resolved but this is going above and beyond.

This explanation does help get some additional insights why the initial scene was adapted differently as well. I can also see how this helps in the relatively easy and predictable handling of the back navigation: the Navigation native core sits in its own thin layer atop React Native and each 'crumb' or view that is constructed finally is just an array populated after the initial view - managed via React Native but taken over by Navigation- is displayed.

Have not actively used ViewPager so far in any project though i was aware of its presence(and not much more) in React Native and core Android. Interesting to note that it uses fragments and hence seems to have the ability to hold more than than just lists. This seems to be a valid - or possibly only option in this scenario - to ensure that the view information is current.

I will try out (no harm in doing so: i am on a fresh install) just to get a feel of the code though i cannot be sure if it will be successful.

Regarding the assumption, currently yes. For my current project I have some flexibility so caveats are perfectly fine.

I guess if this is a strict requirement for full support for a DrawerLayourAndroid in the long run, a HoC can easily be shipped along (similar to how RNGH handles it via gestureHandlerRootHOC ) or simple documentation could suffice instructing users to roll out their own HoC. Just a thought.

@grahammendick
Copy link
Owner

From the DrawerLayout Android doc

DrawerLayout acts as a top-level container for window content

Makes things easier because we don't need to worry about HOC

@grahammendick
Copy link
Owner

Just to be clear, the Navigation router doesn't use the ViewPager. I've lifted the re-layout code out of the ViewPager and plonked it into the SceneView. Initial testing looks good, I'll do some more tomorrow

@arunmenon1975
Copy link
Contributor Author

Thank you. I just did the same and it does work perfectly fine. Ive introduced a few more scenes to verify and the drawer works as expected, both the gesture and programmatic invocation.

Please do let me know if i can close this issue or perhaps you may wish to do it after testing.

@grahammendick
Copy link
Owner

Thanks for testing. I've just merged the fix in and closed the issue. I'll do a new release shortly

@grahammendick
Copy link
Owner

Just released navigation-react-native v5.3.1!

@grahammendick
Copy link
Owner

I've released navigation-react-native v6.0.0. The Navigation router now supports the React Native Gesture Handler. I encourage you to upgrade.

The NavigationStack renders scenes inside Fragments instead of Activities so you can't use the DrawerLayoutAndroid component anymore. Instead you can use React Native's DrawerLayout component on both platforms. To install React Native Gesture Handler follow the normal installation instructions (DO NOT follow the special instructions for RNN).

I wrote a migration guide in the Release Notes. Let me know if you have any questions

@arunmenon1975
Copy link
Contributor Author

Ok. This does open out a whole lot of possibilities and i will need a re-think of my current in-progress implementation.

I am currently on v5.6.1 and will upgrade to v6. I am still in early phases so flexible enough to do an upgrade without thinking too much about breaking changes.

Just FYI (and for other readers browsing through), unless i've read it wrong, i am assuming you meant

...use RNGH's DrawerLayout...

@grahammendick
Copy link
Owner

I did mean that, thanks for the correction 👍

grahammendick added a commit that referenced this issue Sep 8, 2021
Applied the fix from over 2 years ago to the SceneView again and it works, #266!
Not sure why it didn't work for Fragments back then but it does now, #264 (comment)?
@grahammendick
Copy link
Owner

Hey @arunmenon1975 I’ve added support for DrawerLayoutAndroid in navigation-react-native v8.1.0. The funny thing is I just tried the original fix from #266 again and it works now.

So here's a Drawer component that works on both platforms and is 100% native on Android

import {Platform, DrawerLayoutAndroid} from 'react-native';
import DrawerLayout from 'react-native-gesture-handler/DrawerLayout';

const Drawer = Platform.OS === 'android' ? DrawerLayoutAndroid : DrawerLayout;

@arunmenon1975
Copy link
Contributor Author

This is great news and perfect timing as well for me. My current project is scoped rather large at the moment: a PWA solution (with navigation-react powering the routing) as well as native Android/Ios apps. I went a monorepo route for all the benefits it affords and found that the navigation suite of packages is the only routing solution out there that could handle routing requirements across the device landscape (thus my projects landscape) via the same monorepo philosophy of shared packages and cohesive residency while following a uniform approach all through. Less stress/fatigue and better productivity when a suite of products follow a common methodology.

Due to some project requirement changes - as recent as the last couple of days - i had to basically go back to using Nextjs for my PWA which meant that i had to drop navigation-react. Its a trade-off with a lot of factors weighing in and the decision came after considerable thought.

However, navigation-react-native is the decided solution for my mobile apps. in fact i had already installed it in my monorepo even before starting out on the react-native project :) I like its unique approach and the fact that the navigation is native makes it a perfect optimal routing solution. Leaving aside the easy routing and now the drawer improvements, i would have chosen navigation-react-native just for its tabs.

@grahammendick
Copy link
Owner

grahammendick commented Sep 9, 2021

That's great to hear and thanks for the support.

Next.js should replace their router with my Navigation router. The Navigation router is much better suited for Next.js.

Next.js is meant to be file-based navigation. One file per page. But its support for nested routes breaks file-based navigation. Take the example of a list of posts with the selected post's comments displayed inline. This should be a single file because it’s a single page. But if you want your routes to be 'pages/post' for the list and 'pages/post/[pid]' for the comments then you have to create two files!

If Next.js used the Navigation router you’d have only one file because you can register both routes for the same file. You’d configure a combined route of 'pages/post+/{pid}' so it matches both 'pages/post' and ‘pages/post/3'.

@arunmenon1975
Copy link
Contributor Author

I agree with the simplicity and ease factors. But i guess Next was also looking at convention-over-convenience probably: a file system based routing from a mental model perspective is easier to understand and also more familiar(for example in an IDE or an OS file explorer) but does come at a cost as you described above.

If NextJs does decide at some point to use the navigation router it will have solved immediately the only blip in my otherwise perfect monorepo structure (very different routing solutions...everything else seems so aligned). And having used it i am sure my thoughts on the ease, simplicity and uniformity will be reflected by other new users as well.

My reason for using NextJs has nothing to do with its routing solution however. The other big features it ships with are quite useful, which otherwise requires a lot of effort and orchestrating to get right. Generally good DX, static generation, incremental static generation, SSR, in built web vitals, in built asset optimisations etc etc. Has all the required and more SSR-related functionalities and you rarely need to reach out to some other package or roll out on your own if a core feature was required.

I suspect though that Next(and its user-base) is too invested into its current routing solution to consider immediately changing it. But i also notice that they are very keen in constantly improving the product as a whole so there definitely is a possibility that a future version could have the navigation router suite instead if they even felt the need to improve the routing feature. That will be quite a major change though.

Also as a side-note, the creator of Razzle created After.js after having a somewhat similar sentiment regarding the file-system based routing of Next, as mentioned in the repo description .

@grahammendick
Copy link
Owner

grahammendick commented Sep 11, 2021

Routing in Next.js is an underwhelming implementation of a nice idea. I wrote about how they can get rid of nested files and still keep their file-based approach

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants