この記事は、「【連載】初めてのReact Native + Expo開発環境構築入門」の子記事です。環境などの条件は、親記事をご覧ください。
※ 最新のExpo+Native Baseでこの記事のとおりに実施すると、若干デザインの問題が発生します。最新版対応は、こちらの記事におまかせしました。
前回までに、おおよそのアプリのテスト動作は作ったので、今回は画面デザインをそれっぽくしていきます。
Native Baseを使うと、こんなデザインができるようになります。
Native Baseのインストール
Visual Studio CodeでReact Nativeのプロジェクトを開き、Ctrl + @
でPowerShellを開いて、インストールコマンド実行。
npm install native-base
ExpoでNative Baseを使う場合、Native Baseがカスタムフォントを使うので、Expo Font(詳しくはこちら)を使えるようにしていなくてはいけません。Expo Fontをインストールします。
expo install expo-font
Native Baseで使うフォントを読み込む処理を入れる
Native Baseでは、アイコンなど一部で特殊なフォントを使います(詳しくはこちら)。このため、アプリのどこかでFontをロードしなくてはいけません。通常、App.jsで読み込みします。
App.jsがかなり大きくなったように見えますが、やってることは至って簡単です。
App.jsの変更点
import { View, Text } from "react-native";
// expo-fontとアイコンをimportする
import * as Font from "expo-font";
import { Ionicons } from "@expo/vector-icons";
...
export default class App extends React.Component {
// ロードが終わるまでは「loading...」を表示するため、state「isReady」で制御
constructor(props) {
super(props);
this.state = {
isReady: false
};
}
// DidMountのタイミングでフォントリソースをメモリ上に読み込み。終わったらisReadyをオン。
async componentDidMount() {
await Font.loadAsync({
Roboto: require("native-base/Fonts/Roboto.ttf"),
Roboto_medium: require("native-base/Fonts/Roboto_medium.ttf"),
...Ionicons.font
});
this.setState({ isReady: true });
}
render() {
//Wait for font loading... フォントの読み込み中なら、「loading...」を表示
if (!this.state.isReady) {
return (
<View>
<Text>loading...</Text>
</View>
);
}
let globalState = new InvoiceContainer({ initialSeeding: true });
return (
<Provider inject={[globalState]}>
<AppContainer />
</Provider>
);
}
}
フォントの読み込み部分、ファイルのパスの指定に注意してください。もしApp.js以外で読み込む場合、現在編集しているファイルから見た正しいパスを入れないと、以下のようにエラーになります。
ENOENT: no such file or directory, scandir 'C:\ExpoProjects\hello-world\components\node_modules\native-base\Fonts'
Failed building JavaScript bundle.
たとえば、/components/Hoge.js
で実施するなら、以下のように書きます。node_modules
を忘れないでください。
//これは例です
await Font.loadAsync({
Roboto: require("../node_modules/native-base/Fonts/Roboto.ttf"),
Roboto_medium: require("../node_modules/native-base/Fonts/Roboto_medium.ttf"),
...Ionicons.font
});
ボタンデザインをNative Base版にする+調整
では、HomeScreenのButton
とText
を、react-native版からNative Base版に置き換えてみます。
変更前
import { Button, Text, View } from "react-native";
...
class HomeScreenContent extends React.Component {
render() {
...
return (
<View style={styles.container}>
<Text>Open up App.js to start working on your app!</Text>
<Text>Hello World!</Text>
<Button title="Go to InvoiceEdit" onPress={() => this.props.navigation.navigate("InvoiceEdit")} />
<Button title="Go to Summary" onPress={() => this.props.navigation.navigate("Summary")} />
{invoiceList}
</View>
);
...
変更後
import { Container, Text, Button, Icon } from "native-base";
...
return (
<Container style={styles.container}>
<Text>Open up App.js to start working on your app!</Text>
<Text>Hello World!</Text>
<Button onPress={() => this.props.navigation.navigate("InvoiceEdit")}>
<Icon type="FontAwesome5" name="file-invoice-dollar" />
<Text>Go to InvoiceEdit</Text>
</Button>
<Button onPress={() => this.props.navigation.navigate("Summary")}>
<Icon type="FontAwesome5" name="poll-h" />
<Text>Go to Summary</Text>
</Button>
{invoiceList}
</Container>
);
...
アイコンフォントのテストもかねて、ボタンにアイコンを追加しています。Native Baseで使えるアイコンの一覧はこちら。
Buttonのtitle
は互換性がないので、<Text>を使います。これで楽にアイコンも入れられるようになります。HTMLっぽくて楽ですね。
リストデザインをNative Base版にする
リストをNative Base版にしますが、同時にNative Baseのコンポーネント構造ルールに従って、<Content>コンポーネントの中にコンテンツを入れるようにします。
ついでに、ボタンの配置をそれっぽくします。
変更前
render() {
let globalState = this.props.globalState;
let invoiceList = <Text>No invoice</Text>;
if (globalState.state.data.invoices.length) {
invoiceList = globalState.state.data.invoices.map(invoice => {
return <Text key={invoice.id}>{invoice.id + " : " + invoice.date}</Text>;
});
}
return (
<Container style={styles.container}>
<Text>Open up App.js to start working on your app!</Text>
<Text>Hello World!</Text>
<Button onPress={() => this.props.navigation.navigate("InvoiceEdit")}>
<Icon type="FontAwesome5" name="file-invoice-dollar" />
<Text>Go to InvoiceEdit</Text>
</Button>
<Button onPress={() => this.props.navigation.navigate("Summary")}>
<Icon type="FontAwesome5" name="poll-h" />
<Text>Go to Summary</Text>
</Button>
{invoiceList}
</Container>
);
}
変更後
import { View, Container, Content, Text, Button, Icon, List, ListItem, Left, Right } from "native-base";
...
class HomeScreenContent extends React.Component {
render() {
let globalState = this.props.globalState;
let invoiceList = <Text>No invoice</Text>;
if (globalState.state.data.invoices.length) {
invoiceList = (
<List>
{globalState.state.data.invoices.map(invoice => {
return (
<ListItem key={invoice.id} noIndent>
<Left>
<Text key={invoice.id}>{invoice.id + " : " + invoice.date}</Text>
</Left>
<Right>
<Icon name="arrow-forward" />
</Right>
</ListItem>
);
})}
</List>
);
}
return (
<Container>
<Content>
<View style={{ flexDirection: "row" }}>
<Left>
<Button onPress={() => this.props.navigation.navigate("InvoiceEdit")}>
<Icon type="FontAwesome5" name="file-invoice-dollar" />
<Text style={{ paddingLeft: 0 }}>InvoiceEdit</Text>
</Button>
</Left>
<Right>
<Button onPress={() => this.props.navigation.navigate("Summary")}>
<Icon type="FontAwesome5" name="poll-h" />
<Text style={{ paddingLeft: 0 }}>Summary</Text>
</Button>
</Right>
</View>
{invoiceList}
</Content>
</Container>
);
}
}
Native Baseでは、Container
の中に、Header
, Content
, Footer
を入れる前提となっています。そのため、コンテンツはContent
に入れないと正しく(あるいはまったく)表示されないなどの問題が発生します。
今回ヘッダーはnavigationがすでに描画してくれているので、Native BaseのHeader
は使わないことにします。デザインの都合で多機能ヘッダーにしたくなったら、navigationのヘッダーをオフにして、Native Baseのヘッダーを使うのもいいですね。ただしその場合はバックボタンなどは自分で描画することになります。
フッターは画面の下に張り付いてくれる使いやすいフッターです。必要になったらつけることにしますが、今はつけません。
ボタンの配置変更は、まず<View style={{ flexDirection: "row" }}>
で横方向配置モードにして、<Left>
と<Right>
で配置しています。
アイコンとテキストの間が空きすぎないように、<Text>
のpaddingLeft
をゼロにして、おおよそOKくらいに調整しています。
このあたりのStyleの調整は、本来ならstyles.jsにまとめて入れるべきです。