Cómo crear un lector de noticias con React Native: Configuración y elemento de noticias
Spanish (Español) translation by Elías Nicolás (you can also view the original English article)
En este tutorial, crearemos una aplicación de lector de noticias con React Native. En esta serie de dos partes, voy a suponer que esta no es tu primera aplicación React Native y no entraré en detalles sobre la configuración de tu máquina y la ejecución de la aplicación en un dispositivo. Dicho esto, explico el proceso de desarrollo en detalle.
Aunque implementaremos
en Android, el código utilizado en este tutorial también debería
funcionar en iOS. Así es como se ve el resultado final.



Puede encontrar el código fuente utilizado en este tutorial en GitHub.
Requisitos previos
Si eres nuevo en React Native y aún no has configurado tu máquina, asegúrate de consultar la guía de introducción de la documentación de React Native o lee el tutorial introductorio de Ashraff sobre Envato Tuts+. No olvides instalar el SDK de Android si deseas implementarlo en Android o instalar Xcode y el SDK para iOS.
Una vez que haya terminado, instale NodeJS y la herramienta de línea de comandos React Native usando npm.
1 |
npm install -g react-native-cli |
1. Configuración del proyecto
Ahora estamos listos para construir el proyecto. Antes de comenzar, me gustaría ofrecer una breve descripción de cómo se organiza el proyecto. Creamos dos componentes personalizados:
-
NewsItemsque representa las noticias -
WebPageque representa la página web cuando el usuario toca una noticia
Estos se importan al archivo de punto de entrada principal para Android (index.android.js) y para iOS (index.ios.js). Eso es todo lo que necesita saber por
ahora.
Paso 1: Creando una nueva aplicación
Comience por navegar a su directorio de trabajo. Abra una nueva ventana de terminal dentro de ese directorio y ejecute el siguiente comando:
1 |
react-native init HnReader |
Esto crea una nueva carpeta llamada HnReader y contiene los archivos necesarios para compilar la aplicación.
React Native ya viene con algunos componentes predeterminados, pero también hay otros personalizados creados por otros desarrolladores. Puede encontrarlos en el sitio web react.parts. Sin embargo, no todos los componentes funcionan tanto en Android como en iOS. Incluso algunos de los componentes predeterminados no son multiplataforma. Es por eso que debe tener cuidado al elegir los componentes, ya que pueden diferir en cada plataforma o pueden no funcionar correctamente en todas las plataformas.
Es una buena idea ir a la página de problemas del repositorio de GitHub del componente que planea usar y buscar compatibilidad con Android o iOS para comprobar rápidamente si el componente funciona en ambas plataformas.
Paso 2: instalar
dependencias
La aplicación que vamos a construir depende de unas pocas bibliotecas de terceros y componentes de React. Puede instalarlos abriendo package.json en la raíz de su directorio de trabajo. Agregue lo siguiente a package.json:
1 |
{
|
2 |
"name": "HnReader", |
3 |
"version": "0.0.1", |
4 |
"private": true, |
5 |
"scripts": { |
6 |
"start": "react-native start" |
7 |
},
|
8 |
"dependencies": { |
9 |
"lodash": "^4.0.1", |
10 |
"moment": "^2.11.1", |
11 |
"react-native": "^0.18.1", |
12 |
"react-native-button": "^1.3.1", |
13 |
"react-native-gifted-spinner": "0.0.3" |
14 |
}
|
15 |
}
|
A
continuación, abra una ventana de terminal en el directorio de trabajo y
ejecute npm install para instalar las dependencias especificadas en
package.json. Aquí hay una breve descripción de lo que hace cada biblioteca en el
proyecto:
- lodash se usa para truncar cadenas. Puede ser un poco excesivo, pero una línea menos de código que tiene que escribir significa una responsabilidad menos.
- moment se utiliza para determinar si las noticias en el almacenamiento local ya están allí por un día.
- react-native es el marco React Native. Esto se instala de
manera predeterminada cuando ejecutaste el
react-native initantes. - react-native-button es un componente react native utilizado para crear botones.
- react-native-gifted-spinner se utiliza como un indicador de actividad al realizar solicitudes de red.
2. Componente principal
Como mencioné anteriormente, el punto de entrada para todos los proyectos de React Native es index.android.js e index.ios.js. Ese es el enfoque de esta sección. Reemplace el contenido de estos archivos con lo siguiente:
1 |
'use strict'; |
2 |
var React = require('react-native'); |
3 |
|
4 |
var { |
5 |
AppRegistry, |
6 |
StyleSheet, |
7 |
Navigator
|
8 |
} = React; |
9 |
|
10 |
|
11 |
var NewsItems = require('./components/news-items'); |
12 |
var WebPage = require('./components/webpage'); |
13 |
|
14 |
var ROUTES = { |
15 |
news_items: NewsItems, |
16 |
web_page: WebPage |
17 |
};
|
18 |
|
19 |
var HnReader = React.createClass({ |
20 |
|
21 |
renderScene: function(route, navigator) { |
22 |
|
23 |
var Component = ROUTES[route.name]; |
24 |
return ( |
25 |
<Component route={route} navigator={navigator} url={route.url} /> |
26 |
);
|
27 |
},
|
28 |
|
29 |
render: function() { |
30 |
return ( |
31 |
<Navigator |
32 |
style={styles.container} |
33 |
initialRoute={{name: 'news_items', url: ''}} |
34 |
renderScene={this.renderScene} |
35 |
configureScene={() => { return Navigator.SceneConfigs.FloatFromRight; }} /> |
36 |
);
|
37 |
|
38 |
},
|
39 |
|
40 |
|
41 |
});
|
42 |
|
43 |
|
44 |
var styles = StyleSheet.create({ |
45 |
container: { |
46 |
flex: 1 |
47 |
}
|
48 |
});
|
49 |
|
50 |
AppRegistry.registerComponent('HnReader', () => HnReader); |
Déjame desglosarlo. Primero, habilitamos el modo estricto usando la directiva use script. Esto hace que el analizador realice más comprobaciones en su código. Por
ejemplo, se quejará si inicializa una variable sin agregar la palabra
clave var.
1 |
'use strict'; |
A continuación, importamos el marco React Native. Esto nos permite crear componentes personalizados y agregar estilo a la aplicación.
1 |
var React = require('react-native'); |
Luego extraemos toda la funcionalidad que necesitamos del objeto React.
1 |
var { |
2 |
AppRegistry, |
3 |
StyleSheet, |
4 |
Navigator
|
5 |
} = React; |
Si es nuevo en ES6 (ECMAScript 6), el fragmento de arriba es idéntico a:
1 |
var AppRegistry = React.AppRegistry; |
2 |
var StyleSheet = React.StyleSheet; |
3 |
var Navigator = React.Navigator; |
Es el azúcar sintáctico introducido en ES6 para facilitar la asignación de propiedades de objeto a variables. Esto se llama asignación de desestructuración.
Aquí hay una breve
descripción de lo que hace cada una de las propiedades que hemos
extraído:
-
AppRegistryse usa para registrar el componente principal de la aplicación. -
StyleSheetse utiliza para declarar estilos para ser utilizados por los componentes. -
Navigatorse usa para cambiar entre las diferentes páginas de la aplicación.
A continuación, importamos
los componentes personalizados utilizados por la aplicación. Vamos a
crear estos más tarde.
1 |
var NewsItems = require('./components/news-items'); |
2 |
var WebPage = require('./components/webpage'); |
Cree una variable ROUTES y asigne un objeto utilizando los dos componentes anteriores como el valor de sus propiedades. Esto nos permite mostrar el componente haciendo referencia a cada una de las claves que hemos definido.
1 |
var ROUTES = { |
2 |
news_items: NewsItems, |
3 |
web_page: WebPage |
4 |
};
|
Cree el componente principal de la aplicación llamando al método createClass desde el objeto React. El método createClass acepta un objeto como argumento.
1 |
var HnReader = React.createClass({ |
2 |
...
|
3 |
});
|
Dentro del objeto está el método renderScene, que se llama cada vez que cambia la ruta. La route y navigator se pasan como un argumento para este método. La
route contiene información sobre la ruta actual (por ejemplo, el nombre
de la ruta).
El navigator contiene métodos que se pueden usar para
navegar entre diferentes rutas. Dentro
del método renderScene, obtenemos el componente que queremos
representar pasando el nombre de la ruta actual al objeto ROUTES. A
continuación, presentamos el componente y pasamos la route, el navigator y
la url como atributos. Más adelante, verás cómo se usan dentro de cada
uno de los componentes. Por
ahora, solo recuerde que cuando quiera pasar datos desde el componente
principal a un componente secundario, todo lo que tiene que hacer es
agregar un nuevo atributo y usar los datos que desea pasar como valor.
1 |
renderScene: function(route, navigator) { |
2 |
|
3 |
var Component = ROUTES[route.name]; //get the component for this specific route |
4 |
|
5 |
//render the component and pass along the route, navigator and the url
|
6 |
return ( |
7 |
<Component route={route} navigator={navigator} url={route.url} /> |
8 |
);
|
9 |
},
|
El método
render es un método obligatorio al crear componentes porque
es responsable de representar la interfaz de usuario del componente. En este método, renderizamos el componente Navigator y transmitimos algunos atributos.
1 |
render: function() { |
2 |
return ( |
3 |
<Navigator |
4 |
style={styles.container} |
5 |
initialRoute={{name: 'news_items', url: ''}} |
6 |
renderScene={this.renderScene} |
7 |
configureScene={() => { return Navigator.SceneConfigs.FloatFromRight; }} /> |
8 |
);
|
9 |
|
10 |
},
|
Permítanme explicar lo que hace cada atributo:
- el
stylese usa para agregar estilos al componente. -
initialRoutese utiliza para especificar la ruta inicial que utilizará el navigator. Como puede ver, hemos pasado un objeto que contiene una propiedad de nombrenamecon su valor establecido ennews_items. Este objeto es lo que se está pasando al argumento deroutedel métodorenderScene, que definimos anteriormente. Esto significa que este código particular representaría el componenteNewsItemsde forma predeterminada.
1 |
var Component = ROUTES[route.name]; |
La url se establece en una cadena vacía porque no tenemos una página web para procesar de forma predeterminada.
-
renderScenees responsable de representar el componente para una ruta específica. -
configureScenees responsable de especificar las animaciones y los gestos que se utilizarán al navegar entre las rutas. En este caso, estamos pasando una función que devuelve la animaciónFloatFromRight. Esto significa que, cuando navega hacia una ruta con un índice más alto, la nueva página flota de derecha a izquierda. Y cuando retrocede, flota de izquierda a derecha. Esto también agrega un gesto de deslizar hacia la izquierda como un medio para volver a la ruta anterior.
1 |
() => { return Navigator.SceneConfigs.FloatFromRight; } |
Los estilos se definen después de la definición del componente principal. Llamamos al método create desde el objeto StyleSheet y pasamos un objeto
que contiene los estilos. En este caso, solo tenemos uno, definiendo
que va a ocupar toda la pantalla.
1 |
var styles = StyleSheet.create({ |
2 |
container: { |
3 |
flex: 1 |
4 |
}
|
5 |
});
|
Por último, registramos el componente.
1 |
AppRegistry.registerComponent('HnReader', () => HnReader); |
3. Componente NewsItem
El componente NewsItem se usa para representar las noticias. Los
componentes personalizados se almacenan en el directorio components. Dentro de este directorio, cree news-items.js y añádale el siguiente
código:
1 |
'use strict'; |
2 |
var React = require('react-native'); |
3 |
|
4 |
var { |
5 |
AppRegistry, |
6 |
StyleSheet, |
7 |
Text, |
8 |
ListView, |
9 |
View, |
10 |
ScrollView, |
11 |
TouchableHighlight, |
12 |
AsyncStorage
|
13 |
} = React; |
14 |
|
15 |
var Button = require('react-native-button'); |
16 |
var GiftedSpinner = require('react-native-gifted-spinner'); |
17 |
|
18 |
var api = require('../src/api.js'); |
19 |
|
20 |
var moment = require('moment'); |
21 |
|
22 |
var TOTAL_NEWS_ITEMS = 10; |
23 |
|
24 |
var NewsItems = React.createClass({ |
25 |
|
26 |
getInitialState: function() { |
27 |
return { |
28 |
title: 'HN Reader', |
29 |
dataSource: new ListView.DataSource({ |
30 |
rowHasChanged: (row1, row2) => row1 !== row2, |
31 |
}),
|
32 |
news: {}, |
33 |
loaded: false |
34 |
}
|
35 |
},
|
36 |
|
37 |
render: function() { |
38 |
|
39 |
return ( |
40 |
<View style={styles.container}> |
41 |
<View style={styles.header}> |
42 |
<View style={styles.header_item}> |
43 |
<Text style={styles.header_text}>{this.state.title}</Text> |
44 |
</View> |
45 |
<View style={styles.header_item}> |
46 |
{ !this.state.loaded && |
47 |
<GiftedSpinner /> |
48 |
}
|
49 |
</View> |
50 |
</View> |
51 |
<View style={styles.body}> |
52 |
<ScrollView ref="scrollView"> |
53 |
{
|
54 |
this.state.loaded && |
55 |
|
56 |
<ListView initialListSize={1} dataSource={this.state.news} style={styles.news} renderRow={this.renderNews}></ListView> |
57 |
|
58 |
}
|
59 |
</ScrollView> |
60 |
</View> |
61 |
</View> |
62 |
);
|
63 |
|
64 |
},
|
65 |
|
66 |
componentDidMount: function() { |
67 |
|
68 |
AsyncStorage.getItem('news_items').then((news_items_str) => { |
69 |
|
70 |
var news_items = JSON.parse(news_items_str); |
71 |
|
72 |
if(news_items != null){ |
73 |
|
74 |
AsyncStorage.getItem('time').then((time_str) => { |
75 |
var time = JSON.parse(time_str); |
76 |
var last_cache = time.last_cache; |
77 |
var current_datetime = moment(); |
78 |
|
79 |
var diff_days = current_datetime.diff(last_cache, 'days'); |
80 |
|
81 |
if(diff_days > 0){ |
82 |
this.getNews(); |
83 |
}else{ |
84 |
this.updateNewsItemsUI(news_items); |
85 |
}
|
86 |
|
87 |
});
|
88 |
|
89 |
|
90 |
}else{ |
91 |
this.getNews(); |
92 |
}
|
93 |
|
94 |
}).done(); |
95 |
|
96 |
},
|
97 |
|
98 |
renderNews: function(news) { |
99 |
return ( |
100 |
<TouchableHighlight onPress={this.viewPage.bind(this, news.url)} underlayColor={"#E8E8E8"} style={styles.button}> |
101 |
<View style={styles.news_item}> |
102 |
<Text style={styles.news_item_text}>{news.title}</Text> |
103 |
</View> |
104 |
</TouchableHighlight> |
105 |
);
|
106 |
},
|
107 |
|
108 |
viewPage: function(url){ |
109 |
this.props.navigator.push({name: 'web_page', url: url}); |
110 |
},
|
111 |
|
112 |
updateNewsItemsUI: function(news_items){ |
113 |
|
114 |
if(news_items.length == TOTAL_NEWS_ITEMS){ |
115 |
|
116 |
var ds = this.state.dataSource.cloneWithRows(news_items); |
117 |
this.setState({ |
118 |
'news': ds, |
119 |
'loaded': true |
120 |
});
|
121 |
|
122 |
}
|
123 |
|
124 |
},
|
125 |
|
126 |
updateNewsItemDB: function(news_items){ |
127 |
|
128 |
if(news_items.length == TOTAL_NEWS_ITEMS){ |
129 |
AsyncStorage.setItem('news_items', JSON.stringify(news_items)); |
130 |
}
|
131 |
|
132 |
},
|
133 |
|
134 |
getNews: function() { |
135 |
|
136 |
var TOP_STORIES_URL = 'https://hacker-news.firebaseio.com/v0/topstories.json'; |
137 |
var news_items = []; |
138 |
|
139 |
AsyncStorage.setItem('time', JSON.stringify({'last_cache': moment()})); |
140 |
|
141 |
api(TOP_STORIES_URL).then( |
142 |
(top_stories) => { |
143 |
|
144 |
for(var x = 0; x <= 10; x++){ |
145 |
|
146 |
var story_url = "https://hacker-news.firebaseio.com/v0/item/" + top_stories[x] + ".json"; |
147 |
|
148 |
api(story_url).then( |
149 |
(story) => { |
150 |
|
151 |
news_items.push(story); |
152 |
this.updateNewsItemsUI(news_items); |
153 |
this.updateNewsItemDB(news_items); |
154 |
|
155 |
}
|
156 |
);
|
157 |
|
158 |
}
|
159 |
|
160 |
|
161 |
}
|
162 |
|
163 |
|
164 |
|
165 |
);
|
166 |
|
167 |
|
168 |
}
|
169 |
|
170 |
});
|
171 |
|
172 |
|
173 |
|
174 |
var styles = StyleSheet.create({ |
175 |
container: { |
176 |
flex: 1 |
177 |
},
|
178 |
header: { |
179 |
backgroundColor: '#FF6600', |
180 |
padding: 10, |
181 |
flex: 1, |
182 |
justifyContent: 'space-between', |
183 |
flexDirection: 'row' |
184 |
},
|
185 |
body: { |
186 |
flex: 9, |
187 |
backgroundColor: '#F6F6EF' |
188 |
},
|
189 |
header_item: { |
190 |
paddingLeft: 10, |
191 |
paddingRight: 10, |
192 |
justifyContent: 'center' |
193 |
},
|
194 |
header_text: { |
195 |
color: '#FFF', |
196 |
fontWeight: 'bold', |
197 |
fontSize: 15 |
198 |
},
|
199 |
button: { |
200 |
borderBottomWidth: 1, |
201 |
borderBottomColor: '#F0F0F0' |
202 |
},
|
203 |
news_item: { |
204 |
paddingLeft: 10, |
205 |
paddingRight: 10, |
206 |
paddingTop: 15, |
207 |
paddingBottom: 15, |
208 |
marginBottom: 5 |
209 |
},
|
210 |
news_item_text: { |
211 |
color: '#575757', |
212 |
fontSize: 18 |
213 |
}
|
214 |
});
|
215 |
|
216 |
module.exports = NewsItems; |
Paso 1: Importación de componentes y bibliotecas
Primero, importamos los componentes y las bibliotecas que necesitamos
para el componente NewsItem. También creamos una variable global que
almacena la cantidad total de noticias que se almacenarán en caché.
1 |
'use strict'; |
2 |
var React = require('react-native'); |
3 |
|
4 |
var { |
5 |
AppRegistry, |
6 |
StyleSheet, |
7 |
Text, |
8 |
ListView, |
9 |
View, |
10 |
ScrollView, |
11 |
TouchableHighlight, |
12 |
AsyncStorage
|
13 |
} = React; |
14 |
|
15 |
var Button = require('react-native-button'); |
16 |
var GiftedSpinner = require('react-native-gifted-spinner'); |
17 |
|
18 |
var api = require('../src/api.js'); |
19 |
|
20 |
var moment = require('moment'); |
21 |
|
22 |
var TOTAL_NEWS_ITEMS = 10; |
Usamos algunos componentes que no hemos usado anteriormente.
- El
Textse usa para mostrar texto en React Native. - La
Viewes el bloque de construcción básico para crear componentes. Piense en ello como undiven las páginas web. -
ListViewse utiliza para representar una matriz de objetos. -
ScrollViewse usa para agregar barras de desplazamiento. React Native no es como páginas web. Las barras de desplazamiento no se agregan automáticamente cuando el contenido es más grande que la vista o la pantalla. Es por eso que necesitamos usar este componente. -
TouchableHighlightse utiliza para hacer que un componente responda a eventos táctiles. -
AsyncStorageno es realmente un componente. Es una API utilizada para almacenar datos locales en React Native. - El
Buttones un componente de terceros para crear botones. -
GiftedSpinnerse usa para crear spinners cuando se cargan datos de la red. -
apies un módulo personalizado que ajustafetch, la forma de React Native para hacer solicitudes de red. Hay una gran cantidad de código repetitivo necesario para obtener los datos devueltos por una solicitud de red y es por eso que estamos envolviendo dentro de un módulo. Esto nos permite escribir menos código cuando hacemos solicitudes de red. -
momentes una biblioteca utilizada para todo lo relacionado con el tiempo.
Paso 2: Creando el componente
NewsItems
A continuación, creamos el componente NewsItems:
1 |
var NewsItems = React.createClass({ |
2 |
...
|
3 |
});
|
En
este componente se encuentra la función getInitialState, que se utiliza
para especificar el estado predeterminado para este componente. En React Native, el estado se usa para almacenar datos que están
disponibles en todo el componente. Aquí,
almacenamos el título de la aplicación, el dataSource para el
componente ListView, las news actuales y un valor booleano, loaded,
que indica si las noticias se están cargando o no desde la red. La
variable loaded se usa para determinar si se muestra o no el spinner. Lo configuramos en false para que el spinner sea visible por defecto.
Una
vez que las noticias se cargan, ya sea desde el almacenamiento local o
desde la red, se establece en true para ocultar el cargador. DataSource se utiliza para definir el diseño de la fuente de datos que
se utilizará para el componente ListView. Piense en ello como una clase
para padres en la que heredarán todas las fuentes de datos que definirá. Esto requiere un objeto que contenga la función rowHasChanged, que le
dice a ListView que vuelva a procesar cuando una fila ha cambiado.
Por
último, el objeto news contiene el valor inicial para la fuente
de datos de ListView.
1 |
getInitialState: function() { |
2 |
return { |
3 |
title: 'HN Reader', |
4 |
dataSource: new ListView.DataSource({ |
5 |
rowHasChanged: (row1, row2) => row1 !== row2, |
6 |
}),
|
7 |
news: {}, |
8 |
loaded: false |
9 |
}
|
10 |
},
|
Paso 3: Implementando la función render
La función de renderizado representa la interfaz de usuario para este
componente. Primero, envolvemos todo en una View. Entonces, adentro
tenemos el encabezado y el cuerpo. El encabezado contiene el título y el
spinner. El cuerpo contiene el ListView. Todo
dentro del cuerpo está envuelto dentro de ScrollView para que se
agregue automáticamente una barra de desplazamiento si el contenido
excede el espacio disponible.
1 |
render: function() { |
2 |
|
3 |
return ( |
4 |
<View style={styles.container}> |
5 |
<View style={styles.header}> |
6 |
<View style={styles.header_item}> |
7 |
<Text style={styles.header_text}>{this.state.title}</Text> |
8 |
</View> |
9 |
<View style={styles.header_item}> |
10 |
{ !this.state.loaded && |
11 |
<GiftedSpinner /> |
12 |
}
|
13 |
</View> |
14 |
</View> |
15 |
<View style={styles.body}> |
16 |
<ScrollView ref="scrollView"> |
17 |
{
|
18 |
this.state.loaded && |
19 |
|
20 |
<ListView initialListSize={1} dataSource={this.state.news} style={styles.news} renderRow={this.renderNews}></ListView> |
21 |
|
22 |
}
|
23 |
</ScrollView> |
24 |
</View> |
25 |
</View> |
26 |
);
|
27 |
|
28 |
},
|
Dentro del encabezado hay dos vistas:
- uno que contiene el título
- uno que contiene el spinner
Lo
estamos haciendo de esta manera en lugar de enviar el texto y la ruleta
directamente para que podamos controlar el estilo utilizando flexbox. Puede ver cómo se hace esto en la sección de estilo, más
adelante.
Podemos referirnos al título almacenado en el estado usando
this.state, seguido por el nombre de la propiedad. Como habrás notado,
cada vez que necesitamos referirnos a un objeto, lo envolvemos con
llaves. En
la otra vista, estamos verificando si la propiedad loaded en el estado
está configurada como false y, si es así, damos salida a la ruleta.
1 |
<View style={styles.header_item}> |
2 |
<Text style={styles.header_text}>{this.state.title}</Text> |
3 |
</View>
|
4 |
<View style={styles.header_item}> |
5 |
{ !this.state.loaded &&
|
6 |
<GiftedSpinner /> |
7 |
} |
8 |
</View>
|
El siguiente es el cuerpo
1 |
<ScrollView ref="scrollView"> |
2 |
{
|
3 |
this.state.loaded &&
|
4 |
|
5 |
<ListView initialListSize={1} dataSource={this.state.news} style={styles.news} renderRow={this.renderNews}></ListView> |
6 |
|
7 |
} |
8 |
</ScrollView>
|
Tenga en cuenta que hemos pasado un atributo ref al ScrollView. ref es un atributo predefinido en React Native que nos permite asignar
un identificador a un componente. Podemos usar este identificador para
referirnos al componente y llamar a sus métodos. Aquí hay un ejemplo de
cómo funciona esto:
1 |
scrollToTop: function(){ |
2 |
this.refs.scrollView.scrollTo(0); |
3 |
}
|
Luego puede tener un botón y hacer que llame a la función presionando. Esto desplazará automáticamente ScrollView a la parte superior del componente.
1 |
<Button onPress={this.scrollToTop}>scroll to top</Button> |
No usaremos esto en la aplicación, pero es bueno saber que existe.
Dentro de ScrollView, verificamos si la propiedad loaded en el estado
ya está configurada en true. Si es true, significa que la fuente
de datos ya está disponible para ser utilizada por ListView y podemos
renderizarla.
1 |
{
|
2 |
this.state.loaded &&
|
3 |
|
4 |
<ListView initialListSize={1} dataSource={this.state.news} renderRow={this.renderNews}></ListView> |
5 |
|
6 |
} |
Hemos pasado los siguientes atributos en el ListView:
-
initialListSizese usa para especificar cuántas filas renderizar cuando el componente se monta inicialmente. Lo hemos establecido en1, lo que significa que tomará un cuadro para representar cada fila. Lo configuré en1como una forma de optimización del rendimiento para que el usuario vea algo lo antes posible. -
dataSourcees la fuente de datos que se utilizará. -
renderRowes la función utilizada para representar cada fila en la lista.
Paso 4: Implementación de la función componentDidMount
A
continuación, tenemos la función componentDidMount, que se llama cuando
se monta este componente:
1 |
componentDidMount: function() { |
2 |
|
3 |
AsyncStorage.getItem('news_items').then((news_items_str) => { |
4 |
|
5 |
var news_items = JSON.parse(news_items_str); |
6 |
|
7 |
if(news_items != null){ |
8 |
|
9 |
AsyncStorage.getItem('time').then((time_str) => { |
10 |
var time = JSON.parse(time_str); |
11 |
var last_cache = time.last_cache; |
12 |
var current_datetime = moment(); |
13 |
|
14 |
var diff_days = current_datetime.diff(last_cache, 'days'); |
15 |
|
16 |
if(diff_days > 0){ |
17 |
this.getNews(); |
18 |
}else{ |
19 |
this.updateNewsItemsUI(news_items); |
20 |
}
|
21 |
|
22 |
});
|
23 |
|
24 |
|
25 |
}else{ |
26 |
this.getNews(); |
27 |
}
|
28 |
|
29 |
}).done(); |
30 |
|
31 |
},
|
Dentro de la función, tratamos de buscar las noticias que están actualmente almacenadas en el almacenamiento local. Usamos el método getItem de la API AsyncStorage. Devuelve una promesa
para que podamos obtener acceso a los datos devueltos llamando al método
then y pasando una función:
1 |
AsyncStorage.getItem('news_items').then((news_items_str) => { |
2 |
...
|
3 |
}).done(); |
AsyncStorage
solo puede almacenar datos de cadena, por lo que usamos JSON.parse para
convertir la cadena JSON a un objeto JavaScript. Si es null, llamamos al método getNews, que obtiene los datos de la red.
1 |
var news_items = JSON.parse(news_items_str); |
2 |
|
3 |
if(news_items != null){ |
4 |
...
|
5 |
}else{ |
6 |
this.getNews(); |
7 |
}
|
Si no está vacío, usamos AsyncStorage para buscar la última vez que se almacenaron las noticias en el almacenamiento local. Luego lo comparamos con la hora actual. Si la diferencia es al menos un
día (24 horas), buscamos las noticias de la red. Si no es así, usamos
los que están en el almacenamiento local.
1 |
AsyncStorage.getItem('time').then((time_str) => { |
2 |
var time = JSON.parse(time_str); |
3 |
var last_cache = time.last_cache; //extract the last cache time |
4 |
var current_datetime = moment(); //get the current time |
5 |
|
6 |
//get the difference in days
|
7 |
var diff_days = current_datetime.diff(last_cache, 'days'); |
8 |
|
9 |
if(diff_days > 0){ |
10 |
this.getNews(); //fetch from the network |
11 |
}else{ |
12 |
this.updateNewsItemsUI(news_items); //use the one in the cache |
13 |
}
|
14 |
|
15 |
});
|
Paso 5: Implementando la función renderNews
A continuación está la función para representar cada fila en la lista. Anteriormente en el ListView, hemos definido un atributo renderRow, que
tiene un valor de this.renderNews. Esta es esa función.
El elemento
actual en la iteración se pasa como un argumento a esta función. Esto
nos permite acceder al title y la url de cada noticia. Todo está
envuelto dentro del componente TouchableHighlight y dentro de él
mostramos el título de cada noticia.
El
componente TouchableHighlight acepta el atributo onPress, que
especifica qué función ejecutar cuando el usuario toca el elemento. Aquí
llamamos a la función viewPage y vinculamos la URL a ella. UnderlayColor especifica el color de fondo del componente cuando se
toca.
1 |
renderNews: function(news) { |
2 |
return ( |
3 |
<TouchableHighlight onPress={this.viewPage.bind(this, news.url)} underlayColor={"#E8E8E8"} style={styles.button}> |
4 |
<View style={styles.news_item}> |
5 |
<Text style={styles.news_item_text}>{news.title}</Text> |
6 |
</View> |
7 |
</TouchableHighlight> |
8 |
);
|
9 |
},
|
En la
función viewPage, obtenemos el atributo de navigator que hemos pasado
anteriormente desde index.android.js a través de los accesorios. En React Native, los apoyos se utilizan para acceder a los atributos que
se pasan desde el componente principal. Nos referimos a this.props,
seguido del nombre del atributo.
Aquí, estamos usando
this.props.navigator para referirnos al objeto navigator. A
continuación, llamamos al método push para insertar la ruta web_page en el navegador junto con la URL de la página web que abrirá
el componente WebPage. Esto hace que la aplicación haga la transición al
componente WebPage.
1 |
viewPage: function(url){ |
2 |
this.props.navigator.push({name: 'web_page', url: url}); |
3 |
},
|
Paso 6: Implementando la función updateNewsItemsUI
La
función updateNewsItemsUI actualiza el origen de datos y el estado en
función de la matriz de elementos de noticias que se pasó como
argumento. Solo lo hacemos si el total de news_items es igual al valor
que establecimos anteriormente para TOTAL_NEWS_ITEMS. En React Native,
la actualización del estado activa la interfaz de usuario para volver a
procesar. Esto significa que llamar a setState con una nueva fuente de
datos actualiza la interfaz de usuario con los nuevos elementos.
1 |
updateNewsItemsUI: function(news_items){ |
2 |
|
3 |
if(news_items.length == TOTAL_NEWS_ITEMS){ |
4 |
|
5 |
var ds = this.state.dataSource.cloneWithRows(news_items); //update the data source |
6 |
|
7 |
//update the state
|
8 |
this.setState({ |
9 |
'news': ds, |
10 |
'loaded': true |
11 |
});
|
12 |
|
13 |
}
|
14 |
|
15 |
},
|
Paso 7: actualizar el almacenamiento local
La función updateNewsItemDB actualiza las noticias que están almacenadas
en el almacenamiento local. Usamos la función JSON.stringify para
convertir la matriz en una cadena JSON para que podamos almacenarla
usando AsyncStorage.
1 |
updateNewsItemDB: function(news_items){ |
2 |
|
3 |
if(news_items.length == TOTAL_NEWS_ITEMS){ |
4 |
AsyncStorage.setItem('news_items', JSON.stringify(news_items)); |
5 |
}
|
6 |
|
7 |
},
|
Paso 8: Obteniendo noticias
La
función getNews actualiza el elemento de almacenamiento local que
almacena la última vez que se almacenaron los datos en la memoria caché,
extrae las noticias de Hacker News API, actualiza la interfaz de
usuario y el almacenamiento local en función de los nuevos elementos que
se obtuvieron.
1 |
getNews: function() { |
2 |
|
3 |
var TOP_STORIES_URL = 'https://hacker-news.firebaseio.com/v0/topstories.json'; |
4 |
var news_items = []; |
5 |
|
6 |
AsyncStorage.setItem('time', JSON.stringify({'last_cache': moment()})); |
7 |
|
8 |
api(TOP_STORIES_URL).then( |
9 |
(top_stories) => { |
10 |
|
11 |
for(var x = 0; x <= 10; x++){ |
12 |
|
13 |
var story_url = "https://hacker-news.firebaseio.com/v0/item/" + top_stories[x] + ".json"; |
14 |
|
15 |
api(story_url).then( |
16 |
(story) => { |
17 |
|
18 |
news_items.push(story); |
19 |
this.updateNewsItemsUI(news_items); |
20 |
this.updateNewsItemDB(news_items); |
21 |
|
22 |
}
|
23 |
);
|
24 |
|
25 |
}
|
26 |
}
|
27 |
);
|
28 |
|
29 |
}
|
El recurso de historias principales en Hacker News API devuelve una matriz que se ve así:
1 |
[ 10977819, 10977786, 10977295, 10978322, 10976737, 10978069, 10974929, 10975813, 10974552, 10978077, 10978306, 10973956, 10975838, 10974870... |
Estos son los identificadores de los artículos principales publicados en Hacker News. Es por eso que tenemos que recorrer este conjunto y hacer una solicitud de red para cada elemento a fin de obtener los detalles reales, como el título y la URL.
Luego lo enviamos a la
matriz de news_items y llamamos a las funciones updateNewsItemsUI y
updateNewsItemDB para actualizar la interfaz de usuario y el
almacenamiento local.
1 |
for(var x = 0; x <= 10; x++){ |
2 |
|
3 |
var story_url = "https://hacker-news.firebaseio.com/v0/item/" + top_stories[x] + ".json"; |
4 |
|
5 |
api(story_url).then( |
6 |
(story) => { |
7 |
|
8 |
news_items.push(story); |
9 |
this.updateNewsItemsUI(news_items); |
10 |
this.updateNewsItemDB(news_items); |
11 |
|
12 |
}
|
13 |
);
|
14 |
|
15 |
}
|
Paso 9: estilo
Agregue los siguientes estilos:
1 |
var styles = StyleSheet.create({ |
2 |
container: { |
3 |
flex: 1 |
4 |
}, |
5 |
header: { |
6 |
backgroundColor: '#FF6600', |
7 |
padding: 10, |
8 |
flex: 1, |
9 |
justifyContent: 'space-between', |
10 |
flexDirection: 'row' |
11 |
}, |
12 |
body: { |
13 |
flex: 9, |
14 |
backgroundColor: '#F6F6EF' |
15 |
}, |
16 |
header_item: { |
17 |
paddingLeft: 10, |
18 |
paddingRight: 10, |
19 |
justifyContent: 'center' |
20 |
}, |
21 |
header_text: { |
22 |
color: '#FFF', |
23 |
fontWeight: 'bold', |
24 |
fontSize: 15 |
25 |
}, |
26 |
button: { |
27 |
borderBottomWidth: 1, |
28 |
borderBottomColor: '#F0F0F0' |
29 |
}, |
30 |
news_item: { |
31 |
paddingLeft: 10, |
32 |
paddingRight: 10, |
33 |
paddingTop: 15, |
34 |
paddingBottom: 15, |
35 |
marginBottom: 5 |
36 |
}, |
37 |
news_item_text: { |
38 |
color: '#575757', |
39 |
fontSize: 18 |
40 |
}
|
41 |
}); |
La mayor parte es CSS estándar, pero tenga en cuenta que hemos reemplazado los guiones con la sintaxis de camel case. Esto no se debe a que obtenemos un error de sintaxis si utilizamos algo
como padding-left. Es porque es requerido por React Native. También
tenga en cuenta que no todas las propiedades CSS se pueden
utilizar.
Dicho esto, aquí hay algunas declaraciones que pueden no ser tan intuitivas, especialmente si no has usado flexbox antes:
1 |
container: { |
2 |
flex: 1 |
3 |
}, |
4 |
header: { |
5 |
backgroundColor: '#FF6600', |
6 |
padding: 10, |
7 |
flex: 1, |
8 |
justifyContent: 'space-between', |
9 |
flexDirection: 'row' |
10 |
}, |
11 |
body: { |
12 |
flex: 9, |
13 |
backgroundColor: '#F6F6EF' |
14 |
}, |
Aquí hay una versión simplificada del marcado para el componente NewsItems para ayudarlo a visualizarlo:
1 |
<View style={styles.container}> |
2 |
<View style={styles.header}> |
3 |
... |
4 |
</View>
|
5 |
<View style={styles.body}> |
6 |
... |
7 |
</View>
|
8 |
</View>
|
Hemos configurado container para flex: 1, lo que significa que ocupa toda la pantalla. Dentro del container tenemos el header y el body, que hemos
configurado para flex: 1 y flex: 9, respectivamente. En este caso,
flex: 1 no ocupará toda la pantalla ya que el header tiene un
hermano. Estos dos compartirán la pantalla completa. Esto
significa que toda la pantalla se dividirá en diez secciones, ya que
tenemos flex: 1 y flex: 9. Se suman los valores de flex para cada uno de
los hermanos.
El header ocupa el 10% de la pantalla y body
ocupa el 90% de la misma. La
idea básica es elegir un número que represente la altura o el ancho de
toda la pantalla y luego cada hermano toma una pieza de este número. Sin
embargo, no te excedas con esto. No desea usar 1000 a menos que desee
implementar su aplicación en una sala de cine. Encuentro diez para ser
el número mágico cuando trabajo con la altura.
Para el header hemos
establecido los siguientes estilos:
1 |
header: { |
2 |
backgroundColor: '#FF6600', |
3 |
padding: 10, |
4 |
flex: 1, |
5 |
justifyContent: 'space-between', |
6 |
flexDirection: 'row' |
7 |
}, |
Y para refrescar su memoria, aquí está el marcado simplificado de lo que está dentro del encabezado:
1 |
<View style={styles.header_item}> |
2 |
... |
3 |
</View>
|
4 |
<View style={styles.header_item}> |
5 |
... |
6 |
</View>
|
Y el estilo agregado a aquellos:
1 |
header_item: { |
2 |
paddingLeft: 10, |
3 |
paddingRight: 10, |
4 |
justifyContent: 'center' |
5 |
}, |
Hemos establecido flexDirection para row y justifyContent para space-between en su superior, que es header. Esto
significa que sus secundarios se distribuirán de manera uniforme, con el
primero al comienzo de la línea y el último al final de la
línea.
Por defecto, flexDirection se
establece en column, lo que significa que cada uno ocupa toda la
línea, ya que el movimiento es horizontal. Usar row haría que el flujo
fuera vertical para que cada niño estuviera uno al lado del otro. Si
todavía está confundido acerca de Flexbox o si desea obtener más
información al respecto, consulte CSS: Flexbox Essentials.
Por último,
exponer el componente al mundo exterior:
1 |
module.exports = NewsItems; |
Conclusión
En este punto, debe tener una buena idea sobre cómo hacer las cosas de la manera React Native. Específicamente, ha aprendido cómo crear un nuevo proyecto React Native, instalar bibliotecas de terceros a través de npm, usar varios componentes y agregar estilo a la aplicación.
En el próximo artículo, continuaremos
agregando el componente WebPage a la aplicación News Reader. Siéntase
libre de dejar cualquier pregunta o comentario en la sección de
comentarios a continuación.



