WellAlly Logo
WellAlly康心伴
Development

Create Custom Widgets for iOS & Android with React Native

Learn how to build native home screen widgets for both iOS and Android from a single React Native application. This tutorial guides you through sharing data and displaying key health stats, like daily steps or sleep scores, directly on the user's home screen.

W
2025-12-21
11 min read

In a world of information overload, home screen widgets offer a powerful way to provide users with glanceable, relevant data from your app without requiring them to open it. For health and fitness applications, this is a game-changer. Imagine your users seeing their daily step count or sleep score every time they unlock their phone – a constant, subtle source of motivation.

This tutorial will guide you through the process of building custom home screen widgets for both iOS and Android within a React Native application. We will create a "Health Stats" widget that displays a user's daily steps and sleep score. While React Native doesn't have a built-in "widget" component, we can leverage native modules to create these powerful extensions.

Prerequisites:

  • Basic understanding of React Native.
  • Node.js and npm/yarn installed.
  • A React Native development environment set up for both iOS and Android.
  • For iOS development: a Mac with Xcode installed.
  • For Android development: Android Studio installed.
  • An active Apple Developer account is required for App Group capabilities on iOS.

Why this matters to developers:

Widgets can significantly increase user engagement and app retention. By the end of this tutorial, you'll have the skills to build custom widgets for your React Native apps, providing a more integrated and valuable user experience.

Understanding the Problem

The primary challenge in creating widgets with React Native is that widgets are fundamentally native UI components. They run in a separate process from your main application and cannot directly render JavaScript components. Therefore, we need to build the widget's UI using the platform's native tools (SwiftUI for iOS and XML layouts for Android) and establish a reliable method for sharing data from our React Native app to these native widgets.

Our approach will be to:

  1. Create the widget UI natively for each platform.
  2. Establish a shared data layer that both the React Native app and the native widget can access.
  3. Write data from our React Native app to this shared storage.
  4. Read and display the data in our native widgets.

Prerequisites

Before we start, make sure you have a React Native project set up. If not, you can create one using the following command:

code
npx react-native init HealthStatsWidgetApp
cd HealthStatsWidgetApp
Code collapsed

We will also need a library to facilitate data sharing between the React Native app and the native widgets. For this, we'll use react-native-shared-group-preferences for iOS and a custom native module for Android.

Step 1: Building the iOS Widget

What we're doing

For iOS, we'll create a "Widget Extension" in Xcode and use SwiftUI to design our widget's UI. We'll then set up an "App Group" to enable data sharing between our main app and the widget extension.

Implementation

1. Create the Widget Extension in Xcode:

  • Open your project's ios folder in Xcode (open ios/HealthStatsWidgetApp.xcworkspace).
  • Go to File > New > Target....
  • Select "Widget Extension" and click "Next".
  • Name your widget (e.g., "HealthStatsWidget") and ensure "Include Configuration Intent" is unchecked for this simple example.
  • Click "Finish". Activate the new scheme when prompted.

2. Configure App Groups:

  • In Xcode, select your main app target (HealthStatsWidgetApp).
  • Go to the "Signing & Capabilities" tab and click "+ Capability" to add "App Groups".
  • Click the "+" button under "App Groups" and add a new group with a unique identifier, typically in the format group.com.yourcompany.yourappname.
  • Now, select your widget extension target (HealthStatsWidget) and repeat the same process, adding the exact same App Group.

3. Share Data from React Native to the iOS Widget:

First, install the package for shared preferences:

code
npm install react-native-shared-group-preferences
cd ios && pod install
Code collapsed

Now, in your React Native code (e.g., App.js), you can write data to the shared storage:

code
// src/App.js
import React from 'react';
import { SafeAreaView, Button, View, Text } from 'react-native';
import SharedGroupPreferences from 'react-native-shared-group-preferences';

const App = () => {
  const appGroupIdentifier = 'group.com.yourcompany.HealthStatsWidgetApp'; // Replace with your App Group Identifier

  const updateWidgetData = async () => {
    const healthData = {
      steps: Math.floor(Math.random() * 10000),
      sleepScore: Math.floor(Math.random() * 100),
    };

    try {
      await SharedGroupPreferences.setItem('healthData', healthData, appGroupIdentifier);
      console.log('Data saved to shared storage');
    } catch (error) {
      console.error('Error saving data:', error);
    }
  };

  return (
    <SafeAreaView>
      <View style={{ padding: 20 }}>
        <Text style={{ fontSize: 18, marginBottom: 20 }}>Health Stats App</Text>
        <Button title="Update Widget Data" onPress={updateWidgetData} />
      </View>
    </SafeAreaView>
  );
};

export default App;
Code collapsed

4. Displaying Data in the SwiftUI Widget:

Now, let's modify the widget's Swift code to read and display this data. Open the HealthStatsWidget.swift file in Xcode.

code
// HealthStatsWidget/HealthStatsWidget.swift
import WidgetKit
import SwiftUI

struct HealthData: Codable {
    let steps: Int
    let sleepScore: Int
}

struct Provider: TimelineProvider {
    let appGroupIdentifier = "group.com.yourcompany.HealthStatsWidgetApp" // Replace with your App Group Identifier

    func placeholder(in context: Context) -> SimpleEntry {
        SimpleEntry(date: Date(), healthData: HealthData(steps: 8000, sleepScore: 85))
    }

    func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) {
        let entry = SimpleEntry(date: Date(), healthData: HealthData(steps: 8000, sleepScore: 85))
        completion(entry)
    }

    func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
        let userDefaults = UserDefaults(suiteName: appGroupIdentifier)
        var entries: [SimpleEntry] = []

        if let savedData = userDefaults?.object(forKey: "healthData") as? [String: Any] {
            let steps = savedData["steps"] as? Int ?? 0
            let sleepScore = savedData["sleepScore"] as? Int ?? 0
            let entry = SimpleEntry(date: Date(), healthData: HealthData(steps: steps, sleepScore: sleepScore))
            entries.append(entry)
        } else {
            let entry = SimpleEntry(date: Date(), healthData: HealthData(steps: 0, sleepScore: 0))
            entries.append(entry)
        }

        let timeline = Timeline(entries: entries, policy: .atEnd)
        completion(timeline)
    }
}

struct SimpleEntry: TimelineEntry {
    let date: Date
    let healthData: HealthData
}

struct HealthStatsWidgetEntryView : View {
    var entry: Provider.Entry

    var body: some View {
        VStack {
            Text("Health Stats")
                .font(.headline)
            Text("Steps: \(entry.healthData.steps)")
            Text("Sleep Score: \(entry.healthData.sleepScore)")
        }
    }
}

@main
struct HealthStatsWidget: Widget {
    let kind: String = "HealthStatsWidget"

    var body: some WidgetConfiguration {
        StaticConfiguration(kind: kind, provider: Provider()) { entry in
            HealthStatsWidgetEntryView(entry: entry)
        }
        .configurationDisplayName("Health Stats")
        .description("Your daily health stats.")
    }
}
Code collapsed

How it works

  • App Groups create a secure, shared container on the device that both your main app and its extensions can access.
  • react-native-shared-group-preferences provides a bridge to the native UserDefaults (for iOS), allowing us to write data to this shared container from JavaScript.
  • The TimelineProvider in our Swift code is responsible for fetching the data and determining when the widget should update.
  • The getTimeline function reads the data from UserDefaults using the same App Group identifier and creates a timeline entry to be displayed by the widget's SwiftUI view.

Common pitfalls

  • Ensure the App Group identifier is identical in your main app target, widget extension target, and your React Native code.
  • You must have an active Apple Developer account to use App Groups.
  • Widget updates are not instantaneous and are managed by the OS to conserve battery.

Step 2: Building the Android Widget

What we're doing

For Android, we'll use Android Studio to create an AppWidgetProvider and a layout for our widget in XML. To share data, we will create a custom native module that utilizes SharedPreferences.

Implementation

1. Create the Widget in Android Studio:

  • Open the android folder of your project in Android Studio.
  • Right-click on the app folder, then go to New > Widget > App Widget.
  • Set the Class Name to HealthStatsWidgetProvider and the Layout Name to health_stats_widget. Click "Finish".

2. Design the Widget Layout:

Android Studio will create health_stats_widget.xml in the res/layout directory. Open it and add the following:

code
<!-- res/layout/health_stats_widget.xml -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:gravity="center"
    android:background="@android:color/white"
    android:padding="16dp">

    <TextView
        android:id="@+id/widget_title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Health Stats"
        android:textColor="@android:color/black"
        android:textSize="18sp"
        android:textStyle="bold" />

    <TextView
        android:id="@+id/widget_steps"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Steps: 0"
        android:textColor="@android:color/black" />

    <TextView
        android:id="@+id/widget_sleep"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Sleep Score: 0"
        android:textColor="@android:color/black" />
</LinearLayout>
Code collapsed

3. Create a Native Module for Data Sharing:

We need to create a bridge so our React Native app can communicate with the native Android side.

First, create a new Java class SharedStorageModule.java:

code
// android/app/src/main/java/com/healthstatswidgetapp/SharedStorageModule.java
package com.healthstatswidgetapp;

import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import android.content.Context;
import android.content.SharedPreferences;

public class SharedStorageModule extends ReactContextBaseJavaModule {
    ReactApplicationContext context;

    public SharedStorageModule(ReactApplicationContext reactContext) {
        super(reactContext);
        context = reactContext;
    }

    @Override
    public String getName() {
        return "SharedStorage";
    }

    @ReactMethod
    public void set(String data) {
        SharedPreferences.Editor editor = context.getSharedPreferences("DATA", Context.MODE_PRIVATE).edit();
        editor.putString("healthData", data);
        editor.apply();
    }
}
Code collapsed

Next, create a package to register this module, SharedStoragePackage.java:

code
// android/app/src/main/java/com/healthstatswidgetapp/SharedStoragePackage.java
package com.healthstatswidgetapp;

import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class SharedStoragePackage implements ReactPackage {

    @Override
    public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
        return Collections.emptyList();
    }

    @Override
    public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
        List<NativeModule> modules = new ArrayList<>();
        modules.add(new SharedStorageModule(reactContext));
        return modules;
    }
}
Code collapsed

Finally, register the package in MainApplication.java:

code
// android/app/src/main/java/com/healthstatswidgetapp/MainApplication.java
// ...
import com.healthstatswidgetapp.SharedStoragePackage; // Add this import

public class MainApplication extends Application implements ReactApplication {
    // ...
    @Override
    protected List<ReactPackage> getPackages() {
        @SuppressWarnings("UnnecessaryLocalVariable")
        List<ReactPackage> packages = new PackageList(this).getPackages();
        // Packages that cannot be autolinked yet can be added manually here, for example:
        packages.add(new SharedStoragePackage()); // Add this line
        return packages;
    }
    // ...
}
Code collapsed

4. Update the Widget Provider to Display Data:

Modify HealthStatsWidgetProvider.java to read from SharedPreferences and update the widget's views.

code
// android/app/src/main/java/com/healthstatswidgetapp/HealthStatsWidgetProvider.java
package com.healthstatswidgetapp;

import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetProvider;
import android.content.Context;
import android.content.SharedPreferences;
import android.widget.RemoteViews;
import org.json.JSONObject;

public class HealthStatsWidgetProvider extends AppWidgetProvider {

    static void updateAppWidget(Context context, AppWidgetManager appWidgetManager, int appWidgetId) {
        try {
            SharedPreferences sharedPref = context.getSharedPreferences("DATA", Context.MODE_PRIVATE);
            String healthDataString = sharedPref.getString("healthData", "{\"steps\":0,\"sleepScore\":0}");

            JSONObject healthData = new JSONObject(healthDataString);

            // Construct the RemoteViews object
            RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.health_stats_widget);
            views.setTextViewText(R.id.widget_steps, "Steps: " + healthData.getInt("steps"));
            views.setTextViewText(R.id.widget_sleep, "Sleep Score: " + healthData.getInt("sleepScore"));

            // Instruct the widget manager to update the widget
            appWidgetManager.updateAppWidget(appWidgetId, views);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
        // There may be multiple widgets active, so update all of them
        for (int appWidgetId : appWidgetIds) {
            updateAppWidget(context, appWidgetManager, appWidgetId);
        }
    }
}
Code collapsed

5. Send Data from React Native to the Android Widget:

Now, let's update our React Native code to use the new native module for Android.

code
// src/App.js (with Android additions)
import React from 'react';
import { SafeAreaView, Button, View, Text, NativeModules, Platform } from 'react-native';
import SharedGroupPreferences from 'react-native-shared-group-preferences';

const { SharedStorage } = NativeModules;

const App = () => {
  const appGroupIdentifier = 'group.com.yourcompany.HealthStatsWidgetApp'; // For iOS

  const updateWidgetData = async () => {
    const healthData = {
      steps: Math.floor(Math.random() * 10000),
      sleepScore: Math.floor(Math.random() * 100),
    };

    if (Platform.OS === 'ios') {
      try {
        await SharedGroupPreferences.setItem('healthData', healthData, appGroupIdentifier);
        console.log('iOS Data saved to shared storage');
      } catch (error) {
        console.error('Error saving iOS data:', error);
      }
    } else if (Platform.OS === 'android') {
        try {
            await SharedStorage.set(JSON.stringify(healthData));
            console.log('Android Data saved to shared storage');
        } catch (error) {
            console.error('Error saving Android data:', error);
        }
    }
  };

  return (
    <SafeAreaView>
      <View style={{ padding: 20 }}>
        <Text style={{ fontSize: 18, marginBottom: 20 }}>Health Stats App</Text>
        <Button title="Update Widget Data" onPress={updateWidgetData} />
      </View>
    </SafeAreaView>
  );
};

export default App;
Code collapsed

How it works

  • We created a custom Native Module SharedStorage that exposes a set method to our JavaScript code.
  • This method uses Android's SharedPreferences to store the health data as a JSON string.
  • The HealthStatsWidgetProvider is an AppWidgetProvider, which is a broadcast receiver that handles widget updates.
  • In the onUpdate method, we read the data from SharedPreferences, parse the JSON, and use RemoteViews to update the text in our widget's layout.

Putting It All Together

After implementing the steps for both iOS and Android, you can run your app on both platforms.

Testing Steps:

  1. Run the app on your simulator or device:
    • For iOS: npx react-native run-ios
    • For Android: npx react-native run-android
  2. Open the app and press the "Update Widget Data" button.
  3. Go to your home screen and add the "Health Stats" widget.
  4. You should see the health data displayed on your widget.
  5. Go back to the app, press the button again, and the widget should update (note: widget updates on Android can have a slight delay).

Security Best Practices

  • Sensitive Data: For this example, we're using simple, non-sensitive data. If you need to display sensitive information, consider encrypting the data before storing it in shared preferences.
  • Data Validation: Always validate the data on the native side before displaying it in the widget to prevent crashes from malformed data.

Conclusion

You've successfully built a cross-platform home screen widget for your React Native application! While it requires diving into native code, the process is manageable and unlocks a powerful way to keep your users engaged. We've seen how to create native UI for widgets on both iOS and Android and how to establish a data bridge to share information from your React Native app.

Next Steps for Readers:

  • Explore different widget sizes and layouts.
  • Implement background fetching to update the widget periodically without user interaction.
  • Add deep linking so that tapping the widget opens a specific screen in your app.

Resources

#

Article Tags

reactnativemobileiosandroidtutorial
W

WellAlly's core development team, comprised of healthcare professionals, software engineers, and UX designers committed to revolutionizing digital health management.

Expertise

Healthcare TechnologySoftware DevelopmentUser ExperienceAI & Machine Learning

Found this article helpful?

Try KangXinBan and start your health management journey

© 2024 康心伴 WellAlly · Professional Health Management