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 * 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 {StyleSheet, View} from "react-native";
import ProxiwashConstants from "../../constants/ProxiwashConstants"; 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 * Component used to display a proxiwash item, showing machine progression and state
*
* @param props Props to pass to the component
* @return {*}
*/ */
function ProxiwashListItem(props) { class ProxiwashListItem extends React.Component<Props> {
const {colors} = props.theme;
let stateColors = {}; stateColors: Object;
stateColors[ProxiwashConstants.machineStates.TERMINE] = colors.proxiwashFinishedColor; stateStrings: Object;
stateColors[ProxiwashConstants.machineStates.DISPONIBLE] = colors.proxiwashReadyColor;
stateColors[ProxiwashConstants.machineStates["EN COURS"]] = colors.proxiwashRunningColor; title: string;
stateColors[ProxiwashConstants.machineStates.HS] = colors.proxiwashBrokenColor;
stateColors[ProxiwashConstants.machineStates.ERREUR] = colors.proxiwashErrorColor; constructor(props) {
const icon = ( super(props);
props.isWatched ? this.stateColors = {};
<Avatar.Icon 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'} icon={'bell-ring'}
size={45} size={45}
color={colors.primary} color={colors.primary}
style={styles.icon} style={styles.icon}
/> : />
<Avatar.Icon : <Avatar.Icon
icon={props.isDryer ? 'tumble-dryer' : 'washing-machine'} icon={props.isDryer ? 'tumble-dryer' : 'washing-machine'}
color={colors.text}
size={40} size={40}
color={colors.text}
style={styles.icon} style={styles.icon}
/> />;
); this.updateStateColors();
return ( return (
<Card <Surface
style={{ style={{
margin: 5, ...styles.container,
height: props.height, height: props.height,
justifyContent: 'center', borderRadius: 4,
}} }}
onPress={props.onPress} >
> {
{ProxiwashConstants.machineStates[props.state] === ProxiwashConstants.machineStates["EN COURS"] ? !isReady
<Card style={{ ? <ProgressBar
...styles.backgroundCard, style={{
backgroundColor: colors.proxiwashRunningBgColor, ...styles.progressBar,
height: props.height
}}/> : null }}
} progress={progress}
color={this.stateColors[ProxiwashConstants.machineStates[machineState]]}
<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}
/> />
</View>)} : null
/> }
</Card> <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({ const styles = StyleSheet.create({
container: {
margin: 5,
justifyContent: 'center',
elevation: 1
},
icon: { icon: {
backgroundColor: 'transparent' backgroundColor: 'transparent'
}, },
backgroundCard: { progressBar: {
height: '100%',
position: 'absolute', position: 'absolute',
left: 0, left: 0,
width: '100%', borderRadius: 4,
elevation: 0,
}, },
progressionCard: {
height: '100%',
position: 'absolute',
left: 0,
elevation: 0,
},
title: {
backgroundColor: 'transparent',
}
}); });
export default withTheme(ProxiwashListItem); 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 { export default {
machineStates: { machineStates: {
"TERMINE": "0", "TERMINE": "0",
@ -7,4 +6,11 @@ export default {
"HS": "3", "HS": "3",
"ERREUR": "4" "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 CustomModal from "../../components/Custom/CustomModal";
import AprilFoolsManager from "../../managers/AprilFoolsManager"; import AprilFoolsManager from "../../managers/AprilFoolsManager";
import MaterialHeaderButtons, {Item} from "../../components/Custom/HeaderButton"; 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"; const DATA_URL = "https://etud.insa-toulouse.fr/~amicale_app/washinsa/washinsa.json";
let stateStrings = {};
let modalStateStrings = {}; let modalStateStrings = {};
let stateIcons = {};
const REFRESH_TIME = 1000 * 10; // Refresh every 10 seconds const REFRESH_TIME = 1000 * 10; // Refresh every 10 seconds
const LIST_ITEM_HEIGHT = 64; const LIST_ITEM_HEIGHT = 64;
@ -30,7 +29,6 @@ type Props = {
type State = { type State = {
refreshing: boolean, refreshing: boolean,
firstLoading: boolean,
modalCurrentDisplayItem: React.Node, modalCurrentDisplayItem: React.Node,
machinesWatched: Array<string>, machinesWatched: Array<string>,
bannerVisible: boolean, bannerVisible: boolean,
@ -45,22 +43,12 @@ class ProxiwashScreen extends React.Component<Props, State> {
modalRef: Object; modalRef: Object;
onAboutPress: Function;
getRenderItem: Function;
getRenderSectionHeader: Function;
createDataset: Function;
onHideBanner: Function;
onModalRef: Function;
fetchedData: Object; fetchedData: Object;
colors: Object;
state = { state = {
refreshing: false, refreshing: false,
firstLoading: true,
fetchedData: {},
machinesWatched: [],
modalCurrentDisplayItem: null, modalCurrentDisplayItem: null,
machinesWatched: [],
bannerVisible: AsyncStorageManager.getInstance().preferences.proxiwashShowBanner.current === '1', bannerVisible: AsyncStorageManager.getInstance().preferences.proxiwashShowBanner.current === '1',
}; };
@ -69,53 +57,31 @@ class ProxiwashScreen extends React.Component<Props, State> {
*/ */
constructor(props) { constructor(props) {
super(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.TERMINE] = i18n.t('proxiwashScreen.modal.finished');
modalStateStrings[ProxiwashConstants.machineStates.DISPONIBLE] = i18n.t('proxiwashScreen.modal.ready'); modalStateStrings[ProxiwashConstants.machineStates.DISPONIBLE] = i18n.t('proxiwashScreen.modal.ready');
modalStateStrings[ProxiwashConstants.machineStates["EN COURS"]] = i18n.t('proxiwashScreen.modal.running'); modalStateStrings[ProxiwashConstants.machineStates["EN COURS"]] = i18n.t('proxiwashScreen.modal.running');
modalStateStrings[ProxiwashConstants.machineStates.HS] = i18n.t('proxiwashScreen.modal.broken'); modalStateStrings[ProxiwashConstants.machineStates.HS] = i18n.t('proxiwashScreen.modal.broken');
modalStateStrings[ProxiwashConstants.machineStates.ERREUR] = i18n.t('proxiwashScreen.modal.error'); 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. * Callback used when closing the banner.
* This hides the banner and saves to preferences to prevent it from reopening * This hides the banner and saves to preferences to prevent it from reopening
*/ */
onHideBanner() { onHideBanner = () => {
this.setState({bannerVisible: false}); this.setState({bannerVisible: false});
AsyncStorageManager.getInstance().savePref( AsyncStorageManager.getInstance().savePref(
AsyncStorageManager.getInstance().preferences.proxiwashShowBanner.key, AsyncStorageManager.getInstance().preferences.proxiwashShowBanner.key,
'0' '0'
); );
} };
/** /**
* Setup notification channel for android and add listeners to detect notifications fired * Setup notification channel for android and add listeners to detect notifications fired
*/ */
componentDidMount() { componentDidMount() {
const rightButton = this.getAboutButton.bind(this);
this.props.navigation.setOptions({ this.props.navigation.setOptions({
headerRight: rightButton, headerRight: this.getAboutButton,
}); });
if (AsyncStorageManager.getInstance().preferences.expoToken.current !== '') { if (AsyncStorageManager.getInstance().preferences.expoToken.current !== '') {
// Get latest watchlist from server // Get latest watchlist from server
@ -142,20 +108,17 @@ class ProxiwashScreen extends React.Component<Props, State> {
* Callback used when pressing the about button. * Callback used when pressing the about button.
* This will open the ProxiwashAboutScreen. * This will open the ProxiwashAboutScreen.
*/ */
onAboutPress() { onAboutPress = () => this.props.navigation.navigate('proxiwash-about');
this.props.navigation.navigate('proxiwash-about');
}
/** /**
* Gets the about header button * Gets the about header button
* *
* @return {*} * @return {*}
*/ */
getAboutButton() { getAboutButton = () =>
return <MaterialHeaderButtons> <MaterialHeaderButtons>
<Item title="information" iconName="information" onPress={this.onAboutPress}/> <Item title="information" iconName="information" onPress={this.onAboutPress}/>
</MaterialHeaderButtons>; </MaterialHeaderButtons>;
}
/** /**
* Extracts the key for the given item * Extracts the key for the given item
@ -261,7 +224,7 @@ class ProxiwashScreen extends React.Component<Props, State> {
* @param fetchedData * @param fetchedData
* @return {*} * @return {*}
*/ */
createDataset(fetchedData: Object) { createDataset = (fetchedData: Object) => {
let data = fetchedData; let data = fetchedData;
if (AprilFoolsManager.getInstance().isAprilFoolsEnabled()) { if (AprilFoolsManager.getInstance().isAprilFoolsEnabled()) {
data = JSON.parse(JSON.stringify(fetchedData)); // Deep copy data = JSON.parse(JSON.stringify(fetchedData)); // Deep copy
@ -284,7 +247,7 @@ class ProxiwashScreen extends React.Component<Props, State> {
keyExtractor: this.getKeyExtractor keyExtractor: this.getKeyExtractor
}, },
]; ];
} };
/** /**
* Shows a modal for the given item * 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 item The item to display information for in the modal
* @param isDryer True if the given item is a dryer * @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({ this.setState({
modalCurrentDisplayItem: this.getModalContent(title, item, isDryer) modalCurrentDisplayItem: this.getModalContent(title, item, isDryer)
}); });
if (this.modalRef) { if (this.modalRef) {
this.modalRef.open(); this.modalRef.open();
} }
} };
/** /**
* Callback used when the user clicks on enable notifications for a machine * 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} title={title}
left={() => <Avatar.Icon left={() => <Avatar.Icon
icon={isDryer ? 'tumble-dryer' : 'washing-machine'} icon={isDryer ? 'tumble-dryer' : 'washing-machine'}
color={this.colors.text} color={this.props.theme.colors.text}
style={{backgroundColor: 'transparent'}}/>} style={{backgroundColor: 'transparent'}}/>}
/> />
@ -390,9 +353,9 @@ class ProxiwashScreen extends React.Component<Props, State> {
* *
* @param ref * @param ref
*/ */
onModalRef(ref: Object) { onModalRef = (ref: Object) => {
this.modalRef = ref; this.modalRef = ref;
} };
/** /**
* Gets the number of machines available * Gets the number of machines available
@ -420,43 +383,16 @@ class ProxiwashScreen extends React.Component<Props, State> {
* @param section The section to render * @param section The section to render
* @return {*} * @return {*}
*/ */
getRenderSectionHeader({section}: Object) { getRenderSectionHeader = ({section}: Object) => {
const isDryer = section.title === i18n.t('proxiwashScreen.dryers'); const isDryer = section.title === i18n.t('proxiwashScreen.dryers');
const nbAvailable = this.getMachineAvailableNumber(isDryer); const nbAvailable = this.getMachineAvailableNumber(isDryer);
const subtitle = nbAvailable + ' ' + ((nbAvailable <= 1) ? i18n.t('proxiwashScreen.numAvailable')
: i18n.t('proxiwashScreen.numAvailablePlural'));
return ( return (
<View style={{ <ProxiwashSectionHeader
flexDirection: 'row', title={section.title}
marginLeft: 5, nbAvailable={nbAvailable}
marginRight: 5, isDryer={isDryer}/>
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>
); );
} };
/** /**
* Gets the list item to be rendered * 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 * @param section The object describing the current SectionList section
* @returns {React.Node} * @returns {React.Node}
*/ */
getRenderItem({item, section}: Object) { 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;
const isDryer = section.title === i18n.t('proxiwashScreen.dryers'); 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 ( return (
<ProxiwashListItem <ProxiwashListItem
title={machineName} item={item}
description={isMachineRunning ? item.startTime + '/' + item.endTime : ''} onPress={this.showModal}
onPress={onPress}
progress={width}
state={item.state}
isWatched={this.isMachineWatched(item.number)} isWatched={this.isMachineWatched(item.number)}
isDryer={isDryer} isDryer={isDryer}
statusText={stateStrings[ProxiwashConstants.machineStates[item.state]]}
statusIcon={stateIcons[ProxiwashConstants.machineStates[item.state]]}
height={LIST_ITEM_HEIGHT} height={LIST_ITEM_HEIGHT}
/> />
); );
} };
render() { render() {
const nav = this.props.navigation; const nav = this.props.navigation;

View file

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

View file

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