お昼に何を食べよかなとめぼしいあたりをうろうろしてるうちに時間がなくなってきて結局いつも行く感じのお店になってしまったり、なんてーときでも大丈夫なように、前回作った「近場で済ますアプリ」にツイッターでおいしい話題になっているつぶやきを表示する機能を実装しました。
※配布しているアプリでなくて、個人で実装を楽しんでいるアプリです。
※配布しているアプリでなくて、個人で実装を楽しんでいるアプリです。
詰まる
はい、アプリはExpoというReact Native開発支援を使っていて、「それExpoには含まれてないんだな」なエラーが表示されちゃって、一部のライブラリが動かなかったりしたため急遽React Nativeでプロジェクトを作りなおしました。
EXPOでやっていくならその辺をぶち抜ける技術がいるかもしれません。
何もしなくてもアプリがビルドできるから便利は便利ですが、そういったこともあったので、まさにこれから始めるって人はExpoを使わないで開発を始めてみた方が良いかもしれないなあという場面もあるかも。VSCodeと対話しながらすすめるStyle。
はい、アプリはExpoというReact Native開発支援を使っていて、「それExpoには含まれてないんだな」なエラーが表示されちゃって、一部のライブラリが動かなかったりしたため急遽React Nativeでプロジェクトを作りなおしました。
EXPOでやっていくならその辺をぶち抜ける技術がいるかもしれません。
何もしなくてもアプリがビルドできるから便利は便利ですが、そういったこともあったので、まさにこれから始めるって人はExpoを使わないで開発を始めてみた方が良いかもしれないなあという場面もあるかも。VSCodeと対話しながらすすめるStyle。
npx react-native init APPNAME
作成はこれ一発です。基本的にはReact NativeはReact Nativeなのでほとんどそのままで大丈夫なんだけど、位置情報の取得で同じ手法がとれなかったので今回は「Google Location Services API」を使う「react-native-location」ライブラリを使用することにしました。
位置情報を取得してからHOTPEPPERさんのAPIを叩く必要があるので、自己満足でタイトル画面を作っていたのが功を奏して、タイトルで位置を取得してお店一覧にPropsとして送ることにしました。
import RNLocation from "react-native-location"; const [lat, setLat] = useState(""); const [lng, setLng] = useState(""); RNLocation.configure({ distanceFilter: 5.0, }); RNLocation.requestPermission({ ios: "whenInUse", android: { detail: "fine", rationale: { title: "Location permission", message: "We use your location to demo the library", buttonPositive: "OK", buttonNegative: "Cancel", }, }, }).then((granted) => { if (granted) { startUpLocation(); } }); const startUpLocation = () => { RNLocation.subscribeToLocationUpdates((locations) => { setLat(locations[0].latitude); setLng(locations[0].longitude); }); }; useEffect(() => { (async () => { await getLocation(); })(); }, [getLocation]);
これunsubscribeする場所をまだ模索中です。
あとはツイッターのAPI取得は、「mamamama-su-daさんのtwitter-sample」のロジックを参考に一部のメソッドを使って「#おいしい」な感じのワードで検索するようにしました。
apikeyとか一式は持ってるものを使って、なんやかんやの処理はやってません。
なんやかんやについてはTwitterAPIのOAuth1.0認証について調べてみてください。
apikeyとか一式は持ってるものを使って、なんやかんやの処理はやってません。
なんやかんやについてはTwitterAPIのOAuth1.0認証について調べてみてください。
タブバー使いたかった けど
ほんとはタブバーを使いたかったんですが、ひとまずはツイッター検索を追加するという目標を達成しようということにして、テキストを追加して、Navigatorで画面遷移を追加して一覧表示の画面を作りました。またCardコンポーネントベースの画面やけども。
ほんとはタブバーを使いたかったんですが、ひとまずはツイッター検索を追加するという目標を達成しようということにして、テキストを追加して、Navigatorで画面遷移を追加して一覧表示の画面を作りました。またCardコンポーネントベースの画面やけども。
Twitter表示する画面サンプル import React, {useEffect, useState} from "react"; import {StyleSheet, ScrollView, Text, View, Image} from "react-native"; import {Card, Avatar} from "react-native-elements"; import tw from "../Api/TwitterManager"; const ListView = ({image, data}) => { return ( <View style={styles.container}> <View style={styles.subtitle}> <Avatar xlarge rounded source={{uri: image}} /> </View> <View style={styles.information}> <Text>{data}</Text> </View> </View> ); }; const LunchTweet = ({route}) => { const [tweets, setTweets] = useState([]); useEffect(() => { (async () => { const value = await tw .getTwitter( "/search/tweets.json", Object.assign( {}, {q: "#おいしい", lang: "ja", result_type: "resent"}, ), ) .catch((err) => { console.warn("search tweets", err); }); setTweets(value.statuses); })(); }, []); return ( <ScrollView style={styles.flexOne}> {tweets && tweets.map((data, i) => ( <Card key={i} title={data.user.name}> <View style={styles.flexOne}> <ListView image={data.user.profile_image_url_https} data={data.text} /> </View> {data.extended_entities && data.extended_entities.media.map((m, key) => ( <View key={key}> <Image style={styles.postPict} source={{uri: m.media_url_https}} /> </View> ))} </Card> ))} </ScrollView> ); }; const styles = StyleSheet.create({ flexOne: { flex: 1, }, container: { flex: 1, flexDirection: "row", }, subtitle: { flex: 2, padding: 2, }, information: { flex: 10, padding: 10, }, postPict: { width: 300, height: 300, }, }); export default LunchTweet;
挙動はこんな感じ。
さぐりさぐり
LINTエラーの中にuseEffectの初期値関連のエラーが出てきて、そういえばそうだったと思い出しながらさぐりさぐりでuseCallbackを入れてみたさぐりさぐり感のある書き直したお店一覧。
LINTエラーの中にuseEffectの初期値関連のエラーが出てきて、そういえばそうだったと思い出しながらさぐりさぐりでuseCallbackを入れてみたさぐりさぐり感のある書き直したお店一覧。
import React, {useState, useEffect, useCallback} from "react"; import api from "../Api/Api"; import { View, Text, StyleSheet, Image, ScrollView, TouchableOpacity, TextInput, } from "react-native"; import {Card, Button} from "react-native-elements"; import {Overlay} from "react-native-elements"; const Menu = ({navigation, route}) => { navigation.setOptions({ headerRight: () => ( <Button type="clear" onPress={() => setIsVisible(true)} title="Keyword" color="#fff" /> ), }); const [shops, setShops] = useState([]); const [isVisible, setIsVisible] = useState(false); const [keyword, setKeyword] = useState(""); const getShops = useCallback(async () => { const res = await api.getShops( "", route.params.Data.Lat, route.params.Data.Lng, ); return res.data.results; }, [route.params.Data]); const setKeywordSearch = (word) => { (async () => { const res = await api.getShops( word, route.params.Data.Lat, route.params.Data.Lng, ); const data = res.data.results; setShops({data}); })(); setIsVisible(false); }; const toTweetLunch = () => { setIsVisible(false); navigation.navigate("lunchTweet"); }; useEffect(() => { (async () => { const data = await getShops(); await setShops({ data, }); })(); }, [getShops]); return ( <ScrollView style={styles.overlay}> <Overlay height="auto" isVisible={isVisible} onBackdropPress={() => setIsVisible(false)}> <View> <Text>検索に含ませたいキーワードがあれば入力してください。</Text> <TextInput style={styles.searchBox} onChangeText={(v) => setKeyword(v)} value={keyword} /> <Button type="clear" color="#009488" title="検索する" onPress={() => setKeywordSearch(keyword)} /> <TouchableOpacity onPress={() => toTweetLunch()}> <View style={styles.twView}> <Image resizeMode="contain" style={styles.pict} source={require("../assets/twbird.png")} /> <Text style={styles.tweet}>Twitterで話題のランチを調べる</Text> </View> </TouchableOpacity> </View> </Overlay> <View style={styles.container}> {shops.data && shops.data.shop.map((data, i) => ( <Card key={i}> <View style={styles.shopBox}> <View style={styles.shopBoxRow}> <View style={styles.shopBoxCol}> <Text>{data.name}</Text> <Text>{data.genre.name}</Text> </View> <View style={styles.shopImage}> <TouchableOpacity onPress={() => navigation.navigate("shop", { Data: {shop: data.id}, }) }> <Image style={styles.shopPict} source={{uri: data.photo.mobile.l}} /> </TouchableOpacity> </View> </View> </View> </Card> ))} </View> </ScrollView> ); }; const black = "#000"; const styles = StyleSheet.create({ overlay: { flex: 1, }, shopPict: { width: 100, height: 100, }, shopImage: { flex: 2, justifyContent: "flex-end", alignItems: "flex-end", }, searchBox: { marginTop: 20, marginBottom: 10, height: 50, borderColor: black, borderWidth: 1, }, shopBox: { width: 280, flex: 1, }, shopBoxRow: { flex: 1, flexDirection: "row", }, shopBoxCol: { flex: 3, flexDirection: "column", }, tweet: { margin: 1, marginTop: 5, fontSize: 15, borderStyle: "solid", }, pict: { width: 30, height: 30, }, twView: { marginTop: 10, flexDirection: "row", }, }); export default Menu;
さぐりさぐりでした。
次か、次の次か、またいつかの回かでタブバーやるか。