React Native: полное руководство по созданию виджета для домашнего экрана для iOS и Android

Как работает виджет?

Виджет работает как расширение приложения. Он не функционирует как самостоятельное приложение. Виджеты доступны в трех размерах (Small, Medium и Large) и могут быть статичными и настраиваемыми. Виджет ограничен в плане взаимодействия. Его нельзя скроллить, а можно только касаться. Малый виджет может иметь только один тип области взаимодействия, в то время как средний и большой — несколько.

Зачем разрабатывать виджеты?

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

Виджеты для взаимодействия с React Native

К сожалению, создать виджет для домашнего экрана с помощью React Native невозможно. Но не волнуйтесь, решение есть! В этом руководстве мы рассмотрим, как использовать нативный виджет для взаимодействия с приложением React Native. 

Примеры  —  в этом репозитории.

Настройка

  1. Создайте новое приложение:
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. Не нужно устанавливать никаких дополнительных библиотек, так как SharedStorage включен в пакет React Native. Значение будет представлять собой сериализованный объект JavaScript, который преобразуется в строку и сохранится с помощью метода set SharedStorage. 

Итак, работаем с нативным кодом.

Реализация для iOS

Откройте проект приложения в Xcode и выберите File > New > Target.

2. В группе Application Extension выберите Widget Extension, а затем нажмите Next.

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

4. Если виджет предоставляет настраиваемые пользователем свойства, отметьте галочкой Include Configuration Intent.

5. Нажмите Finish.

6. Если появится запрос на активацию схемы, нажмите Activate.

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

8. Прежде чем перейти к следующему шагу, протестируйте базовый виджет. Для этого просто откройте терминал и запустите приложение с помощью следующей команды:

react-native run-ios

9. Чтобы добавить виджет, нужно нажать и удерживать домашний экран, пока в левом верхнем углу не появится значок “+”. При нажатии на него появится список приложений, где должно быть недавно созданное.

10. Для взаимодействия с виджетом React Native необходимо добавить “App Group”.

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 и протестируйте приложение на виртуальном или реальном устройстве.

После установки приложения останется только выбрать виджет из списка виджетов и поместить его на домашний экран.

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

Вот и все. Теперь посмотрим, как проделать то же самое на Android.

Реализация для Android

  1. Откройте папку Android в Android Studio. Затем в Android Studio щелкните правой кнопкой мыши на res > New > Widget > App Widget.

2. Назовите и настройте виджет, далее нажмите кнопку Finish.

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;

/**
* Внедрение функциональности App Widget.
*/
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) {
// Возможно, активны несколько виджетов, поэтому обновите их все.
for (int appWidgetId : appWidgetIds) {
updateAppWidget(context, appWidgetManager, appWidgetId);
}
}

@Override
public void onEnabled(Context context) {
// Введите соответствующую функциональность для момента создания первого виджета.
}

@Override
public void onDisabled(Context context) {
// Введите соответствующую функциональность для момента отключения последнего виджета.
}
}

8. Теперь поговорим о внешнем виде виджета. Этот шаг необязателен, но мы будем использовать тот же дизайн, что и в примере с iOS. В Android Studio перейдите к файлу app > res > layout > streak_widget.xml. Можете ознакомиться с предварительным просмотром дизайна следующим образом:

9. Запустите результат в тестовом режиме на устройстве Android:

Вы научились создавать AppWidget с помощью React Native. Даже если эта тема для вас нова, не волнуйтесь  —  вы легко справитесь с добавлением виджетов в ваши приложения.

Читайте также:

Читайте нас в TelegramVK и Дзен


Перевод статьи Rushit Jivani: React Native: Ultimate Guide to Create a Home Screen Widget for iOS and Android

Предыдущая статья5 удивительных скрытых возможностей Python. Часть 1
Следующая статьяСетевое программирование в Go