Improved list update performance

This commit is contained in:
Arnaud Vergnet 2020-04-12 17:05:38 +02:00
parent 08de6765a7
commit 47fd8b7474
6 changed files with 244 additions and 190 deletions

View file

@ -1,107 +1,169 @@
import * as React from 'react';
import {Avatar, Card, Text, withTheme} from 'react-native-paper';
import {Avatar, List, ProgressBar, Surface, Text, withTheme} from 'react-native-paper';
import {StyleSheet, View} from "react-native";
import ProxiwashConstants from "../../constants/ProxiwashConstants";
import i18n from "i18n-js";
import AprilFoolsManager from "../../managers/AprilFoolsManager";
type Props = {
item: Object,
onPress: Function,
isWatched: boolean,
isDryer: boolean,
height: number,
}
/**
* Component used to display a proxiwash item, showing machine progression and state
*
* @param props Props to pass to the component
* @return {*}
*/
function ProxiwashListItem(props) {
const {colors} = props.theme;
let stateColors = {};
stateColors[ProxiwashConstants.machineStates.TERMINE] = colors.proxiwashFinishedColor;
stateColors[ProxiwashConstants.machineStates.DISPONIBLE] = colors.proxiwashReadyColor;
stateColors[ProxiwashConstants.machineStates["EN COURS"]] = colors.proxiwashRunningColor;
stateColors[ProxiwashConstants.machineStates.HS] = colors.proxiwashBrokenColor;
stateColors[ProxiwashConstants.machineStates.ERREUR] = colors.proxiwashErrorColor;
const icon = (
props.isWatched ?
<Avatar.Icon
class ProxiwashListItem extends React.Component<Props> {
stateColors: Object;
stateStrings: Object;
title: string;
constructor(props) {
super(props);
this.stateColors = {};
this.stateStrings = {};
this.updateStateStrings();
let displayNumber = props.item.number;
if (AprilFoolsManager.getInstance().isAprilFoolsEnabled())
displayNumber = AprilFoolsManager.getProxiwashMachineDisplayNumber(parseInt(props.item.number));
this.title = props.isDryer
? i18n.t('proxiwashScreen.dryer')
: i18n.t('proxiwashScreen.washer');
this.title += ' n°' + displayNumber;
}
shouldComponentUpdate(nextProps: Props): boolean {
const props = this.props;
return (nextProps.theme.dark !== props.theme.dark)
|| (nextProps.item.state !== props.item.state)
|| (nextProps.item.donePercent !== props.item.donePercent)
|| (nextProps.isWatched !== props.isWatched);
}
updateStateStrings() {
this.stateStrings[ProxiwashConstants.machineStates.TERMINE] = i18n.t('proxiwashScreen.states.finished');
this.stateStrings[ProxiwashConstants.machineStates.DISPONIBLE] = i18n.t('proxiwashScreen.states.ready');
this.stateStrings[ProxiwashConstants.machineStates["EN COURS"]] = i18n.t('proxiwashScreen.states.running');
this.stateStrings[ProxiwashConstants.machineStates.HS] = i18n.t('proxiwashScreen.states.broken');
this.stateStrings[ProxiwashConstants.machineStates.ERREUR] = i18n.t('proxiwashScreen.states.error');
}
updateStateColors() {
const colors = this.props.theme.colors;
this.stateColors[ProxiwashConstants.machineStates.TERMINE] = colors.proxiwashFinishedColor;
this.stateColors[ProxiwashConstants.machineStates.DISPONIBLE] = colors.proxiwashReadyColor;
this.stateColors[ProxiwashConstants.machineStates["EN COURS"]] = colors.proxiwashRunningColor;
this.stateColors[ProxiwashConstants.machineStates.HS] = colors.proxiwashBrokenColor;
this.stateColors[ProxiwashConstants.machineStates.ERREUR] = colors.proxiwashErrorColor;
}
onListItemPress = () => this.props.onPress(this.title, this.props.item, this.props.isDryer);
render() {
const props = this.props;
const colors = props.theme.colors;
const machineState = props.item.state;
const isRunning = ProxiwashConstants.machineStates[machineState] === ProxiwashConstants.machineStates["EN COURS"];
const isReady = ProxiwashConstants.machineStates[machineState] === ProxiwashConstants.machineStates.DISPONIBLE;
const description = isRunning ? props.item.startTime + '/' + props.item.endTime : '';
const stateIcon = ProxiwashConstants.stateIcons[machineState];
const stateString = this.stateStrings[ProxiwashConstants.machineStates[machineState]];
const progress = isRunning
? props.item.donePercent !== ''
? parseInt(props.item.donePercent) / 100
: 0
: 1;
const icon = props.isWatched
? <Avatar.Icon
icon={'bell-ring'}
size={45}
color={colors.primary}
style={styles.icon}
/> :
<Avatar.Icon
/>
: <Avatar.Icon
icon={props.isDryer ? 'tumble-dryer' : 'washing-machine'}
color={colors.text}
size={40}
color={colors.text}
style={styles.icon}
/>
);
return (
<Card
style={{
margin: 5,
height: props.height,
justifyContent: 'center',
}}
onPress={props.onPress}
>
{ProxiwashConstants.machineStates[props.state] === ProxiwashConstants.machineStates["EN COURS"] ?
<Card style={{
...styles.backgroundCard,
backgroundColor: colors.proxiwashRunningBgColor,
}}/> : null
}
<Card style={{
...styles.progressionCard,
width: props.progress,
backgroundColor: stateColors[ProxiwashConstants.machineStates[props.state]],
}}/>
<Card.Title
title={props.title}
titleStyle={{fontSize: 17}}
subtitle={props.description}
style={styles.title}
left={() => icon}
right={() => (
<View style={{flexDirection: 'row'}}>
<View style={{justifyContent: 'center'}}>
<Text style={
ProxiwashConstants.machineStates[props.state] === ProxiwashConstants.machineStates.TERMINE ?
{fontWeight: 'bold',} : {}}
>
{props.statusText}
</Text>
</View>
<Avatar.Icon
icon={props.statusIcon}
color={colors.text}
size={30}
style={styles.icon}
/>;
this.updateStateColors();
return (
<Surface
style={{
...styles.container,
height: props.height,
borderRadius: 4,
}}
>
{
!isReady
? <ProgressBar
style={{
...styles.progressBar,
height: props.height
}}
progress={progress}
color={this.stateColors[ProxiwashConstants.machineStates[machineState]]}
/>
</View>)}
/>
</Card>
);
: null
}
<List.Item
title={this.title}
description={description}
style={{
height: props.height,
justifyContent: 'center',
}}
onPress={this.onListItemPress}
left={() => icon}
right={() => (
<View style={{flexDirection: 'row',}}>
<View style={{justifyContent: 'center',}}>
<Text style={
ProxiwashConstants.machineStates[machineState] === ProxiwashConstants.machineStates.TERMINE ?
{fontWeight: 'bold',} : {}}
>
{stateString}
</Text>
</View>
<View style={{justifyContent: 'center',}}>
<Avatar.Icon
icon={stateIcon}
color={colors.text}
size={30}
style={styles.icon}
/>
</View>
</View>)}
/>
</Surface>
);
}
}
const styles = StyleSheet.create({
container: {
margin: 5,
justifyContent: 'center',
elevation: 1
},
icon: {
backgroundColor: 'transparent'
},
backgroundCard: {
height: '100%',
progressBar: {
position: 'absolute',
left: 0,
width: '100%',
elevation: 0,
borderRadius: 4,
},
progressionCard: {
height: '100%',
position: 'absolute',
left: 0,
elevation: 0,
},
title: {
backgroundColor: 'transparent',
}
});
export default withTheme(ProxiwashListItem);

View file

@ -0,0 +1,69 @@
import * as React from 'react';
import {Avatar, Text, withTheme} from 'react-native-paper';
import {StyleSheet, View} from "react-native";
import i18n from "i18n-js";
type Props = {
title: string,
isDryer: boolean,
nbAvailable: number,
}
/**
* Component used to display a proxiwash item, showing machine progression and state
*/
class ProxiwashListItem extends React.Component<Props> {
constructor(props) {
super(props);
}
shouldComponentUpdate(nextProps: Props) {
return (nextProps.theme.dark !== this.props.theme.dark)
|| (nextProps.nbAvailable !== this.props.nbAvailable)
}
render() {
const props = this.props;
const subtitle = props.nbAvailable + ' ' + (
(props.nbAvailable <= 1)
? i18n.t('proxiwashScreen.numAvailable')
: i18n.t('proxiwashScreen.numAvailablePlural'));
return (
<View style={styles.container}>
<Avatar.Icon
icon={props.isDryer ? 'tumble-dryer' : 'washing-machine'}
color={this.props.theme.colors.primary}
style={styles.icon}
/>
<View style={{justifyContent: 'center'}}>
<Text style={styles.text}>
{props.title}
</Text>
<Text style={{color: this.props.theme.colors.subtitle}}>
{subtitle}
</Text>
</View>
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flexDirection: 'row',
marginLeft: 5,
marginRight: 5,
marginBottom: 10,
marginTop: 20,
},
icon: {
backgroundColor: 'transparent'
},
text: {
fontSize: 20,
fontWeight: 'bold',
}
});
export default withTheme(ProxiwashListItem);

View file

@ -1,4 +1,3 @@
export default {
machineStates: {
"TERMINE": "0",
@ -7,4 +6,11 @@ export default {
"HS": "3",
"ERREUR": "4"
},
stateIcons: {
"TERMINE": 'check-circle',
"DISPONIBLE": 'radiobox-blank',
"EN COURS": 'progress-check',
"HS": 'alert-octagram-outline',
"ERREUR": 'alert'
}
};

View file

@ -13,12 +13,11 @@ import ProxiwashConstants from "../../constants/ProxiwashConstants";
import CustomModal from "../../components/Custom/CustomModal";
import AprilFoolsManager from "../../managers/AprilFoolsManager";
import MaterialHeaderButtons, {Item} from "../../components/Custom/HeaderButton";
import ProxiwashSectionHeader from "../../components/Lists/ProxiwashSectionHeader";
const DATA_URL = "https://etud.insa-toulouse.fr/~amicale_app/washinsa/washinsa.json";
let stateStrings = {};
let modalStateStrings = {};
let stateIcons = {};
const REFRESH_TIME = 1000 * 10; // Refresh every 10 seconds
const LIST_ITEM_HEIGHT = 64;
@ -30,7 +29,6 @@ type Props = {
type State = {
refreshing: boolean,
firstLoading: boolean,
modalCurrentDisplayItem: React.Node,
machinesWatched: Array<string>,
bannerVisible: boolean,
@ -45,22 +43,12 @@ class ProxiwashScreen extends React.Component<Props, State> {
modalRef: Object;
onAboutPress: Function;
getRenderItem: Function;
getRenderSectionHeader: Function;
createDataset: Function;
onHideBanner: Function;
onModalRef: Function;
fetchedData: Object;
colors: Object;
state = {
refreshing: false,
firstLoading: true,
fetchedData: {},
machinesWatched: [],
modalCurrentDisplayItem: null,
machinesWatched: [],
bannerVisible: AsyncStorageManager.getInstance().preferences.proxiwashShowBanner.current === '1',
};
@ -69,53 +57,31 @@ class ProxiwashScreen extends React.Component<Props, State> {
*/
constructor(props) {
super(props);
stateStrings[ProxiwashConstants.machineStates.TERMINE] = i18n.t('proxiwashScreen.states.finished');
stateStrings[ProxiwashConstants.machineStates.DISPONIBLE] = i18n.t('proxiwashScreen.states.ready');
stateStrings[ProxiwashConstants.machineStates["EN COURS"]] = i18n.t('proxiwashScreen.states.running');
stateStrings[ProxiwashConstants.machineStates.HS] = i18n.t('proxiwashScreen.states.broken');
stateStrings[ProxiwashConstants.machineStates.ERREUR] = i18n.t('proxiwashScreen.states.error');
modalStateStrings[ProxiwashConstants.machineStates.TERMINE] = i18n.t('proxiwashScreen.modal.finished');
modalStateStrings[ProxiwashConstants.machineStates.DISPONIBLE] = i18n.t('proxiwashScreen.modal.ready');
modalStateStrings[ProxiwashConstants.machineStates["EN COURS"]] = i18n.t('proxiwashScreen.modal.running');
modalStateStrings[ProxiwashConstants.machineStates.HS] = i18n.t('proxiwashScreen.modal.broken');
modalStateStrings[ProxiwashConstants.machineStates.ERREUR] = i18n.t('proxiwashScreen.modal.error');
stateIcons[ProxiwashConstants.machineStates.TERMINE] = 'check-circle';
stateIcons[ProxiwashConstants.machineStates.DISPONIBLE] = 'radiobox-blank';
stateIcons[ProxiwashConstants.machineStates["EN COURS"]] = 'progress-check';
stateIcons[ProxiwashConstants.machineStates.HS] = 'alert-octagram-outline';
stateIcons[ProxiwashConstants.machineStates.ERREUR] = 'alert';
// let dataString = AsyncStorageManager.getInstance().preferences.proxiwashWatchedMachines.current;
this.onAboutPress = this.onAboutPress.bind(this);
this.getRenderItem = this.getRenderItem.bind(this);
this.getRenderSectionHeader = this.getRenderSectionHeader.bind(this);
this.createDataset = this.createDataset.bind(this);
this.onHideBanner = this.onHideBanner.bind(this);
this.onModalRef = this.onModalRef.bind(this);
this.colors = props.theme.colors;
}
/**
* Callback used when closing the banner.
* This hides the banner and saves to preferences to prevent it from reopening
*/
onHideBanner() {
onHideBanner = () => {
this.setState({bannerVisible: false});
AsyncStorageManager.getInstance().savePref(
AsyncStorageManager.getInstance().preferences.proxiwashShowBanner.key,
'0'
);
}
};
/**
* Setup notification channel for android and add listeners to detect notifications fired
*/
componentDidMount() {
const rightButton = this.getAboutButton.bind(this);
this.props.navigation.setOptions({
headerRight: rightButton,
headerRight: this.getAboutButton,
});
if (AsyncStorageManager.getInstance().preferences.expoToken.current !== '') {
// Get latest watchlist from server
@ -142,20 +108,17 @@ class ProxiwashScreen extends React.Component<Props, State> {
* Callback used when pressing the about button.
* This will open the ProxiwashAboutScreen.
*/
onAboutPress() {
this.props.navigation.navigate('proxiwash-about');
}
onAboutPress = () => this.props.navigation.navigate('proxiwash-about');
/**
* Gets the about header button
*
* @return {*}
*/
getAboutButton() {
return <MaterialHeaderButtons>
getAboutButton = () =>
<MaterialHeaderButtons>
<Item title="information" iconName="information" onPress={this.onAboutPress}/>
</MaterialHeaderButtons>;
}
/**
* Extracts the key for the given item
@ -261,7 +224,7 @@ class ProxiwashScreen extends React.Component<Props, State> {
* @param fetchedData
* @return {*}
*/
createDataset(fetchedData: Object) {
createDataset = (fetchedData: Object) => {
let data = fetchedData;
if (AprilFoolsManager.getInstance().isAprilFoolsEnabled()) {
data = JSON.parse(JSON.stringify(fetchedData)); // Deep copy
@ -284,7 +247,7 @@ class ProxiwashScreen extends React.Component<Props, State> {
keyExtractor: this.getKeyExtractor
},
];
}
};
/**
* Shows a modal for the given item
@ -293,14 +256,14 @@ class ProxiwashScreen extends React.Component<Props, State> {
* @param item The item to display information for in the modal
* @param isDryer True if the given item is a dryer
*/
showModal(title: string, item: Object, isDryer: boolean) {
showModal = (title: string, item: Object, isDryer: boolean) => {
this.setState({
modalCurrentDisplayItem: this.getModalContent(title, item, isDryer)
});
if (this.modalRef) {
this.modalRef.open();
}
}
};
/**
* Callback used when the user clicks on enable notifications for a machine
@ -362,7 +325,7 @@ class ProxiwashScreen extends React.Component<Props, State> {
title={title}
left={() => <Avatar.Icon
icon={isDryer ? 'tumble-dryer' : 'washing-machine'}
color={this.colors.text}
color={this.props.theme.colors.text}
style={{backgroundColor: 'transparent'}}/>}
/>
@ -390,9 +353,9 @@ class ProxiwashScreen extends React.Component<Props, State> {
*
* @param ref
*/
onModalRef(ref: Object) {
onModalRef = (ref: Object) => {
this.modalRef = ref;
}
};
/**
* Gets the number of machines available
@ -420,43 +383,16 @@ class ProxiwashScreen extends React.Component<Props, State> {
* @param section The section to render
* @return {*}
*/
getRenderSectionHeader({section}: Object) {
getRenderSectionHeader = ({section}: Object) => {
const isDryer = section.title === i18n.t('proxiwashScreen.dryers');
const nbAvailable = this.getMachineAvailableNumber(isDryer);
const subtitle = nbAvailable + ' ' + ((nbAvailable <= 1) ? i18n.t('proxiwashScreen.numAvailable')
: i18n.t('proxiwashScreen.numAvailablePlural'));
return (
<View style={{
flexDirection: 'row',
marginLeft: 5,
marginRight: 5,
marginBottom: 10,
marginTop: 20,
}}>
<Avatar.Icon
icon={isDryer ? 'tumble-dryer' : 'washing-machine'}
color={this.colors.primary}
style={{backgroundColor: 'transparent'}}
/>
<View style={{
justifyContent: 'center',
}}>
<Text style={{
fontSize: 20,
fontWeight: 'bold',
}}>
{section.title}
</Text>
<Text style={{
color: this.colors.subtitle,
}}>
{subtitle}
</Text>
</View>
</View>
<ProxiwashSectionHeader
title={section.title}
nbAvailable={nbAvailable}
isDryer={isDryer}/>
);
}
};
/**
* Gets the list item to be rendered
@ -465,34 +401,18 @@ class ProxiwashScreen extends React.Component<Props, State> {
* @param section The object describing the current SectionList section
* @returns {React.Node}
*/
getRenderItem({item, section}: Object) {
const isMachineRunning = ProxiwashConstants.machineStates[item.state] === ProxiwashConstants.machineStates["EN COURS"];
let displayNumber = item.number;
if (AprilFoolsManager.getInstance().isAprilFoolsEnabled())
displayNumber = AprilFoolsManager.getProxiwashMachineDisplayNumber(parseInt(item.number));
const machineName = (section.title === i18n.t('proxiwashScreen.dryers') ?
i18n.t('proxiwashScreen.dryer') :
i18n.t('proxiwashScreen.washer')) + ' n°' + displayNumber;
getRenderItem = ({item, section}: Object) => {
const isDryer = section.title === i18n.t('proxiwashScreen.dryers');
const onPress = this.showModal.bind(this, machineName, item, isDryer);
let width = item.donePercent !== '' ? (parseInt(item.donePercent)).toString() + '%' : 0;
if (ProxiwashConstants.machineStates[item.state] === '0')
width = '100%';
return (
<ProxiwashListItem
title={machineName}
description={isMachineRunning ? item.startTime + '/' + item.endTime : ''}
onPress={onPress}
progress={width}
state={item.state}
item={item}
onPress={this.showModal}
isWatched={this.isMachineWatched(item.number)}
isDryer={isDryer}
statusText={stateStrings[ProxiwashConstants.machineStates[item.state]]}
statusIcon={stateIcons[ProxiwashConstants.machineStates[item.state]]}
height={LIST_ITEM_HEIGHT}
/>
);
}
};
render() {
const nav = this.props.navigation;

View file

@ -19,7 +19,6 @@ export const AmicaleWebsiteScreen = (props: Object) => {
props.navigation.dispatch(CommonActions.setParams({path: null}));
}
}
console.log(path);
return (
<WebViewScreen
{...props}

View file

@ -38,9 +38,7 @@ export async function initExpoToken() {
let expoToken = await Notifications.getExpoPushTokenAsync();
// Save token for instant use later on
AsyncStorageManager.getInstance().savePref(AsyncStorageManager.getInstance().preferences.expoToken.key, expoToken);
} catch (e) {
console.log(e);
}
} catch (e) {}
}
}