Найти в Дзене
Nuances of programming

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

Источник: Nuances of Programming Как работает виджет? Виджет работает как расширение приложения. Он не функционирует как самостоятельное приложение. Виджеты доступны в трех размерах (Small, Medium и Large) и могут быть статичными и настраиваемыми. Виджет ограничен в плане взаимодействия. Его нельзя скроллить, а можно только касаться. Малый виджет может иметь только один тип области взаимодействия, в то время как средний и большой — несколько. Зачем разрабатывать виджеты? Виджеты обычно создаются не только для того, чтобы предоставлять важную информацию и доступ к приложению на домашнем экране, но и для того, чтобы выделять приложение на фоне конкурентов и поддерживать вовлеченность пользователей. Виджеты для взаимодействия с React Native К сожалению, создать виджет для домашнего экрана с помощью React Native невозможно. Но не волнуйтесь, решение есть! В этом руководстве мы рассмотрим, как использовать нативный виджет для взаимодействия с приложением React Native. Примеры  —  в этом реп
Оглавление

Источник: Nuances of Programming

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

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

-2

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

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

Виджеты для взаимодействия с 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.

-3

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

-4

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

-5

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

-6

5. Нажмите Finish.

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

-7

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

-8

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

react-native run-ios

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

-9

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

-10

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

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

-11

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

-12

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

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

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

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

-14

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.

-15

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. Можете ознакомиться с предварительным просмотром дизайна следующим образом:

-16

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

-17

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

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

Читайте нас в Telegram, VK

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