Headless JS는 앱이 백그라운드에 있는 동안 JavaScript로 태스크를 실행하는 방법입니다. 예를 들어 새로운 데이터를 동기화하거나 푸시 알림을 처리하거나 음악을 재생하는 데 사용할 수 있습니다.
태스크는 AppRegistry 에 등록하는 비동기 함수로, React 애플리케이션을 등록하는 것과 유사합니다.
import {AppRegistry} from 'react-native';
AppRegistry.registerHeadlessTask('SomeTaskName', () =>
require('SomeTaskName'),
);
그런 다음 SomeTaskName.js
module.exports = async taskData => {
// do stuff
};
UI를 건드리지 않는 한 네트워크 요청, 타이머 등 태스크에서 무엇이든 할 수 있습니다. 태스크가 완료되면(즉, promise가 resolve되면) React Native는 "paused" 모드로 전환됩니다(다른 태스크가 실행 중이지 않거나 포그라운드 앱이 없는 경우).
예, 여전히 약간의 네이티브 코드가 필요하지만 매우 간단합니다. HeadlessJsTaskService를 확장하고 getTaskConfig를 재정의해야 합니다.
package com.your_application_name;
import android.content.Intent;
import android.os.Bundle;
import com.facebook.react.HeadlessJsTaskService;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.jstasks.HeadlessJsTaskConfig;
import javax.annotation.Nullable;
public class MyTaskService extends HeadlessJsTaskService {
@Override
protected @Nullable HeadlessJsTaskConfig getTaskConfig(Intent intent) {
Bundle extras = intent.getExtras();
if (extras != null) {
return new HeadlessJsTaskConfig(
"SomeTaskName",
Arguments.fromBundle(extras),
5000, // timeout in milliseconds for the task
false // optional: defines whether or not the task is allowed in foreground. Default is false
);
}
return null;
}
}
package com.your_application_name;
import android.content.Intent
import com.facebook.react.HeadlessJsTaskService
import com.facebook.react.bridge.Arguments
import com.facebook.react.jstasks.HeadlessJsTaskConfig
class MyTaskService : HeadlessJsTaskService() {
override fun getTaskConfig(intent: Intent): HeadlessJsTaskConfig? {
return intent.extras?.let {
HeadlessJsTaskConfig(
"SomeTaskName",
Arguments.fromBundle(it),
5000, // timeout for the task
false // optional: defines whether or not the task is allowed in foreground.
// Default is false
)
}
}
}
그런 다음 AndroidManifest.xml 파일의 application 태그 내에 서비스를 추가합니다.
<service android:name="com.example.MyTaskService" />
이제 정기적인 태스크나 시스템 이벤트 / broadcast에 대한 응답 등으로 서비스를 시작할 때마다 JS가 스핀업되어 작업을 실행한 다음 스핀다운됩니다.
예시:
Intent service = new Intent(getApplicationContext(), MyTaskService.class);
Bundle bundle = new Bundle();
bundle.putString("foo", "bar");
service.putExtras(bundle);
getApplicationContext().startForegroundService(service);
val service = Intent(applicationContext, MyTaskService::class.java)
val bundle = Bundle()
bundle.putString("foo", "bar")
service.putExtras(bundle)
applicationContext.startForegroundService(service)
기본적으로 headless JS 태스크는 재시도를 수행하지 않습니다. 재시도를 수행하려면 HeadlessJsRetryPolicy를 생성하고 특정 Error를 던져야 합니다.
LinearCountingRetryPolicy는 각 시도 사이에 고정된 지연 시간으로 최대 재시도 횟수를 지정할 수 있는 HeadlessJsRetryPolicy의 implementation입니다. 이 방법이 적합하지 않은 경우 자체적인 HeadlessJsRetryPolicy를 구현할 수 있습니다. 이러한 정책은 다음과 같이 HeadlessJsTaskConfig 생성자에 추가 인수로 전달할 수 있습니다.
HeadlessJsRetryPolicy retryPolicy = new LinearCountingRetryPolicy(
3, // Max number of retry attempts
1000 // Delay between each retry attempt
);
return new HeadlessJsTaskConfig(
'SomeTaskName',
Arguments.fromBundle(extras),
5000,
false,
retryPolicy
);
val retryPolicy: HeadlessJsTaskRetryPolicy =
LinearCountingRetryPolicy(
3, // Max number of retry attempts
1000 // Delay between each retry attempt
)
return HeadlessJsTaskConfig("SomeTaskName", Arguments.fromBundle(extras), 5000, false, retryPolicy)
재시도는 특정 Error가 발생했을 때만 시도됩니다. headless JS 태스크 내에서 error를 가져와서 재시도가 필요할 때 error를 던질 수 있습니다.
예시:
import {HeadlessJsTaskError} from 'HeadlessJsTask';
module.exports = async taskData => {
const condition = ...;
if (!condition) {
throw new HeadlessJsTaskError();
}
};
모든 error가 재시도를 유발하도록 하려면 error를 포착하여 위의 error를 던져야 합니다.
boolean 인수를 전달하여 이 동작을 제어할 수 있습니다.BroadcastReceiver에서 서비스를 시작하는 경우, onReceive()에서 반환하기 전에 HeadlessJsTaskService.acquireWakeLockNow()를 호출해야 합니다.서비스는 Java API에서 시작할 수 있습니다. 먼저 서비스를 언제 시작해야 하는지 결정하고 그에 따라 솔루션을 구현해야 합니다. 다음은 네트워크 연결 변경에 반응하는 예제입니다.
다음 라인은 broadcast receiver 등록을 위한 Android manifest 파일의 일부입니다.
<receiver android:name=".NetworkChangeReceiver" >
<intent-filter>
<action android:name="android.net.conn.CONNECTIVITY_CHANGE" />
</intent-filter>
</receiver>
그런 다음 broadcast receiver는 onReceive 함수에서 브로드캐스트된 인텐트를 처리합니다. 앱이 포그라운드에 있는지 여부를 확인하기에 좋은 곳입니다. 앱이 포그라운드에 있지 않은 경우 putExtra를 사용하여 정보나 추가 정보를 번들로 제공하지 않고 시작할 인텐트를 준비할 수 있습니다(번들은 parcelable value만 처리할 수 있음에 유의하세요). 최종적으로 서비스가 시작되고 wakelock이 획득됩니다.
import android.app.ActivityManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.net.ConnectivityManager;
import android.net.Network;
import android.net.NetworkCapabilities;
import android.net.NetworkInfo;
import android.os.Build;
import com.facebook.react.HeadlessJsTaskService;
public class NetworkChangeReceiver extends BroadcastReceiver {
@Override
public void onReceive(final Context context, final Intent intent) {
/**
This part will be called every time network connection is changed
e.g. Connected -> Not Connected
**/
if (!isAppOnForeground((context))) {
/**
We will start our service and send extra info about
network connections
**/
boolean hasInternet = isNetworkAvailable(context);
Intent serviceIntent = new Intent(context, MyTaskService.class);
serviceIntent.putExtra("hasInternet", hasInternet);
context.startForegroundService(serviceIntent);
HeadlessJsTaskService.acquireWakeLockNow(context);
}
}
private boolean isAppOnForeground(Context context) {
/**
We need to check if app is in foreground otherwise the app will crash.
https://stackoverflow.com/questions/8489993/check-android-application-is-in-foreground-or-not
**/
ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
List<ActivityManager.RunningAppProcessInfo> appProcesses =
activityManager.getRunningAppProcesses();
if (appProcesses == null) {
return false;
}
final String packageName = context.getPackageName();
for (ActivityManager.RunningAppProcessInfo appProcess : appProcesses) {
if (appProcess.importance ==
ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND &&
appProcess.processName.equals(packageName)) {
return true;
}
}
return false;
}
public static boolean isNetworkAvailable(Context context) {
ConnectivityManager cm = (ConnectivityManager)
context.getSystemService(Context.CONNECTIVITY_SERVICE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
Network networkCapabilities = cm.getActiveNetwork();
if(networkCapabilities == null) {
return false;
}
NetworkCapabilities actNw = cm.getNetworkCapabilities(networkCapabilities);
if(actNw == null) {
return false;
}
if(actNw.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) || actNw.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) || actNw.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET)) {
return true;
}
return false;
}
// deprecated in API level 29
NetworkInfo netInfo = cm.getActiveNetworkInfo();
return (netInfo != null && netInfo.isConnected());
}
}
import android.app.ActivityManager
import android.app.ActivityManager.RunningAppProcessInfo
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import android.os.Build
import com.facebook.react.HeadlessJsTaskService
class NetworkChangeReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent?) {
/**
* This part will be called every time network connection is changed e.g. Connected -> Not
* Connected
*/
if (!isAppOnForeground(context)) {
/** We will start our service and send extra info about network connections */
val hasInternet = isNetworkAvailable(context)
val serviceIntent = Intent(context, MyTaskService::class.java)
serviceIntent.putExtra("hasInternet", hasInternet)
context.startForegroundService(serviceIntent)
HeadlessJsTaskService.acquireWakeLockNow(context)
}
}
private fun isAppOnForeground(context: Context): Boolean {
/**
* We need to check if app is in foreground otherwise the app will crash.
* https://stackoverflow.com/questions/8489993/check-android-application-is-in-foreground-or-not
*/
val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
val appProcesses = activityManager.runningAppProcesses ?: return false
val packageName: String = context.getPackageName()
for (appProcess in appProcesses) {
if (appProcess.importance == RunningAppProcessInfo.IMPORTANCE_FOREGROUND &&
appProcess.processName == packageName
) {
return true
}
}
return false
}
companion object {
fun isNetworkAvailable(context: Context): Boolean {
val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
var result = false
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val networkCapabilities = cm.activeNetwork ?: return false
val actNw = cm.getNetworkCapabilities(networkCapabilities) ?: return false
result =
when {
actNw.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> true
actNw.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> true
actNw.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> true
else -> false
}
return result
} else {
cm.run {
// deprecated in API level 29
cm.activeNetworkInfo?.run {
result =
when (type) {
ConnectivityManager.TYPE_WIFI -> true
ConnectivityManager.TYPE_MOBILE -> true
ConnectivityManager.TYPE_ETHERNET -> true
else -> false
}
}
}
}
return result
}
}
}