On this tutorial, we are going to create a photo gallery app with ionic and react. Then we will build the app with Android Studio and generate a APK file you can put in your phone and install the app.
As of 9/3/2020 my current version:
PS C:\Users\15t> ionic -v
6.11.7
PS C:\Users\15t> npm -v
6.14.4
PS C:\Users\15t> node -v
v12.16.3
cd F:\apachefriends\xampp\htdocs\IONIC\React\NEW-APP-START-TO-FINISH-ON-PHONE
ionic start photo-gallery tabs --type=react –capacitor
OPEN photo-gallery/ PROJECT IN VISUAL CODE
Open a terminal in visual code: Ctl + Shift + ` (Tilde button) npm install @ionic/react-hooks @ionic/pwa-elements
Next, import @ionic/pwa-elements by editing src/index.tsx
IF YOU DON’T DO THIS STEP, YOU WILL SEE ERROR: Cannot find module '@ionic/react-hooks/camera'. TS2307
ADD: import { defineCustomElements } from '@ionic/pwa-elements/loader';
AND // Call the element loader after the platform has been bootstrapped defineCustomElements(window);
Ionic serve
Open another terminal
LET CREATE A NEW COMPONENT WHICH WILL HAVE THE CODE FOR THE CAMERA FEATURE: src/hooks/usePhotoGallery.ts
COPY/PAST THIS CODE:
// Import the necessary libraries/components import { useState, useEffect } from "react"; import { useCamera } from '@ionic/react-hooks/camera';
import { useFilesystem, base64FromPath } from '@ionic/react-hooks/filesystem'; import { useStorage } from '@ionic/react-hooks/storage'; import { isPlatform } from '@ionic/react'; import { CameraResultType, CameraSource, CameraPhoto, Capacitor, FilesystemDirectory } from "@capacitor/core"; const PHOTO_STORAGE = "photos"; export function usePhotoGallery() { const { getPhoto } = useCamera(); const [photos, setPhotos] = useState<Photo[]>([]); const { deleteFile, readFile, writeFile } = useFilesystem(); const { get, set } = useStorage(); useEffect(() => { const loadSaved = async () => { const photosString = await get('photos'); const photosInStorage = (photosString ? JSON.parse(photosString) : []) as Photo[]; if (!isPlatform('hybrid')) { for (let photo of photosInStorage) { const file = await readFile({ path: photo.filepath, directory: FilesystemDirectory.Data }); photo.base64 = `data:image/jpeg;base64,${file.data}`; } } setPhotos(photosInStorage); }; loadSaved(); }, [get, readFile]); const takePhoto = async () => { const cameraPhoto = await getPhoto({ resultType: CameraResultType.Uri, source: CameraSource.Camera, quality: 100 }); const fileName = new Date().getTime() + '.jpeg'; const savedFileImage = await savePicture(cameraPhoto, fileName); const newPhotos = [savedFileImage, ...photos]; setPhotos(newPhotos); //Storage API
set(PHOTO_STORAGE, isPlatform('hybrid') ? JSON.stringify(newPhotos) : JSON.stringify(newPhotos.map(p => { // Don't save the base64 representation of the photo data, // since it's already saved on the Filesystem const photoCopy = { ...p }; delete photoCopy.base64; return photoCopy; }))); }; const savePicture = async (photo: CameraPhoto, fileName: string): Promise<Photo> => { let base64Data: string; // "hybrid" will detect Cordova or Capacitor; if (isPlatform('hybrid')) { const file = await readFile({ path: photo.path! }); base64Data = file.data; } else { base64Data = await base64FromPath(photo.webPath!); } const savedFile = await writeFile({ path: fileName, data: base64Data, directory: FilesystemDirectory.Data }); if (isPlatform('hybrid')) { // Display the new image by rewriting the 'file://' path to HTTP // Details: https://ionicframework.com/docs/building/webview#file-protocol return { filepath: savedFile.uri, webviewPath: Capacitor.convertFileSrc(savedFile.uri), }; } else { // Use webPath to display the new image instead of base64 since it's // already loaded into memory return { filepath: fileName, webviewPath: photo.webPath
}; } }; const deletePhoto = async (photo: Photo) => { // Remove this photo from the Photos reference data array const newPhotos = photos.filter(p => p.filepath !== photo.filepath); // Update photos array cache by overwriting the existing photo array set(PHOTO_STORAGE, JSON.stringify(newPhotos)); // delete photo file from filesystem const filename = photo.filepath.substr(photo.filepath.lastIndexOf('/') + 1); await deleteFile({ path: filename, directory: FilesystemDirectory.Data }); setPhotos(newPhotos); }; return { deletePhoto, photos, takePhoto }; } export interface Photo { filepath: string; webviewPath?: string; base64?: string; }
CHANGE src/App.tsx to:
import React from 'react'; import { Redirect, Route } from 'react-router-dom'; import { IonApp, IonIcon, IonLabel, IonRouterOutlet, IonTabBar, IonTabButton, IonTabs
} from '@ionic/react'; import { IonReactRouter } from '@ionic/react-router'; import { images, home, informationCircleOutline } from 'ionicons/icons'; import Tab1 from './pages/Tab1'; import Tab2 from './pages/Tab2'; import Tab3 from './pages/Tab3'; /* Core CSS required for Ionic components to work properly */ import '@ionic/react/css/core.css'; /* Basic CSS for apps built with Ionic */ import '@ionic/react/css/normalize.css'; import '@ionic/react/css/structure.css'; import '@ionic/react/css/typography.css'; /* Optional CSS utils that can be commented out */ import '@ionic/react/css/padding.css'; import '@ionic/react/css/float-elements.css'; import '@ionic/react/css/text-alignment.css'; import '@ionic/react/css/text-transformation.css'; import '@ionic/react/css/flex-utils.css'; import '@ionic/react/css/display.css'; /* Theme variables */ import './theme/variables.css'; const App: React.FC = () => ( <IonApp> <IonReactRouter> <IonTabs> <IonRouterOutlet> <Route path="/tab1" component={Tab1} exact={true} /> <Route path="/tab2" component={Tab2} exact={true} /> <Route path="/tab3" component={Tab3} /> <Route path="/" render={() => <Redirect to="/tab1" />} exact={true} /> </IonRouterOutlet> <IonTabBar slot="bottom"> <IonTabButton tab="tab1" href="/tab1"> <IonIcon icon={home} /> <IonLabel>Home</IonLabel> </IonTabButton> <IonTabButton tab="tab2" href="/tab2"> <IonIcon icon={images} /> <IonLabel>Photos</IonLabel> </IonTabButton>
<IonTabButton tab="tab3" href="/tab3"> <IonIcon icon={informationCircleOutline} /> <IonLabel>About</IonLabel> </IonTabButton> </IonTabBar> </IonTabs> </IonReactRouter> </IonApp > ); export default App;
pages/tab1.tsx
import React from 'react'; import { IonPage, IonContent, IonIcon } from '@ionic/react'; import { camera } from 'ionicons/icons'; import './Tab1.css'; const Tab1: React.FC = () => { return ( <IonPage> <IonContent fullscreen> <div className="App"> <header className="App-header"> <IonIcon icon={camera} className="App-logo-icon" /> <p>Photo Gallery App by</p> <a className="App-link" href="https://www.edwinaquino.com" target="_blank" rel="noopener noreferrer" > Edwin Aquino </a> </header> </div> </IonContent> </IonPage> ); }; export default Tab1;
tab1.css
/* This file is used by tab1.tsx for styling*/ .App { text-align: center; } /* Display a large camera icon */ .App-logo { height: 100vmin; pointer-events: none; color: #fff; } .App-logo-icon { font-size: 200px; } .App-header { background: rgb(76, 49, 162); min-height: 100vh; display: flex; flex-direction: column; align-items: center; justify-content: center; font-size: calc(10px + 4vmin); color: white; background: linear-gradient( 180deg, rgba(76, 49, 162, 1) 0%, rgb(247, 234, 252) 100%, rgba(0, 212, 255, 1) 100% ); } .App-link { color: #1f07aa; text-decoration: none; }
Tab2.tsx
import React, { useState } from 'react'; import './Tab2.css'; import { camera, trash, close } from 'ionicons/icons'; import { IonContent, IonHeader, IonPage, IonTitle, IonToolbar, IonFab, IonFabButton, IonIcon, IonGrid, IonRow, IonCol, IonImg, IonActionSheet } from '@ionic/react';
import { usePhotoGallery, Photo } from '../hooks/usePhotoGallery'; const Tab2: React.FC = () => { const { photos, takePhoto, deletePhoto } = usePhotoGallery(); //function takePhoto(){} const [photoToDelete, setPhotoToDelete] = useState<Photo>(); return ( <IonPage> <IonHeader> <IonToolbar> <IonTitle>Photo Gallery</IonTitle> </IonToolbar> </IonHeader> <IonActionSheet isOpen={!!photoToDelete} buttons={[{ text: 'Delete', role: 'destructive', icon: trash, handler: () => { if (photoToDelete) { deletePhoto(photoToDelete); setPhotoToDelete(undefined); } } }, { text: 'Cancel', icon: close, role: 'cancel' }]} onDidDismiss={() => setPhotoToDelete(undefined)} />
<IonContent> <IonGrid> <IonRow> {photos.map((photo, index) => ( <IonCol size="6" key={index}> <IonImg onClick={() => setPhotoToDelete(photo)} src={photo.base64 ?? photo.webviewPath} /> </IonCol> ))} </IonRow> </IonGrid> <IonFab vertical="bottom" horizontal="center" slot="fixed"> <IonFabButton onClick={() => takePhoto()}> <IonIcon icon={camera}></IonIcon> </IonFabButton> </IonFab> </IonContent> </IonPage> ); }; export default Tab2;
tabs2.css [leave it blank]
tab3.tsx
import React from 'react'; import { IonImg, IonCard, IonCardHeader, IonCardSubtitle, IonCardTitle, IonCardContent, IonItem, IonIcon, IonLabel, IonContent, IonButton, IonHeader, IonPage, IonTitle, IonToolbar } from '@ionic/react'; import './Tab3.css'; import { informationCircleSharp } from 'ionicons/icons'; import cardImg from '../assets/golden-gate-park.jpg'; const Tab1: React.FC = () => { return ( <IonPage> <IonHeader> <IonToolbar> <IonTitle>About</IonTitle> </IonToolbar> </IonHeader>
<IonContent>
<IonCard>
<IonImg src={cardImg} alt="Card Image" />
{/* <img src={cardImg}alt={cardImg} style={{color: "green", padding:"10
px"}}/> */}
<IonCardHeader>
<IonCardSubtitle>Photo App</IonCardSubtitle>
<IonCardTitle>About</IonCardTitle>
</IonCardHeader>
<IonCardContent>
This is a very simple photo gallery app created using Ionic and React
. Have fun taking pictures and storing them in your device.
</IonCardContent>
</IonCard>
<IonCard>
<IonItem>
<IonIcon icon={informationCircleSharp} slot="start" />
<IonLabel>By: Edwin Aquino</IonLabel>
<IonButton fill="outline" slot="end" href="https://github.com/edwinaq
uino"
target="_blank"
rel="noopener noreferrer">Download</IonButton>
</IonItem>
<IonCardContent>
Version 1.0 - September 2, 2020
</IonCardContent>
</IonCard>
</IonContent>
</IonPage>
);
};
export default Tab1;
tab3.css [leave blank]
golden-gate-park.jpg [attache a image file]
Place this or any of your favorite image into the src/assets/ folder [Create folder if it doesn’t exist]
see how the app looks, it should look like this:
OK, now for the fun part, lets see it on your phone
Stop running ionic serve from terminal 1 in visual code and this the next command ionic build
FOR IOSionic cap add ios
FOR ANDROID:
ionic cap add android
* These two commands will generate a /android and /ios folders
Every time you perform a build (e.g. ionic build) that updates your web directory (default: build), you'll need to copy those changes into your native projects:
ionic cap copy
Note: After making updates to the native portion of the code (such as adding a new plugin), use the sync command:
ionic cap sync
FOR ANDROID: [be sure you have android studio installed!]
setup your phone for developers: on my phone this is how I did it, you can google it if it’s a different or newer phone on how to enable developers options. I am using the galaxy note 4
Settings > General > About Device > Build Number [tap 7 times] Developer options [if you have this enabled, you are ready]
Go now to Settings > General > Developer options > and enable: USB debugging [x]
Connect the phone to your PC with a usb cable
ionic cap open android
Similar to iOS, we must enable the correct permissions to use the Camera. Configure these in the AndroidManifest.xml file. Android Studio will likely open this file automatically, but in case it doesn't,locate it under android/app/src/main/AndroidManifest.xml
Scroll to the Permissions section and ensure these entries are included:
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
If you did everything correctly, you will see your phone in the devices:
Use this command if you want to do a live view, meaning as you code, you can see the changes happen.
ionic cap run ios -l –-external
ionic cap run android -l –-external
$ ionic cap run android --livereload --external
BUILD the APK FILE: Build > Build Bundles/APK(s) > Build APK(s)
In the even log, you will see a link to locate the APK:
9:14 PM Build APK(s)
APK(s) generated successfully for 1 module:
Module 'app': locate or analyze the APK.
When I clicked on it, it took me here:
F:\apachefriends\xampp\htdocs\IONIC\React\NEW-APP-START-TO-FINISH-ON-PHONE\photo-gallery\android\app\build\outputs\apk\debug
This is relative to the app in Visual Code:
android\app\build\outputs\apk\debug
open the phone’s folders with file explorer and copy and paste the apk file into a folder within the phone:
Now, close android studio and disconnect the usb.
Go to the phone’s application manager and uninstall any old version [just in case you are updating this]
Go to settings > General > Security > Unknown Sources [x] CHECKED!
Go to the main screen, find app > My Files > Device Storage > Download > App-debg.apk [open app]
You will get a message if you have not changed your security settings.
Click Install
You may get a message that says “Blocked by Play Protect” because its not signed. So its ok, go and click Install Anyway
Click Open
DONE.