Chuyện của dev's blog

Xây dựng ứng dụng #chuyencuadev với React Native trong 10 phút như thế nào

 19/10/2017  6802

Bạn có đang có 1 chiếc Smart phone, bạn thường phải đọc tài liệu, đọc báo, cả coi xxx nữa ngay trên chính smart phone đó. Bạn đang là dev, bạn có suy nghĩ app mobile bây giờ có thể được build cực kì đơn giản không? Dưới đây, mình sẽ hướng dẫn các bạn build 1 app #chuyencuadev cực nhanh gọn trên nền kiến thức React Native mà mình tích lũy được, có thể không phải là perfect nên mong sự đóng góp thêm ý kiến từ các bạn :) Let's start !!!

Giới thiệu

React Native (RN) là một công nghệ mới đầy hứa hẹn cho việc phát triển ứng dụng mobile chạy trên đa nền tảng. Từ một React developer mình đã nhanh chóng làm quen, nghiên cứu và ứng dụng vào các projects. React Native sử dụng React để phát triển ứng dụng đem lại nhiều lợi ích như tái sử dụng code base, học một lần và sử dụng được nhiều nền tảng khác nhau.

Điều gì đã khiến tôi viết bài này?

Sau nhiều tháng học và phát triển ứng dụng dựa trên React Native, hôm nay mình sẽ thực hiện series Coding Challenge, với mục đích vừa chia sẻ những gì mình đã học được cũng như thử thách bản thân trên những project thực tế. Với mỗi bài viết mình sẽ chọn 1 ứng dụng mobile thuần Việt và viết lại bằng React Native.

Các bạn cũng có thể comment ứng dụng mà bạn thấy hay, mình sẽ chọn và viết bài cách viết ứng dụng đó. Tất nhiên mình không thể cover hết tất cả tính năng, mình sẽ chỉ cover những main features của ứng dụng đó thôi nhé!

Ứng dụng hoạt động như thế nào.

ChuyenCuaDev mockup

Các chức năng chính thì cũng gần tương tự trang chuyencuadev.com.

Ứng dụng sẽ có màn hình chính:

  • Màn hình chính (HomeScreen): hiển thị danh sách bài viết.
  • Khi user click vào bài viết bất kì, app sẽ điều hướng sang PostScreen.
  • Màn hình bài viết (PostScreen): hiển thị chi tiết thông tin về bài viết.
  • Khi user click icon Back, app sẽ điều hướng về HomeScreen
  • Khi user click vào icon Comment, app sẽ chuyển sang màn hình CommentScreen
  • Màn hình bình luận (PostCommentsScreen): Hiển thị danh sách bình luận của bài viết.
  • Khi user click icon Back, app sẽ điều hướng về PostScreen

Điều kiện tiên quyết

Trong bài viết này mình chỉ tập trung vào phần code chứ không giải thích thêm về setup môi trường, react là gì, … nên bạn chịu khó tìm hiểu trước nhé, chỉ cần google là cả đống tài liệu thoai :D

Vậy nên bạn cần chuẩn bị trước khi đọc phần code nhé.

  • Javascript + Ecmascript 2016 (ES6)
  • React căn bản
  • CSS căn bản
  • Máy tính chạy Mac OS nếu như muốn viết app iOS

Khởi tạo ứng dụng

react-native init ChuyenCuaDev

Sau khi quá trình khởi tạo project kết thúc, làm theo hướng dẫn nhé

cd ./ChuyenCuaDev

# iOS (Mac only)
react-native run-ios
# Android, bạn phải mở android emulator lên trước khi chạy
react-native run-android

Câu lệnh trên sẽ chạy các tiến trình sau:

  • Build Native App (lần đầu)
  • Start React Packager
  • Start simulator (iOS)
  • Install app

Dependencies

Trong ứng dụng này mình sẽ cài thêm 1 số thư viện

  • react-navigation điều hướng trong app.
  • faker tạo dữ liệu mẫu
  • moment format ngày tháng

Let's Code

App.js

Giờ hãy mở file App.js lên, mình sẽ giới thiệu sơ qua về file này.

App.js sẽ là entry point mà React Native sẽ đọc vào khi chạy ứng dụng. Nội dung của nó bao gồm 3 phần:

1 - Imports
import React, { Component } from 'react';
import {
  Platform,
  StyleSheet,
  Text,
  View
} from 'react-native';

Dòng đầu tiên, import React là bắt buộc khi viết một react component.

Dòng kế tiếp chỉ là import 4 components của react native.

2- App Component
export default class App extends Component<{}> {
  render() {
    return (
      <View style={styles.container}>
        <Text style={styles.welcome}>
          Welcome to React Native!
        </Text>
        <Text style={styles.instructions}>
          To get started, edit App.js
        </Text>
        <Text style={styles.instructions}>
          {instructions}
        </Text>
      </View>
    );
  }
}

Phần kế tiếp là tạo component và export nó thành root component, để react native có thể lấy được nội dung và render ứng dụng. Component default ở đây họ định nghĩa bằng ES6 class. Trong component này họ đã định nghĩa method render và nó return JSX content.

<View/> là một container chứa nội dung bên trong và hỗ trợ layout, … View tương tự như tag div trong html.

<Text/>là component chỉ chứa nội dung chữ. Tương tự tag span trong html.

Nếu bạn đã từng làm việc với html, css thì đoạn trên có thể hiểu như sau

<div style={styles.container}>
  <span style={styles.welcome}>
    Welcome to React Native!
  </span>
  <span style={styles.instructions}>
    To get started, edit App.js
  </span>
  <span style={styles.instructions}>
    {instructions}
  </span>
</div>
3-Style
const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#F5FCFF',
  },
  welcome: {
    fontSize: 20,
    textAlign: 'center',
    margin: 10,
  },
  instructions: {
    textAlign: 'center',
    color: '#333333',
    marginBottom: 5,
  },
});

Đoạn cuối cùng đó là định nghĩa styles, style strong react native có sự tương đồng khá lớn với CSS. Thực tế React Native đã làm bộ style giống như CSS để developer không cảm thấy xa lạ và giúp họ học RN nhanh hơn.

Đoạn trên nếu chuyển sang CSS thì có thể viết như sau

.container {
  display: flex;
  flex: 1;
  justify-content: center;
  align-items: center;
  background-color: #F5FCFF;
}
.welcome {
  font-size: 20;
  text-align: center;
  margin: 10;
}
instructions {
  text-align: center;
  color: #333333;
  margin-bottom: 5;
}

Screens

Giờ đến bước xây dựng các screens (màn hình) có trong app.

Trong phần App Flow mình đã phân tích là app ChuyenCuaDev sẽ có 3 screens: HomeScreen, PostScreen và CommentsScreen.

Giờ trong file App.js, mình sẽ xóa hết nội dung và chỉ để lại phần import, sau đó mình sẽ tạo 3 components

import React from 'react';
import { StyleSheet, Text, View } from 'react-native';

class HomeScreen extends React.Component {
  render() {
    return (
      <View>
        <Text>HomeScreen</Text>
      </View>
    );
  }
}

class PostScreen extends React.Component {
  render() {
    return (
      <View>
        <Text>PostScreen</Text>
      </View>
    );
  }
}

class PostCommentsScreen extends React.Component {
  render() {
    return (
      <View>
        <Text>PostCommentsScreen</Text>
      </View>
    );
  }
}

Chưa có gì đặc biệt, chỉ là tạo ra 3 components, render ra đoạn text là tên của screen đó.

Sau khi đã có 3 màn hình, chúng ta cần tạo ra 1 navigator để điều hướng qua lại giữa các screens.

Mình chọn thư viện react-navigation để làm điều này vì nó đơn giản, dễ dùng.

Trước hết hãy cài đặt đã nào, mở terminal và vào trong folder project, chạy lệnh

yarn add react-navigation
# hoặc
npm install --save react-navigation

Import thư viện vào app.js

import { StackNavigator } from 'react-navigation';

Khởi tạo navigator và export nó thành root component luôn (vì thật ra nó cũng là component)

const AppNavigator = new StackNavigator({
  home: {
    screen: HomeScreen,
    navigationOptions: {
      title: '#ChuyenCuaDev'
    }
  },
  post: {
    screen: PostScreen,
    navigationOptions: {
      title: '#ChuyenCuaDev'
    }
  },
  postComments: {
    screen: PostCommentsScreen,
    navigationOptions: {
      title: '#ChuyenCuaDev'
    }
  }
});

export default AppNavigator;

Okie, save lại và đến bước này trên điện thoại sẽ thấy được nội dung của màn hình HomeScreen.

Tuy nhiên hiện tại vẫn chưa thể điều hướng qua các screens. Trước khi viết phần nội dung cho các màn hình, mình code thêm vài dòng để test thử phần navigation nhé.

// Import thêm component Button để user click vào thì app sẽ navigate sang các screen khác
import { Platform, StatusBar, StyleSheet, Text, View, Button } from 'react-native';

class HomeScreen extends React.Component {
  render() {
    return (
      <View>
        <Text>HomeScreen</Text>
        <Button 
          title="Go to PostScreen" 
          onPress={() => {
            this.props.navigation.navigate('post');
          }}
        />
      </View>
    );
  }
}

class PostScreen extends React.Component {
  render() {
    return (
      <View>
        <Text>PostScreen</Text>
        <Button 
          title="Go to PostCommentsScreen" 
          onPress={() => {
            this.props.navigation.navigate('postComments');
          }}
        />
      </View>
    );
  }
}

ChuyenCuaDev step 3

Trang chủ

HomeScreen Structure

Mình có vẽ mockup để mọi người dễ hình dung những gì mình sẽ làm HomeScreen.

Cấu trúc cũng khá đơn giản:

  • NavigationBar thì react-navigation đã lo rồi. Không đụng đến nữa.
  • Post List thì mình sẽ dùng một component list view để render các bài viết. Trong react native có nhiều loại list view, trong ứng dụng này mình sẽ dùng <FlatList />.
  • Mỗi bài viết (PostItem) sẽ render các nội dung
    • Photo: chắc chắn sẽ dùng component <Image />
    • Short content: nó chỉ là nội dung text, nên mình sẽ dùng <Text />
    • Post Meta: tương tự short content.

Trước khi bắt đầu, mình tổ chức code lại cho gọn nhé.

Tạo folder tên là screens, folder này sẽ chứa các component screen.

Sau đó tạo 3 files: HomeScreen.js, PostScreen.jsPostCommentsScreen.js.

Giờ mở file HomeScreen.js lên thêm phần UI cho nó nhé.

Theo như mockup mình đã vẽ, thì mình cần import thêm 1 số components:

import {
  StyleSheet,
  Text,
  View,
  TouchableOpacity,
  FlatList,
  Dimensions,
  Image
} from 'react-native';
import faker from 'faker';
import moment from 'moment';
  • <TouchableOpacity /> render một button, nhưng không có style gì cả, khi user click vào nó sẽ có hiệu ứng mờ đi một chút, nhìn nó sẽ nhẹ nhàng hơn.
  • <FlatList /> dùng để render một list.
  • <Image /> hiển thị hình ảnh.
  • Dimensions api để lấy kích thước màn hình.
  • faker mình sử dụng thư viện này để tạo dữ liệu mẫu, ChuyenCuaDev không cho mình API - hehe.
  • moment datetime library.

Sau khi import xong mình dùng faker generate khoảng 100 bài viết mẫu

const POSTS = [];

let n = 1;
while (n <= 100) {
  POSTS.push({
    id: faker.random.uuid(),
    author: faker.name.firstName(),
    viewed: 123,
    comments: 456,
    datetime: moment(faker.date.recent()).format('DD/MM/YYYY hh:mm'),
    imageUrl: 'https://placeimg.com/800/600/any',
    content: faker.lorem.paragraphs()
  });
  n++;
}

Vào component HomeScreen mình render một list view

class HomeScreen extends React.Component {
  //...
  	
   render() {
    return (
      <FlatList
        data={POSTS}
        keyExtractor={item => item.id}
        renderItem={this.renderItem}
        ItemSeparatorComponent={this.renderSeparator}
      />
    );
  }
  
  //...
}

Cách dùng FlatList cũng khá đơn giản, bạn chỉ cần truyền vào list data. nếu mỗi item đều có thuộc tính key thì không cần viết keyExtractor

renderItem là prop cần truyền vào một function trả về component mà khi FlatList render mỗi item, nó sẽ gọi function để lấy nội dung sau đó render vào list.

Mỗi bài viết, mình render hình ảnh bài viết, bên dưới hình ảnh thì mình render các thông tin liên quan đến tác giả, số lượng view và comment.

và sau đó là một đoạn nội dung mà mình đã cắt bớt và lấy 80 words.

Mình sử dụng TouchableOpacity để wrap toàn bộ post item, và bind sự kiện press, khi user click vào bài viết, mình sẽ navigate đến PostScreen.

renderItem = row => {
    const post = row.item;
    const imageWidth = Dimensions.get('window').width;
    const imageHeight = imageWidth * (9 / 16);
    const imageStyle = {
      width: imageWidth,
      height: imageHeight,
      resizeMode: 'cover'
    };
    const shortContent = post.content
      .split(/\s+/)
      .slice(0, 80)
      .join(' ');
  
    return (
      <TouchableOpacity
        style={styles.container}
        onPress={this.handlePostPressed.bind(this, post)}
      >
        <Image source={{ uri: post.imageUrl }} style={imageStyle} />
        <View style={styles.meta}>
          <Text style={styles.metaText}>{post.author}</Text>
          <Text style={styles.metaText}>comments: {post.viewed}</Text>
          <Text style={styles.metaText}>viewed: {post.viewed}</Text>
        </View>
        <Text style={styles.content}>{shortContent}...</Text>
      </TouchableOpacity>
    );
  };

ItemSeparatorComponent thì không bắt buộc, tuy nhiên mình render một khoảng trắng với background tối để dễ phân biệt 2 bài viết.

renderSeparator() {
  return <View style={styles.separator} />;
}
/*
  separator: {
    flex: 1,
    height: 16,
    backgroundColor: '#eee'
  },
*/

các bạn để ý phần config sự kiện handlePostPressed vào component TouchableOpacity , mình dùng method bind để có thể truyền thêm extra argument vào trong method mà không cần phải viết kiểu

      <TouchableOpacity
        style={styles.container}
        onPress={() => this.handlePostPressed(post)}
      >

Toàn bộ code của HomeScreen

import React from 'react';
import {
  StyleSheet,
  Text,
  View,
  TouchableOpacity,
  FlatList,
  Dimensions,
  Image
} from 'react-native';
import faker from 'faker';
import moment from 'moment';

const POSTS = [];

let n = 1;
while (n <= 100) {
  POSTS.push({
    id: faker.random.uuid(),
    author: faker.name.firstName(),
    viewed: 123,
    comments: 456,
    datetime: moment(faker.date.recent()).format('DD/MM/YYYY hh:mm'),
    imageUrl: 'https://placeimg.com/800/600/any',
    content: faker.lorem.paragraphs()
  });
  n++;
}

class HomeScreen extends React.Component {
  handlePostPressed = post => {
    this.props.navigation.navigate('post', {
      post: post
    });
  };
  render() {
    return (
      <FlatList
        data={POSTS}
        keyExtractor={item => item.id}
        renderItem={this.renderItem}
        ItemSeparatorComponent={this.renderSeparator}
        style={{ flex: 1 }}
      />
    );
  }
  renderItem = row => {
    const post = row.item;
    const imageWidth = Dimensions.get('window').width;
    const imageHeight = imageWidth * (9 / 16);
    const imageStyle = {
      width: imageWidth,
      height: imageHeight,
      resizeMode: 'cover'
    };
    const shortContent = post.content
      .split(/\s+/)
      .slice(0, 80)
      .join(' ');
    return (
      <TouchableOpacity
        style={styles.container}
        onPress={this.handlePostPressed.bind(this, post)}
      >
        <Image source={{ uri: post.imageUrl }} style={imageStyle} />
        <View style={styles.meta}>
          <Text style={styles.metaText}>{post.author}</Text>
          <Text style={styles.metaText}>comments: {post.viewed}</Text>
          <Text style={styles.metaText}>viewed: {post.viewed}</Text>
        </View>
        <Text style={styles.content}>{shortContent}...</Text>
      </TouchableOpacity>
    );
  };
  renderSeparator() {
    return <View style={styles.separator} />;
  }
}

const fontScale = Dimensions.get('window').fontScale;

const styles = {
  container: {
    flex: 1,
    backgroundColor: '#fff'
  },
  separator: {
    flex: 1,
    height: 16,
    backgroundColor: '#eee'
  },
  meta: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    padding: 8,
    paddingBottom: 0
  },
  metaText: {
    fontSize: 14 * fontScale,
    fontWeight: 'bold'
  },
  content: {
    padding: 8,
    fontSize: 14 * fontScale,
    lineHeight: 14 * fontScale * 1.5
  }
};

export default HomeScreen;

vậy là xong phần UI cho màn hình HomeScreen

ChuyenCuaDev HomeScreen

PostScreen (Màn hình đăng bài)

Nếu đã xong phần màn hình HomeScreen, thì khi làm UI cho PostScreen các bạn sẽ thấy quen hơn rồi đó.

ChuyenCuaDev PostScreen

Mình chia màn hình chi tiết bài viết (PostScreen) làm 2 phần lớn:

  • Một view (container) chứa hình ảnh và nội dung bài viết, tuy nhiên ở đây do nội dung có thể khá dài, mình sẽ dùng <ScrollView /> thay vì <View /> để user có thể scroll được.
<ScrollView>
  <Image
    source={{ uri: post.imageUrl }}
    style={{ width: imageWidth, height: imageWidth }}
    resizeMode="cover"
   />
  <Text style={styles.content}>{post.content}</Text>
</ScrollView>
  • Một view dưới cùng để hiển thị thông tin về tác giả, số lượt view và comments.
<View style={styles.meta}>
  <Text style={styles.metaText}>{post.author}</Text>
  <TouchableOpacity onPress={this.handleCommentPress}>
    <Text style={styles.metaText}>comments: {post.viewed}</Text>
  </TouchableOpacity>
  <Text style={styles.metaText}>viewed: {post.viewed}</Text>
</View>

Riêng ở số lượt comments, khi user click vào, mình sẽ điều hướng đến màn hình CommentListScreen.

Màn hình này cũng khá là đơn giản, nội dung cũng không có quá nhiều. Bên dưới là toàn bộ code của màn hình này.

import React from 'react';
import {
  View,
  Text,
  Image,
  Dimensions,
  ScrollView,
  TouchableOpacity
} from 'react-native';

class PostScreen extends React.Component {
  handleCommentPress = () => {
    this.props.navigation.navigate('commentList');
  };
  render() {
    const { post } = this.props.navigation.state.params;
    const imageWidth = Dimensions.get('window').width;

    return (
      <View style={styles.container}>
        <ScrollView>
          <Image
            source={{ uri: post.imageUrl }}
            style={{ width: imageWidth, height: imageWidth }}
            resizeMode="cover"
          />
          <Text style={styles.content}>{post.content}</Text>
        </ScrollView>
        <View style={styles.meta}>
          <Text style={styles.metaText}>{post.author}</Text>
          <TouchableOpacity onPress={this.handleCommentPress}>
            <Text style={styles.metaText}>comments: {post.viewed}</Text>
          </TouchableOpacity>
          <Text style={styles.metaText}>viewed: {post.viewed}</Text>
        </View>
      </View>
    );
  }
}

const styles = {
  container: {
    flex: 1,
    backgroundColor: '#FFF'
  },
  content: {
    padding: 16,
    fontSize: 16,
    lineHeight: 16 * 1.5,
    textAlign: 'justify'
  },
  meta: {
    flexDirection: 'row',
    padding: 16,
    paddingTop: 8,
    paddingBottom: 8,
    alignItems: 'center',
    justifyContent: 'space-between',
    backgroundColor: '#005ea0'
  },
  metaText: {
    color: '#f8f8f8',
    fontWeight: 'bold'
  }
};

export default PostScreen;

Và đây là kết quả cho màn hình Postscreen

ChuyenCuaDev PostScreen

Màn hình hiển thị comment

ChuyenCuaDev CommentListScreen

Hiển thị danh sách comment tương tự như hiển thị danh sách bài viết, nhưng đơn giản hơn nhiều.

Ở màn hình này chúng ta chỉ cần render 1 list view, mỗi list item là một comment, mỗi comment có 3 thông tin: tác giả, nội dung comment và thời gian comment.

import React from 'react';
import {
  View,
  Text,
  FlatList
} from 'react-native';
import faker from 'faker';
import moment from 'moment';

Mình chỉ import components: FlatList để hiển thị danh sách comments, View làm layout và Text để hiển thị nội dung text.

faker để tạo dữ liệu mẫu.

const COMMENTS = [];

let n = 1;
while (n <= 100) {
  COMMENTS.push({
    id: faker.random.uuid(),
    author: faker.name.findName(),
    text: faker.lorem.sentences(),
    datetime: moment(faker.date.recent()).format('DD/MM/YYYY hh:mm')
  });
  n++;
}
<FlatList
  data={COMMENTS}
  keyExtractor={item => item.id}
  renderItem={this.renderComment}
  style={{ flex: 1 }}
/>

Phần render list comment thì tương tự như render posts, chỉ khác ở việc render list item là khác nhau.

renderComment(row) {
  const comment = row.item;
  return (
    <View style={styles.comment}>
      <View>
        <Text style={styles.author}>{comment.author}</Text>
      </View>
      <View>
        <Text style={styles.text}>{comment.text}</Text>
        <Text style={styles.datetime}>{comment.datetime}</Text>
      </View>
    </View>
  );
}

Bên dưới là toàn bộ code cho màn hình này.

import React from 'react';
import {
  View,
  Text,
  FlatList
} from 'react-native';
import faker from 'faker';
import moment from 'moment';

const COMMENTS = [];

let n = 1;
while (n <= 100) {
  COMMENTS.push({
    id: faker.random.uuid(),
    author: faker.name.findName(),
    text: faker.lorem.sentences(),
    datetime: moment(faker.date.recent()).format('DD/MM/YYYY hh:mm')
  });
  n++;
}

class CommentListScreen extends React.Component {
  render() {
    return (
      <FlatList
        data={COMMENTS}
        keyExtractor={item => item.id}
        renderItem={this.renderComment}
        style={{ flex: 1 }}
      />
    );
  }
  renderComment(row) {
    const comment = row.item;
    return (
      <View style={styles.comment}>
        <View>
          <Text style={styles.author}>{comment.author}</Text>
        </View>
        <View>
          <Text style={styles.text}>{comment.text}</Text>
          <Text style={styles.datetime}>{comment.datetime}</Text>
        </View>
      </View>
    );
  }
}

const styles = {
  comment: {
    padding: 8,
    marginBottom: 8,
    borderBottomWidth: 1,
    borderColor: '#bdbdbd'
  },
  author: {
    fontSize: 14,
    fontWeight: 'bold',
    marginBottom: 8
  },
  text: {
    fontSize: 14,
    lineHeight: 14 * 1.2
  },
  datetime: {
    fontSize: 12,
    color: '#898989'
  }
};

export default CommentListScreen;

ChuyenCuaDev CommentListScreen

Kết luận

Cuối cùng cũng đã xong App đầu tiên, bạn có thể xem demo bên dưới nhé.

ChuyenCuaDev Demo

Mình cũng đã port code của mình lên Expo Snack, một tool share code và app tương tự như jsBin hay jsfiddle.
Bạn chỉ cần cài đặt ứng dụng Expo trên Android Play Store or iOS App Store. Mở link bên dưới và scan QR code.
https://snack.expo.io/SJHW-MSpW

Author: Phu Nguyen