Material Design React Native App
If you're building a cross-platform mobile app, it's a good idea to base your app's UI/UX on Material Design, Google's own design language, which it uses in all its mobile apps. Why? One of the best reasons is that many of the most popular mobile apps heavily use Material Design concepts: Whatsapp, Uber, Lyft, Google Maps, SpotAngels, etc. This means your users are already familiar with the look and feel of Material Design, and they will quickly and easily understand how to use your app if you adhere to the design language of their favorite, most commonly used apps.
The heavy hitter of Material Design component libraries on React Native is react-native-paper, and this guide will focus on using react-native-paper to set up a starter app with the some of the most prominent and recognizable Material Design features: Hamburger Menu, Drawer Navigation, FAB (Floating Action Button), and Contextual Action Bar.
Demo
This is what the starter app I'm going to build will eventually look like. As you read through the guide, you can reference the full code of this demo, which resides in the following GitHub repo: material-ui-in-react-native.
Setup
First, I'll initialize my React Native app using Expo. You don't have to use Expo, it just helps me get started so I can focus on the UI in this example.
If you don't have expo-cli
installed, then first run:
npm install -g expo-cli <
Now run the following:
expo init material-ui-in-react-native -t expo-template-blank-typescript cd material-ui-in-react-native yarn add react-native-paper
You can also follow these additional installation instructions to enable tree shaking, reduce bundle size, etc. with react-native-paper.
I'm also adding react-navigation to this project. I recommend you use it as well. It's the most popular navigation library for React Native, and there's more support for running it alongside react-native-paper compared to other navigation libraries. Follow the installation instructions for react-navigation since they are slightly different depending on whether you use Expo or plain React Native.
Initial Screens
Create the following two files in your app's main directory (if you want the styles used, remember everything for this example is available in this GitHub repo):
MyFriends.tsx
import React from 'react'; import {View} from 'react-native'; import {Title} from 'react-native-paper'; import base from './styles/base'; interface IMyFriendsProps {} const MyFriends: React.FunctionComponent<IMyFriendsProps> = (props) => { return ( <View style={base.centered}> <Title>MyFriends</Title> </View> ); }; export default MyFriends;
Profile.tsx
import React from 'react'; import {View} from 'react-native'; import {Title} from 'react-native-paper'; import base from './styles/base'; interface IProfileProps {} const Profile: React.FunctionComponent<IProfileProps> = (props) => { return ( <View style={base.centered}> <Title>Profile</Title> </View> ); }; export default Profile;
Over the course of this guide, I'll link these screens to each other using a Navigation Drawer (or Hamburger Menu) and add Material UI components to each of them.
Hamburger Menu / Drawer Navigation
Material Design promotes the usage of a Navigation Drawer, so I'll use this type of UI to make the My Friends and Profile screens navigable to and from each other.
First, I'll add React Navigation's drawer
library:
yarn add @react-navigation/native @react-navigation/drawer
Now I'll add the following into my App.tsx
to enable the Drawer Navigation. It should look like the following:
App.tsx
import React from 'react'; import {createDrawerNavigator} from '@react-navigation/drawer'; import {NavigationContainer} from '@react-navigation/native'; import {StatusBar} from 'expo-status-bar'; import {SafeAreaProvider} from 'react-native-safe-area-context'; import MyFriends from './MyFriends'; import Profile from './Profile'; export default function App() { const Drawer = createDrawerNavigator(); return ( <SafeAreaProvider> <NavigationContainer> <Drawer.Navigator> <Drawer.Screen name='My Friends' component={MyFriends} /> <Drawer.Screen name='Profile' component={Profile} /> </Drawer.Navigator> </NavigationContainer> <StatusBar style='auto' /> </SafeAreaProvider> ); }
This drawer also needs a button to open it. That button should look like the classic hamburger icon (≡) and it should open the navigation drawer when pressed. Here's what that button might look like:
components/MenuIcon.tsx
import React from 'react'; import {IconButton} from 'react-native-paper'; import {DrawerActions, useNavigation} from '@react-navigation/native'; import {useCallback} from 'react'; export default function MenuIcon() { const navigation = useNavigation(); const openDrawer = useCallback(() => { navigation.dispatch(DrawerActions.openDrawer()); }, []); return <IconButton icon='menu' size={24} onPress={openDrawer} />; }
A few things to notice here:
React-navigation's useNavigation
hook is how we are going to execute most navigation actions, from changing screens to opening drawers.
The <IconButton>
component is from react-native-paper. It supports all the Material Design icons by name and optionally supports any React Node that you want to pass in there, which allows one to add in any desired icon from any third party library.
Now I'll add my <MenuIcon>
to my Navigation Drawer by replacing this from App.tsx
:
<Drawer.Navigator> ... </Drawer.Navigator>
With the following:
import MenuIcon from './components/MenuIcon.tsx'; ... <Drawer.Navigator screenOptions={{headerShown: true, headerLeft: () => <MenuIcon />}} > ... </Drawer.Navigator>
Lastly, I can customize my Navigation Drawer using the drawerContent
prop of the same <Drawer.Navigator>
component I just altered.
I'll show an example which adds a header image to the top of the drawer. Feel free to customize with whatever you want to put in the drawer:
components/MenuContent.tsx
import React from 'react'; import { DrawerContentComponentProps, DrawerContentScrollView, DrawerItemList, } from '@react-navigation/drawer'; import {Image} from 'react-native'; const MenuContent: React.FunctionComponent<DrawerContentComponentProps> = ( props ) => { return ( <DrawerContentScrollView {...props}> <Image resizeMode='cover' style={{width: '100%', height: 140}} source={require('../assets/drawerHeaderImage.jpg')} /> <DrawerItemList {...props} /> </DrawerContentScrollView> ); }; export default MenuContent;
Now I'll pass <MenuContent>
into <Drawer.Navigator>
. To do this, I'll make the following change in App.tsx
from this:
import MenuIcon from './components/MenuIcon.tsx'; ... <Drawer.Navigator screenOptions={{headerShown: true, headerLeft: () => <MenuIcon />}} > ... </Drawer.Navigator>
to this:
import MenuIcon from './components/MenuIcon.tsx'; import MenuContent from './components/MenuContent.tsx'; ... <Drawer.Navigator screenOptions={{headerShown: true, headerLeft: () => <MenuIcon />}} drawerContent={(props) => <MenuContent {...props} />} > ... </Drawer.Navigator>
And now, I have fully functioning Drawer Navigation with a custom image header. Here's the result:
Next, I'll flesh out the main screens with more Material Design concepts.
Floating Action Button (FAB)
One of the hallmarks of Material Design is the Floating Action Button (or FAB). The <FAB>
and <FAB.Group>
components provide a useful implementation of the Floating Action Button according to Material Design principles. With minimal setup, I'll add this to the My Friends screen right now.
First, I'll need to add the <Provider>
component from react-native-paper and wrap that component around the <NavigationContainer
> in App.tsx
as follows:
App.tsx
import {Provider} from 'react-native-paper'; ... <Provider> <NavigationContainer> ... </NavigationContainer> </Provider>
Now I'll add my Floating Action Button to the My Friends screen. To do so, I'll need the following:
- The
<Portal>
and<FAB.Group>
components from react-native-paper - A state variable
fabIsOpen
to keep track of whether the FAB is open or closed - Some information about whether or not this screen is currently visible to the user (
isScreenFocused
). I needisScreenFocused
because without it, I might end up with the FAB being visible on other screens than the My Friends screen
Here's what the My Friends screen looks like with all that added in:
MyFriends.tsx
import {useIsFocused} from '@react-navigation/native'; import React, {useState} from 'react'; import {View} from 'react-native'; import {FAB, Portal, Title} from 'react-native-paper'; import base from './styles/base'; interface IMyFriendsProps {} const MyFriends: React.FunctionComponent<IMyFriendsProps> = (props) => { const isScreenFocused = useIsFocused(); const [fabIsOpen, setFabIsOpen] = useState(false); return ( <View style={base.centered}> <Title>MyFriends</Title> <Portal> <FAB.Group visible={isScreenFocused} open={fabIsOpen} onStateChange={({open}) => setFabIsOpen(open)} icon={fabIsOpen ? 'close' : 'account-multiple'} actions={[ { icon: 'plus', label: 'Add new friend', onPress: () => {}, }, { icon: 'file-export', label: 'Export friend list', onPress: () => {}, }, ]} /> </Portal> </View> ); }; export default MyFriends;
Now the My Friends screen behaves like the following:
Next, I'll add a Contextual Action Bar, which can be activated whenever an item in one of the screens is long pressed.
Contextual Action Bar
Apps like Gmail and Google Photos make use of a Material Design concept called the Contextual Action Bar. I'll implement a version of this quickly in the current app.
First, I'll build the ContextualActionBar
component itself using the Appbar component from react-native-paper. It should look something like this, to start with:
./components/ContextualActionBar.tsx
import React from 'react'; import {Appbar} from 'react-native-paper'; interface IContextualActionBarProps {} const ContextualActionBar: React.FunctionComponent<IContextualActionBarProps> = ( props ) => { return ( <Appbar.Header {...props} style={{width: '100%'}}> <Appbar.Action icon='close' onPress={() => {}} /> <Appbar.Content title='' /> <Appbar.Action icon='delete' onPress={() => {}} /> <Appbar.Action icon='content-copy' onPress={() => {}} /> <Appbar.Action icon='magnify' onPress={() => {}} /> <Appbar.Action icon='dots-vertical' onPress={() => {}} /> </Appbar.Header> ); }; export default ContextualActionBar;
Now I want this component to render on top of the given screen's header whenever an item is long pressed. Back in the My Friends screen, I've added some items for this purpose. On that screen, here's how I'll render the Contextual Action Bar over the screen's header:
MyFriends.tsx
import {useNavigation} from '@react-navigation/native'; import ContextualActionBar from './components/ContextualActionBar'; ... const [cabIsOpen, setCabIsOpen] = useState(false); const navigation = useNavigation(); const openHeader = useCallback(() => { setCabIsOpen(!cabIsOpen); }, [cabIsOpen]); useEffect(() => { if (cabIsOpen) { navigation.setOptions({ // have to use props: any since that's the type signature // from react-navigation... header: (props: any) => (<ContextualActionBar {...props} />), }); } else { navigation.setOptions({header: undefined}); } }, [cabIsOpen]); ... return ( ... <List.Item title='Friend #1' description='Mar 18 | 3:31 PM' style={{width: '100%'}} onPress={() => {}} onLongPress={openHeader} /> ... );
Above, I'm toggling a state boolean value (cabIsOpen
) whenever a given item is long pressed. Based on that value, I either switch the React Navigation header to render the <ContextualActionBar>
or switch back to render the default React Navigation header.
Now I should have a Contextual Action Bar appear when I long press the "Friend #1" item. However, the title is still empty and I cannot do anything in any of the actions because the <ContextualActionBar>
is unaware of any of the state of either the "Friend #1" item or the larger My Friends screen as a whole.
Thus, the next step is to add a title into the <ContextualActionBar>
and pass in a function that can close the bar and be triggered by one of the buttons in the bar.
To do this, I have to add another state variable to the My Friends screen:
const [selectedItemName, setSelectedItemName] = useState('');
I also need to create a function which will close the header and reset the above state variable:
const closeHeader = useCallback(() => { setCabIsOpen(false); setSelectedItemName(''); }, []);
Then I need to pass both selectedItemName
and closeHeader
as props to <ContextualActionBar>
:
useEffect(() => { if (cabIsOpen) { navigation.setOptions({ header: (props: any) => ( <ContextualActionBar {...props} title={selectedItemName} close={closeHeader} /> ), }); } else { navigation.setOptions({header: undefined}); } }, [cabIsOpen, selectedItemName]);
Lastly, I need to set selectedItemName
to the title of the item that's been long pressed:
... const openHeader = useCallback((str: string) => { setSelectedItemName(str); setCabIsOpen(!cabIsOpen); }, [cabIsOpen]); ... return ( ... <List.Item title='Friend #1' ... onLongPress={() => openHeader('Friend #1')} /> );
And now I can use the title
and close
props in <ContextualActionBar>
as follows:
./components/ContextualActionBar.tsx
interface IContextualActionBarProps { title: string; close: () => void; } ... return ( ... <Appbar.Action icon='close' onPress={props.close} /> <Appbar.Content title={props.title} /> ... );
Now, I have a functional, Material Design-inspired Contextual Action Bar, utilizing react-native-paper and react-navigation, which looks like the following:

Theming
The last thing I want to do is theme my app so I can change the primary color, secondary color, text colors, etc.
Theming is a little tricky because both react-navigation and react-native-paper have their own ThemeProvider
components, and they can easily conflict with each other. Fortunately, there's a great guide available on how to theme an app which uses both react-native-paper and react-navigation. If you follow this, you should be all set to go.
I'll add in a little extra help for those who use Typescript and would run into esoteric errors trying to follow the above guide.
First, I'll create a theme file which looks like the following. A few things to note are:
- The return type of
combineThemes
encompasses bothReactNavigationTheme
andReactNativePaper.Theme
- I changed the
primary
andaccent
colors, which will affect the CAB and FAB respectively - I added a new color to the theme called
animationColor
. If you don't want to add a new color, you don't need to declare the global namespace
theme.ts
import { DarkTheme as NavigationDarkTheme, DefaultTheme as NavigationDefaultTheme, Theme, } from '@react-navigation/native'; import {ColorSchemeName} from 'react-native'; import { DarkTheme as PaperDarkTheme, DefaultTheme as PaperDefaultTheme, } from 'react-native-paper'; declare global { namespace ReactNativePaper { interface ThemeColors { animationColor: string; } interface Theme { statusBar: 'light' | 'dark' | 'auto' | 'inverted' | undefined; } } } interface ReactNavigationTheme extends Theme { statusBar: 'light' | 'dark' | 'auto' | 'inverted' | undefined; } export function combineThemes( themeType: ColorSchemeName ): ReactNativePaper.Theme | ReactNavigationTheme { const CombinedDefaultTheme: ReactNativePaper.Theme = { ...NavigationDefaultTheme, ...PaperDefaultTheme, statusBar: 'dark', colors: { ...NavigationDefaultTheme.colors, ...PaperDefaultTheme.colors, animationColor: '#2922ff', primary: '#079c20', accent: '#2922ff', }, }; const CombinedDarkTheme: ReactNativePaper.Theme = { ...NavigationDarkTheme, ...PaperDarkTheme, mode: 'adaptive', statusBar: 'light', colors: { ...NavigationDarkTheme.colors, ...PaperDarkTheme.colors, animationColor: '#6262ff', primary: '#079c20', accent: '#2922ff', }, }; return themeType === 'dark' ? CombinedDarkTheme : CombinedDefaultTheme; }
Then, back in App.tsx
I'll add my theme to both the react-native-paper Provider
component and the NavigationContainer
component from react-navigation as follows:
App.tsx
import {useColorScheme} from 'react-native'; import {NavigationContainer, Theme} from '@react-navigation/native'; import {combineThemes} from './theme'; ... const colorScheme = useColorScheme() as 'light' | 'dark'; const theme = combineThemes(colorScheme); ... <Provider theme={theme as ReactNativePaper.Theme}> <NavigationContainer theme={theme as Theme}> </NavigationContainer> </Provider>
I am using Expo, so I additionally need to add the following in app.json
to enable dark mode. You may not need to, however.
"userInterfaceStyle": "automatic",
And now, a custom themed, dark mode enabled, Material Design-inspired app! It looks great!
Conclusion
If you followed along to the end with me here, then you should have your own cross-platform app with Material Design elements from the react-native-paper library like Drawer Navigation (with custom designs in the drawer menu), Floating Action Buttons, and Contextual Action Bars. You should also have theming enabled which plays nicely with both the react-native-paper and react-navigation libraries. This setup should enable you to quickly and stylishly build out your next mobile app with ease.
LogRocket: Instantly recreate issues in your React Native apps.
LogRocket is a React Native monitoring solution that helps you reproduce issues instantly, prioritize bugs, and understand performance in your React Native apps.
LogRocket also helps you increase conversion rates and product usage by showing you exactly how users are interacting with your app. LogRocket's product analytics features surface the reasons why users don't complete a particular flow or don't adopt a new feature.
Start proactively monitoring your React Native apps — try LogRocket for free.
Material Design React Native App
Source: https://blog.logrocket.com/using-material-ui-in-react-native/
Posted by: shanerattle1974.blogspot.com
0 Response to "Material Design React Native App"
Post a Comment