Android app development with React Native
Tags: reactnative, android
Published: 2022-11-10 12:52:27

---
title: Android app development with React Native
published: true
tags: ['reactnative', 'android']
canonical_url: https://sevic.dev/react-native-android-app/
---

This post covers the main notes needed, from bootstrapping the app to publishing to the Play store.

### Prerequisites

- experience with React
- installed Android studio

### Bootstrapping the app

Run the following commands

```bash
npx react-native init <app_name>
cd <app_name>
```

### Running the app on the device via USB

Enable developer options, USB debugging, and USB file transfer on the device. Run the following commands

```bash
npx react-native start
npx react-native run-android
```

Install the app via the following command

```bash
npx react-native run-android --variant=release
```

### App name

It can be changed in `android/app/src/main/res/values/strings.xml` file

### Logo

[Icon Kitchen](https://icon.kitchen) can be used for generating images. Downloaded images should be stored in mipmap (`android/app/src/main/res/mipmap-*hdpi/ic_launcher.png`) folders.

### Splash screen

A splash screen is the first thing user sees after opening the app, and it usually shows an app logo with optional animations. More details are covered in [Splash screen with React Native post](https://sevic.dev/notes/splash-screen-react-native)

### Bottom navigation bar

`react-native-paper` provides a bottom navigation bar component, and route keys are mapped with the components. `react-native-vector-icons` is needed for the proper vector rendering, and a list of available icons can be found [here](https://callstack.github.io/react-native-paper/icons.html)

```bash
npm i react-native-paper react-native-vector-icons
```

```js
// App.js
import React, { useState } from 'react';
import { StyleSheet } from 'react-native';
import { BottomNavigation, Text } from 'react-native-paper';

const HomeRoute = () => <Text style={style.text}>Home</Text>;
const SettingsRoute = () => <Text style={style.text}>Settings</Text>;

const style = StyleSheet.create({
  text: {
    textAlign: 'center'
  }
});

const App = () => {
  const [index, setIndex] = useState(0);
  const [routes] = useState([
    {
      key: 'home',
      title: 'Home',
      icon: 'home'
    },
    {
      key: 'settings',
      title: 'Settings',
      icon: 'settings-helper'
    }
  ]);

  const renderScene = BottomNavigation.SceneMap({
    home: HomeRoute,
    settings: SettingsRoute
  });

  return (
    <BottomNavigation
      navigationState={{ index, routes }}
      onIndexChange={setIndex}
      renderScene={renderScene}
    />
  );
};

export default App;
```

file: `android/app/build.gradle`

```gradle
apply plugin: "com.android.application"

import com.android.build.OutputFile
import org.apache.tools.ant.taskdefs.condition.Os

apply from: "../../node_modules/react-native-vector-icons/fonts.gradle" // <-- ADD THIS
// ...
```

### Forms

[formik](https://github.com/jaredpalmer/formik) and [yup](https://github.com/jquense/yup) libraries can handle custom forms and complex form validations (including the case when one field validation depends on other fields' value). Values inside nested components can be set with `setFieldValue` function.

```js
const FormSchema = Yup.object().shape({
  rentOrSale: Yup.string().required(customErrorMessage),
  furnished: Yup.array()
    .of(Yup.string())
    .when('rentOrSale', (rentOrSale, schema) => {
      if (rentOrSale === 'rent') {
        return schema.min(1, customErrorMessage);
      }
      return schema;
    })
});

export const CustomForm = () => {
  const handleCustomSubmit = async (values) => {
    // ...
  };

  return (
    <Formik
      initialValues={
        {
          // ...
        }
      }
      validationSchema={FormSchema}
      onSubmit={handleCustomSubmit}
    >
      {({ errors, touched, handleSubmit, setFieldValue }) => (
        <View>
          {/* */}
          {touched.furnished && errors.furnished && (
            <Text style={style.errorMessage}>{errors.furnished}</Text>
          )}
          <Button style={style.submitButton} onPress={handleSubmit}>
            Submit
          </Button>
        </View>
      )}
    </Formik>
  );
};
```

### Lists

`FlatList` component can handle list data. It shouldn't be nested inside the `ScrollView` component. Its header and footer should be defined in ListHeaderComponent and ListFooterComponent props.

```js
// ...
return (
  <FlatList
    // ...
    renderItem={({ item }) => <ApartmentCard apartment={item} />}
    ListHeaderComponent={/* */}
    ListFooterComponent={/* */}
  />
);
// ...
```

Its child component should be wrapped as a higher-order component with `memo` to optimize rendering.

```js
import React, { memo } from 'react';

const ApartmentCard = ({ apartment }) => {
  /* */
};

export default memo(ApartmentCard);
```

#### Loading data

`FlatList` can show a refresh indicator (loader) when data is loading. `progressViewOffset` prop sets the vertical position of the loader.

```js
import { Dimensions, FlatList, RefreshControl } from 'react-native';
// ...
<FlatList
  // ...
  refreshControl={
    <RefreshControl
      colors={['#3366CC']}
      progressViewOffset={Dimensions.get('window').height / 2}
      onRefresh={() => {
        console.log('loading data...');
      }}
      refreshing={isLoading}
    />
  }
/>;
```

#### Scrolling

`FlatList` also provides a scrolling (to its items) feature when its size changes. Specify its reference and fallback function for scrolling (`onScrollToIndexFailed`).

```js
import React, { useRef } from 'react';
// ...
const listRef = useRef();
// ...
return (
  <FlatList
    // ...
    ref={listRef}
    onContentSizeChange={() => {
      // some custom logic
      listRef?.current?.scrollToIndex({ index, animated: false });
    }}
    onScrollToIndexFailed={(info) => {
      console.error('scrolling failed', info);
    }}
  />
);
// ...
```

One of the additional scrolling functions is based on the offset.

```js
import { Dimensions } from 'react-native';

// ...
listRef?.current?.scrollToOffset({
  offset: Dimensions.get('window').height + 250
});
```

Scrolling to the top can be done with offset 0.

```js
// ...
listRef?.current?.scrollToOffset({
  offset: 0,
  animated: false
});
```

### Links

`Linking.openURL(url)` method opens a specific link in an external browser. A webview can open a link inside the app, and it can also override the back button handler.

```js
// ...
import { BackHandler /* */ } from 'react-native';
import { WebView } from 'react-native-webview';

const handleClosingWebview = () => {
  // some custom logic
};

useEffect(() => {
  const backHandler = BackHandler.addEventListener(
    'hardwareBackPress',
    function() {
      handleClosingWebview();
      return true;
    }
  );

  return () => backHandler.remove();
}, []);

// ...
if (isWebviewOpen) {
  return (
    <SafeAreaView style={style.webview}>
      <TouchableOpacity onPress={handleClosingWebview}>
        <Icon
          style={style.webviewCloseButton}
          size={25}
          color={theme.colors.primary}
          name="close-circle-outline"
        />
      </TouchableOpacity>
      <WebView
        source={{ uri: webviewUrl }}
        style={style.webview}
        startInLoadingState
        renderLoading={() => (
          <View style={style.webviewLoader}>
            <ActivityIndicator color={theme.colors.primary} />
          </View>
        )}
      />
    </SafeAreaView>
  );
}
// ...
```

### SVG files

`react-native-svg` library can be used for handling SVG files.

```js
import React from 'react';
import { SvgXml } from 'react-native-svg';

export function Logo() {
  const xml = `<svg>...</svg>`;

  return <SvgXml xml={xml} />;
}
```

### State management

React provides Context to deal with state management without external libraries.

#### Context setup with app wrapper

```js
// src/context/index.js
import { createContext, useContext, useMemo, useReducer } from 'react';
import { appReducer, initialState } from './reducer';

const appContext = createContext(initialState);

export function AppWrapper({ children }) {
  const [state, dispatch] = useReducer(appReducer, initialState);

  const contextValue = useMemo(() => {
    return { state, dispatch };
  }, [state, dispatch]);

  return (
    <appContext.Provider value={contextValue}>{children}</appContext.Provider>
  );
}

export function useAppContext() {
  return useContext(appContext);
}
```

#### Reducer setup

```js
// src/context/reducer.js
import { INCREMENT_COUNTER } from './constants';

export const initialState = {
  counter: 0
};

export const appReducer = (state, action) => {
  switch (action.type) {
    case INCREMENT_COUNTER: {
      return {
        ...state,
        counter: state.counter + 1
      };
    }
    default:
      return state;
  }
};
```

```js
// src/context/constants.js
export const INCREMENT_COUNTER = 'INCREMENT_COUNTER';
```

#### Wrapped app with Context and its usage

```js
// App.js
import React, { useEffect, useState } from 'react';
import { StyleSheet } from 'react-native';
import SplashScreen from 'react-native-splash-screen';
import { BottomNavigation, Button, Text } from 'react-native-paper';
import { AppWrapper, useAppContext } from './src/context';
import { INCREMENT_COUNTER } from './src/context/constants';

const HomeRoute = () => {
  const { state } = useAppContext();

  return <Text style={style.text}>counter: {state.counter}</Text>;
};

const SettingsRoute = () => {
  const { dispatch } = useAppContext();

  const onPress = () => {
    dispatch({ type: INCREMENT_COUNTER });
  };

  return <Button onPress={onPress}>Increment counter</Button>;
};

const style = StyleSheet.create({
  text: {
    textAlign: 'center'
  }
});

const App = () => {
  const [index, setIndex] = useState(0);
  const [routes] = useState([
    {
      key: 'home',
      title: 'Home',
      icon: 'home'
    },
    {
      key: 'settings',
      title: 'Settings',
      icon: 'settings-helper'
    }
  ]);

  const renderScene = BottomNavigation.SceneMap({
    home: HomeRoute,
    settings: SettingsRoute
  });

  useEffect(() => SplashScreen.hide(), []);

  return (
    <AppWrapper>
      <BottomNavigation
        navigationState={{ index, routes }}
        onIndexChange={setIndex}
        renderScene={renderScene}
      />
    </AppWrapper>
  );
};

export default App;
```

### Custom events

React Native provides `NativeEventEmitter` for handling custom events so components can communicate with each other in that way.

```js
import { NativeEventEmitter } from 'react-native';
const eventEmitter = new NativeEventEmitter();

eventEmitter.emit('custom-event', { data: 'test' });

eventEmitter.addListener('custom-event', (event) => {
  console.log(event); // { data: 'test' }
});
```

### Local storage

[@react-native-async-storage/async-storage](https://github.com/react-native-async-storage/async-storage) can handle storage system in asynchronous way

```js
import AsyncStorage from '@react-native-async-storage/async-storage';

export async function getItem(key) {
  try {
    const value = await AsyncStorage.getItem(key);
    if (value) {
      return JSON.parse(value);
    }

    return value;
  } catch (error) {
    console.error(`Failed getting the item ${key}`, error);
    return null;
  }
}

export async function setItem(key, value) {
  try {
    await AsyncStorage.setItem(key, JSON.stringify(value));
  } catch (error) {
    console.error(`Failed setting the item ${key}`, error);
  }
}
```

### Error tracing

[Sentry](https://sentry.io/) can be used for it

#### Prerequisites

- React Native project created

#### Setup

Run the following commands

```bash
npm i @sentry/react-native
npx @sentry/wizard -i reactNative -p android
```

### Analytics

It is helpful to have more insights about app usage, like custom events, screen views, numbers of installations/uninstallations, etc. React Native Firebase provides analytics as one of the services.

#### Prerequisites

- created Firebase project

#### Setup

Create an Android app within created Firebase project. The package name should be the same as the one specified in the Android manifest (`android/app/src/main/AndroidManifest.xml`). Download `google-service.json` file and place it inside `android/app` folder.

Extend the following files

file: `android/app/build.gradle`

```gradle
apply plugin: "com.android.application"
apply plugin: "com.google.gms.google-services" <-- ADD THIS
```

file: `android/build.gradle`

```gradle
buildscript {
  // ...
  dependencies {
    // ...
    classpath("com.google.gms:google-services:4.3.14") <-- ADD THIS
  }
}
// ...
```

Run the following commands

```bash
npm i @react-native-firebase/app @react-native-firebase/analytics
```

#### Usage

The following change can log screen views.

```js
// App.js
import analytics from '@react-native-firebase/analytics';
// ...
const App = () => {
  // ...
  const onIndexChange = (i) => {
    if (index === i) {
      return;
    }

    setIndex(i);
    analytics()
      .logScreenView({
        screen_class: routes[i].key,
        screen_name: routes[i].key
      })
      .catch(() => {});
  };
  // ...
  return (
    <AppWrapper>
      <BottomNavigation
        navigationState={{ index, routes }}
        onIndexChange={onIndexChange}
        renderScene={renderScene}
      />
    </AppWrapper>
  );
};
// ...
```

The following code can log custom events.

```js
// src/utils/analytics.js
import analytics from '@react-native-firebase/analytics';

export const trackCustomEvent = async (eventName, params) => {
  analytics()
    .logEvent(eventName, params)
    .catch(() => {});
};
```

### Publishing to the Play store

#### Prerequisites

- Verified developer account on Google Play Console
- Paid one-time fee (25$)

#### Internal testing

Internal testing on Google Play Console is used for testing app versions before releasing them to the end users. Read more about it on [Internal testing React Native apps post](https://sevic.dev/notes/internal-testing-react-native-android)

#### Screenshots

[Screenshots.pro](https://screenshots.pro/) can be used for creation of screenshots

### Boilerplate

Here is the [link](https://sevic.dev/react-native-starter?ref=devto) to the boilerplate I use for the development.