[React Native] Android Widget Counter 생성 - Native Module 연결

mainsain·2023년 9월 18일

React Native

목록 보기
5/9
post-thumbnail

이제 위젯과 앱을 연동해보자

기본 개념

Native Modules

  • React Native 애플리케이션은 JS로 작성되지만, 네이티브 코드와 상호작용이 필요한 경우가 있음.
  • 이때 Native Modules를 사용하면 JS에서 네이티브 기능을 호출할 수 있다.

Bridge

  • 말 그대로 React Native는 Bridge를 통해 JS와 네이티브 코드 간의 통신을 한다.
  • 이 Bridge를 통해 데이터를 주고받을 수 있다.

React Native는 Main Thread → JavaScript Thread → Shadow Thread → Native side의 일련의 실행과정으로 실행이 된다.

구현

Native Module 생성

JS에서 Native Module 호출

Native Module 생성

// StopWatchModule.java
public class StopWatchModule extends ReactContextBaseJavaModule {
    private static ReactApplicationContext reactContext;

    public StopWatchModule(ReactApplicationContext reactContext) {
        super(reactContext);
        this.reactContext = reactContext;
    }

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

    @ReactMethod
    public void getNumber(int appWidgetId) {
        Log.d("StopWatchModule", "This is a simple log from Native Module!");
    }
}

getNumber을 호출하면 Log가 찍히는 함수를 간단하게 만들었다.

@ReactMethod를 사용해야 JS에서 접근이 가능하다.

React Native 코드 작성

import { NativeModules } from "react-native";
const { StopWatchModule } = NativeModules;

const App = () => {
	const [widgetData, setWidgetData] = React.useState(null);
	const getWidgetData = async () => {
		const widgetData = await StopWatchModule.getNumber(1);
		setWidgetData(widgetData);
	};
  • appWidgetId는 이후에 처리한다. 일단 목표는 getNumber을 연결했을때 Log.d가 찍히는지 확인하는 것.

패키지 코드 작성

// StopWatchPackage.java
public class StopWatchPackage implements ReactPackage {

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

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

React Native에 등록할 패키지이며, reactContext를 인자로 받아와 모듈 인스턴스에 등록한다.

createViewManagers는 지금 필요없으므로, 빈 값을 넘겨준다.

// MainApplication.java
public class MainApplication extends Application implements ReactApplication {

  private final ReactNativeHost mReactNativeHost =
    new ReactNativeHostWrapper(this, new DefaultReactNativeHost(this) {
      @Override
      public boolean getUseDeveloperSupport() {
        return BuildConfig.DEBUG;
      }

        @Override
        protected List<ReactPackage> getPackages() {
            List<ReactPackage> packages = new PackageList(this).getPackages();
            packages.add(new StopWatchPackage());
            return packages;
        }

만든 패키지를 넘겨주는 것을 끝으로 Native Module이 연결완료되었다.

테스트

버튼클릭하면?

로그가 잘 나온다. 연결은 되었다.

이제 appWidgetId를 통해 위젯을 관리하고, 그 데이터를 불러와야한다.

appWidgetId는 이제부터 그냥 id라고 말하겠다.

id는 위젯의 인스턴스를 식별하기 위해 시스템에 의해 자동으로 생성된다. 따라서 위젯이 추가되거나 제거되면 id는 변경된다. 이런 상황에서 구별할 수 있는 방법을 생각해봤다.

  1. 고정 ID사용
    1. 고정된 ID를 사용하면 안정성은 모르겠고 구현하기 쉽지않을까 생각했었다.
    2. 당연하지만 시스템에 의해 자동으로 생성되는데, 이를 고정시켜버리면 예상하지 못한 에러가 발생할수밖에 없겠다.
  2. ID 저장 및 재사용
    1. id를 처음 얻을 때, React Native가 가지고있는다.
    2. 제거되거나 추가되면 업데이트해준다.

구현해보자,,

**Event Emitte 생성**

public static void emitDeviceEvent(String eventName, @Nullable WritableMap eventData) {
        if (StopWatchModule.reactContext != null) {
            StopWatchModule.reactContext
                    .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
                    .emit(eventName, eventData);
        }
    }

이벤트를 전달하기위한 메서드. StopWatchModule에 작성해준다.

onUpdate 메서드 수정

@Override
    public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
        for (int appWidgetId : appWidgetIds) {
            updateAppWidget(context, appWidgetManager, appWidgetId);
            WritableMap map = Arguments.createMap();
            map.putInt("appWidgetId", appWidgetId);
            StopWatchModule.emitDeviceEvent("onAppWidgetUpdate", map);
        }
    }

위젯이 업데이트될 때마다, 다시말해 위젯이 홈 화면에 추가 될 때, id를 React Native에 전달하기위한 메서드. StopWatch.java에 작성한다.

React Native 코드 작성

import React, { useEffect } from "react";
import { View, Text, Button } from "react-native";
import { NativeModules, DeviceEventEmitter } from "react-native";
import * as SecureStore from "expo-secure-store";

const { StopWatchModule } = NativeModules;

const WidgetText = () => {
	const [widgetData, setWidgetData] = React.useState<String | null>(null);
	const [appWidgetId, setAppWidgetId] = React.useState<String | null>(null);

	useEffect(() => {
		const fetchStoredAppWidgetId = async () => {
			const storedId = await SecureStore.getItemAsync("appWidgetId");
			if (storedId) {
				setAppWidgetId(storedId);
			}
		};

		fetchStoredAppWidgetId();

		const listener = DeviceEventEmitter.addListener(
			"onAppWidgetUpdate",
			async (data) => {
				setAppWidgetId(data.appWidgetId);
				await SecureStore.setItemAsync(
					"appWidgetId",
					data.appWidgetId.toString(),
				);
			},
		);

		return () => {
			if (listener) {
				listener.remove();
			}
		};
	}, []);

	const getWidgetData = async () => {
		if (appWidgetId) {
			const widgetData = await StopWatchModule.getNumber(Number(appWidgetId));
			console.warn(
				"appWidgetId : ",
				appWidgetId + " widgetData : ",
				widgetData,
			);
			setWidgetData(widgetData);
		} else {
			console.warn("AppWidgetId is not yet available.");
		}
	};

	return (
		<View>
			<Text>{widgetData}</Text>
			<Button
				title="get widget data"
				onPress={() => getWidgetData()}
				disabled={!appWidgetId}
			/>
			<Text>{appWidgetId}</Text>
		</View>
	);
};

export default WidgetText;

React 코드 작성하는게 이렇게 반가울줄은 몰랐다..

SecureStore는 react app이 종료되었을때도 id를 가지고있어야하기 때문에 id를 저장해두는 용도이다.

useEffect로 store에 id있으면 불러와주고, addListener로 위젯에 이벤트가 발생한다면 그 id값을 상태에도 저장하고 storage에도 저장한다.

끝으로 불필요한 리스너를 메모리에서 제거해 누수를 방지한다.

이로서 정상적으로 id를 받아오고, 앱을 종료해도 id를 가지고있는 로직이 완성되었다.

결과 확인

위젯에서 버튼들을 눌러 69의 숫자의 상태이다.

앱 켜서 위젯 데이터를 받아왔더니, 69의 값을 정상적으로 받아오는 걸 확인했다.

profile
새로운 자극을 주세요.

0개의 댓글