NLP full-stack dengan React: Ionic vs Cordova vs React Native
Diterbitkan: 2022-03-11Dalam waktu sekitar 15 tahun sejak Apple merilis iPhone pertama, lanskap pengembangan perangkat lunak telah berubah secara dramatis. Karena smartphone mendapatkan adopsi yang luas dan terus tumbuh dalam kemampuan unik, pengguna semakin memilih untuk mengakses layanan perangkat lunak melalui perangkat seluler daripada desktop atau laptop. Ponsel cerdas menawarkan fitur seperti geolokasi, autentikasi biometrik, dan pendeteksi gerakan, yang banyak di antaranya baru mulai disalin oleh platform desktop. Dalam beberapa demografi, smartphone atau perangkat seluler serupa adalah sarana utama konsumsi perangkat lunak, melewati komputer sama sekali.
Perusahaan telah memperhatikan pergeseran ini dan memperkuatnya dengan cara-cara utama. Aplikasi seluler tidak lagi menjadi renungan. Aplikasi mulai dari Robinhood, perusahaan pialang keuangan, Instagram, perusahaan media sosial, hingga Uber, perusahaan transportasi online, mengadopsi strategi pengembangan yang mengutamakan seluler. Jika ada aplikasi desktop, seringkali ditawarkan sebagai pelengkap aplikasi seluler, bukan sebagai fokus utama.
Untuk pengembang full-stack, beradaptasi dengan tren yang berubah ini sangat penting. Untungnya, ada banyak teknologi yang matang dan didukung dengan baik yang tersedia untuk membantu pengembang web menerapkan keterampilan mereka untuk pengembangan seluler. Hari ini, kita akan mengeksplorasi tiga teknologi tersebut: Cordova, Ionic, dan React Native. Kami akan menggunakan React.js, salah satu kerangka kerja paling populer untuk pengembangan web front-end, sebagai teknologi pengembangan inti kami. Sementara kami akan berfokus pada pengembangan aplikasi iPhone, ini adalah teknologi lintas platform dan akan dapat dikompilasi silang ke platform Android.
Yang Akan Kami Bangun Hari Ini
Kami akan membangun aplikasi yang menggunakan Natural Language Processing (NLP) untuk memproses dan mengkurasi feed Twitter. Aplikasi ini akan memungkinkan pengguna untuk memilih satu set pegangan Twitter, menarik pembaruan terbaru menggunakan API Twitter, dan mengkategorikan tweet berdasarkan sentimen dan topik. Pengguna kemudian dapat melihat tweet berdasarkan sentimen atau topik.
Ujung Belakang
Sebelum kita membangun ujung depan, kita akan ingin membangun ujung belakang. Kami akan menjaga bagian belakangnya tetap sederhana untuk saat ini - kami akan menggunakan analisis sentimen dasar yang siap pakai dan penandaan bagian-of-speech, bersama dengan sedikit pembersihan data untuk menangani masalah khusus kumpulan data. Kami akan menggunakan perpustakaan NLP open-source yang disebut TextBlob dan menyajikan hasilnya melalui Flask.
Analisis Sentimen, Pemberian Tag Part-of-speech, dan NLP: Panduan Singkat
Jika Anda belum pernah bekerja dengan aplikasi analisis bahasa alami sebelumnya, istilah ini mungkin sangat asing bagi Anda. NLP adalah istilah umum untuk teknologi yang menganalisis dan memproses data bahasa manusia alami. Meskipun ini adalah topik yang luas, ada banyak tantangan yang umum untuk semua teknologi yang menangani bidang ini. Misalnya, bahasa manusia, tidak seperti bahasa pemrograman atau data numerik, cenderung terstruktur secara longgar karena sifat tata bahasa manusia yang permisif. Selain itu, bahasa manusia cenderung sangat kontekstual, dan frasa yang diucapkan atau ditulis dalam satu konteks mungkin tidak diterjemahkan ke konteks lain. Akhirnya, selain struktur dan konteks, bahasa sangat kompleks. Kata-kata yang lebih jauh dalam paragraf dapat mengubah arti kalimat di awal paragraf. Kosakata dapat ditemukan, didefinisikan ulang, atau diubah. Semua kompleksitas ini membuat teknik analisis data sulit untuk diterapkan secara silang.
Analisis Sentimen adalah subbidang NLP yang berfokus pada pemahaman emosionalitas dari bagian bahasa alami. Sementara emosi manusia secara inheren subjektif, dan karena itu sulit untuk dijabarkan secara teknologi, analisis sentimen adalah subbidang yang memiliki janji komersial yang sangat besar. Beberapa aplikasi analisis sentimen termasuk mengklasifikasikan ulasan produk untuk mengidentifikasi penilaian positif dan negatif dari berbagai fitur, mendeteksi suasana email atau pidato, dan mengelompokkan lirik lagu berdasarkan suasana hati. Jika Anda sedang mencari penjelasan yang lebih mendalam tentang Analisis Sentimen, Anda dapat membaca artikel saya tentang membangun aplikasi berbasis Analisis Sentimen di sini.
Penandaan bagian-of-speech, atau penandaan POS, adalah subbidang yang sangat berbeda. Tujuan dari POS tagging adalah untuk mengidentifikasi part of speech dari sebuah kata tertentu dalam sebuah kalimat menggunakan informasi gramatikal dan kontekstual. Mengidentifikasi hubungan ini adalah tugas yang jauh lebih sulit daripada yang terlihat sebelumnya - sebuah kata dapat memiliki bagian ucapan yang sangat berbeda berdasarkan konteks dan struktur kalimat, dan aturannya tidak selalu jelas bahkan bagi manusia. Untungnya, banyak model off-the-shelf saat ini menyediakan model yang kuat dan serbaguna yang terintegrasi dengan sebagian besar bahasa pemrograman utama. Jika Anda ingin mempelajari lebih lanjut, Anda dapat membaca artikel saya tentang penandaan POS di sini.
Flask, TextBlob, dan Tweepy
Untuk back end NLP kami, kami akan menggunakan Flask, TextBlob, dan Tweepy. Kami akan menggunakan Flask untuk membangun server kecil dan ringan, TextBlob untuk menjalankan pemrosesan bahasa alami kami, dan Tweepy untuk mendapatkan tweet dari API Twitter. Sebelum memulai pengkodean, Anda juga ingin mendapatkan kunci pengembang dari Twitter sehingga Anda dapat mengambil tweet.
Kita dapat menulis bagian belakang yang jauh lebih canggih dan menggunakan teknologi NLP yang lebih kompleks, tetapi untuk tujuan kita hari ini, kami akan membuat bagian belakang sesederhana mungkin.
Kode Back-end
Sekarang, kita siap untuk memulai pengkodean. Jalankan editor dan terminal Python favorit Anda, dan mari kita mulai!
Pertama, kita akan ingin menginstal paket-paket yang diperlukan.
pip install flask flask-cors textblob tweepy python -m textblob.download_corpora
Sekarang, mari kita tulis kode untuk fungsionalitas kita.
Buka skrip Python baru, beri nama server.py, dan impor pustaka yang diperlukan:
import tweepy from textblob import TextBlob from collections import defaultdict
Sekarang mari kita menulis beberapa fungsi pembantu:
# simple, average a list of numbers with a guard clause to avoid division by zero def mean(lst): return sum(lst)/len(lst) if len(lst) > 0 else 0 # call the textblob sentiment analysis API and noun phrases API and return it as a dict def get_sentiment_and_np(sentence): blob = TextBlob(sentence) return{ 'sentiment': mean([s.sentiment.polarity for s in blob.sentences if s.sentiment.polarity != 0.0]), 'noun_phrases': list(blob.noun_phrases) } # use the tweepy API to get the last 50 posts from a user's timeline # We will want to get the full text if the text is truncated, and we will also remove retweets since they're not tweets by that particular account. def get_tweets(handle): auth = tweepy.OAuthHandler('YOUR_DEVELOPER_KEY') auth.set_access_token('YOUR_DEVELOPER_SECRET_KEY') api = tweepy.API(auth) tl = api.user_timeline(handle, count=50) tweets = [] for tl_item in tl: if 'retweeted_status' in tl_item._json: Continue # this is a retweet if tl_item._json['truncated']: status = api.get_status(tl_item._json['id'], tweet_mode='extended') # get full text tweets.append(status._json['full_text']) else: tweets.append(tl_item._json['text']) return tweets # http and https are sometimes recognized as noun phrases, so we filter it out. # We also try to skip noun phrases with very short words to avoid certain false positives # If this were a commercial app, we would want a more sophisticated filtering strategy. def good_noun_phrase(noun_phrase): noun_phrase_list = noun_phrase.split(' ') for np in noun_phrase_list: if np in {'http', 'https'} or len(np) < 3: return False return True
Sekarang kita memiliki fungsi pembantu yang ditulis, kita dapat menggabungkan semuanya dengan beberapa fungsi sederhana:
# reshapes the tagged tweets into dictionaries that can be easily consumed by the front-end app def group_tweets(processed_tweets): # Sort it by sentiment sentiment_sorted = sorted(processed_tweets, key=lambda x: x['data']['sentiment']) # collect tweets by noun phrases. One tweet can be present in the list of more than one noun phrase, obviously. tweets_by_np = defaultdict(list) for pt in processed_tweets: for np in pt['data']['noun_phrases']: tweets_by_np[np].append(pt) grouped_by_np = {np.title(): tweets for np, tweets in tweets_by_np.items() if len(tweets) > 1 and good_noun_phrase(np)} return sentiment_sorted, grouped_by_np # download, filter, and analyze the tweets def download_analyze_tweets(accounts): processed_tweets = [] for account in accounts: for tweet in get_tweets(account): processed_tweet = ' '.join([i for i in tweet.split(' ') if not i.startswith('@')]) res = get_sentiment_and_np(processed_tweet) processed_tweets.append({ 'account': account, 'tweet': tweet, 'data': res }) sentiment_sorted, grouped_by_np = group_tweets(processed_tweets) return processed_tweets, sentiment_sorted, grouped_by_np
Anda sekarang dapat menjalankan fungsi download_analyze_tweets
pada daftar pegangan yang ingin Anda ikuti, dan Anda akan melihat hasilnya.
Saya menjalankan kode berikut:
if __name__ == '__main__': accounts = ['@spacex', '@nasa'] processed_tweets, sentiment_sorted, grouped_by_np = download_analyze_tweets(accounts) print(processed_tweets) print(sentiment_sorted) print(grouped_by_np)
Menjalankan ini menghasilkan hasil berikut. Hasilnya jelas bergantung pada waktu, jadi jika Anda melihat sesuatu yang serupa, Anda berada di jalur yang benar.
[{'account': '@spacex', 'tweet': 'Falcon 9… [{'account': '@nasa', 'tweet': 'Our Mars rove… {'Falcon': [{'account': '@spacex', 'tweet': 'Falc….
Sekarang kita dapat membangun server Flask, yang cukup sederhana. Buat file kosong bernama server.py dan tulis kode berikut:
from flask import Flask, request, jsonify from twitter import download_analyze_tweets from flask_cors import CORS app = Flask(__name__) CORS(app) @app.route('/get_tweets', methods=['POST']) def get_tweets(): accounts = request.json['accounts'] processed_tweets, sentiment_sorted, grouped_by_np = download_analyze_tweets(accounts) return jsonify({ 'processedTweets': processed_tweets, 'sentimentSorted': sentiment_sorted, 'groupedByNp': grouped_by_np }) if __name__ == '__main__': app.run(debug=True)
Jalankan server, dan Anda sekarang dapat mengirim permintaan posting ke server menggunakan klien HTTP pilihan Anda. Teruskan {accounts: [“@NASA”, “@SpaceX”]} sebagai argumen json, dan Anda akan melihat API mengembalikan sesuatu yang mirip dengan apa yang dikembalikan dalam kode analisis Twitter.
Sekarang kami memiliki server, kami siap untuk mengkodekan front end. Karena nuansa kecil dalam jaringan melalui emulator telepon, saya sarankan Anda menggunakan API Anda di suatu tempat. Jika tidak, Anda akan ingin mendeteksi apakah aplikasi Anda berjalan pada emulator dan mengirim permintaan ke <Your Computer IP>:5000
alih-alih localhost:5000
saat berada di emulator. Jika Anda menerapkan kode, Anda cukup mengeluarkan permintaan ke URL itu.
Ada banyak opsi untuk menyebarkan server. Untuk server debug gratis dan sederhana dengan pengaturan minimal yang diperlukan, saya merekomendasikan sesuatu seperti PythonAnywhere, yang seharusnya dapat menjalankan server ini di luar kotak.
Sekarang kita telah mengkode server back-end, mari kita lihat front end. Kami akan mulai dengan salah satu opsi paling nyaman untuk pengembang web: Cordova.
Implementasi Apache Cordova
Cordova Primer
Apache Cordova adalah teknologi perangkat lunak untuk membantu pengembang web menargetkan platform seluler. Dengan memanfaatkan kemampuan browser web yang diimplementasikan pada platform smartphone, Cordova membungkus kode aplikasi web ke dalam wadah aplikasi asli untuk membuat aplikasi. Cordova bukan sekadar peramban web yang mewah. Melalui Cordova API, pengembang web dapat mengakses banyak fitur khusus ponsel cerdas seperti dukungan offline, layanan lokasi, dan kamera di perangkat.
Untuk aplikasi kita, kita akan menulis aplikasi menggunakan React.js sebagai framework JS dan React-Bootstrap sebagai framework CSS. Karena Bootstrap adalah kerangka kerja CSS yang responsif, ia sudah memiliki dukungan untuk berjalan di layar yang lebih kecil. Setelah aplikasi ditulis, kami akan mengkompilasinya menjadi aplikasi web menggunakan Cordova.
Mengonfigurasi Aplikasi
Kita akan mulai dengan melakukan sesuatu yang unik untuk mengatur aplikasi Cordova React. Dalam artikel Medium , pengembang Shubham Patil menjelaskan apa yang kami lakukan. Pada dasarnya, kami menyiapkan lingkungan pengembangan React menggunakan React CLI, dan kemudian lingkungan pengembangan Cordova menggunakan CLI Cordova, sebelum akhirnya menggabungkan keduanya.
Untuk memulai, jalankan dua perintah berikut di folder kode Anda:
cordova create TwitterCurationCordova create-react-app twittercurationreact
Setelah pengaturan selesai, kita akan ingin memindahkan konten folder publik dan src aplikasi React ke aplikasi Cordova. Kemudian, di package.json, salin skrip, daftar browser, dan dependensi dari proyek React. Tambahkan juga "homepage": "./"
di root package.json untuk mengaktifkan kompatibilitas dengan Cordova.
Setelah package.json digabungkan, kita ingin mengubah file public/index.html agar berfungsi dengan Cordova. Buka file dan salin tag meta dari www/index.html serta skrip di akhir tag tubuh saat Cordova.js dimuat.
Selanjutnya, ubah file src/index.js untuk mendeteksi apakah itu berjalan di Cordova. Jika dijalankan di Cordova, kami ingin menjalankan kode render di dalam event handler deviceready. Jika berjalan di browser biasa, segera render.
const renderReactDom = () => { ReactDOM.render( <React.StrictMode> <App /> </React.StrictMode>, document.getElementById('root') ); } if (window.cordova) { document.addEventListener('deviceready', () => { renderReactDom(); }, false); } else { renderReactDom(); }
Terakhir, kita perlu menyiapkan jalur penyebaran kita. Tambahkan definisi di bawah ini ke dalam file config.xml:
<hook type="before_prepare" src="hooks/prebuild.js" />
Dan masukkan skrip berikut ke dalam prebuild.js:
const path = require('path'); const { exec } = require('child_process'); const fs = require('fs'); const rimraf = require('rimraf'); function renameOutputFolder(buildFolderPath, outputFolderPath) { return new Promise((resolve, reject) => { fs.rename(buildFolderPath, outputFolderPath, (err) => { if (err) { reject(err); } else { resolve('Successfully built!'); } }); }); } function execPostReactBuild(buildFolderPath, outputFolderPath) { return new Promise((resolve, reject) => { if (fs.existsSync(buildFolderPath)) { if (fs.existsSync(outputFolderPath)) { rimraf(outputFolderPath, (err) => { if (err) { reject(err); return; } renameOutputFolder(buildFolderPath, outputFolderPath) .then(val => resolve(val)) .catch(e => reject(e)); }); } else { renameOutputFolder(buildFolderPath, outputFolderPath) .then(val => resolve(val)) .catch(e => reject(e)); } } else { reject(new Error('build folder does not exist')); } }); } module.exports = () => { const projectPath = path.resolve(process.cwd(), './node_modules/.bin/react-scripts'); return new Promise((resolve, reject) => { exec(`${projectPath} build`, (error) => { if (error) { console.error(error); reject(error); return; } execPostReactBuild(path.resolve(__dirname, '../build/'), path.join(__dirname, '../www/')) .then((s) => { console.log(s); resolve(s); }) .catch((e) => { console.error(e); reject(e); }); }); }); };
Ini mengeksekusi build React dan menempatkan folder build ke tempat yang sesuai sebelum build Cordova dimulai, sehingga mengotomatiskan proses penerapan.
Sekarang, kita dapat mencoba menjalankan aplikasi kita. Jalankan yang berikut ini di baris perintah:
npm install rimraf npm install npm run start
Anda akan melihat aplikasi React diatur dan dijalankan di browser. Tambahkan Cordova sekarang:
cordova platform add iOS cordova run iOS
Dan Anda akan melihat aplikasi React berjalan di emulator.
Pengaturan Router dan Paket
Untuk menyiapkan beberapa infrastruktur yang kita perlukan untuk membangun aplikasi, mari kita mulai dengan menginstal paket-paket yang diperlukan:
npm install react-bootstrap react-router react-router-dom
Kami sekarang akan mengatur perutean, dan saat melakukannya, kami juga akan menyiapkan objek status global sederhana yang akan dibagikan oleh semua komponen. Dalam aplikasi produksi, kami ingin menggunakan sistem manajemen status seperti Redux atau MobX, tetapi kami akan membuatnya tetap sederhana untuk saat ini. Buka App.js dan konfigurasikan rute sebagai berikut:
import { BrowserRouter as Router, Redirect, Route, } from "react-router-dom"; function App() { const [curatedTweets, setCuratedTweets] = useState(); return <Router> <Route path="/" exact render={() => <Input setCuratedTweets={setCuratedTweets} />} /> <Route path="/display" render={() => <Display curatedTweets={curatedTweets} />} /> <Route path="*" exact render={() => <Redirect to="/" />} /> </Router> }
Dengan definisi rute ini, kami telah memperkenalkan dua rute yang perlu kami implementasikan: Input dan Tampilan. Perhatikan bahwa variabel curatedTweets
diteruskan ke Display, dan variabel setCuratedTweets
diteruskan ke Input. Ini berarti komponen input akan dapat memanggil fungsi untuk mengatur variabel curatedTweets
, dan Display akan menampilkan variabel tersebut.
Untuk mulai mengkodekan komponen, mari buat folder di bawah /src bernama /src/components. Di bawah /src/components, buat folder lain bernama /src/components/input dan dua file di bawahnya: input.js dan input.css. Lakukan hal yang sama untuk komponen Display - buat /src/components/display dan di bawahnya: display.js dan display.css.
Di bawah itu, mari buat komponen rintisan, seperti:
import React from 'react'; import 'input.css' const Input = () => <div>Input</div>; export default Input
Dan hal yang sama untuk Tampilan:
import React from 'react'; import display.css' const Display = () => <div>Display</div>; export default Display
Dengan itu, wireframing kami selesai dan aplikasi akan berjalan. Sekarang mari kita mengkode halaman Input.
Halaman Masukan
Rencana gambaran besar
Sebelum kita menulis kode, mari kita pikirkan apa yang kita inginkan untuk dilakukan oleh halaman Input kita. Jelas, kami menginginkan cara bagi pengguna untuk memasukkan dan mengedit pegangan Twitter yang ingin mereka tarik. Kami juga ingin pengguna dapat menunjukkan bahwa mereka telah selesai. Ketika pengguna menunjukkan bahwa mereka selesai, kami ingin menarik tweet yang dikuratori dari API kurasi Python kami dan akhirnya menavigasi ke komponen Display.
Sekarang kita tahu apa yang kita ingin komponen kita lakukan, kita siap untuk membuat kode.
Menyiapkan File
Mari kita mulai dengan mengimpor library React Router withRouter
untuk mendapatkan akses ke fungsionalitas navigasi, Komponen React Bootstrap yang kita butuhkan, seperti:
import React, {useState} from 'react'; import {withRouter} from 'react-router-dom'; import {ListGroup, Button, Form, Container, Row, Col} from 'react-bootstrap'; import './input.css';
Sekarang, mari kita definisikan fungsi rintisan untuk Input. Kami tahu bahwa Input mendapatkan fungsi setCuratedTweets
, dan kami juga ingin memberikannya kemampuan untuk menavigasi ke rute tampilan setelah menyetel tweet yang dikuratori dari Python API kami. Oleh karena itu, kami ingin mengambil dari props setCuratedTweets
dan history (untuk navigasi).
const Input = ({setCuratedTweets, history}) => { return <div>Input</div> }
Untuk memberikan akses API histori, kami akan membungkusnya dengan withRouter
dalam pernyataan ekspor di akhir file:
export default withRouter(Input);
Wadah Data
Mari kita siapkan wadah data menggunakan React Hooks. Kami telah mengimpor kait useState
sehingga kami dapat menambahkan kode berikut ke badan komponen Input:
const [handles, setHandles] = useState([]); const [handleText, setHandleText] = useState('');
Ini membuat wadah dan pengubah untuk pegangan, yang akan menampung daftar pegangan yang ingin diambil pengguna, dan handleText
, yang akan menampung konten kotak teks yang digunakan pengguna untuk memasukkan pegangan.
Sekarang, mari buat kode komponen UI.
Komponen UI
Komponen UI akan cukup sederhana. Kami akan memiliki satu baris Bootstrap yang berisi kotak teks input bersama dengan dua tombol, satu untuk menambahkan konten kotak input saat ini ke daftar pegangan, dan satu untuk menarik dari API. Kami akan memiliki baris Bootstrap lain yang menampilkan daftar pegangan yang ingin ditarik pengguna menggunakan grup daftar Bootstrap. Dalam kode, terlihat seperti ini:
return ( <Container className="tl-container"> <Row> <Col> <Form.Control type="text" value={handleText} onChange={changeHandler} placeholder="Enter handle to pull" /> </Col> </Row> <Row className='input-row'> <Col> <Button variant="primary" onClick={getPull}>Pull</Button> {' '} <Button variant="success" onClick={onAddClicked}>Add</Button> </Col> </Row> <Row> <Col> <ListGroup className="handles-lg"> {handles.map((x, i) => <ListGroup.Item key={i}> {x} <span onClick={groupItemClickedBuilder(i)} className="delete-btn-span"> <Button variant="danger" size="sm"> delete </Button> </span> </ListGroup.Item>)} </ListGroup> </Col> </Row> </Container> );
Selain komponen UI, kami ingin mengimplementasikan tiga event handler UI yang menangani perubahan data. Event handler getPull
, yang memanggil API, akan diimplementasikan di bagian berikutnya.
// set the handleText to current event value const textChangeHandler = (e) => { e.preventDefault(); setHandleText(e.target.value); } // Add handleText to handles, and then empty the handleText const onAddClicked = (e) => { e.preventDefault(); const newHandles = [...handles, handleText]; setHandles(newHandles); setHandleText(''); } // Remove the clicked handle from the list const groupItemClickedBuilder = (idx) => (e) => { e.preventDefault(); const newHandles = [...handles]; newHandles.splice(idx, 1); setHandles(newHandles); }
Sekarang, kami siap untuk mengimplementasikan panggilan API.
Panggilan API
Untuk panggilan API, kami ingin mengambil pegangan yang ingin kami tarik, mengirimkannya ke API Python dalam permintaan POST, dan memasukkan hasil JSON yang dihasilkan ke dalam variabel curatedTweets
. Kemudian, jika semuanya berjalan dengan baik, kami ingin menavigasi secara terprogram ke /display route. Jika tidak, kami akan mencatat kesalahan ke konsol sehingga kami dapat melakukan debug dengan lebih mudah.
Dalam mode kode, tampilannya seperti ini:
const pullAPI = (e) => { e.preventDefault(); fetch('http://prismatic.pythonanywhere.com/get_tweets', { method: 'POST', mode: 'cors', headers: { 'Accept': 'application/json', 'Content-Type': 'application/json', }, body: JSON.stringify({ accounts: handles }) }).then(r=>r.json()).then(j => { setCuratedTweets(j); history.push('/display'); }) .catch(e => { console.log(e); }) }
Dan dengan itu, kita harus baik-baik saja. Jangan ragu untuk menjalankan aplikasi, menambahkan beberapa pegangan, dan mengirim permintaan ke API.
Sekarang, kita siap untuk mengkode halaman sentimen.
Mode Pengurutan Sentimen
Karena Python API sudah mengurutkan tweet berdasarkan sentimen, setelah kita mendapatkan hasil dari Python API, halaman sentimen sebenarnya tidak terlalu sulit.
Rencana gambaran besar
Kami akan menginginkan antarmuka daftar untuk menampilkan tweet. Kami juga ingin beberapa komponen navigasi beralih ke mode pengelompokan topik dan kembali ke halaman Input.
Untuk memulai, mari kita definisikan sub-komponen mode SentimentDisplay di file display.js.
Komponen Tampilan Sentimen
SentimentDisplay akan mengambil objek curatedTweets
dan menampilkan tweet yang diurutkan berdasarkan sentimen dalam daftar. Dengan bantuan React-Bootstrap, komponennya cukup sederhana:
const SentimentDisplay = ({curatedTweets}) => { return <ListGroup> {curatedTweets.sentimentSorted.map((x, i) => <ListGroup.Item key={i}> <div className="account-div">{x.account}:</div> <span className="tweet-span">{x.tweet}</span> <span className="sentiments-span">({x.data.sentiment.toPrecision(2)})</span> </ListGroup.Item> )} </ListGroup> }
Sementara kita melakukannya, mari juga menambahkan beberapa gaya. Masukkan yang berikut ini ke dalam display.css dan impor:
.account-div { font-size: 12px; font-weight: 600; } .tweet-span { font-size: 11px; font-weight: 500; } .sentiments-span { font-size: 10px; } .tl-container { margin-top: 10px; } .disp-row { margin-top: 5px; }
Kita sekarang dapat menampilkan komponen SentimentDisplay. Ubah fungsi Display
seperti ini:
const Display = ({curatedTweets}) => { return <SentimentDisplay curatedTweets={curatedTweets} /> };
Mari kita juga mengambil kesempatan ini untuk mengkodekan komponen navigasi. Kami akan menginginkan dua tombol - tombol "Kembali untuk mengedit" dan Mode Grup Topik.
Kita dapat mengimplementasikan tombol-tombol ini di baris Bootstrap terpisah tepat di atas komponen SentimentDisplay, seperti:
Return <Container className="tl-container"> <Row> <Col> <Link to="/"><Button variant='primary'>Back</Button></Link> {' '} <Button variant='success'>View by Topic</Button> </Col> </Row> <Row className="disp-row"> <Col> <SentimentDisplay curatedTweets={curatedTweets} /> </Col> </Row> </Container>
Jalankan aplikasi dan tarik tweet dari beberapa pegangan. Terlihat cukup bagus!
Mode Pengelompokan Topik
Sekarang, kami ingin menerapkan Mode Pengelompokan Topik. Ini sedikit lebih kompleks daripada SentimentDisplay, tetapi sekali lagi, beberapa komponen Bootstrap yang sangat berguna sangat membantu kami.
Rencana gambaran besar
Kami akan mendapatkan semua frase kata benda dan menampilkannya sebagai daftar akordeon. Kami kemudian akan membuat tweet yang berisi frasa kata benda setelah daftar akordeon diperluas.
Menerapkan Beralih ke Mode Pengelompokan Topik
Pertama, mari kita terapkan logika untuk beralih dari Mode Sentimen ke Mode Pengelompokan Topik. Mari kita mulai dengan membuat komponen rintisan terlebih dahulu:
const TopicDisplay = () => { return <div>Topic Display</div> }
Dan atur beberapa logika untuk membuat mode untuk menampilkannya. Di Komponen Tampilan utama, tambahkan baris berikut untuk membuat logika komponen mana yang akan ditampilkan.
// controls the display mode. Remember to import {useState} from 'react' const [displayType, setDisplayType] = useState('Sentiment'); // Switch the Display Mode const toggleDisplayType = () => { setDisplayType(displayType === 'Sentiment' ? 'Topic': 'Sentiment'); } // determines the text on the mode switch button const switchStr = displayType === 'Sentiment'? 'View by Topic': 'View by Sentiment'
Dan ubah JSX menjadi berikut untuk menambahkan logika:
Return <Container className="tl-container"> <Row> <Col> <Link to="/"><Button variant='primary'>Back</Button></Link> {' '} <Button variant='success' onClick={toggleDisplayType}>{switchStr}</Button> </Col> </Row> <Row className="disp-row"> <Col> { displayType === 'Sentiment'? <SentimentDisplay curatedTweets={curatedTweets} />: <TopicDisplay curatedTweets={curatedTweets} /> } </Col> </Row> </Container>
Sekarang, Anda akan melihat rintisan Tampilan Grup Topik saat Anda beralih.
Komponen Tampilan Topik
Sekarang, kita siap untuk mengkodekan komponen TopicDisplay
. Seperti yang dibahas sebelumnya, itu akan memanfaatkan Daftar Akordeon Bootstrap. Implementasinya sebenarnya cukup sederhana:
const TopicDisplay = ({curatedTweets}) => { return <Accordion> {Object.keys(curatedTweets.groupedByNp).map((x, i) => <Card key={i}> <Card.Header> <Accordion.Toggle as={Button} variant="link" eventKey={i}> {x} ({curatedTweets.groupedByNp[x].length}) </Accordion.Toggle> </Card.Header> <Accordion.Collapse eventKey={i}> <Card.Body> <ListGroup> {curatedTweets.groupedByNp[x].map((y, i2) => <ListGroup.Item key={i2}> <div className="account-div">{y.account}:</div> <span className="tweet-span">{y.tweet}</span> <span className="sentiments-span">({y.data.sentiment.toPrecision(2)})</span> </ListGroup.Item> )} </ListGroup> </Card.Body> </Accordion.Collapse> </Card> )} </Accordion> }
Jalankan aplikasi, dan Anda akan melihat Tampilan Topik.
Sekarang aplikasi sudah selesai, dan kita siap membangun aplikasi untuk emulator.
Menjalankan Aplikasi di Emulator
Cordova membuatnya sangat mudah untuk menjalankan aplikasi di emulator. Cukup jalankan:
cordova platform add ios # if you haven't done so already cordova run ios
Dan Anda akan melihat aplikasi di emulator. Karena Bootstrap adalah aplikasi web responsif, aplikasi web menyesuaikan dengan lebar iPhone, dan semuanya terlihat cukup bagus.
Setelah aplikasi Cordova selesai, sekarang mari kita lihat implementasi Ionic.
Implementasi Reaksi Ionik
Primer ionik
Ionic adalah pustaka komponen web dan toolkit CLI yang membuat pembuatan aplikasi hybrid menjadi lebih mudah. Awalnya, Ionic dibangun di atas AngularJS dan Cordova, tetapi mereka telah merilis komponen mereka di React.js dan mulai mendukung Capacitor, sebuah platform yang mirip dengan Cordova. Apa yang membedakan Ionic adalah meskipun Anda menggunakan komponen web, komponennya terasa sangat mirip dengan antarmuka seluler asli. Selain itu, tampilan dan nuansa komponen Ionic secara otomatis beradaptasi dengan sistem operasi yang dijalankannya, yang sekali lagi membantu aplikasi terlihat dan terasa lebih asli dan alami. Terakhir, meskipun ini berada di luar cakupan artikel kami, Ionic juga menyediakan beberapa alat pembangunan yang membuat penerapan aplikasi Anda sedikit lebih mudah.
Untuk aplikasi kita, kita akan menggunakan komponen React Ionic untuk membangun UI sambil memanfaatkan beberapa logika JavaScript yang kita buat selama bagian Cordova.
Mengonfigurasi Aplikasi
Pertama, kita ingin menginstal alat Ionic. Jadi mari kita jalankan yang berikut ini:
npm install -g @Ionic/cli native-run cordova-res
Dan setelah instalasi selesai, mari kita masuk ke folder proyek. Sekarang, kami menggunakan Ionic CLI untuk membuat folder proyek baru kami:
ionic start twitter-curation-Ionic blank --type=react --capacitor
Saksikan keajaiban terjadi, dan sekarang masuk ke folder dengan:
cd twitter-curation-Ionic
Dan jalankan aplikasi kosong dengan:
ionic serve
Dengan demikian, aplikasi kami telah disiapkan dan siap digunakan. Mari kita tentukan beberapa rute.
Sebelum kita melanjutkan, Anda akan melihat bahwa Ionic memulai proyek menggunakan TypeScript. Meskipun saya tidak berusaha keras untuk menggunakan TypeScript, TypeScript memiliki beberapa fitur yang sangat bagus, dan kami akan menggunakannya untuk implementasi ini.
Pengaturan Router
Untuk implementasi ini, kita akan menggunakan tiga rute - input, sentimentDisplay
, dan topicDisplay
. Kami melakukan ini karena kami ingin memanfaatkan fitur transisi dan navigasi yang disediakan oleh Ionic dan karena kami menggunakan komponen Ionic, dan daftar akordeon tidak dikemas sebelumnya dengan Ionic. Kita dapat mengimplementasikannya sendiri, tentu saja, tetapi untuk tutorial ini, kita akan tetap menggunakan komponen Ionic yang disediakan.
Jika Anda menavigasi ke App.tsx, Anda akan melihat rute dasar yang sudah ditentukan.
Halaman Masukan
Rencana gambaran besar
Kami akan menggunakan banyak logika dan kode yang serupa dengan implementasi Bootstrap, dengan beberapa perbedaan utama. Pertama, kita akan menggunakan TypeScript, yang berarti kita akan memiliki anotasi ketik untuk kode kita, yang akan Anda lihat di bagian selanjutnya. Kedua, kita akan menggunakan komponen Ionic, yang gayanya sangat mirip dengan Bootstrap tetapi akan peka terhadap OS dalam gayanya. Terakhir, kita akan menavigasi secara dinamis menggunakan API riwayat seperti pada versi Bootstrap tetapi mengakses riwayat sedikit berbeda karena implementasi Ionic Router.
Pengaturan
Mari kita mulai dengan menyiapkan komponen input dengan komponen rintisan. Buat folder di bawah halaman bernama input dan buat di bawahnya file bernama Input.tsx. Di dalam file itu, masukkan kode berikut untuk membuat komponen React. Perhatikan bahwa karena kami menggunakan TypeScript, ini sedikit berbeda.
import React, {useState} from 'react'; const Input : React.FC = () => { return <div>Input</div>; } export default Input;
Dan ubah komponen di App.tsx menjadi:
const App: React.FC = () => ( <IonApp> <IonReactRouter> <IonRouterOutlet> <Route path="/input" component={Input} exact={true} /> <Route exact path="/" render={() => <Redirect to="/input" />} /> </IonRouterOutlet> </IonReactRouter> </IonApp> );
Sekarang, ketika Anda me-refresh aplikasi, Anda akan melihat komponen Input stub.
Wadah Data
Mari kita buat wadah data sekarang. Kami ingin wadah untuk pegangan Twitter yang dimasukkan serta konten kotak masukan saat ini. Karena kita menggunakan TypeScript, kita perlu menambahkan anotasi tipe ke pemanggilan useState
kita di fungsi komponen:
const Input : React.FC = () => { const [text, setText] = useState<string>(''); const [accounts, setAccounts] = useState<Array<string>>([]); return <div>Input</div>; }
Kami juga menginginkan wadah data untuk menyimpan nilai yang dikembalikan dari API. Karena konten itu perlu dibagikan dengan rute lain, kami mendefinisikannya di tingkat App.tsx. Impor useState
dari React di file App.tsx dan ubah fungsi wadah aplikasi menjadi di bawah ini:
const App: React.FC = () => { const [curatedTweets, setCuratedTweets] = useState<CuratedTweets>({} as CuratedTweets); return ( <IonApp> <IonReactRouter> <IonRouterOutlet> <Route path="/input" component={Input} exact={true} /> <Route exact path="/" render={() => <Redirect to="/input" />} /> </IonRouterOutlet> </IonReactRouter> </IonApp> ); }
Pada titik ini, jika Anda menggunakan editor dengan penyorotan sintaks seperti Visual Studio Code, Anda akan melihat CuratedTweets menyala. Ini karena file tersebut tidak mengetahui seperti apa tampilan antarmuka CuratedTweets. Mari kita definisikan itu sekarang. Buat folder di bawah src bernama interfaces dan buat file di dalamnya bernama CuratedTweets.tsx. Dalam file tersebut, tentukan antarmuka CuratedTweets sebagai berikut:
interface TweetRecordData { noun_phrases: Array<string>, sentiment: number } export interface TweetRecord { account: string, data: TweetRecordData, tweet: string } export default interface CuratedTweets { groupedByNp: Record<string, Array<TweetRecord>>, processedTweets: Array<TweetRecord>, sentimentSorted: Array<TweetRecord> }
Sekarang aplikasi mengetahui tentang struktur data pengembalian API. Impor antarmuka CuratedTweets di App.tsx. Anda akan melihat kompilasi App.tsx tanpa masalah sekarang.
Kita perlu melakukan beberapa hal lagi di sini. Kita perlu meneruskan fungsi setCuratedTweets
ke dalam komponen Input dan membuat komponen Input mengetahui fungsi ini.
Di App.tsx, ubah rute Input seperti ini:
<Route path="/input" render={() => <Input setCuratedTweets={setCuratedTweets}/>} exact={true} />
Sekarang, Anda akan melihat editor menandai sesuatu yang lain - Input tidak tahu tentang prop baru yang diteruskan ke sana sehingga kami ingin mendefinisikannya di Input.tsx.
First, import the CuratedTweets interface, then define the ContainerProps interface like so:
interface ContainerProps { setCuratedTweets: React.Dispatch<React.SetStateAction<CuratedTweets>> }
And finally, change the Input component definition like so:

const Input : React.FC<ContainerProps> = ({setCuratedTweets}) => { const [text, setText] = useState<string>(''); const [accounts, setAccounts] = useState<Array<string>>([]); return <div>Input</div>; }
We are done defining the data containers, and now, onto building the UI components.
UI Components
For the UI component, we will want to build an input component and a list display component. Ionic provides some simple containers for these.
Let's start by importing the library components we'll be using:
import { IonInput, IonItem, IonList, IonButton, IonGrid, IonRow, IonCol } from '@Ionic/react';
Now, we can replace the stub component with the IonInput
, wrapped in an IonGrid:
return <IonGrid> <IonRow> <IonCol> <IonInput value={text} placeholder="Enter accounts to pull from" onIonChange={e => setText(e.detail.value!)} /> </IonCol> </IonRow> </IonGrid>
Notice that the event listener is onIonChange
instead of onChange
. Otherwise, it should look very familiar.
When you open the app in your browser, it may not look like the Bootstrap app. However, if you set your browser to emulator mode, the UI will make more sense. It will look even better once you deploy it on mobile, so look forward to it.
Now, let's add some buttons. We will want an “Add to list” button and a “Pull API” button. For that, we can use IonButton. Change the size of the input's IonCol to 8 and add the following two buttons with columns:
<IonCol size="8"> <IonInput value={text} placeholder="Enter accounts to pull from" onIonChange={e => setText(e.detail.value!)} /> </IonCol> <IonCol size="2"> <IonButton style={{float: 'right'}} color="primary" size="small" onClick={onAddClicked}>Add</IonButton> </IonCol> <IonCol size="2"> <IonButton style={{float: 'right'}} color="success" size="small" onClick={onPullClicked}>Pull</IonButton> </IonCol>
Since we're writing the buttons, let's write the event handlers as well.
The handler to add a Twitter handle to the list is simple:
const onAddClicked = () => { if (text === undefined || text.length === 0) { return; } const newAccounts: Array<string> = [...accounts, text]; setAccounts(newAccounts); setText(''); }
We will implement the API call in the next section, so let's just put a stub function for onPullClicked
:
const onPullClicked = () => {}
Now, we need to write the component for displaying the list of handles that has been inputted by the user. For that, we will use IonList, put into a new IonRow:
<IonRow> <IonCol> <IonList> {accounts.map((x:string, i:number) => <IonItem key={i}> <IonGrid> <IonRow> <IonCol size="8" style={{paddingTop: '12px'}}>{x}</IonCol> <IonCol><IonButton style={{float: 'right'}} color="danger" size="small" onClick={deleteClickedBuilder(i)}>Delete</IonButton></IonCol> </IonRow> </IonGrid> </IonItem>)} </IonList> </IonCol> </IonRow>
Each list item is displaying the handle and a delete button in its very own IonGrid. For this code to compile, we will want to implement the deleteClickedHandler
as well. It should be very familiar from the previous section but with TypeScript annotations.
const deleteClickedBuilder = (idx: number) => () => { const newAccounts: Array<string> = [...accounts]; newAccounts.splice(idx, 1); setAccounts(newAccounts); }
Save this, and you should see the Input page with all the UI components implemented. We can add handles, delete handles, and click the button to invoke the API.
As a final exercise, let's move the in-line styles to CSS. Create a file in the input folder called input.css and import it in the Input.tsx file. Then, add the following styles:
.input-button { float: right; } .handle-display { padding-top: 12px; }
Now, add className="input-button”
on all of the IonButtons and className=”handle-display”
on the handle list item IonCol that is displaying the intended Twitter handle. Save the file, and you should see everything looking quite good.
API Call
The code to pull the API is very familiar from the previous section, with one exception - we have to get access to the history component to be able to dynamically change routes. We will do this using the withHistory
hook.
We first import the hook:
import { useHistory } from 'react-router';
And then implement the handler in the input component:
const history = useHistory(); const switchToDisplay = () => { history.push('/display'); } const onPullClicked = () => { fetch('http://prismatic.pythonanywhere.com/get_tweets', { method: 'POST', mode: 'cors', headers: { 'Accept': 'application/json', 'Content-Type': 'application/json', }, body: JSON.stringify({ accounts }) }).then(r=>r.json()).then(j => { setCuratedTweets(j); switchToDisplay(); }) .catch(e => { console.log(e); }) }
Menambahkan Header
Our Input page looks quite nice, but it looks a little bare due to Ionic's mobile-centric styling. To make the UI look more natural, Ionic provides a header feature that lets us provide a more natural user experience. When running on mobile, the header will also simulate the native OS's mobile platform, which makes the user experience even more natural.
Change your component import to:
import { IonPage, IonHeader, IonToolbar, IonTitle, IonContent, IonInput, IonItem, IonList, IonButton, IonGrid, IonRow, IonCol } from '@Ionic/react';
And now wrap the UI in an Ionic page with a header, like so:
return <IonPage> <IonHeader> <IonToolbar> <IonTitle>Twitter Curation App</IonTitle> </IonToolbar> </IonHeader> <IonContent> <IonHeader collapse="condense"> <IonToolbar> <IonTitle size="large">Twitter Curation App</IonTitle> </IonToolbar> </IonHeader> <IonGrid> <IonRow> <IonCol size="8"> <IonInput value={text} placeholder="Enter accounts to pull from" onIonChange={e => setText(e.detail.value!)} /> </IonCol> <IonCol size="2"> <IonButton className="input-button" color="primary" size="small" onClick={onAddClicked}>Add</IonButton> </IonCol> <IonCol size="2"> <IonButton className="input-button" color="success" size="small" onClick={onPullClicked}>Pull</IonButton> </IonCol> </IonRow> <IonRow> <IonCol> <IonList> {accounts.map((x:string, i:number) => <IonItem key={i}> <IonGrid> <IonRow> <IonCol size="8" className="handle-display">{x}</IonCol> <IonCol><IonButton className="input-button" color="danger" size="small" onClick={deleteClickedBuilder(i)}>Delete</IonButton></IonCol> </IonRow> </IonGrid> </IonItem>)} </IonList> </IonCol> </IonRow> </IonGrid> </IonContent> </IonPage>
Sekarang terlihat bagus!
Halaman Terurut Sentimen
Rencana gambaran besar
Halaman yang diurutkan sentimen sebagian besar akan mirip dengan halaman yang diurutkan sentimen dari halaman Bootstrap tetapi menggunakan TypeScript dan Ionic. Kami juga akan menerapkan Tampilan Topik sebagai rute terpisah untuk memanfaatkan fitur navigasi Ionic saat berjalan di ponsel, jadi kami perlu memberikan halaman ini kemampuan untuk menavigasi ke rute Tampilan Topik juga.
Pengaturan Rute
Mari kita mulai dengan membuat folder baru bernama sentimensorted dan file bernama SentimentSorted.tsx di bawahnya. Ekspor komponen rintisan seperti:
import React from 'react'; const SentimentSorted: React.FC = () => { return <div>Sentiment Sorted</div> } export default SentimentSorted;
Dan di App.tsx, impor komponen:
import SentimentSorted from './pages/sentimentsorted/SentimentSorted';
Dan tambahkan rute:
<Route path="/display" render={() => <SentimentSorted curatedTweets={curatedTweets} />} />
Anda akan mendapatkan kesalahan TypeScript yang mengatakan bahwa SentimentSorted
tidak mengharapkan props curatedTweets, jadi mari kita selesaikan itu sekarang di bagian berikutnya.
Komponen UI
Mari kita mulai dengan mendefinisikan props container. Sama seperti komponen input:
import CuratedTweets from '../../interfaces/CuratedTweets'; interface ContainerProps { curatedTweets: CuratedTweets }
Dan sekarang, ubah tampilan rintisan:
const SentimentSorted: React.FC<ContainerProps> = ({curatedTweets}) => { return <div>Sentiment Sorted</div> }
Dan semuanya harus dikompilasi.
Tampilannya sangat sederhana, hanya sebuah IonList dengan komponen tampilan:
return <IonGrid> <IonRow> <IonCol> <IonList> {(curatedTweets.sentimentSorted).map((x, i) => <IonItem key={i}> <IonLabel className="ion-text-wrap"> <h2>{x.account}:</h2> <h3>{x.tweet}</h3> <p>({x.data.sentiment.toPrecision(2)})</p> </IonLabel> </IonItem>)} </IonList> </IonCol> </IonRow> </IonGrid>
Jika Anda menyimpan dan menarik beberapa tweet menggunakan komponen input, Anda akan melihat tweet ditampilkan dalam daftar.
Sekarang, mari tambahkan tombol navigasi. Tambahkan ke IonGrid:
<IonRow> <IonCol> <IonButton color='primary' onClick={switchToInput}>Back</IonButton> <IonButton color="success" onClick={toggleDisplayType}>{switchStr}</IonButton> </IonCol> </IonRow>
switchToInput
sangat mudah diterapkan dengan API riwayat:
const switchToInput = () => { history.goBack(); }
Dan ToggleDisplayType
juga harus familier:
const toggleDisplayType = () => { setDisplayType(displayType === 'Sentiment' ? 'Topic': 'Sentiment'); } const switchStr = displayType === 'Sentiment'? 'View by Topic': 'View by Sentiment'
Sekarang kita telah mengimplementasikan komponen SentimentDisplay
. Sekarang, sebelum kita mengimplementasikan Halaman Tampilan Topik, kita perlu mengimplementasikan komponen yang menampilkan semua topik. Kami akan melakukannya di bagian selanjutnya.
Komponen Grup Topik
Mari tambahkan opsi tampilan daftar topik dan tampilkan secara kondisional. Untuk melakukan itu, kita perlu mendobrak daftar Tampilan Sentimen. Ganti nama SentimentDisplay menjadi Display, dan mari kita pecahkan daftar tampilan sentimen:
interface SentimentDisplayProps { sentimentSorted: Array<TweetRecord> } const SentimentDisplay: React.FC<SentimentDisplayProps> = ({sentimentSorted}) => { return <IonList> {(sentimentSorted || []).map((x, i) => <IonItem key={i}> <IonLabel className="ion-text-wrap"> <h2>{x.account}:</h2> <h3>{x.tweet}</h3> <p>({x.data.sentiment.toPrecision(2)})</p> </IonLabel> </IonItem>)} </IonList> }
Perhatikan bagaimana kita menggunakan salah satu definisi kelas di antarmuka CuratedTweets. Itu karena komponen ini tidak membutuhkan seluruh objek CuratedTweets tetapi hanya subset. Daftar topik sangat mirip:
interface TopicDisplayProps { groupedByNP: Record<string, Array<TweetRecord>> } const TopicDisplay: React.FC<TopicDisplayProps> = ({groupedByNP}) => { return <IonList> {Object.keys(groupedByNP || {}).map((x, i) => <IonItem key={i} routerLink={`/topicDisplay/${encodeURIComponent(x)}`}> <IonLabel className="ion-text-wrap"> <h2>{x} ({groupedByNP[x].length})</h2> </IonLabel> </IonItem>)} </IonList> }
Dan sekarang, tampilan bersyarat mudah diatur di Komponen Tampilan:
return ( <IonGrid> <IonRow> <IonCol> <IonButton color='primary' onClick={switchToInput}>Back</IonButton> <IonButton color="success" onClick={toggleDisplayType}>{switchStr}</IonButton> </IonCol> </IonRow> { displayType === 'Sentiment'? <SentimentDisplay sentimentSorted={curatedTweets.sentimentSorted} /> : <TopicDisplay groupedByNP={curatedTweets.groupedByNp} /> } </IonGrid> );
Pastikan untuk mengubah ekspor default, dan sekarang kami siap untuk menerapkan Halaman Tampilan Topik.
Halaman Tampilan Topik
Rencana gambaran besar
Halaman tampilan topik adalah tampilan daftar yang mirip dengan tampilan sentimen, tetapi kita akan mencari topik yang dimaksud dari parameter rute.
Pengaturan Rute
Jika Anda sudah sejauh ini, Anda seharusnya sudah tahu apa yang harus dilakukan. Mari buat folder halaman bernama topicdisplay dan TopicDisplay.tsx, tulis komponen rintisan, dan impor ke halaman App.tsx. Sekarang, mari kita atur rute:
<Route path="/topicDisplay/:topic" render={() => <TopicDisplay curatedTweets={curatedTweets} /> } />
Sekarang kita siap untuk mengimplementasikan komponen UI.
Komponen UI
Pertama, mari kita buat definisi ContainerProps
:
interface ContainerProps { curatedTweets: CuratedTweets } const TopicDisplay: React.FC<ContainerProps> = ({curatedTweets}) => { Return <div>Topic Display</div> }
Sekarang, kita perlu mengambil topik dari nama jalur URL. Untuk melakukan itu, kita akan menggunakan API riwayat. Jadi mari impor useHistory
, buat instance API riwayat, dan tarik topik dari nama path. Sementara kita melakukannya, mari kita juga menerapkan fungsionalitas beralih kembali:
const TopicDisplay: React.FC<ContainerProps> = ({curatedTweets}) => { const history = useHistory(); const switchToDisplay = () => { history.goBack(); } const topic = history.location.pathname.split('/')[2]; const tweets = (curatedTweets.groupedByNp || {})[topic];
Sekarang kita memiliki tweet dengan topik tertentu, menampilkannya sebenarnya cukup sederhana:
return ( <IonGrid> <IonRow> <IonCol> <IonButton color='primary' onClick={switchToDisplay}>Back</IonButton> </IonCol> </IonRow> <IonRow> <IonCol> <IonList> {(tweets || []).map((x, i) => <IonItem key={i}> <IonLabel className="ion-text-wrap"> <h2>{x.account}:</h2> <h3>{x.tweet}</h3> <p>({x.data.sentiment.toPrecision(2)})</p> </IonLabel> </IonItem>)} </IonList> </IonCol> </IonRow> </IonGrid> );
Simpan dan jalankan, dan semuanya akan terlihat bagus.
Menjalankan Aplikasi di Emulator
Untuk menjalankan aplikasi di emulator, kita cukup menjalankan beberapa perintah Ionic untuk menambahkan platform seluler dan menyalin kodenya, mirip dengan cara kita mengatur berbagai hal dengan Cordova.
ionic build # builds the app ionic cap add ios # adds iOS as one of the platforms, only have to run once ionic cap copy # copy the build over ionic cap sync # only need to run this if you added native plugins ionic cap open ios # run the iOS emulator
Dan Anda akan melihat aplikasi muncul.
Bereaksi Implementasi Asli
React Native Primer
React Native mengambil pendekatan yang sangat berbeda dari pendekatan berbasis web pada bagian sebelumnya. React Native merender kode React Anda sebagai komponen asli. Ini datang dengan beberapa keuntungan. Pertama, integrasi dengan sistem operasi yang mendasari jauh lebih dalam, yang memungkinkan pengembang untuk memanfaatkan fitur smartphone baru dan fitur khusus OS yang mungkin tidak tersedia melalui Cordova/Capacitor. Kedua, karena tidak ada mesin rendering berbasis web di tengah, aplikasi React Native umumnya lebih cepat daripada yang ditulis menggunakan Cordova. Akhirnya, karena React Native memungkinkan integrasi komponen asli, pengembang dapat menggunakan kontrol yang lebih halus atas aplikasi mereka.
Untuk aplikasi kita, kita akan menggunakan logika dari bagian sebelumnya dan menggunakan pustaka komponen React Native yang disebut NativeBase untuk mengkodekan UI kita.
Mengonfigurasi Aplikasi
Pertama, Anda ingin menginstal semua komponen React Native yang diperlukan dengan mengikuti petunjuk di sini.
Setelah React Native terinstal, mari kita mulai proyeknya:
react-native init TwitterCurationRN
Biarkan skrip pengaturan berjalan, dan akhirnya, folder tersebut harus dibuat. Cd ke dalam folder dan jalankan react-native run-ios, dan Anda akan melihat emulator muncul dengan aplikasi contoh.
Kami juga ingin menginstal NativeBase karena itu adalah pustaka komponen kami. Untuk melakukan itu, kami menjalankan:
npm install --save native-base react-native link
Kami juga ingin menginstal navigator tumpukan React Native. Ayo lari:
npm install --save @react-navigation/stack @react-navigation/native
Dan
react-native link cd ios pod-install cd
Untuk menyelesaikan penautan dan pemasangan plugin asli.
Pengaturan Router
Untuk perutean, kami akan menggunakan navigator tumpukan yang kami instal pada langkah di atas.
Impor komponen router:
import { NavigationContainer } from '@react-navigation/native'; import {createStackNavigator} from '@react-navigation/stack';
Dan sekarang, kami membuat navigator tumpukan:
const Stack = createStackNavigator();
Ubah konten komponen Aplikasi untuk menggunakan navigator tumpukan:
const App = () => { return ( <> <StatusBar bar /> <NavigationContainer> <Stack.Navigator initialRouteName="Entry"> <Stack.Screen name="Entry" component={Entry} /> </Stack.Navigator> </NavigationContainer> </> ); };
Pada titik ini, Anda akan mendapatkan kesalahan karena Entri belum ditentukan. Mari kita definisikan elemen rintisan hanya untuk membuatnya bahagia.
Buat folder komponen di proyek Anda, buat file bernama Entry.jsx, dan tambahkan komponen rintisan seperti:
import React, {useState} from 'react'; import { Text } from 'native-base'; export default Entry = ({navigation}) => { return <Text>Entry</Text>; // All text must be wrapped in a <Text /> component or <TextView /> if you're not using NativeBase. }
Impor komponen Entry di aplikasi Anda, dan itu harus dibangun.
Sekarang, kita siap untuk mengkode halaman Input.
Halaman Masukan
Rencana gambaran besar
Kami akan mengimplementasikan halaman yang sangat mirip dengan yang diimplementasikan di atas tetapi menggunakan komponen NativeBase. Sebagian besar JavaScript dan React API yang kami gunakan, seperti hook dan fetch, semuanya masih tersedia.
Satu-satunya perbedaan adalah cara kami bekerja dengan API navigasi, yang akan Anda lihat nanti.
Komponen UI
Komponen NativeBase tambahan yang akan kita gunakan adalah Container, Content, Input, List, ListItem, dan Button. Ini semua memiliki analog di Ionic dan Bootstrap React, dan pembuat NativeBase telah membuatnya sangat intuitif untuk orang yang akrab dengan perpustakaan ini. Cukup impor seperti:
import { Container, Content, Input, Item, Button, List, ListItem, Text } from 'native-base';
Dan komponennya adalah:
return <Container> <Content> <Item regular> <Input placeholder='Input Handles Here' onChangeText={inputChange} value={input} /> <Button primary onPress={onAddClicked}><Text> Add </Text></Button> <Text> </Text> <Button success onPress={onPullClicked}><Text> Pull </Text></Button> </Item> <Item> <List style={{width: '100%'}}> {handles.map((item) => <ListItem key={item.key}> <Text>{item.key}</Text> </ListItem>)} </List> </Item> </Content> </Container>
Dan sekarang, mari kita implementasikan state dan event handler:
const [input, changeInput] = useState(''); const [handles, changeHandles] = useState([]); const inputChange = (text) => { changeInput(text); } const onAddClicked = () => { const newHandles = [...handles, {key: input}]; changeHandles(newHandles); changeInput(''); }
Dan akhirnya, panggilan API:
const onPullClicked = () => { fetch('http://prismatic.pythonanywhere.com/get_tweets', { method: 'POST', mode: 'cors', headers: { 'Accept': 'application/json', 'Content-Type': 'application/json', }, body: JSON.stringify({ accounts: handles.map(x => x.key) }) }).then(r=>r.json()).then(data => { navigation.navigate('SentimentDisplay', { data }); }) .catch(e => { console.log(e); }) }
Perhatikan bahwa implementasi ini sama dengan implementasi NativeBase, kecuali kita menavigasi dengan cara yang berbeda. Navigator tumpukan meneruskan ke komponennya prop yang disebut "navigasi", dan itu dapat digunakan untuk menavigasi antar rute dengan .navigate
. Selain hanya menavigasi, Anda juga dapat meneruskan data ke komponen target. Kami akan menggunakan mekanisme ini untuk melewatkan data.
Untuk membuat aplikasi dikompilasi, kita masih perlu membuat Entry
mengetahui komponen navigasi. Untuk melakukan itu, kita perlu mengubah deklarasi fungsi komponen:
export default Entry = ({navigation}) => {
Sekarang simpan, dan Anda akan melihat halaman tersebut.
Halaman Terurut Sentimen
Rencana gambaran besar
Kami akan menerapkan halaman sentimen seperti bagian sebelumnya, tetapi kami akan menata halaman sedikit berbeda, dan kami juga akan menggunakan perpustakaan navigasi secara berbeda untuk mendapatkan nilai pengembalian panggilan API.
Karena React Native tidak memiliki CSS, kita perlu mendefinisikan objek StyleSheet atau cukup mengkodekan gaya secara in-line. Karena kita akan berbagi beberapa gaya di seluruh komponen, mari buat lembar gaya global. Kami akan melakukannya setelah pengaturan rute.
Selain itu, karena StackNavigator
memiliki tombol navigasi Kembali bawaan, kita tidak perlu mengimplementasikan tombol Kembali kita sendiri.
Pengaturan Rute
Definisi rute di StackNavigator
sangat sederhana. Kami cukup membuat yang baru bernama Stack Screen dan memberikan komponennya, seperti router React.
<NavigationContainer> <Stack.Navigator initialRouteName="Entry"> <Stack.Screen name="Entry" component={Entry} /> <Stack.Screen name="SentimentDisplay" component={SentimentDisplay} /> </Stack.Navigator> </NavigationContainer>
Untuk membuat ini berfungsi, tentu saja kita perlu membuat komponen rintisan di component/SentimentDisplay.js:
import React from 'react'; import {Text} from 'native-base'; const SentimentDisplay = () => { return <Text>Sentiment Display</Text>; } export default SentimentDisplay;
Dan impor itu:
import SentimentDisplay from './components/SentimentDisplay';
Sekarang, kita siap untuk membuat stylesheet global.
Lembar Gaya Global
Pertama, buat file bernama globalStyles.js. Kemudian, impor komponen StyleSheet dari React Native dan tentukan gayanya:
import {StyleSheet} from 'react-native'; export default StyleSheet.create({ tweet: {paddingTop: 5}, accountName: {fontWeight: '600'}, })
Dan kami siap untuk mengkodekan UI.
Komponen UI
Komponen UI cukup familiar, dengan pengecualian bagaimana kita bekerja dengan route. Kami ingin menggunakan navigasi dan rute props khusus StackNavigator
untuk mendapatkan status aplikasi saat ini dan untuk menavigasi ke tampilan topik jika pengguna ingin melihat halaman itu.
Ubah definisi komponen untuk mengakses alat peraga navigasi:
const SentimentDisplay = ({route, navigation}) => {
Dan sekarang, kami menerapkan pembacaan status aplikasi dan fungsi navigasi:
const {params: {data}} = route; const viewByTopicClicked = () => { navigation.navigate('TopicDisplay', { data }); }
Impor gaya global:
import globalStyles from './globalStyles';
Dan komponennya:
import { View } from 'react-native'; import {List, Item, Content, ListItem, Container, Text, Button} from 'native-base';
Dan akhirnya, komponen:
return <Container> <Content> <Item> <Button primary onPress={viewByTopicClicked}><Text>View By Topic</Text></Button> </Item> <Item> <List style={{width: '100%'}}> {data.sentimentSorted.map((item, i) => <ListItem key={i}> <View style={globalStyles.listItem}> <Text style={globalStyles.accountName}>{item.account}:</Text> <Text style={globalStyles.tweet}>{item.tweet} ({item.data.sentiment.toPrecision(2)})</Text> </View> </ListItem>)} </List> </Item> </Content> </Container>;
Simpan dan coba tarik beberapa tweet, dan Anda akan melihat tampilan sentimen. Sekarang ke halaman pengelompokan topik.
Halaman Pengelompokan Topik
Rencana gambaran besar
Tampilan Topik sekali lagi sangat mirip. Kami akan menggunakan pembuat handler untuk membangun fungsi navigasi untuk menavigasi ke halaman tampilan untuk item topik tertentu, dan kami juga akan mendefinisikan stylesheet yang khusus untuk halaman ini.
Satu hal baru yang akan kita lakukan adalah mengimplementasikan TouchableOpacity, yang merupakan komponen khusus React Native yang berfungsi seperti tombol.
Pengaturan Rute
Definisi rute sama seperti sebelumnya:
<Stack.Navigator initialRouteName="Entry"> <Stack.Screen name="Entry" component={Entry} /> <Stack.Screen name="SentimentDisplay" component={SentimentDisplay} /> <Stack.Screen name="TopicDisplay" component={TopicDisplay} /> </Stack.Navigator>
Komponen komponen rintisan/TopicDisplay.js:
import React from 'react'; import {Text} from 'native-base'; const TopicDisplay = () => { return <Text>Topic Display</Text>; } export default TopicDisplay;
Dan impor itu:
import TopicDisplay from './components/TopicDisplay;
Komponen UI
Banyak dari ini akan terlihat sangat akrab. Impor fungsi perpustakaan:
import { View, TouchableOpacity, StyleSheet } from 'react-native'; import {List, Item, Content, ListItem, Container, Text} from 'native-base';
Impor gaya global:
import globalStyles from './globalStyles';
Tentukan gaya kustom:
const styles = StyleSheet.create({ topicName: {fontWeight: '600'}, })
Tentukan alat peraga navigasi:
export default TopicDisplay = ({route, navigation}) => {
Tentukan data dan penangan tindakan. Perhatikan kita menggunakan pembuat handler, fungsi yang mengembalikan fungsi:
const {params: {data}} = route; const specificItemPressedHandlerBuilder = (topic) => () => { navigation.navigate('TopicDisplayItem', { data, topic }); }
Dan sekarang, komponennya. Perhatikan bahwa kita menggunakan TouchableOpacity, yang dapat memiliki handler onPress
. Kami dapat menggunakan TouchableTransparency juga, tetapi animasi klik-dan-tahan TouchableOpacity lebih cocok untuk aplikasi kami.
return <Container> <Content> <Item> <List style={{width: '100%'}}> {Object.keys(data.groupedByNp).map((topic, i) => <ListItem key={i}> <View style={globalStyles.listItem}> <TouchableOpacity onPress={specificItemPressedHandlerBuilder(topic)}> <Text style={styles.topicName}>{topic}</Text> </TouchableOpacity> </View> </ListItem>)} </List> </Item> </Content> </Container>;
Dan ini harus melakukannya. Simpan dan coba aplikasinya!
Sekarang, ke Halaman Item Tampilan Topik.
Halaman Item Tampilan Topik
Rencana gambaran besar
Halaman Item Tampilan Topik sangat mirip dan semua keanehan ditangani di bagian lain sehingga harus lancar dari sini.
Pengaturan Rute
Kami akan menambahkan definisi rute:
<Stack.Screen name="TopicDisplayItem" component={TopicDisplayItem} />
Tambahkan impor:
import TopicDisplayItem from './components/TopicDisplayItem';
Dan buat komponen rintisan. Alih-alih hanya komponen kosong, mari impor juga komponen NativeBase yang akan kita gunakan dan tentukan props rute:
import React from 'react'; import {View} from 'react-native'; import {List, Item, Content, ListItem, Container, Text} from 'native-base'; import globalStyles from './globalStyles'; const TopicDisplayItem = ({route}) => { const {params: {data, topic}} = route; return <Text>Topic Display Item</Text>; } export default TopicDisplayItem;
Komponen UI
Komponen UI cukup sederhana. Kami telah melihatnya sebelumnya dan kami tidak benar-benar menerapkan logika khusus apa pun. Jadi, mari kita lakukan! Ambil napas dalam-dalam…
return <Container> <Content> <Item> <List style={{width: '100%'}}> {data.groupedByNp[topic].map((item, i) => <ListItem key={i}> <View style={globalStyles.listItem}> <Text style={globalStyles.accountName}>{item.account}:</Text> <Text style={globalStyles.tweet}>{item.tweet} ({item.data.sentiment.toPrecision(2)})</Text> </View> </ListItem>)} </List> </Item> </Content> </Container>;
Simpan, dan kita harus siap! Sekarang kita siap menjalankan aplikasi di emulator… tunggu, belum?
Menjalankan Aplikasi
Nah, karena Anda bekerja dengan React Native, Anda sudah menjalankan aplikasi di emulator jadi bagian ini sudah diurus. Ini adalah salah satu hal hebat tentang lingkungan pengembangan React Native.
Wah! Dengan itu, kita selesai dengan bagian pengkodean artikel ini. Mari kita lihat apa yang kita pelajari tentang teknologi.
Membandingkan Teknologi
Cordova: Pro dan Kontra
Hal terbaik tentang Cordova adalah kecepatan yang digunakan oleh pengembang web yang terampil untuk membuat kode sesuatu yang fungsional dan cukup rapi. Keterampilan pengembangan web dan pengalaman ditransfer dengan mudah karena, bagaimanapun, Anda sedang mengkodekan aplikasi web. Proses pengembangannya cepat dan sederhana, dan mengakses Cordova API juga sederhana dan intuitif.
Kelemahan menggunakan Cordova secara langsung sebagian besar berasal dari ketergantungan yang berlebihan pada komponen web. Pengguna telah mengharapkan pengalaman pengguna dan desain antarmuka yang spesifik saat menggunakan aplikasi seluler, dan ketika aplikasi terasa seperti situs seluler, pengalamannya bisa sedikit menggelegar. Selain itu, sebagian besar fitur yang ada di dalam aplikasi, seperti animasi transisi dan utilitas navigasi, harus diterapkan secara manual.
Ionik: Pro dan Kontra
Bagian terbaik dari Ionic adalah berapa banyak fitur mobile-centric yang saya dapatkan secara "gratis". Dengan pengkodean seperti saya akan mengkodekan aplikasi web, saya dapat membangun aplikasi yang terlihat jauh lebih ramah seluler daripada hanya menggunakan Cordova dan React-Bootstrap. Ada animasi navigasi, tombol dengan gaya yang tampak asli, dan banyak opsi antarmuka pengguna yang membuat pengalaman pengguna menjadi sangat mulus.
Kelemahan menggunakan Ionic sebagian disebabkan oleh kekuatannya. Pertama, terkadang sulit untuk membayangkan bagaimana aplikasi akan berperilaku di berbagai lingkungan. Hanya karena aplikasi terlihat satu arah, bukan berarti penempatan UI yang sama akan terlihat sama di lingkungan lain. Kedua, Ionic berada di atas banyak teknologi yang mendasarinya, dan mendapatkan akses ke beberapa komponen terbukti sulit. Terakhir, ini khusus untuk Ionic-React, tetapi karena Ionic pertama kali dibuat untuk Angular, banyak fitur Ionic-React tampaknya memiliki dokumentasi dan dukungan yang lebih sedikit. Namun, tim Ionic tampaknya sangat memperhatikan kebutuhan pengembang React dan memberikan fitur baru dengan cepat.
Bereaksi Asli: Pro dan Kontra
React Native memiliki pengalaman pengguna yang sangat lancar berkembang di seluler. Dengan menghubungkan langsung ke emulator, tidak heran bagaimana tampilan aplikasinya. Antarmuka debugger berbasis web sangat membantu dalam menerapkan teknik debugging silang dari dunia aplikasi web, dan ekosistemnya cukup kuat.
Kelemahan dari React Native berasal dari kedekatannya dengan antarmuka asli. Banyak perpustakaan berbasis DOM tidak dapat digunakan, yang berarti harus mempelajari perpustakaan baru dan praktik terbaik. Tanpa manfaat CSS, penataan aplikasi agak kurang intuitif. Akhirnya, dengan banyak komponen baru yang harus dipelajari (misalnya, Lihat alih-alih div, komponen Teks membungkus semuanya, Tombol vs. TouchableOpacity vs. TouchableTransparency, dll.), ada sedikit kurva pembelajaran di awal jika seseorang masuk ke Bereaksi dunia Asli dengan sedikit pengetahuan sebelumnya tentang mekanika.
Kapan Menggunakan Setiap Teknologi
Karena Cordova, Ionic, dan React Native semuanya memiliki pro dan kontra yang sangat kuat, setiap teknologi memiliki konteks di mana ia akan menikmati produktivitas dan kinerja terbaik.
Jika Anda sudah memiliki aplikasi yang mengutamakan web dengan identitas merek yang kuat seputar desain UI serta tampilan dan nuansa umum, pilihan terbaik Anda adalah Cordova, yang memberi Anda akses ke fitur asli ponsel cerdas sambil membiarkan Anda menggunakan kembali sebagian besar komponen web dan pertahankan identitas merek Anda dalam prosesnya. Untuk aplikasi yang relatif sederhana menggunakan kerangka kerja responsif, Anda mungkin dapat membuat aplikasi seluler dengan sedikit perubahan yang diperlukan. Namun, aplikasi Anda tidak akan terlihat seperti aplikasi dan lebih mirip halaman web, dan beberapa komponen yang diharapkan orang dari aplikasi seluler akan dikodekan secara terpisah. Oleh karena itu, saya merekomendasikan Cordova jika Anda berada dalam proyek web-first porting aplikasi ke seluler.
Jika Anda mulai membuat kode aplikasi baru dengan filosofi mengutamakan aplikasi, tetapi keahlian tim Anda terutama dalam pengembangan web, saya sarankan Ionic. Pustaka Ionic memungkinkan Anda dengan cepat menulis kode yang terlihat dan terasa dekat dengan komponen asli sambil membiarkan Anda menerapkan keterampilan dan naluri Anda sebagai pengembang web. Saya menemukan bahwa praktik terbaik pengembangan web siap diterapkan secara silang untuk pengembangan dengan Ionic, dan penataan gaya dengan CSS bekerja dengan mulus. Selain itu, versi seluler situs terlihat jauh lebih asli daripada situs web yang dikodekan menggunakan kerangka kerja CSS responsif. Namun, pada beberapa langkah di sepanjang jalan, saya menemukan bahwa integrasi API React-Ionic-Native memerlukan beberapa penyesuaian manual, yang terbukti memakan waktu. Oleh karena itu, saya merekomendasikan Ionic dalam kasus di mana aplikasi Anda sedang dikembangkan dari bawah ke atas dan Anda ingin berbagi sejumlah besar kode antara aplikasi web berkemampuan seluler dan aplikasi seluler.
Jika Anda mengkodekan aplikasi baru dengan beberapa basis kode asli yang diimplementasikan, Anda mungkin ingin mencoba React Native. Bahkan jika Anda tidak menggunakan kode asli, itu juga bisa menjadi pilihan terbaik dalam kasus di mana Anda sudah terbiasa dengan React Native, atau ketika perhatian utama Anda adalah aplikasi seluler daripada aplikasi hibrida. Setelah memfokuskan sebagian besar upaya pengembangan front-end saya pada pengembangan web, saya awalnya menemukan bahwa memulai dengan React Native memiliki lebih banyak kurva belajar daripada Ionic atau Cordova karena perbedaan dalam organisasi komponen dan konvensi pengkodean. Namun, setelah nuansa ini dipelajari, pengalaman pengkodean cukup lancar, terutama dengan bantuan pustaka komponen seperti NativeBase. Mengingat kualitas lingkungan pengembangan dan kontrol atas aplikasi, jika ujung depan proyek Anda terutama adalah aplikasi seluler, saya akan merekomendasikan React Native sebagai alat pilihan Anda.
Topik Masa Depan
Salah satu topik yang tidak sempat saya jelajahi adalah kemudahan mengakses API asli seperti kamera, geolokasi, atau otentikasi biometrik. Salah satu manfaat besar pengembangan seluler adalah aksesibilitas ekosistem API yang kaya yang umumnya tidak dapat diakses di browser.
Di artikel mendatang, saya ingin menjelajahi kemudahan mengembangkan aplikasi berkemampuan API asli ini menggunakan berbagai teknologi lintas platform.
Kesimpulan
Hari ini, kami menerapkan aplikasi kurasi Twitter menggunakan tiga teknologi pengembangan seluler lintas platform yang berbeda. Saya harap ini memberi Anda pemahaman yang baik tentang seperti apa masing-masing teknologi dan menginspirasi Anda untuk mengembangkan aplikasi berbasis React Anda sendiri.
Terima kasih telah membaca!