I'm hoping this is a known problem as I can't provide much to go on to get help solving it.
I'm using react-navigation for my app with the following setup:
AppContainer
Splash Screen
Setup Screen (A stack navigator)
Main screen (A stack navigator)
When my app starts it goes to the splash screen which decides if this is the first time running or not and depending on this decision calls this.props.navigation.navigate() with either main screen or setup screen.
So far this all works and is fairly standard.
When my app is launched for the first time the user is sent to the setup screen where they navigate through a series of screens entering data and selecting a next button to proceed to the next screen. This is where the problem occur. My first screen simply has some text and a next button (which is a regular button component) which when clicked calls this.props.navigation.push('nextviewname', {data: data}) to move to the next view.
The next view contains a textinput as well as back and next buttons which is where I'm having problems. When I reach this screen after freshly installing a release version of my app onto my Android X phone none of the inputs work. I can't:
Click next of back
Click the back arrow in the top left that is part of the header
Click the text input (the cursor does briefly show up in the text input but the keyboard never appears)
Click the hardware back key
On very rare occasions some of the above does work (e.g. the text input will sometimes work) and sometimes I'll even make it to the next step of my setup but it's rare that I make it all the way through
Weirdly this all works when I'm debugging my app on my phone.
Update: I've just tested on an Android 9 emulator and I'm getting the same issue.
Update 2: This is getting weird, when the app is in a broken state I can still bring up the react native debug menu however when I click Toggle Inspector nothing happens (i.e. I don't get the inspector UI). It's looking like this is somehow breaking everything.
Has anyone seen/solved this issue before? At the moment it's effectively made my app useless.
Update 3: Some code to hopefully make things clearer:
const SetupUser = createStackNavigator(
{
SetupUser: WelcomeScreen,
SetupName: UserName,
SetupCurrentWeight: CurrentWeight,
SetupGoalWeight: GoalWeight,
SetupGoalDate: GoalDate,
Summary: Summary,
LogWeight: LogWeight,
},
{
defaultNavigationOptions: {
headerStyle: {
backgroundColor: '#001830',
},
headerTintColor: '#fff',
headerTitleStyle: {
fontWeight: 'bold',
},
},
},
);
const MainApp = createStackNavigator(
{
LogWeight: LogWeight,
LogWeightSummary: LogWeightSummary,
},
{
defaultNavigationOptions: {
headerStyle: {
backgroundColor: '#001830',
},
headerTintColor: '#fff',
headerTitleStyle: {
fontWeight: 'bold',
},
},
},
);
export default createAppContainer(
createSwitchNavigator(
{
MainApp: MainApp,
SplashScreen: SplashScreen,
SetupUser: SetupUser,
},
{
initialRouteName: 'SplashScreen',
},
),
);
In getting this code snippit together (I've removed the tab navigator as the error is still there even without it) I think I've managed to track down the source of the issue however I'm still not sure how to fix it. The first view loaded is the splash screen which looks like this:
export default class SplashScreen extends React.Component {
constructor(props) {
super(props);
GoBinder.verifyDatabase()
.then(GoBinder.getLastUser())
.then(user => {
this.props.navigation.navigate(
user.user == null ? 'SetupUser' : 'MainApp',
);
})
.catch(error => {
GoBinder.toast('Error while checking initial user state: ' + error);
});
}
render() {
return (
<View style={styles.container}>
<ActivityIndicator />
<StatusBar barStyle="default" />
</View>
);
}
}
In the above code, GoBinder.verifyDatabase() and GoBinder.getLastUser() are calls to native code which perform some database operation to get things setup and check to see if there are any existing users. I believe the issue is this.props.navigation.navigate is firing too quickly which is causing react-navigation to load the next screen but get messed up in the process.
I've tried following the suggestions in this post https://www.novemberfive.co/blog/react-performance-navigation-animations about moving blocking code into InteractionManager.runAfterInteractions under componentDidMount however this made no difference. However, it I manually trigger the move to the new screen using a button everything works correctly so its really looking like the act of programatically changing screens is messing things up.
Update 4:
Looks like I jumped the gun a bit, its still freezing up a fair bit, moving my answer into an update:
So I'm not sure if this is the best solution but I have found a workaround. I am now calling my database code in InteractionManager.runArfetInteraction() as suggested in https://www.novemberfive.co/blog/react-performance-navigation-animations. I am then calling setState with the result of the DB functions to store the result. Finally, I am using a callback on setState to trigger the actual navigation. My complete working code is:
componentDidMount() {
InteractionManager.runAfterInteractions(() => {
GoBinder.verifyDatabase()
.then(
GoBinder.getLastUser().then(user => {
this.setState(
{
user: user.user,
},
() => {
this.props.navigation.navigate(
this.state.user == null ? 'SetupUser' : 'MainApp',
);
},
);
}),
)
.catch(error => {
GoBinder.toast('Error while checking initial user state: ' + error);
});
});
}
Thanks for your help
Related
I am trying to hide app content when the user tries to navigate away from the app, like how banking apps or outlook works. I am using appstate inactive for ios, and appstate blur+focus for android. This works on Android when the user pulls down the notification panel or swipes up to multitasking tray or home through a gesture.
However, if the android phone has soft buttons for navigation instead of gestures, the app doesnt show the security screen on blur. I suppose react native loses control of the app too soon for that to happen. The blur event is still triggered, but the UI remains unchanged.
I have a useEffect in a custom hook that changes the state on blur, which triggers the UI update;
const [appStateVisible, setAppStateVisible] = useState(true);
const appState = useRef(AppState.currentState);
useEffect(() => {
const androidFocusSubscription =
Platform.OS === 'android' &&
AppState.addEventListener('focus', () => {
setAppStateVisible(true);
});
const androidBlurSubscription =
Platform.OS === 'android' &&
AppState.addEventListener('blur', () => {
setAppStateVisible(false);
});
return () => {
if (androidFocusSubscription && androidBlurSubscription) {
androidFocusSubscription.remove();
androidBlurSubscription.remove();
}
};
}, []);
return appStateVisible;
And then on App.tsx, I am calling this hook to decide whether to show the security image instead of the actual app content.
The alternative would be to use FLAG_SECURE in the java code to show a white screen when the app isnt active, but I want to show a custom screen with the app logo instead of a white screen.
Any help would be greatly appreciated. Thank you.
I have an App with two Views, blue view must be shown only when app is in active state (foreground) and the green view must be shown when app is put on background (e.g. clocking on hardware square button on Android).
The following picture shows what I currently get when I put the App in background state:
The following one is what I want to do when App is put on background (on iOS works, on Android not so well):
And this is the implementation.
Please, ignore for the moment that the component for iOS is almost identical to that for Android except for the style of the View. I was doing other tests with different components and for the moment it's okay for me to keep them like this. The difference between the two components (iOS / Android) lies in the style, because in iOS the zIndex works, while in Android I have to use elevation to overlap the views.
const showSecurityScreenFromAppState = appState =>
['background', 'inactive'].includes(appState);
const withSecurityScreenIOS = Wrapped => {
return class WithSecurityScreen extends React.Component {
constructor(props) {
super(props);
}
state = {
showSecurityScreen: showSecurityScreenFromAppState(AppState.currentState),
};
componentDidMount() {
AppState.addEventListener('change', this.onChangeAppState);
}
componentWillUnmount() {
AppState.removeEventListener('change', this.onChangeAppState);
}
onChangeAppState = nextAppState => {
const showSecurityScreen = showSecurityScreenFromAppState(nextAppState);
this.setState({showSecurityScreen});
};
//this.state.showSecurityScreen
render() {
return (
<>
<View style={[styles.container, {zIndex: this.state.showSecurityScreen ? 1 : -1}]}>
<View style={{flex: 1, backgroundColor: globalStyles.colors.customerGreen}}>
<View style={styles.upperView}>
<Text style={styles.upperViewText}>MyApp</Text>
</View>
</View>
</View>
<Wrapped {...this.props}/>
</>
);
}
};
};
const withSecurityScreenAndroid = Wrapped => {
return class WithSecurityScreen extends React.Component {
constructor(props) {
super(props);
}
state = {
showSecurityScreen: showSecurityScreenFromAppState(AppState.currentState),
};
componentDidMount() {
AppState.addEventListener('change', this.onChangeAppState);
}
componentWillUnmount() {
AppState.removeEventListener('change', this.onChangeAppState);
}
onChangeAppState = nextAppState => {
const showSecurityScreen = showSecurityScreenFromAppState(nextAppState);
this.setState({showSecurityScreen});
};
//this.state.showSecurityScreen
render() {
return (
<>
<View style={[styles.container, {elevation: this.state.showSecurityScreen ? 1 : -1}]}>
<View style={{flex: 1, backgroundColor: globalStyles.colors.customerGreen}}>
<View style={styles.upperView}>
<Text style={styles.upperViewText}>MyApp</Text>
</View>
</View>
</View>
<Wrapped {...this.props}/>
</>
);
}
};
};
export const withSecurityScreen =
Platform.OS === 'ios' ? withSecurityScreenIOS : withSecurityScreenAndroid;
There is not problems in the code, it works very well in iOS, but on Android sometimes it works and sometimes it doesn't. I suspect the cause is that Android OS, when the square button is pressed, it takes a screenshot of the current view and uses it as a background image of the App. Sometimes, however, it happens that the change of layer (elevation) is faster than the Android screenshot and therefore I can see the green screen in the background, as Android is using it as an image.
Is there a way to synchronize these operations? That is: can I make sure that when I press the square button, Android waits for the view to change and then let it go in the background?
Note: I have tested the functioning of elevation when the app is in the foreground, simply inverting the values of elevation when it is in the background or in the foreground, I assure that elevation is working.
Note: in the following animation, when the View in background tasks is white, is a fail. When the View becomes green in background, is a success. Why is random?
Update 1
I realize now that the question may be badly set.
First of all I used incorrect terms to refer to the background status of the apps, the view I speak of in Android is called Recent Apps (or Task View). Then, secondly, in Recent Apps (Task View) what Android does, after pressing the square or circle button, a screenshot of the current view and then shows the Tasks View, using as the image for my App, the screenshot that he just did. The problem is quite annoying, because (as shown in the GIF above) sometimes the screenshot is done AFTER the change of view. Sometimes instead (and often times) the screenshot occurs BEFORE the change of view (the green one).
This is why sometimes in Task View I see the blue screen and sometimes the green one. It's almost a competition problem, let's say. My approach is probably not suitable, I should find another one. Is it possible to manually change the image used in Task View for my App?
You can try like this:
onChangeAppState = async nextAppState => {
const showSecurityScreen = await showSecurityScreenFromAppState(nextAppState);
this.setState({showSecurityScreen});
};
I have created an app and added splach screen. It takes 1 second to load the app on Android Emulator. However after I publish the app in the store, it takes 4 seconds to load.
This is pretty annoying for such a simple app.
I was thinking it was because of _loadResourcesAsync func to load pictures. Therefore I commented out those lines but nothing has changed.
Any recommendation to speed up my app launch.
Here you can find my app.js
import React from 'react';
import { Platform, StatusBar, StyleSheet, View } from 'react-native';
import { AppLoading, Asset } from 'expo';
import AppNavigator from './navigation/AppNavigator';
export default class App extends React.Component {
constructor(props) {
super(props);
this.state = {
isLoadingComplete: false,
};
}
render() {
if (!this.state.isLoadingComplete && !this.props.skipLoadingScreen) {
return (
<AppLoading
startAsync={this._loadResourcesAsync}
onError={this._handleLoadingError}
onFinish={this._handleFinishLoading}
/>
);
} else {
return (
<View style={styles.container}>
{Platform.OS === 'ios' && <StatusBar barStyle="default" />}
<AppNavigator />
</View>
);
}
}
_loadResourcesAsync = async () => {
return Promise.all([
Asset.loadAsync([
// require('./assets/images/big_bottle.png'),
// require('./assets/images/bottle.png'),
// require('./assets/images/coffee.png'),
// require('./assets/images/juice.png'),
// require('./assets/images/menu.png'),
// require('./assets/images/tea.png'),
// require('./assets/images/water-glass.png'),
]),
]);
};
_handleLoadingError = error => {
// In this case, you might want to report the error to your error
// reporting service, for example Sentry
console.warn(error);
};
_handleFinishLoading = () => {
this.setState({ isLoadingComplete: true });
};
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
},
});
Based on my experience, hybrid apps, in general, tend to launch slower.
Based on my tests, even a completely blank Expo app, based on the device performance, can take anywhere from 1-4s to launch. This is the time it takes from the moment the user taps (opens) the app to the time the splash screen gets hidden (and the first app screen is visible).
What you can do to speed it up is a really broad topic. Here are two recommendations, that helped a lot for a project of mine:
Cashe your assets.
Bundling assets into your binary will provide for the best user experience as your assets will be available immediately. Instead of having to make a network request to the CDN to fetch your published assets, your app will fetch them from the local disk resulting in a faster, more efficient loading experience.
Always load a cached version of your app first.
You can configure the "updates" section in your app.json to always starts with the cached version of your app first (fallbackToCacheTimeout: 0) and continue trying to fetch the update in the background (at which point it will be saved into the cache for the next app load).
Unfortunately, as of today, it looks like we're kind of limited to what we can do to improve further the initial loading time on a blank app. There isn't really a reliable way to know how long the 'check for updates' process takes, the general React Native initialization time, the 'load all the JS' time.
There is a feature request for tools for improving the startup time, would be great if the Expo team introduces something similar anytime in the future.
I'm making a "joystick" app on android for my RC-car and I want be able to press two or more buttons at the same time to allow more precise control.
But, right now I can only press one button and the others do not respond until I release pressed button. They are even do not change their opacity, so it's not a problem in the app's logic. And click handlers also do not perform time critical operations, so I don't think that UI is just stuck.
Here's the code for my buttons:
<R.View>
{this.btn(Car.Transmission.Forward, 'Forward')}
</R.View>
<R.View flexDirection="row">
{this.btn(Car.Transmission.Left, 'Left')}
{this.btn(Car.Transmission.Stop, 'Stop')}
{this.btn(Car.Transmission.Right, 'Right')}
</R.View>
<R.View>
{this.btn(Car.Transmission.Backward, 'Backward')}
</R.View>
btn(transmission, text) {
return <MyButton
onPressIn={() => {
this.drive(transmission)
}}
onPressOut={() => {
this.drive(Car.Transmission.Neutral)
}}
text={text} />
}
export class MyButton extends React.Component {
render() {
var {onPressIn, onPressOut, text, disabled} = this.props
let textStyle = disabled ? [style.button, style.disabled] : [style.button]
return (
<R.TouchableOpacity onPressIn={onPressIn} onPressOut={onPressOut} disabled={disabled}>
<R.Text style={textStyle} >{text}</R.Text>
</R.TouchableOpacity>
)
}
}
Maybe I should not be using TouchableOpacity?
EDIT: I've almost done it, but... when I receive touch location in onPanResponderMove callback my nativeEvent.locationY value jumps like a crazy (161, 0, 163, 11, 29, ...) when I click at the same point on the screen... First, I thought this is because of USB-cable and charging, but over Wi-Fi I get the same results. That's strange. Any ideas?
So i am building an app where there will be a page just for a gif animation. I want so that after that gif animation is done, it will automatically go to the next page. How do i do that?
Am i supposed to use a timer, so that the gif page will only appear for 3 seconds and then it goes to the next page? Or what is the best way to approach this?
how i manage routing:
render() {
return (
<Navigator
renderScene={this.renderScene.bind(this)}
navigator={this.props.navigator}
/>
);
}
renderScene(route, navigator) {
return (
...);
}
Well it's not clear from your question what navigation scheme you're using, but regardless, the process is trivially done if the length of the GIF is known. If it's unknown, I suspect you're in for a lot more work, but otherwise simply:
const GIF_LENGTH = 5 * 1000; // 5 second gif
class MyComponent extends React.Component {
componentDidMount() {
setTimeout(() => nav.push({ name: 'NextPage' }), GIF_LENGTH);
}
render() {
return < gif display here >;
}
}
Obviously replace nav.push(...) with the particulars of however you manage routing in your application.