Giter Site home page Giter Site logo

pchmn / expo-material3-theme Goto Github PK

View Code? Open in Web Editor NEW
53.0 1.0 1.0 64.79 MB

Manage Material 3 theme in your React Native App

License: MIT License

JavaScript 8.31% Kotlin 29.70% TypeScript 43.43% Ruby 9.41% Objective-C 0.90% Objective-C++ 7.32% Swift 0.31% Shell 0.35% C 0.28%
android ios react-native expo

expo-material3-theme's Introduction

image

license npm latest package platform - android platform - ios

Expo Material 3 Theme

This expo module allows you retrieve the Material 3 dynamic theme from Android 12+ devices, so that you can use it in your expo (or bare react-native) app.

For devices not compatible (iOS or older Android versions) a fallback theme is returned.


✨ Features


example-android


Installation

Installation in managed Expo projects

This library works with Expo Go, but you won't be able to retrieve the system theme (you'll get a fallback theme) because it requires custom native code and Expo Go doesn't support it

npx expo install @pchmn/expo-material3-theme

If you use development build, you'll have to rebuild development client (only android) after adding the library because it contains native code (https://docs.expo.dev/develop/development-builds/use-development-builds/#rebuild-a-development-build) :

npx expo prebuild --platform android
npx expo run:android

Installation in bare React Native projects

For bare React Native projects, you must ensure that you have installed and configured the expo package before continuing.

npx expo install @pchmn/expo-material3-theme
npx pod-install

Usage

Retrieve system theme

A basic usage would be to retrieve the Material 3 theme from user device (or a fallback theme if not supported) by using useMaterial3Theme hook:

import { useMaterial3Theme } from '@pchmn/expo-material3-theme';
import { useColorScheme, View, Button } from 'react-native';

function App() {
  const colorScheme = useColorScheme();
  // If the device is not compatible, it will return a theme based on the fallback source color (optional, default to #6750A4)
  const { theme } = useMaterial3Theme({ fallbackSourceColor: '#3E8260' });

  return (
    <View style={{ backgroundColor: theme[colorScheme].background }}>
      <Button color={theme[colorScheme].primary}>Themed button</Button>
    </View>
  );
}

Use a custom theme

If you want to use a theme based on a specific color instead of the system theme, just pass the sourceColor param to useMaterial3Theme hook:

import { useMaterial3Theme } from '@pchmn/expo-material3-theme';
import { useColorScheme, View, Button } from 'react-native';

function App() {
  const colorScheme = useColorScheme();
  // Theme returned will be based on #3E8260 color
  const { theme } = useMaterial3Theme({ sourceColor: '#3E8260' });

  return (
    <View style={{ backgroundColor: theme[colorScheme].background }}>
      <Button color={theme[colorScheme].primary}>Themed button</Button>
    </View>
  );
}

Change theme

You may also want to update the theme by generating a new one, or go back to the default theme (to let users personalize your app for example). You can do it with useMaterial3Theme hook:

import { useMaterial3Theme } from '@pchmn/expo-material3-theme';
import { useColorScheme, View, Button } from 'react-native';

function App() {
  const colorScheme = useColorScheme();
  const { theme, updateTheme, resetTheme } = useMaterial3Theme();

  return (
    <View style={{ backgroundColor: theme[colorScheme].background }}>
      {/* Update theme by generating a new one based on #3E8260 color */}
      <Button onPress={() => updateTheme('#3E8260')}>Update theme</Button>
      {/* Reset theme to default (system or fallback) */}
      <Button onPress={() => resetTheme()}>Reset theme</Button>
    </View>
  );
}

ℹ️ updateTheme() and resetTheme() will change the theme returned by useMaterial3Theme(), it will not change theme at system level

Usage with react-native-paper

Basic example

@pchmn/expo-material3-theme provides a theme compatible with react-native-paper, so you can combine both libraries easily:

import { useMaterial3Theme } from '@pchmn/expo-material3-theme';
import { useMemo } from 'react';
import { useColorScheme } from 'react-native';
import { Button, MD3DarkTheme, MD3LightTheme, Provider as PaperProvider } from 'react-native-paper';

function App() {
  const colorScheme = useColorScheme();
  const { theme } = useMaterial3Theme();

  const paperTheme = useMemo(
    () =>
      colorScheme === 'dark' ? { ...MD3DarkTheme, colors: theme.dark } : { ...MD3LightTheme, colors: theme.light },
    [colorScheme, theme]
  );

  return (
    <PaperProvider theme={paperTheme}>
      <Button>Themed react native paper button</Button>
    </PaperProvider>
  );
}

Advanced usage

Override react-native-paper theme (Typescript)

Some colors present in Material3Theme from this library are not present in MD3Theme of react-native-paper. You can create a typed useAppTheme() hook and use it instead of useTheme() hook to fix this :

import { Material3Scheme } from '@pchmn/expo-material3-theme';
import { MD3Theme, useTheme } from 'react-native-paper';

export const useAppTheme = useTheme<MD3Theme & { colors: Material3Scheme }>;

// Now use useAppTheme() instead of useTheme()
Create a Material3ThemeProvider that includes PaperProvider
// Material3ThemeProvider.tsx
import { Material3Scheme, Material3Theme, useMaterial3Theme } from '@pchmn/expo-material3-theme';
import { createContext, useContext } from 'react';
import { useColorScheme } from 'react-native';
import {
  MD3DarkTheme,
  MD3LightTheme,
  MD3Theme,
  Provider as PaperProvider,
  ProviderProps,
  useTheme,
} from 'react-native-paper';

type Material3ThemeProviderProps = {
  theme: Material3Theme;
  updateTheme: (sourceColor: string) => void;
  resetTheme: () => void;
};

const Material3ThemeProviderContext = createContext<Material3ThemeProviderProps>({} as Material3ThemeProviderProps);

export function Material3ThemeProvider({
  children,
  sourceColor,
  fallbackSourceColor,
  ...otherProps
}: ProviderProps & { sourceColor?: string; fallbackSourceColor?: string }) {
  const colorScheme = useColorScheme();

  const { theme, updateTheme, resetTheme } = useMaterial3Theme({
    sourceColor,
    fallbackSourceColor,
  });

  const paperTheme =
    colorScheme === 'dark' ? { ...MD3DarkTheme, colors: theme.dark } : { ...MD3LightTheme, colors: theme.light };

  return (
    <Material3ThemeProviderContext.Provider value={{ theme, updateTheme, resetTheme }}>
      <PaperProvider theme={paperTheme} {...otherProps}>
        {children}
      </PaperProvider>
    </Material3ThemeProviderContext.Provider>
  );
}

export function useMaterial3ThemeContext() {
  const ctx = useContext(Material3ThemeProviderContext);
  if (!ctx) {
    throw new Error('useMaterial3ThemeContext must be used inside Material3ThemeProvider');
  }
  return ctx;
}

export const useAppTheme = useTheme<MD3Theme & { colors: Material3Scheme }>;


// App.tsx
import { Material3ThemeProvider, useAppTheme, useMaterial3ThemeContext } from '../Material3ThemeProvider';
import { View, Button } from 'react-native';

function App() {
  return (
    <Material3ThemeProvider>
      <AppContent />
    </Material3ThemeProvider>
  )
}

function AppContent() {
  const { updateTheme, resetTheme } = useMaterial3ThemeContext();
  // react-native-paper theme is always in sync
  const theme = useAppTheme();

  return (
    <View style={{ backgroundColor: theme.colors.background }}>
      {/* Update theme by generating a new one based on #3E8260 color */}
      <Button onPress={() => updateTheme('#3E8260')}>Update theme</Button>
      {/* Reset theme to default (system or fallback) */}
      <Button onPress={() => resetTheme()}>Reset theme</Button>
    </View>
  );
}

Example

You can see an example app in the /example folder.

Android example

Extract zip file, and install expo-material3-theme-example-android.apk on your device.

iOS example

This is a file for iOS simulator. Extract zip file, and drag and drop expo-material3-theme-example-ios into your emulator.

⚠️ Activity recreation

When Material 3 dynamic theme is changed on Android 12+ devices, it is a configuration change and the system will recreate an Activity.

This configuration change can't be disable: "Some configuration changes always cause the activity to restart. You can't disable them. For example, you can't disable the dynamic colors change introduced in API 32" (cf official doc).

So be aware that when users change their theme then go back to your app, all local state may be lost (and may cause some flickering) if your don't handle it.

License

This project is released under the MIT License.

expo-material3-theme's People

Contributors

pchmn avatar semantic-release-bot avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar

Forkers

jeslabs

expo-material3-theme's Issues

[Error: Cannot find native module 'ExpoMaterial3Theme']

Hi, whenever I try to use the Dynamic Color Scheme of my device in the app, I get the message that this is not supported (even despite Android 13 and the fact that it works in other RN apps).

After some searching I was able to find out what the problem is. Retrieving the native module ExpoMaterial3Theme fails.

How can the missing module be installed to enable dynamic color schemes?

import { registerRootComponent } from 'expo';

import * as React from 'react';
import { Platform, useColorScheme } from 'react-native';
import { getHeaderTitle } from '@react-navigation/elements';
import { useMaterial3Theme, isDynamicThemeSupported } from '@pchmn/expo-material3-theme';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { DrawerNavigationProp, createDrawerNavigator } from '@react-navigation/drawer';
import {
  InitialState,
  NavigationContainer,
  DarkTheme as NavigationDarkTheme,
  DefaultTheme as NavigationDefaultTheme,
} from '@react-navigation/native';
import { StatusBar } from 'expo-status-bar';
import {
  Provider as PaperProvider,
  MD3DarkTheme,
  MD3LightTheme,
  adaptNavigationTheme,
  Appbar,
} from 'react-native-paper';
import { SafeAreaInsetsContext } from 'react-native-safe-area-context';

import { CardStyleInterpolators } from '@react-navigation/stack';
import { Screens } from './src/screens';
import DrawerItems from './src/DrawerItems';
import { requireNativeModule } from 'expo-modules-core';
import { StoreProvider } from './src/context/Store.context';
import { SnackbarProvider } from './src/context/Snackbar.context';

// Equal to checks running by isDynmicThemeSupported
const isEnabled = () => {
  const getModule = () => {
    let ExpoMaterial3ThemeModule = undefined;
    try {
      ExpoMaterial3ThemeModule = requireNativeModule('ExpoMaterial3Theme');
    } catch (err) {
      console.error(err);
    } finally {
      return ExpoMaterial3ThemeModule;
    }
  };

  return !!getModule() && Platform.OS === 'android' && Platform.Version >= 31;
};


const PERSISTENCE_KEY = 'NAVIGATION_STATE';
const PREFERENCE_KEY = "APP_PREFERENCES";

const Drawer = createDrawerNavigator();

export default function App() {
  const colorScheme = useColorScheme();
  const { theme: mdTheme } = useMaterial3Theme();
  const isDarkMode = React.useMemo(() => colorScheme == 'dark', [colorScheme]);
  const [isReady, setIsReady] = React.useState(false);
  const [initialState, setInitialState] = React.useState<InitialState | undefined>();
  
  const theme = React.useMemo(() => {
    if (!isEnabled()) {
      return isDarkMode ? MD3DarkTheme : MD3LightTheme;
    }

    return isDarkMode
      ? { ...MD3DarkTheme, colors: mdTheme.dark }
      : { ...MD3LightTheme, colors: mdTheme.light };
  }, [isDarkMode, mdTheme]);

  React.useEffect(() => {
    const restoreState = async () => {
      try {
        const savedStateString = await AsyncStorage.getItem(PERSISTENCE_KEY);
        const state = JSON.parse(savedStateString || '');

        console.log(state)

        setInitialState(state);
      } catch (e) {
        // ignore error
      } finally {
        setIsReady(true);
      }
    };

    if (!isReady) {
      restoreState();
    }
  }, [isReady]);

  React.useEffect(() => {
    const restorePrefs = async () => {
      try {
        const prefString = await AsyncStorage.getItem(PREFERENCE_KEY);
        const preferences = JSON.parse(prefString || '');

        if (preferences) {
          // TODO: Load pres
        }
      } catch (e) {
        // ignore error
      }
    };

    restorePrefs();
  }, []);


  if (!isReady) {
    return null;
  }

  const cardStyleInterpolator =
    Platform.OS === 'android'
      ? CardStyleInterpolators.forFadeFromBottomAndroid
      : CardStyleInterpolators.forHorizontalIOS;
      
  const { LightTheme, DarkTheme } = adaptNavigationTheme({
    reactNavigationLight: NavigationDefaultTheme,
    reactNavigationDark: NavigationDarkTheme,
  });

  const CombinedDefaultTheme = {
    ...MD3LightTheme,
    ...LightTheme,
    colors: {
      ...MD3LightTheme.colors,
      ...LightTheme.colors,
    },
  };

  const CombinedDarkTheme = {
    ...MD3DarkTheme,
    ...DarkTheme,
    colors: {
      ...MD3DarkTheme.colors,
      ...DarkTheme.colors,
    },
  };

  const combinedTheme = isDarkMode ? CombinedDarkTheme : CombinedDefaultTheme;
  

  return (
    <PaperProvider theme={theme}>
        <StoreProvider>
          <SnackbarProvider>
            <NavigationContainer
              theme={combinedTheme}
              initialState={initialState}
              onStateChange={(state) => AsyncStorage.setItem(PERSISTENCE_KEY, JSON.stringify(state))}
            >
              <SafeAreaInsetsContext.Consumer>
                {(insets) => {
                  return (
                    <Drawer.Navigator
                      initialRouteName={Screens[0].name}
                      drawerContent={(props) => <DrawerItems {...props} />}
                      screenOptions={({navigation}) => ({
                        drawerStyle: { width: "75%" },
                        detachPreviousScreen: !navigation.isFocused(),
                        cardStyleInterpolator,
                        header: ({ navigation, route, options }) => {
                          const title = getHeaderTitle(options, route.name);
                          return (
                            <Appbar.Header elevated>
                              {(navigation as any).openDrawer ? (
                                <Appbar.Action
                                  icon="menu"
                                  isLeading
                                  onPress={() => (navigation as any as DrawerNavigationProp<{}>).openDrawer()}
                                />
                              ) : null}
                              <Appbar.Content title={title} />
                            </Appbar.Header>
                          );
                        },
                      })}
                      
                    >
                      {/* <Drawer.Screen name="Home" component={App} options={{ headerShown: false }} /> */}
                      {Screens.map((screen) => (
                        <Drawer.Screen
                          key={screen.name}
                          name={screen.name}
                          component={screen.component}
                          options={{ title: screen.label }}
                        />
                      ))}
                    </Drawer.Navigator>
                  );
                }}
              </SafeAreaInsetsContext.Consumer>
              <StatusBar style={!isDarkMode ? 'dark' : 'light'} />
            </NavigationContainer>
          </SnackbarProvider>
        </StoreProvider>
    </PaperProvider>
  );
}

registerRootComponent(App);

Add missing Material 3 colors

M3 has some color tokens which doesn't seem exist in this library. The colors that I've found to be missing are:

  • Surface dim
  • Surface bright
  • Surface container lowest
  • Surface container low
  • Surface container
  • Surface container high
  • Surface container highest
  • Surface tint

Is it possible to add all of these? Thank you.

M3 color tokens table

Error building android app when using expo sdk v50

Issue described by @ondrejnedoma here :

On RN 0.73 and Expo 50, this package refuses to compile with an error message, unless this change is made. This project really has no reason to require JDK 11 anymore, as JDK 17 is recommended by RN docs.


More details on this error:

When a RN 0.73, Expo 50 app tries to run with JDK 11:
Android Gradle plugin requires Java 17 to run. You are currently using Java 11.

When an app that also includes expo-material3-theme CURRENTLY gets ran with JDK 17:
Execution failed for task ':pchmn-expo-material3-theme:compileDebugKotlin'. 'compileDebugJavaWithJavac' task (current target is 17) and 'compileDebugKotlin' task (current target is 11) jvm target compatibility should be set to the same Java version.

This commit makes it so that with JDK 17, the app runs successfully

Theme from useMaterial3Theme doesnt update

I have setup the exact same ThemeProvider as the example but when I change the theme it does change the paper components color but the theme from useMaterial3Theme remains the same as the source color. If required I can share the code.

Update: I am using the useTheme hook from react native paper which seems to change upon updateTheme from the ThemeProvider

Cannot compile after upgrading react-native

$ npm run build:android:release

> [email protected] build:android:release
> cd android/ && ./gradlew assembleRelease && ./gradlew bundleRelease

Downloading https://services.gradle.org/distributions/gradle-8.0.1-all.zip
...............10%................20%................30%................40%................50%................60%................70%................80%................90%................100%

Welcome to Gradle 8.0.1!

Here are the highlights of this release:
 - Improvements to the Kotlin DSL
 - Fine-grained parallelism from the first build with configuration cache
 - Configurable Gradle user home cache cleanup

For more details see https://docs.gradle.org/8.0.1/release-notes.html

Starting a Gradle Daemon (subsequent builds will be faster)

FAILURE: Build completed with 2 failures.

1: Task failed with an exception.
-----------
* Where:
Build file '/workspaces/*/node_modules/@pchmn/expo-material3-theme/android/build.gradle' line: 40

* What went wrong:
A problem occurred evaluating project ':pchmn-expo-material3-theme'.
> Could not set unknown property 'classifier' for task ':pchmn-expo-material3-theme:androidSourcesJar' of type org.gradle.api.tasks.bundling.Jar.

* Try:
> Run with --stacktrace option to get the stack trace.
> Run with --info or --debug option to get more log output.
> Run with --scan to get full insights.
==============================================================================

2: Task failed with an exception.
-----------
* What went wrong:
A problem occurred configuring project ':expo'.
> compileSdkVersion is not specified. Please add it to build.gradle

* Try:
> Run with --stacktrace option to get the stack trace.
> Run with --info or --debug option to get more log output.
> Run with --scan to get full insights.
==============================================================================

* Get more help at https://help.gradle.org

BUILD FAILED in 1m 35s
5 actionable tasks: 5 executed

DIFF of settings.gradle

rootProject.name = 'x'
apply from: file("../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesSettingsGradle(settings)
include ':app'
- includeBuild('../node_modules/react-native-gradle-plugin')
+ includeBuild('../node_modules/@react-native/gradle-plugin')
apply from: new File(["node", "--print", "require.resolve('expo/package.json')"].execute(null, rootDir).text.trim(), "../scripts/autolinking.gradle")
useExpoModules()

App crashes after changing colors

I'm using bare react native project not expo.
Android Version: 12L

ThemeProvider.tsx

import { useMemo } from "react";
import type { FC, PropsWithChildren } from "react";
import { useColorScheme } from "react-native";
import { useMaterial3Theme } from "@pchmn/expo-material3-theme";

import {
  Provider as MaterialYouTheme,
  MD3LightTheme,
  MD3DarkTheme,
} from "react-native-paper";

export const MaterialYouThemeProvider: FC<PropsWithChildren> = ({
  children,
}) => {
  const colorScheme = useColorScheme();
  const { theme } = useMaterial3Theme();

  // Material You Theme
  const materialTheme = useMemo(
    () =>
      colorScheme === "dark"
        ? { ...MD3DarkTheme, colors: theme.dark }
        : { ...MD3LightTheme, colors: theme.light },
    [colorScheme, theme],
  );

  // Render
  return <MaterialYouTheme theme={materialTheme}>{children}</MaterialYouTheme>;
};

Video

screen-20240422-135628.mp4

Github Project Repo - Link

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.