Создавайте потрясающие виджеты на главном экране с помощью React Native + DuoLingo!

Виджеты — это замечательные инструменты, которые делают ваш домашний экран более привлекательным, а также предоставляют вам быструю и полезную информацию. В этом посте мы покажем вам, как создавать виджеты для Android и iOS и как включать их в ваше приложение React Native.
Как работает виджет?
Виджет работает как расширение вашего приложения. Оно не может работать как отдельное приложение. Виджеты доступны в трех размерах (маленький, средний и большой) и могут быть статическими или настраиваемыми. Виджет ограничен с точки зрения взаимодействия. Его нельзя прокручивать, только нажимать. У маленького виджета может быть только один тип области взаимодействия, тогда как у среднего и большого виджета может быть несколько областей взаимодействия, на которые можно нажимать.
Зачем разрабатывать виджет?
Виджеты обычно создаются не только для того, чтобы информировать пользователей о важной информации и обеспечивать доступ к своим приложениям с главного экрана, но также для того, чтобы отличать эти приложения от конкурентов и поддерживать вовлеченность пользователей.
Виджеты с React Native
⚠️ К сожалению, с помощью React Native невозможно создать виджет домашнего экрана. Но не волнуйтесь, у нас есть решение для вас! В этом руководстве мы рассмотрим, как использовать собственный виджет для связи с вашим приложением React Native. Давайте начнем!
TL;DR
Если вы предпочитаете 🍖, а не 📖, вы можете поиграть с образцами в этом хранилище. Запросы на вытягивание и другие типы вкладов приветствуются!
🛠️ Настройка
- Создать новое приложение
react-native init RNWidget
2. Добавьте зависимость, которая будет создавать мост между виджетом и приложением.
yarn add react-native-shared-group-preferences
3. Для связи с нативным модулем добавьте этот код в свой App.js
import React, {useState} from 'react';
import {
View,
TextInput,
StyleSheet,
NativeModules,
SafeAreaView,
Text,
Image,
ScrollView,
KeyboardAvoidingView,
Platform,
ToastAndroid,
} from 'react-native';
import SharedGroupPreferences from 'react-native-shared-group-preferences';
import AwesomeButton from 'react-native-really-awesome-button';
import {KeyboardAwareScrollView} from 'react-native-keyboard-aware-scroll-view';
const group = 'group.streak';
const SharedStorage = NativeModules.SharedStorage;
const App = () => {
const [text, setText] = useState('');
const widgetData = {
text,
};
const handleSubmit = async () => {
try {
// iOS
await SharedGroupPreferences.setItem('widgetKey', widgetData, group);
} catch (error) {
console.log({error});
}
const value = `${text} days`;
// Android
SharedStorage.set(JSON.stringify({text: value}));
ToastAndroid.show('Change value successfully!', ToastAndroid.SHORT);
};
return (
<SafeAreaView style={styles.safeAreaContainer}>
<KeyboardAwareScrollView
enableOnAndroid
extraScrollHeight={100}
keyboardShouldPersistTaps="handled">
<View style={styles.container}>
<Text style={styles.heading}>Change Widget Value</Text>
<View style={styles.bodyContainer}>
<View style={styles.instructionContainer}>
<View style={styles.thoughtContainer}>
<Text style={styles.thoughtTitle}>
Enter the value that you want to display on your home widget
</Text>
</View>
<View style={styles.thoughtPointer}></View>
<Image
source={require('./assets/bea.png')}
style={styles.avatarImg}
/>
</View>
<TextInput
style={styles.input}
onChangeText={newText => setText(newText)}
value={text}
keyboardType="decimal-pad"
placeholder="Enter the text to display..."
/>
<AwesomeButton
backgroundColor={'#33b8f6'}
height={50}
width={'100%'}
backgroundDarker={'#eeefef'}
backgroundShadow={'#f1f1f0'}
style={styles.actionButton}
onPress={handleSubmit}>
Submit
</AwesomeButton>
</View>
</View>
</KeyboardAwareScrollView>
</SafeAreaView>
);
};
export default App;
const styles = StyleSheet.create({
safeAreaContainer: {
flex: 1,
width: '100%',
backgroundColor: '#fafaf3',
},
container: {
flex: 1,
width: '100%',
padding: 12,
},
heading: {
fontSize: 24,
color: '#979995',
textAlign: 'center',
},
input: {
width: '100%',
// fontSize: 20,
minHeight: 50,
borderWidth: 1,
borderColor: '#c6c6c6',
borderRadius: 8,
padding: 12,
},
bodyContainer: {
flex: 1,
margin: 18,
},
instructionContainer: {
margin: 25,
paddingHorizontal: 20,
paddingTop: 30,
borderWidth: 1,
borderRadius: 12,
backgroundColor: '#ecedeb',
borderColor: '#bebfbd',
marginBottom: 35,
},
avatarImg: {
height: 180,
width: 180,
resizeMode: 'contain',
alignSelf: 'flex-end',
},
thoughtContainer: {
minHeight: 50,
borderRadius: 12,
borderWidth: 1,
padding: 12,
backgroundColor: '#ffffff',
borderColor: '#c6c6c6',
},
thoughtPointer: {
width: 0,
height: 0,
borderStyle: 'solid',
overflow: 'hidden',
borderTopWidth: 12,
borderRightWidth: 10,
borderBottomWidth: 0,
borderLeftWidth: 10,
borderTopColor: 'blue',
borderRightColor: 'transparent',
borderBottomColor: 'transparent',
borderLeftColor: 'transparent',
marginTop: -1,
marginLeft: '50%',
},
thoughtTitle: {
fontSize: 14,
},
actionButton: {
marginTop: 40,
},
});
позвольте мне объяснить, как использовать SharedGroupPreferences и SharedStorage в вашем приложении. SharedGroupPreferences импортируется из библиотеки, и вы можете использовать его, сохранив элемент с помощью метода setItem, используя ключ, значение и группу. В этом примере ключом будет widgetKey, значением будет widgetData, объект JavaScript, который содержит пользовательский ввод, а группа будет именем группы, которая будет обмениваться информацией между приложением и виджетом. Мы поговорим об этом подробнее, когда перейдем к коду Swift.
Теперь для Android мы будем использовать SharedStorage. Для этого не нужно устанавливать никаких дополнительных библиотек, так как они включены в пакет React Native. Значением будет сериализованный объект JavaScript, который будет преобразован в строку и сохранен с использованием заданного метода SharedStorage. Легко, не так ли?
Для нативного кода. Начнем с iOS.
🍏 Реализация для iOS
- Откройте проект приложения в Xcode и выберите «Файл» > «Создать» > «Цель».

2. В группе «Расширение приложения» выберите «Расширение виджета» и нажмите «Далее».

3. Введите имя вашего расширения.

4. Если виджет предоставляет настраиваемые пользователем свойства, установите флажок «Включить намерение конфигурации».

5. Нажмите Готово.
6. Если будет предложено активировать схему, нажмите Активировать.

7. Ваш виджет готов к работе! Теперь у вас есть новая папка, содержащая все необходимые файлы для вашего виджета.

8. Прежде чем мы перейдем к следующему шагу, давайте протестируем ваш базовый виджет. Для этого просто откройте свой терминал и запустите приложение, используя следующую команду
react-native run-ios
9. Чтобы добавить виджет, нажмите и удерживайте главный экран, пока в верхнем левом углу не появится значок ➕. При нажатии на значок появится список приложений. Наше недавно созданное приложение должно быть включено.

10. Для связи с виджетом React Native нам нужно добавить «Группу приложений».

11. Теперь для нашего виджета Streak отредактируйте StreakWidget.swift с помощью приведенного ниже кода.
import WidgetKit
import SwiftUI
import Intents
struct WidgetData: Decodable {
var text: String
}
struct Provider: IntentTimelineProvider {
func placeholder(in context: Context) -> SimpleEntry {
SimpleEntry(date: Date(), configuration: ConfigurationIntent(), text: "Placeholder")
}
func getSnapshot(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (SimpleEntry) -> ()) {
let entry = SimpleEntry(date: Date(), configuration: configuration, text: "Data goes here")
completion(entry)
}
func getTimeline(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (Timeline<SimpleEntry>) -> Void) {
let userDefaults = UserDefaults.init(suiteName: "group.streak")
if userDefaults != nil {
let entryDate = Date()
if let savedData = userDefaults!.value(forKey: "widgetKey") as? String {
let decoder = JSONDecoder()
let data = savedData.data(using: .utf8)
if let parsedData = try? decoder.decode(WidgetData.self, from: data!) {
let nextRefresh = Calendar.current.date(byAdding: .minute, value: 5, to: entryDate)!
let entry = SimpleEntry(date: nextRefresh, configuration: configuration, text: parsedData.text)
let timeline = Timeline(entries: [entry], policy: .atEnd)
completion(timeline)
} else {
print("Could not parse data")
}
} else {
let nextRefresh = Calendar.current.date(byAdding: .minute, value: 5, to: entryDate)!
let entry = SimpleEntry(date: nextRefresh, configuration: configuration, text: "No data set")
let timeline = Timeline(entries: [entry], policy: .atEnd)
completion(timeline)
}
}
}
}
struct SimpleEntry: TimelineEntry {
let date: Date
let configuration: ConfigurationIntent
let text: String
}
struct StreakWidgetEntryView : View {
var entry: Provider.Entry
var body: some View {
HStack {
VStack(alignment: .leading, spacing: 0) {
HStack(alignment: .center) {
Image("streak")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 37, height: 37)
Text(entry.text)
.foregroundColor(Color(red: 1.00, green: 0.59, blue: 0.00))
.font(Font.system(size: 21, weight: .bold, design: .rounded))
.padding(.leading, -8.0)
}
.padding(.top, 10.0)
.frame(maxWidth: .infinity)
Text("Way to go!")
.foregroundColor(Color(red: 0.69, green: 0.69, blue: 0.69))
.font(Font.system(size: 14))
.frame(maxWidth: .infinity)
Image("duo")
.renderingMode(.original)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(maxWidth: .infinity)
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
@main
struct StreakWidget: Widget {
let kind: String = "StreakWidget"
var body: some WidgetConfiguration {
IntentConfiguration(kind: kind, intent: ConfigurationIntent.self, provider: Provider()) { entry in
StreakWidgetEntryView(entry: entry)
}
.configurationDisplayName("My Widget")
.description("This is an example widget.")
}
}
struct StreakWidget_Previews: PreviewProvider {
static var previews: some View {
StreakWidgetEntryView(entry: SimpleEntry(date: Date(), configuration: ConfigurationIntent(), text: "Widget preview"))
.previewContext(WidgetPreviewContext(family: .systemSmall))
}
}
В основном:
- Прочитайте объект
UserDefaultsиз общей группы, которую мы создали ранее.
let userDefaults = UserDefaults.init(suiteName: "group.streak")
- Получить данные (которые были закодированы в виде строки)
let savedData = userDefaults!.value(forKey: "widgetKey")
- Декодировать в объект
let parsedData = try? decoder.decode(WidgetData.self, from: data!)
- Создайте временную шкалу указанных объектов
let nextRefresh = Calendar.current.date(byAdding: .minute, value: 5, to: entryDate)!
Обратите внимание, что объекты, добавленные в структуру Timeline, должны соответствовать протоколу TimelineEntry, что означает, что они должны иметь поле Date и ничего больше. Это важная информация, которую нужно иметь в виду.
Это все, что нужно для iOS. Просто запустите npm start и протестируйте свое приложение на виртуальном или реальном устройстве.
После того, как вы установили приложение, все, что вам нужно сделать, это выбрать виджет из списка виджетов и поместить его на главный экран.

Затем откройте приложение и введите что-нибудь в поле ввода, нажмите ввод и вернитесь на главный экран.

И это все для iOS. Теперь давайте рассмотрим, как мы можем сделать то же самое на Android.
🤖 Реализация для Android
- Откройте папку Android в Android Studio. Затем в Android Studio щелкните правой кнопкой мыши res > New > Widget > App Widget:

2. Назовите и настройте свой виджет и нажмите Готово:

3. Теперь запустите приложение, и вы увидите доступный виджет.
4. Для связи между виджетом и приложением React Native мы будем использовать нативный модуль SharedPreferences для Android, который аналогичен UserDefaults для iOS.
Это включает в себя добавление новых файлов SharedStorage.java и SharedStoragePackager.java в тот же каталог, что и ваш MainApplication.java.
SharedStorage.java
package com.rnwidget;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import android.app.Activity;
import android.appwidget.AppWidgetManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.util.Log;
public class SharedStorage extends ReactContextBaseJavaModule {
ReactApplicationContext context;
public SharedStorage(ReactApplicationContext reactContext) {
super(reactContext);
context = reactContext;
}
@Override
public String getName() {
return "SharedStorage";
}
@ReactMethod
public void set(String message) {
SharedPreferences.Editor editor = context.getSharedPreferences("DATA", Context.MODE_PRIVATE).edit();
editor.putString("appData", message);
editor.commit();
Intent intent = new Intent(getCurrentActivity().getApplicationContext(), StreakWidget.class);
intent.setAction(AppWidgetManager.ACTION_APPWIDGET_UPDATE);
int[] ids = AppWidgetManager.getInstance(getCurrentActivity().getApplicationContext()).getAppWidgetIds(new ComponentName(getCurrentActivity().getApplicationContext(), StreakWidget.class));
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, ids);
getCurrentActivity().getApplicationContext().sendBroadcast(intent);
}
}
SharedStoragePackager.java
package com.rnwidget;
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;
}
}
5. Измените имя пакета для своего приложения, как показано в файле AndroidManifest.xml внутри android › app › src › main.

6. После внесения необходимых изменений добавьте этот код в файл MainApplication.java в методе getPackages.
packages.add(new SharedStoragePackager());
7. Настроив мост, переходим к получению данных в StreakWidget.java. Чтобы обновить содержимое виджета, нам нужно использовать SharedPreferences и управлять им с помощью метода updateAppWidget. Вот код для замены существующего:
package com.rnwidget;
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 StreakWidget 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);
RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.streak_widget);
views.setTextViewText(R.id.appwidget_text, appData.getString("text"));
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
}
}
8. Теперь поговорим о внешнем виде виджета. Этот шаг необязателен, но мы будем использовать тот же дизайн, что и в примере с iOS. В Android Studio перейдите к файлу app › res › layout › streak_widget.xml. И вы можете проверить предварительный просмотр дизайна, как это

9. Вот и все! Проведите тестовый запуск на устройстве Android.

Заключение
Отличная работа! Научившись создавать AppWidget с помощью React Native, вы добавили ценный навык в свой набор инструментов. Даже если эта тема для вас новая, не волнуйтесь — ее легко и просто использовать в ваших приложениях. Продолжайте хорошую работу!
Похлопайте 👏🏻 и поделитесь с друзьями!
Пожалуйста, оставьте комментарий с вашим отзывом.
Если вы хотите поддержать меня как писателя, рассмотрите возможность подписаться на меня в Medium и связаться со мной в LinkedIn.