Headless JS
Headless JSは、アプリがバックグラウンドにある間にJavaScriptでタスクを実行する方法です。たとえば、新しいデータの同期、プッシュ通知の処理、音楽の再生などに使用できます。
JS API
タスクは、Reactアプリケーションを登録するのと同様に、AppRegistry
に登録する非同期関数です。
import {AppRegistry} from 'react-native';
AppRegistry.registerHeadlessTask('SomeTaskName', () =>
require('SomeTaskName'),
);
次に、SomeTaskName.js
で
module.exports = async taskData => {
// do stuff
};
ネットワークリクエスト、タイマーなど、UIに触れない限り、タスク内で何でも実行できます。タスクが完了すると(つまり、Promiseが解決されると)、React Nativeは「一時停止」モードになります(他のタスクが実行中であるか、フォアグラウンドアプリがある場合を除きます)。
プラットフォームAPI
はい、これには依然としていくつかのネイティブコードが必要ですが、非常にわずかです。HeadlessJsTaskService
を拡張し、getTaskConfig
をオーバーライドする必要があります。例:
- Java
- Kotlin
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" />
これで、サービスを開始するたびに、たとえば定期的なタスクとして、または何らかのシステムイベント/ブロードキャストに応じて、JSが起動し、タスクを実行して、停止します。
例
- Java
- Kotlin
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
の実装です。それがニーズに合わない場合は、独自のHeadlessJsRetryPolicy
を実装できます。これらのポリシーは、HeadlessJsTaskConfig
コンストラクターに追加の引数として渡すことができます。例:
- Java
- Kotlin
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タスク内では、エラーをインポートし、再試行が必要な場合にスローできます。
例
import {HeadlessJsTaskError} from 'HeadlessJsTask';
module.exports = async taskData => {
const condition = ...;
if (!condition) {
throw new HeadlessJsTaskError();
}
};
すべてのエラーが再試行を引き起こすようにするには、それらをキャッチして上記のエラーをスローする必要があります。
注意点
- デフォルトでは、アプリがフォアグラウンドにある間にタスクを実行しようとすると、アプリがクラッシュします。これは、開発者がタスクで多くの作業を行い、UIを遅くすることで自滅するのを防ぐためです。この動作を制御するために、4番目の
boolean
引数を渡すことができます。 BroadcastReceiver
からサービスを開始する場合は、onReceive()
から戻る前に、必ずHeadlessJsTaskService.acquireWakeLockNow()
を呼び出してください。
使用例
サービスはJava APIから開始できます。まず、サービスをいつ開始するかを決定し、それに応じてソリューションを実装する必要があります。これは、ネットワーク接続の変更に反応する例です。
以下の行は、ブロードキャストレシーバーを登録するためのAndroidマニフェストファイルの一部を示しています。
<receiver android:name=".NetworkChangeReceiver" >
<intent-filter>
<action android:name="android.net.conn.CONNECTIVITY_CHANGE" />
</intent-filter>
</receiver>
ブロードキャストレシーバーは、onReceive関数でブロードキャストされたインテントを処理します。これは、アプリがフォアグラウンドにあるかどうかを確認するのに最適な場所です。アプリがフォアグラウンドにない場合は、putExtra
を使用してバンドルされた情報または追加情報なしで、開始されるインテントを準備できます(バンドルはparcelable値のみを処理できることに注意してください)。最後に、サービスが開始され、wakelockが取得されます。
- Java
- Kotlin
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
}
}
}