How To Build a Home Screen Widget for iOS and Android in React Native
Objective: Learn how to build a native widget and share information with your React Native app on click of the widget open the application’s home screen.
The widget is an important addition to an app and often a highly requested feature.
Unfortunately, you can’t use React Native directly to build the widget.
The widget is an important addition to an app and often a highly requested feature. Unfortunately, you can’t use React Native directly to build the widget.
Android
1. Create the widget files
Open the Android folder on Android Studio. Then, inside Android Studio, right-click on res > New > Widget > App Widget:
Name and configure your widget, and click Finish.
The next window will show the several files that are going to be added to the project. The Widget.java
file is where we’ll code the widget behavior. The rest of the files are where we’ll implement the widget UI components and click on the Add.
Now if you run the app, you should see the widget available:
Run the application in Android studio and you will able to see the widget in the emulator. OK, now that we have the widget running, let’s customize it a little bit.
2. Customize the widget UI On Android Studio, open your app folder and select the file res -> layout -> widget.xml
:
This opens the layout of your widget. As you can see, there’s a text view with the text “WIDGET.” You can see more details if you click on it:
Let’s change the text of the label. As you can see on the label properties, the text is referring to “@string/appwidget_text.” This is located at res > values > strings.xml
.
Open this file, and you’ll see the text defined:
If you change the text from “WIDGET” to “HELLO,” save the file, and run the app again. It’ll be reflected on the widget.
OK, now we know how to customize the widget. Next, let’s implement a way to do this via the React Native app.
3. Create the communication channel between the widget and the React Native app
OK, now the fun part again. Let’s have our React Native app control what the widget shows. To do this, we have to implement a way for the React Native app to communicate with the widget. We’re going to do this via a shared storage between the widget and the React Native app. This can be accomplished using the SharedPreferences
Android-native module.
We’ll make the React Native app write to them SharedPreferences
and have the widget read from it. The first problem we found is there is no official React Native way to interact with, and I didn’t find any good library to do this. So let’s implement this ourselves.
In order to call, we’ll create a bridge between React Native and native Android.
Add two files on your project next to MainActivity.java
called SharedStorage.java
and SharedStoragePackager.java
:
Copy the following into your SharedStoragePackager.java
file:
//PUT YOUR PACKAGE NAME HERE, IT'S THE SAME AS IN MainApplication.java
package com.reactnativecreatewidgettutorial;
import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.JavaScriptModule;
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 SharedStoragePackager 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 SharedStorage(reactContext));
return modules;
}
}
Important:
Change the package name com.reactnativecreatewidgettutorial
to your own. And also the Widget.class
to the name of your widget class.
Now you are able to call SharedStorage
on React Native. As you can see on the code, all it does is receive a JSON store it on the SharedPreferences
storage, and then tell the widget to update itself.
In order for Android to know your module exists, add it to your package list in your MainApplication.java
file.
new SharedStoragePackager()
4. Control the widget content with the React Native app
On the React Native side, let’s import the module.
Note: If you already modified the React Native code, skip this part.
import { NativeModules } from 'react-native';
const SharedStorage = NativeModules.SharedStorage;
And then let’s send some data to the storage:
SharedStorage.set(
JSON.stringify({text: 'This is data from the React Native app'})
);
You can do this, for example, on your App.tsx
or wherever you find it appropriate to set the data on your React Native code:
Now all that’s left is to have the widget read the data and insert it on the UI. We’re going to connect the widget to the, read its data, and then print the data on the “HELLO” label.
Go to your Widget.java
file in the Widget folder, and import the following modules:
import android.content.SharedPreferences;
import org.json.JSONException;
import org.json.JSONObject;
Now modify the updateAppWidget
function in widget.java like this:
package com.reactnativecreatewidgettutorial;
import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetProvider;
import android.content.Context;
import android.widget.RemoteViews;
import android.content.SharedPreferences;
import org.json.JSONException;
import org.json.JSONObject;
/**
* Implementation of App Widget functionality.
*/
public class Widget extends AppWidgetProvider {
static void updateAppWidget(Context context, AppWidgetManager appWidgetManager,
int appWidgetId) {
try {
SharedPreferences sharedPref = context.getSharedPreferences("DATA", Context.MODE_PRIVATE);
String appString = sharedPref.getString("appData", "{\"text\":'no data'}");
JSONObject appData = new JSONObject(appString);
// Construct the RemoteViews object
RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.widget);
views.setTextViewText(R.id.appwidget_text, appData.getString("text"));
// Instruct the widget manager to update the widget
appWidgetManager.updateAppWidget(appWidgetId, views);
}catch (JSONException 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);
}
}
@Override
public void onEnabled(Context context) {
// Enter relevant functionality for when the first widget is created
}
@Override
public void onDisabled(Context context) {
// Enter relevant functionality for when the last widget is disabled
}
}
We are modifying the function updateAppWidget
, which is responsible for updating the widget content, to make the widget read from the SharedPreferences
db and print the data on the text label.
That’s it — now the React Native app controls the widget content. Let’s run the application, and check the widget (note that you have to run and open the app in order to let it write to the SharedStorage
):
iOS
1. Create the widget files
Create the widget (on iOS, it’s called the Today Extension) by opening your workspace file on Xcode, and click on File > New > Target:
Select Today Extension, and click Next:
Give it a name, and choose your preferred language. In this case, I’m going to choose Swift. Click Finish:
You should see the Widget folder on your project:
Now you should see the widget on your widget screen, and you can already activate it:
Now, let’s check out the structure of the widget. Go to your Widget folder, and click on the storyboard file. Click on the assistant-editor button (the one with two circles on the top right of the screen):
You can see there are two functions called viewDidLoad
and widgetPerformUpdate
. The function viewDidLoad
runs every time the user switches to the widget screen. So this is the place where you should initialize variables, labels, or views. The function widgetPerformUpdate
is called whenever you need to update the widget contents.
2. Customize the widget UIYou can also see there’s a label with the text “Hello World.” Let’s customize that label by dragging it into our code. Right-click on the label, and drag the New Referencing Outlet directly to the code inside the class.
3. Create the communication channel between the widget and the React Native app
OK, now the fun part. Let’s have our React Native app control what the widget shows. To do this, we have to implement a way for the React Native app to communicate with the widget. We’re going to do this via a shared storage between the widget and the React Native app. This can be accomplished using the UserDefaults
iOS native module.
We will make the React Native app written to UserDefaults
and have the widget read from it. The first problem we found is there is no official React Native way to interact with, and I didn’t find any good library to do this. So let’s implement this ourselves by creating a bridge between React Native and native iOS.
First, we’ll create a shared space inside our app that’ll allow communication between the widget and the app. This can be done with App Groups, which is located under the Capabilities tab.
Let’s activate it and then select a group or add one if
Select your project, and right-click to add a new file:
Select Cocoa Touch Class, and click Next:
Because this is a storage that’ll be shared by the widget and the React Native app, let’s call it SharedStorage
. Select Objective-C, and click next:
Now you should see the new files on your project:
Let’s edit those files. First copy this into the SharedStorage.h
file
//
// SharedStorage.h
// hodl
//
// Created by Andre Pimenta on 19/09/2018.
// Copyright © 2018 Facebook. All rights reserved.
//
#import "React/RCTBridgeModule.h"
@interface SharedStorage : NSObject <RCTBridgeModule>
@end
And this into the SharedStorage.m
file:
//
// SharedStorage.m
// hodl
//
// Created by Andre Pimenta on 19/09/2018.
// Copyright © 2018 Facebook. All rights reserved.
//
#import "SharedStorage.h"
#import "React/RCTLog.h"
@implementation SharedStorage
RCT_EXPORT_MODULE();
// We can send back a promise to our JavaScript environment :)
RCT_EXPORT_METHOD(set:(NSString *)data
resolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject)
{
@try{
//CHANGE THE GROUP HERE
NSUserDefaults *shared = [[NSUserDefaults alloc]initWithSuiteName:@"group.com.createwidget.pimenta"];
[shared setObject:data forKey:@"data"];
[shared synchronize];
resolve(@"true");
}@catch(NSException *exception){
reject(@"get_error",exception.reason, nil);
}
}
@end
Important: Change the group name (group.com.createwidget.pimenta
) to the one you created on App Groups.
Now, you’re able to call SharedStorage
on React Native. As you can see on the code, all it does is receive a JSON and store it on the UserDefaults
storage.
4. Control the widget content with the React Native app
On the React Native side, let’s import the module:
import { NativeModules } from 'react-native';const SharedStorage = NativeModules.SharedStorage;
And then let’s send some data to the storage:
SharedStorage.set(
JSON.stringify({text: 'This is data from the React Native app'})
);
You can do this, for example, on your App.tsx
or wherever you find it appropriate to set the data on your React Native code:
Now, all that’s left is to have the widget read the data and insert it into the UI. We’re going to connect the widget to the UserDefaults
, read its data, and then print the data on the “Hello World” textLabel
.
Go to your TodayViewController.swift
file in the Widget folder, and edit the viewDidLoad
function, like this:
package com.reactnativecreatewidgettutorial;
import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetProvider;
import android.content.Context;
import android.widget.RemoteViews;
import android.content.SharedPreferences;
import org.json.JSONException;
import org.json.JSONObject;
/**
* Implementation of App Widget functionality.
*/
public class Widget extends AppWidgetProvider {
static void updateAppWidget(Context context, AppWidgetManager appWidgetManager,
int appWidgetId) {
try {
SharedPreferences sharedPref = context.getSharedPreferences("DATA", Context.MODE_PRIVATE);
String appString = sharedPref.getString("appData", "{\"text\":'no data'}");
JSONObject appData = new JSONObject(appString);
// Construct the RemoteViews object
RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.widget);
views.setTextViewText(R.id.appwidget_text, appData.getString("text"));
// Instruct the widget manager to update the widget
appWidgetManager.updateAppWidget(appWidgetId, views);
}catch (JSONException 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);
}
}
@Override
public void onEnabled(Context context) {
// Enter relevant functionality for when the first widget is created
}
@Override
public void onDisabled(Context context) {
// Enter relevant functionality for when the last widget is disabled
}
}
Important: Change the group name (group.com.createwidget.pimenta
) to the one you created on App Groups.
Let’s run the application, and check the widget (note that you have to run and open the app in order to let it write the SharedStorage
):
And that’s it for iOS.