Skip to main content

Choose a photo

Let's see how we could store a plant's photo along with it's details.

Pick the image

For this we'll use image picker that lets you choose images both from the image Gallery as well a take a picture.

Install the image picker library

npx expo install expo-image-picker

Launch image picker

Update: app/new.tsx
@@ -1,4 +1,10 @@
-import { Text, StyleSheet, TextInput, Alert, View } from "react-native";
+import {
+ Text,
+ StyleSheet,
+ TextInput,
+ Alert,
+ TouchableOpacity,
+ Platform
+} from "react-native";
import { theme } from "@/theme";
import { PlantlyButton } from "@/components/PlantlyButton";
import { useState } from "react";
@@ -6,12 +12,14 @@ import { PlantlyImage } from "@/components/PlantlyImage";
import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view";
import { usePlantStore } from "@/store/plantsStore";
import { useRouter } from "expo-router";
+import * as ImagePicker from "expo-image-picker";

export default function NewScreen() {
const router = useRouter();
const addPlant = usePlantStore((state) => state.addPlant);
const [name, setName] = useState<string>();
const [days, setDays] = useState<string>();
+ const [imageUri, setImageUri] = useState<string>();

const handleSubmit = () => {
if (!name) {
@@ -36,15 +44,32 @@ export default function NewScreen() {
router.navigate("/");
};

+ const handleChooseImage = async () => {
+ if (Platform.OS === "web") {
+ return;
+ }
+
+ const result = await ImagePicker.launchImageLibraryAsync({
+ mediaTypes: ImagePicker.MediaTypeOptions.Images,
+ allowsEditing: true,
+ aspect: [1, 1],
+ quality: 1,
+ });
+
+ if (!result.canceled) {
+ setImageUri(result.assets[0].uri);
+ }
+ };
+
return (
<KeyboardAwareScrollView
style={styles.container}
contentContainerStyle={styles.contentContainer}
keyboardShouldPersistTaps="handled"
>
- <View style={styles.centered}>
- <PlantlyImage />
- </View>
+ <TouchableOpacity
+ style={styles.centered}
+ onPress={handleChooseImage}
+ activeOpacity={0.8}
+ >
+ <PlantlyImage imageUri={imageUri} />
+ </TouchableOpacity>
<Text style={styles.label}>Name</Text>
<TextInput
value={name}
@@ -90,5 +115,6 @@ const styles = StyleSheet.create({
},
centered: {
alignItems: "center",
+ marginBottom: 24,
},
});

Show custom image in PlantlyImage

Update: components/PlantlyImage.tsx
@@ -1,14 +1,23 @@
import { Image, useWindowDimensions } from "react-native";

-export function PlantlyImage({ size }: { size?: number }) {
+type Props = {
+ size?: number;
+ imageUri?: string;
+};
+
+export function PlantlyImage({ size, imageUri }: Props) {
const { width } = useWindowDimensions();

const imageSize = size || Math.min(width / 1.5, 400);

return (
<Image
- source={require("@/assets/plantly.png")}
- style={{ width: imageSize, height: imageSize }}
+ source={imageUri ? { uri: imageUri } : require("@/assets/plantly.png")}
+ style={{
+ width: imageSize,
+ height: imageSize,
+ borderRadius: 6,
+ }}
/>
);
}

Store the image

Now that we've chosen an image, we need to also store it when we save the plant. It's not sufficient to simply store the uri we get from the image picker, because that storage is not guaranteed to persist. We'll need to copy the image to the document storage for the app so it will exist as long as the app is installed.

For reading and writing files, we can use expo file system.

Install expo file system

npx expo install expo-file-system

Save the image when a new plant is added

First, let's pass the image uri to the addPlant function:

Update: app/new.tsx
@@ -40,7 +40,7 @@ export default function NewScreen() {
);
}

- addPlant(name, Number(days));
+ addPlant(name, Number(days), imageUri);
router.navigate("/");
};

Now update the plants store to save the plant image along with the rest of the plant data:

Update: store/plantsStore.ts
@@ -1,18 +1,24 @@
import AsyncStorage from "@react-native-async-storage/async-storage";
import { create } from "zustand";
import { persist, createJSONStorage } from "zustand/middleware";
+import * as FileSystem from "expo-file-system";

export type PlantType = {
id: string;
name: string;
wateringFrequencyDays: number;
lastWateredAtTimestamp?: number;
+ imageUri?: string;
};

type PlantsState = {
nextId: number;
plants: PlantType[];
- addPlant: (name: string, wateringFrequencyDays: number) => void;
+ addPlant: (
+ name: string,
+ wateringFrequencyDays: number,
+ imageUri?: string,
+ ) => Promise<void>;
removePlant: (plantId: string) => void;
waterPlant: (plantId: string) => void;
};
@@ -22,7 +28,22 @@ export const usePlantStore = create(
(set) => ({
plants: [],
nextId: 1,
- addPlant: (name: string, wateringFrequencyDays: number) => {
+ addPlant: async (
+ name: string,
+ wateringFrequencyDays: number,
+ imageUri?: string,
+ ) => {
+ const savedImageUri =
+ FileSystem.documentDirectory +
+ `${new Date().getTime()}-${imageUri?.split("/").slice(-1)[0]}`;
+
+ if (imageUri) {
+ await FileSystem.copyAsync({
+ from: imageUri,
+ to: savedImageUri,
+ });
+ }
+
return set((state) => {
return {
...state,
@@ -32,6 +53,7 @@ export const usePlantStore = create(
id: String(state.nextId),
name,
wateringFrequencyDays,
+ imageUri: imageUri ? savedImageUri : undefined,
},
...state.plants,
],

Finally, update the PlantCard to show the custom plant image if chosen:

Update: components/PlantCard.tsx
@@ -6,7 +6,7 @@ import { PlantlyImage } from "./PlantlyImage";
export function PlantCard({ plant }: { plant: PlantType }) {
return (
<View style={styles.plantCard}>
- <PlantlyImage size={100} />
+ <PlantlyImage size={100} imageUri={plant.imageUri} />
<View style={styles.details}>
<Text numberOfLines={1} style={styles.plantName}>
{plant.name}
AndroidiOS