Headless JS
Headless JS é uma forma de executar tarefas em JavaScript enquanto seu aplicativo está em segundo plano. Ele pode ser usado, por exemplo, para sincronizar dados recentes, lidar com notificações push ou reproduzir música.
A API JS
Uma tarefa é uma função assíncrona que você registra no AppRegistry
, semelhante ao registro de aplicativos React:
import {AppRegistry} from 'react-native';
AppRegistry.registerHeadlessTask('SomeTaskName', () =>
require('SomeTaskName'),
);
Então, em SomeTaskName.js
:
module.exports = async taskData => {
// Fazer coisas
};
Você pode fazer qualquer coisa em sua tarefa, como solicitações de rede, temporizadores e assim por diante, desde que não toque na interface do usuário. Assim que sua tarefa for concluída (ou seja, a promessa for resolvida), o React Native entrará no modo "pausado" (a menos que haja outras tarefas em execução ou um aplicativo em primeiro plano).
A API da plataforma
Sim, isso ainda requer algum código nativo, mas é bastante limitado. Você precisa estender HeadlessJsTaskService
e substituir getTaskConfig
, por exemplo:
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
)
}
}
}
Em seguida, adicione o serviço ao seu arquivo AndroidManifest.xml
dentro da tag application
:
<service android:name="com.example.MyTaskService" />
Agora, sempre que você iniciar seu serviço, por exemplo. como uma tarefa periódica ou em resposta a algum evento/transmissão do sistema, o JS irá acelerar, executar sua tarefa e, em seguida, diminuir a rotação.
Exemplo:
Intent service = new Intent(getApplicationContext(), MyTaskService.class);
Bundle bundle = new Bundle();
bundle.putString("foo", "bar");
service.putExtras(bundle);
getApplicationContext().startService(service);
val service = Intent(applicationContext, MyTaskService::class.java)
val bundle = Bundle()
bundle.putString("foo", "bar")
service.putExtras(bundle)
applicationContext.startService(service)
Novas tentativas
Por padrão, a tarefa JS headless não executará nenhuma nova tentativa. Para fazer isso, você precisa criar um HeadlessJsRetryPolicy
e lançar um Error
específico.
LinearCountingRetryPolicy
é uma implementação de HeadlessJsRetryPolicy
que permite especificar um número máximo de novas tentativas com um atraso fixo entre cada tentativa. Se isso não atender às suas necessidades, você pode implementar seu próprio HeadlessJsRetryPolicy
. Essas políticas podem ser passadas como um argumento extra para o construtor HeadlessJsTaskConfig
, por exemplo.
HeadlessJsRetryPolicy retryPolicy = new LinearCountingRetryPolicy(
3, // Número máximo de novas tentativas
1000 // Atraso entre cada nova tentativa
);
return new HeadlessJsTaskConfig(
'SomeTaskName',
Arguments.fromBundle(extras),
5000,
false,
retryPolicy
);
val retryPolicy: HeadlessJsTaskRetryPolicy =
LinearCountingRetryPolicy(
3, // Número máximo de novas tentativas
1000 // Atraso entre cada nova tentativa
)
return HeadlessJsTaskConfig("SomeTaskName", Arguments.fromBundle(extras), 5000, false, retryPolicy)
Uma nova tentativa só será feita quando um Error
específico for lançado. Dentro de uma tarefa JS headless, você pode importar o erro e lançá-lo quando uma nova tentativa for necessária.
Exemplo:
import {HeadlessJsTaskError} from 'HeadlessJsTask';
module.exports = async taskData => {
const condition = ...;
if (!condition) {
throw new HeadlessJsTaskError();
}
};
Se desejar que todos os erros causem uma nova tentativa, você precisará capturá-los e lançar o erro acima.
Ressalvas
- Por padrão, seu aplicativo travará se você tentar executar uma tarefa enquanto o aplicativo estiver em primeiro plano. Isso evita que os desenvolvedores dêem um tiro no próprio pé ao trabalhar muito em uma tarefa e tornar a interface do usuário lenta. Você pode passar um quarto argumento
booleano
para controlar este comportamento. - Se você iniciar seu serviço a partir de um
BroadcastReceiver
, certifique-se de chamarHeadlessJsTaskService.acquireWakeLockNow()
antes de retornar deonReceive()
.
Exemplo de uso
O serviço pode ser iniciado a partir da API Java. Primeiro você precisa decidir quando o serviço deve ser iniciado e implementar sua solução de acordo. Aqui está um exemplo que reage à mudança na conexão de rede.
As linhas a seguir mostram parte do arquivo de manifesto do Android para registrar o receptor de transmissão.
<receiver android:name=".NetworkChangeReceiver" >
<intent-filter>
<action android:name="android.net.conn.CONNECTIVITY_CHANGE" />
</intent-filter>
</receiver>
O receptor de transmissão então lida com a intenção que foi transmitida na função onReceive. Este é um ótimo lugar para verificar se seu aplicativo está em primeiro plano ou não. Se o aplicativo não estiver em primeiro plano, podemos preparar nossa intenção para ser iniciado, sem informações ou informações adicionais agrupadas usando putExtra
(lembre-se de que o pacote pode lidar apenas com valores parcelados). No final o serviço é iniciado e o wakelock é adquirido.
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) {
/**
Esta parte será chamada sempre que a conexão de rede for alterada
por exemplo. Conectado -> Não conectado
**/
if (!isAppOnForeground((context))) {
/**
Iniciaremos nosso serviço e enviaremos informações extras sobre
conexões de rede
**/
boolean hasInternet = isNetworkAvailable(context);
Intent serviceIntent = new Intent(context, MyTaskService.class);
serviceIntent.putExtra("hasInternet", hasInternet);
context.startService(serviceIntent);
HeadlessJsTaskService.acquireWakeLockNow(context);
}
}
private boolean isAppOnForeground(Context context) {
/**
Precisamos verificar se o aplicativo está em primeiro plano, caso contrário ele irá travar.
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;
}
// obsoleto na API nível 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?) {
/**
* Esta parte será chamada sempre que a conexão de rede for alterada, por exemplo. Conectado -> Não Conectado
*/
if (!isAppOnForeground(context)) {
/** Iniciaremos nosso serviço e enviaremos informações extras sobre conexões de rede */
val hasInternet = isNetworkAvailable(context)
val serviceIntent = Intent(context, MyTaskService::class.java)
serviceIntent.putExtra("hasInternet", hasInternet)
context.startService(serviceIntent)
HeadlessJsTaskService.acquireWakeLockNow(context)
}
}
private fun isAppOnForeground(context: Context): Boolean {
/**
* Precisamos verificar se o aplicativo está em primeiro plano, caso contrário ele irá travar.
* 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 {
// obsoleto na API nível 29
cm.activeNetworkInfo?.run {
result =
when (type) {
ConnectivityManager.TYPE_WIFI -> true
ConnectivityManager.TYPE_MOBILE -> true
ConnectivityManager.TYPE_ETHERNET -> true
else -> false
}
}
}
}
return result
}
}
}