Back to blog

Clone Experiment #3: Accessible Taxi Booking app for web and mobile

Amar Somani· Software Engineer·February 18, 2022·8 min read
Clone Experiment #3: Accessible Taxi Booking app for web and mobile

Introduction

This project demonstrates a cross-platform taxi services application built with NativeBase. The goal was to showcase how a unified codebase utilizing NativeBase components can deploy across both web and native platforms. The application draws inspiration from Uber's design. You can explore the web version and mobile version.

Motivation

Why develop native apps when responsive web apps exist?

Native applications deliver superior user experiences and consistent cross-platform behavior that web responsiveness alone cannot achieve.

The Expo framework addresses this gap by enabling development of iOS, Android, and Web applications from a single JavaScript/TypeScript codebase. However, implementing responsive designs in Expo and React Native presents complexity. React Native lacks CSS media query equivalents, forcing developers to create separate style objects and files for responsiveness across different screen sizes.

This is where NativeBase provides significant value. It includes responsive styling capabilities built-in. Responsiveness can be achieved using either Object or Array syntax within component props.

Challenges Faced

Maps

The most significant hurdle was the absence of a unified map API functioning across both native and web platforms. Different APIs needed to be called based on the device type:

import { Platform } from "react-native";
 
const ResponsiveMap = Platform.select({
  native: () => (
    <NativeMap />
  ),
  default: () => <WebMap />,
});

NativeMap.tsx

import React from "react";
import MapView, { PROVIDER_GOOGLE } from "react-native-maps";
 
function NativeMap(props: any) {
  return (
    <MapView
      style={{
        flex: 1,
        minHeight: 120,
      }}
      provider={PROVIDER_GOOGLE}
      region={{
        latitudeDelta: 0.015,
        longitudeDelta: 0.0121,
        latitude: 12.9698,
        longitude: 77.75,
      }}
    >
      <MapCircle
        center={{ latitude: 12.9698, longitude: 77.75 }}
        radius={100}
      ></MapCircle>
    </MapView>
  );
}

WebMap.tsx

import React, { useEffect, useState, useRef } from "react";
import { VStack, View } from "native-base";
import Constants from "expo-constants";
 
function WebMap() {
  const [mapLoaded, setMapLoaded] = useState(false);
  const mapContainerRef = useRef<HTMLElement>(null);    // C1
 
  useEffect(() => {
    // Check if map script is already present in DOM
    if (!document.body.dataset.mapLoaded) {
      const mapScript = document.createElement("script");
      mapScript.src = MAP_SCRIPT_WITH_API_KEY;
 
      mapScript.onload = () => {
        // set dataset property on body to indicate map script has been loaded.
        document.body.dataset.mapLoaded = "true";
        setMapLoaded(true);
      };
      document.head.appendChild(mapScript);
    }
  }, []);
 
  useEffect(() => {
    if (mapLoaded) {
      //@ts-ignore
      const map = new window.google.maps.Map(mapContainerRef.current, {
        zoom: 15,
        mapTypeId: "terrain",
        center: { lat: 12.9698, lng: 77.75 },
      });
    }
  }, [mapLoaded]);
 
  return (
    <>
      {mapLoaded ? (
        <VStack
          safeAreaBottom
          flex="1"
          space={{ base: "4", md: "0" }}
          rounded={{ md: "lg" }}
          borderWidth={{ base: "0", md: "1" }}
          _light={{
            bg: { md: "white", base: "primary.50" },
            borderColor: "coolGray.200",
          }}
          _dark={{
            bg: { base: "coolGray.700", md: "coolGray.700" },
            borderColor: "coolGray.700",
          }}
        >
          <View flex="1" ref={mapContainerRef} />
        </VStack>
      ) : (
        "Loading ..."
      )}
    </>
  );
}

Note the HTMLElement initialization at C1, required for web-based map rendering.

Different sidebar presentations were needed for varying screen sizes:

const isLargeScreen = useBreakpointValue({
    base: false,
    lg: true,
  });
 
return(
...
 
{isLargeScreen ? (
    <Box w="300" bg="white" display={isSidebar ? "flex" : "none"}>
      <Sidebar navigation={navigation} />
    </Box>
) : (
    <Slide in={isSlideOpen} placement="left" w={width} h="100">
    <HStack w="100%" h="100%">
      <Box w={{ base: "80%", lg: "25%" }} bg="white">
        <Sidebar navigation={navigation} />
      </Box>
      <Pressable
        w={{ base: "20%", lg: "75%" }}
        onPress={() => setSlideOpen(false)}
        opacity="0.5"
        bg="black"
      ></Pressable>
    </HStack>
  </Slide>
)}
...
);

Conclusion

This experiment successfully demonstrated NativeBase's capability to transform code into either div elements or React Native components based on the target platform. The component's prop support enabled minimal code while maintaining design consistency across web and mobile interfaces. GeekyAnts continues enhancing NativeBase by converting experiment-derived challenges into valuable features.