Thường lần đầu ước lượng cho 1 app React Native (RN) hay bị sai hoàn toàn.
Nếu bạn search Google về các thành phần như button, footer, v.v… bạn sẽ thấy rằng có rất nhiều thư viện bạn có thể sử dụng. Nó sẽ thật sự có ích nếu bạn không có bất kì layout được thiết kế đặc biệt nào cả, chỉ build 1 page sử dụng những thứ này và đó là tất cả. Nhưng nếu bạn có một vài thiết kế đặc biệt và trong đó, thiết kế của button này khác biệt – bạn sẽ cần phải set custom style cho mỗi button. Điều này có vẻ hơi khôn lanh một chút. Tất nhiên là bạn có thể gói gọn mọi component đã được built trong phần component và set custom style cho chúng ở đây. Nhưng tôi nghĩ sẽ dễ dàng hơn và có nhiều giá trị hơn nếu bạn tự build component của mình sử dụng View, text, touchableOpacity và các module khác từ RN. Tại sao ư? Bạn sẽ hiểu cách thức làm việc với RN khi luyện tập ngày một nhiều hơn. Và bạn cũng nhận ra rằng các phiên bản của component bạn tự tạo ra sẽ ko bao giờ thay đổi vì vậy sẽ không cần phải phụ thuộc vào các phiên bản modules bên ngoài
Điều này chỉ có ích nếu bạn có những layout khác nhau cho các phiên bản iOS và Android. Nếu không, bạn có thể đơn giản sử dụng các Platform API được cung cấp bởi RN để kiểm tra - tùy thuộc vào platform mà điện thoại đang sử dụng
Nếu layout hoàn toàn khác nhau – nên tách các layout khác nhau trong các file khác nhau
Nếu bạn đặt tên 1 file là index.ios.js - khi bắt đầu build iOS RN sẽ sử dụng file này để hiển thị IOS layout. Tương tự đối với index.android.js.
Bạn có thể hỏi “còn code duplication thì sao?” Bạn có thể chuyển các duplicated code sang hepler và sau đó chỉ cần tái sử dụng các helper này.
Đây là một lỗi khá nghiêm trọng
Khi bạn đang lên kế hoạch cho app của mình, chắc chắn bạn nghĩ rất nhiều về layout, và việc xử lí các vấn đề về data thì ít hơn.
Redux giúp chúng ta lưu trữ data một cách đúng đắn. Nếu redux store được lên kế hoạch 1 cách đúng đắn thì nó sẽ trở thành một công cụ quyền lực để quản lí app data. Nếu không, nó có thể làm lộn xộn mọi thứ.
Khi bắt đầu build 1 RN app. Mọi người thường nghĩ về các reducers như là 1 data storage cho mỗi container. Vậy nếu bạn có sign in, forgot password, ToDo list page thì nên các các reducer tương tự: SignIn, Forgot, ToDoList
Sau khi làm việc với kế hoạch lưu trữ này, tôi nhận ra rằng trong trường hợp của tôi thì ko dễ dàng để quản lí data. Tôi có một ToDO page chi tiết và sử dụng kế hoạch lưu trữ này tôi đã cung cấp ToDo Details reducer. Và đó là một sai lầm khủng khiếp. Tại sao?
Khi tôi chọn các item từ ToDo list – tôi cần phải chuyển data đến ToDoDetail reducer. Điều đó có nghĩa là phải sử dụng thêm các hành động để gửi data đến reducer, và điều này thì không quá thoải mái.
Sau khi nghiên cứu thêm, tôi quyết định lên kế hoạch lưu trữ một cách khác biệt và cấu trúc như sau:
Auth được sử dụng để trữ các token chính gốc. Vậy thôi.
Và các reducer Todos và friends được sử dụng để chứa các thực thể nhằm dễ dàng hiểu được ngay từ cái tên. Và khi đi đến màn hình chi tiết ToDo – tôi chỉ cần tìm kiếm thông qua tất cả ToDos bằng ID. Vậy thôi.
Với những cấu trức phức tạp hơn, tôi thật sự đề nghị nên dùng kế hoạch này. Bạn sẽ luôn luôn có thể hiểu rõ được mọi thứ và nơi để tìm kiếm chúng.
Việc bạn lên kế hoạch cho cấu trúc dự án thì luôn khó khăn khi bạn chỉ là người mới bắt đầu. Đầu tiên, bạn cần phải hiểu app của bạn lớn cỡ nào/ Rất lớn, lớn hay nhỏ thôi? Có bao nhiêu screen cần hiển thị trong app của bạn? 20? 30? 10/ 5/ Hello world screen
Cấu trúc đầu tiên tôi từng gặp và bắt đầu thực hiện như vậy:
Đây là một cấu trúc tốt nếu đó ko phải là 1 app lớn. Ví dụ, maximum 10 screen. Nếu app lớn hơn, hãy xem xét đến việc sử dụng cầu trúc như thế này:
Vậy khác biệt ở đây là gì? Như bạn có thể thấy, loại đầu tiên đề xuất để trữ các action và reducer tách biệt khỏi container. Thứ 2, trữ cùng nhau. Nếu app nhỏ thì việc lưu trữ redux tách biệt với container sẽ có lợi hơn. Nếu không, với container bạn sẽ luôn biết được hành động nào sẽ liên quan đến container này
Nếu bạn có style chung (như cho header, footer, và button) bạn có thể tạo ra 1 folder tên là “styles”và set ở đó 1 index.js file và viết các style phổ biến ở đây. Sau đó, bạn chỉ cần tái sử dụng chúng trên mỗi page
Nếu có nhiều cấu trúc khác nhau, bạn nên tìm hiểu xem cái nào trong số đó sẽ phù hợp với yêu cầu của bạn hơn
Khi bạn bắt đầu làm việc với RN và thực hiện app đầu tiên thì có một vài dòng code đã có sẵn trong index.ios.js file. Nếu kiểm tra bạn sẽ thấy các style được lưu trữ trong các object riêng không có sự đánh giá nào trong phương thức render và mọi thứ được xây dựng với các modules được cung cấp bởi RN (View, Text).
Thực tế, bạn sẽ cần phải sử dụng rất nhiều components không chỉ được cung cấp bởi RN. Bạn sẽ build nhiều components để tái sử dụng khi build các container
Hãy xem xét component này:
import React, { Component } from ‘react’;
import {
Text,
TextInput,
View,
TouchableOpacity
} from ‘react-native’;
import styles from ‘./styles.ios’;
export default class SomeContainer extends Component {
constructor(props){
super(props);
this.state = {
username:null
}
}
_usernameChanged(event){
this.setState({
username:event.nativeEvent.text
});
}
_submit(){
if(this.state.username){
console.log(`Hello, ${this.state.username}!`);
}
else{
console.log(‘Please, enter username’);
}
}
render() {
return (
<View style={styles.container}>
<View style={styles.avatarBlock}>
<Image
source={this.props.image}
style={styles.avatar}/>
</View>
<View style={styles.form}>
<View style={styles.formItem}>
<Text>
Username
</Text>
<TextInput
onChange={this._usernameChanged.bind(this)}
value={this.state.username} />
</View>
</View>
<TouchableOpacity onPress={this._submit.bind(this)}>
<View style={styles.btn}>
<Text style={styles.btnText}>
Submit
</Text>
</View>
</TouchableOpacity>
</View>
);
}
}
Trông nó như thế nào?
Như bạn có thể thấy, tất cả các style đều được lưu trữ trong các module khác nhau – tốt. Cho đến thời điểm hiện tại không có code dupliucation – tốt. Nhưng chúng ta có thường chỉ dùng 1 trường trong form ko? Tôi không chắc là sẽ thường xuyên đâu. Mặc dù button component – cái được đặt trong TochableOpacity có thể được tách ra vậy nên chúng ta có thể tái sử dụng trong tương lai. Còn về Image, chúng ta cũng có thể sử dụng block này trong tương lai để nó có thể chuyển tới một component tách biệt
Và sau khi thay đổi, chúng ta có được kết quả như thế này:
import React, { Component, PropTypes } from 'react';
import {
Text,
TextInput,
View,
TouchableOpacity
} from 'react-native';
import styles from './styles.ios';
class Avatar extends Component{
constructor(props){
super(props);
}
render(){
if(this.props.imgSrc){
return(
<View style={styles.avatarBlock}>
<Image
source={this.props.imgSrc}
style={styles.avatar}/>
</View>
)
}
return null;
}
}
Avatar.propTypes = {
imgSrc: PropTypes.object
}
class FormItem extends Component{
constructor(props){
super(props);
}
render(){
let title = this.props.title;
return(
<View style={styles.formItem}>
<Text>
{title}
</Text>
<TextInput
onChange={this.props.onChange}
value={this.props.value} />
</View>
)
}
}
FormItem.propTypes = {
title: PropTypes.string,
value: PropTypes.string,
onChange: PropTypes.func.isRequired
}
class Button extends Component{
constructor(props){
super(props);
}
render(){
let title = this.props.title;
return(
<TouchableOpacity onPress={this.props.onPress}>
<View style={styles.btn}>
<Text style={styles.btnText}>
{title}
</Text>
</View>
</TouchableOpacity>
)
}
}
Button.propTypes = {
title: PropTypes.string,
onPress: PropTypes.func.isRequired
}
export default class SomeContainer extends Component {
constructor(props){
super(props);
this.state = {
username:null
}
}
_usernameChanged(event){
this.setState({
username:event.nativeEvent.text
});
}
_submit(){
if(this.state.username){
console.log(`Hello, ${this.state.username}!`);
}
else{
console.log('Please, enter username');
}
}
render() {
return (
<View style={styles.container}>
<Avatar imgSrc={this.props.image} />
<View style={styles.form}>
<FormItem
title={"Username"}
value={this.state.username}
onChange={this._usernameChanged.bind(this)}/>
</View>
<Button
title={"Submit"}
onPress={this._submit.bind(this)}/>
</View>
);
}
}
Yeah, có thể là có nhiều hơn code bởi vì chúng ta đã add wrapper cho avatar, FormItem và Button component nhưng giờ chúng ta có thể tái sử dụng tất cả component này bất cứ lúc nào ta muốn. Chúng ta có thể chuyển chúng đến các module tách biệt và nhập vào mọi nơi chúng ta cần. Chúng ta có thể add thêm cho chúng một số đạo cụ khác ví dụ như style, textStyle, onLongPress, onBlur, onFocus và các component này hoàn toàn có thể customize
Nhưng hãy đảm bảo rằng không quá đào sâu vào việc customize các component nhỏ để chúng có thể trở nên lớn đột biến - sẽ rất tệ và khó để đọc. Thật đó, và thậm chí nếu tại thời điểm hiện tại ý tưởng về việc add thêm 1 số property xem như là cách dễ nhất để giải quyết vấn đề. Trong tương lai, những property này có thể gây rối cho bạn khi bạn đọc code
Về hệ thống smart/dumb. Hãy xem thử cái này:
class Button extends Component{
constructor(props){
super(props);
}
_setTitle(){
const { id } = this.props;
switch(id){
case 0:
return 'Submit';
case 1:
return 'Draft';
case 2:
return 'Delete';
default:
return 'Submit';
}
}
render(){
let title = this._setTitle();
return(
<TouchableOpacity onPress={this.props.onPress}>
<View style={styles.btn}>
<Text style={styles.btnText}>
{title}
</Text>
</View>
</TouchableOpacity>
)
}
}
Button.propTypes = {
id: PropTypes.number,
onPress: PropTypes.func.isRequired
}
export default class SomeContainer extends Component {
constructor(props){
super(props);
this.state = {
username:null
}
}
_submit(){
if(this.state.username){
console.log(`Hello, ${this.state.username}!`);
}
else{
console.log('Please, enter username');
}
}
render() {
return (
<View style={styles.container}>
<Button
id={0}
onPress={this._submit.bind(this)}/>
</View>
);
}
}
Bạn có thể thấy, chúng ta đã upgrade component button. Có điều gì đã thay đổi? Chúng ta đã thay thế property ‘title” bằng ID. Và bây giờ chúng ta đã có một vài “ linh hoạt” trong button component. Nhập 0 nó sẽ hiện submit. Nhập 2 – delete nhưng cái này rất tệ
Button được tạo ra là một dumb component – chỉ để hiển thị data được thảy qua nó và để thực hiện mọi việc ở level cao hơn. Dumb component không nên biết bất cứ gì về mọi thứ xảy ra quanh chúng. Chỉ đơn giản làm và cho thấy điều chúng được bảo. Và sau title upgrade, chúng ta làm nó trở nên thông minh hơn. Và điều này rất tệ, tại sao?
Chuyện gì sẽ xảy ra nếu chúng ta nhập 5 là ID của component này? Chúng ta sẽ phải update nó để nó có thể làm việc với option này và hơn thế nữa. Dumb component chỉ nên hiển thị và làm điều chúng được nói. Vậy thôi.
Sau khi làm việc một ít với layout ở RN, tôi gặp 1 vấn đề khi viết các style inline. Như thế này:
render() {
return (
<View style={{flex:1, flexDirection:'row', backgroundColor:'transparent'}}>
<Button
title={"Submit"}
onPress={this._submit.bind(this)}/>
</View>
);
}
Khi bạn viết như thế này bạn sẽ nghĩ “ok sau khi check layour trong simulator sẽ chuyển các style để tách module nếu nó ok” và có thể đó là điều bạn thực sự muốn làm. Nhưng, không may, điều này sẽ không xảy ra. Ít nhất không ai trong team của tôi sẽ làm điều đó nếu không được nhắc
Luôn viết các style trong các module tách biệt. Nó sẽ giữ bạn an toàn sau khi inline style
Đây là một trong những lỗi trong project của tôi, có thể cũng sẽ giúp ích cho bạn
Để xác thực biểu mẫu với redux, tôi cần phải tạo ra các action, action type. Các field tách biệt trong reducer và điều này thực sự rất phiền phức.Vì vậy nên tôi quyết định thực hiện với sự giúp đỡ từ state. Không reducer, types, v.v… chỉ có những tính năng thuần khiết trên các level container. Điều này giúp tôi rất nhiều – loại bỏ tất cả các tính năng không cần thiết từ action file, reducer file, không còn thao tác gì với store nữa
Nhiều người chuyển từ lập trình web sang lập trình RN. Trong web thì có CSS chưa các z-index property, nó giúp chúng ta hiển thị các layer chúng ta muốn tại level đó. Điều này trên web thì cực hữu ích. Trong RN không có tính năng đó ngay từ đầu nhưng sau đó đã được add thêm, và vì vậy, tôi đã bắt đàu sử dụng nó. Ban đầu thì rất dễ dàng, render layer tại bất cứ nơi nào bạn muốn và chỉ cần set chúng zIndex như là 1 style, và nó đã thực hiện được, nhưng sau khi thử trên Android, bây giờ tôi chỉ đặt cấu trúc layer của mình theo cách chúng nên hiển thị. Đây là zIndex tốt nhất bạn có thể làm. Thật đấy
Khi bạn muốn tiết kiệm thời gian thì hay sử dụng các module bên ngoài, thường thì chũng sẽ có các documentation, bạn chỉ cần lấy thông tin từ chúng và sử dụng
Nhưng thỉnh thoảng module này có thể bị hỏng hoặc là không chạy như cách nó được mô tả, đó là lí do vì sao bạn cần phải đọc code. Bạn sẽ hiểu được lỗi phát sinh từ đâu, có thể do chất lượng của module này rất tệ hoặc đơn giản chỉ là vì bạn sử dụng chúng sai cách. Thêm vào đó, bạn sẽ học được cách xây dựng module riêng của bạn thân nếu bạn đọc code của các module khác
RN cung cấp cho chúng ta đủ khả năng để xây dựng 1 app native. Và điều gì khiến 1 app trở nên Native? Layout, gesture và animation.
Và nếu layout được cung cấp mặc định, khi bạn sử dụng các module View, Text, TextInput và các RN Module khác, Gesture và Animations nên được quản lí bởi PanResponder và Animated APIs.
Nếu bạn chuyển sang từ web thì bạn sẽ cảm thấy có một chút đáng sợ đấy – lấy user gesture, và khi bắt đầu, khi kết thúc, long press, short press, và thâm chí nó còn không quá rõ ràng – cách để animate một số thứ trong RN
Đây là một Button component tôi built với sự giúp đỡ của PanResponder và Animated. Button này được xây dựng để capture user gesture, Ví dụ - khi user nhấn vào item và kéo ngón tay tới rìa, với một chút giúp sức từ Animated API được xây dựng để thay đổi độ mờ khi nút đó được nhấn:
'use strict';
import React, { Component, PropTypes } from 'react';
import { Animated, View, PanResponder, Easing } from 'react-native';
import moment from 'moment';
export default class Button extends Component {
constructor(props){
super(props);
this.state = {
timestamp: 0
};
this.opacityAnimated = new Animated.Value(0);
this.panResponder = PanResponder.create({
onMoveShouldSetPanResponderCapture: (evt, gestureState) => true,
onStartShouldSetResponder:() => true,
onStartShouldSetPanResponder : () => true,
onMoveShouldSetPanResponder:(evt, gestureState) => true,
onPanResponderMove: (e, gesture) => {},
onPanResponderGrant: (evt, gestureState) => {
/**THIS EVENT IS CALLED WHEN WE PRESS THE BUTTON**/
this._setOpacity(1);
this.setState({
timestamp: moment()
});
this.long_press_timeout = setTimeout(() => {
this.props.onLongPress();
}, 1000);
},
onPanResponderStart: (e, gestureState) => {},
onPanResponderEnd: (e, gestureState) => {},
onPanResponderTerminationRequest: (evt, gestureState) => true,
onPanResponderRelease: (e, gesture) => {
/**THIS EVENT IS CALLED WHEN WE RELEASE THE BUTTON**/
let diff = moment().diff(moment(this.state.timestamp));
if(diff < 1000){
this.props.onPress();
}
clearTimeout(this.long_press_timeout);
this._setOpacity(0);
this.props.releaseBtn(gesture);
}
});
}
_setOpacity(value){
/**SETS OPACITY OF THE BUTTON**/
Animated.timing(
this.opacityAnimated,
{
toValue: value,
duration: 80,
}
).start();
}
render(){
let longPressHandler = this.props.onLongPress,
pressHandler = this.props.onPress,
image = this.props.image,
opacity = this.opacityAnimated.interpolate({
inputRange: [0, 1],
outputRange: [1, 0.5]
});
return(
<View style={styles.btn}>
<Animated.View
{...this.panResponder.panHandlers}
style={[styles.mainBtn, this.props.style, {opacity:opacity}]}>
{image}
</Animated.View>
</View>
)
}
}
Button.propTypes = {
onLongPress: PropTypes.func,
onPressOut: PropTypes.func,
onPress: PropTypes.func,
style: PropTypes.object,
image: PropTypes.object
};
Button.defaultProps = {
onPressOut: ()=>{ console.log('onPressOut is not defined'); },
onLongPress: ()=>{ console.log('onLongPress is not defined'); },
onPress: ()=>{ console.log('onPress is not defined'); },
style: {},
image: null
};
const styles = {
mainBtn:{
width:55,
height:55,
backgroundColor:'rgb(255,255,255)',
}
};
Đầu tiên, chúng ta bắt đầu với PanResponder’s object. Tại đây, chúng ta set handler khác nhau. Điều làm chúng ra cảm thấy thú vị là trên onPanResponderGrand (được gọi khi người sử dụng chạm vào button) và onPanResponderRelease(được gọi khi user bỏ tay ra khỏi màn hình) handlers;
Chúng ta cũng setup animated object giúp làm việc với animation. Set giá trị của nó tới zero, sau đó define _setOpacity, làm thay đổi giá trị của .opacityAnimated này. Và trước khi render, chúng ta thêm this.opacity vào giá trị opacity bình thường. Chúng ta không sử dụng module View mà là Animated.View để tự động thay đổi độ mờ
Như bạn có thể thấy, không quá khó để có thể hiểu được mọi thứ. Tất nhiên là bạn sẽ cần phải đọc các documentation về API này để khiến app trở nên hoàn hảo. Nhưng tôi hi vọng rằng ví dụ này sẽ giúp bạn bắt đầu
Bạn có thể làm hầu hết mọi thứ với React Native. Nếu không, bạn có thể làm với Swift/Object C hay Java và sau đó chỉ cần chuyển nó đến React Native.
Via Medium