Cómo minimizar el efecto de las actualizaciones regulares

Las solicitudes que tu app le realiza a la red son una de las principales causas del agotamiento de la batería porque activan radios móviles o Wi-Fi que consumen mucha energía. Además de la energía necesaria para enviar y recibir paquetes, estas radios consumen energía adicional solo cuando se activan y se mantienen activas. Algo tan simple como una solicitud de red cada 15 segundos puede mantener activada la radio móvil de forma continua y agotar rápidamente la batería.

Hay tres tipos generales de actualizaciones regulares:

  • Iniciada por el usuario: Realizar una actualización en función de algún comportamiento del usuario, como un gesto de "deslizar para actualizar"
  • Iniciado por la app: Realizar actualizaciones de forma recurrente
  • Iniciada por el servidor: Realizar una actualización en respuesta a una notificación de un servidor

En este tema, se analizan cada una de ellas y se analizan otras formas de optimizarlas para reducir el agotamiento de la batería.

Cómo optimizar las solicitudes iniciadas por el usuario

Las solicitudes iniciadas por el usuario generalmente ocurren en respuesta a algún comportamiento del usuario. Por ejemplo, una app que se usa para leer los artículos de noticias más recientes puede permitir que el usuario realice un gesto de "deslizar hacia abajo para actualizar" y buscar artículos nuevos. Puedes usar las siguientes técnicas para responder a solicitudes iniciadas por el usuario y, al mismo tiempo, optimizar el uso de la red.

Limita las solicitudes de los usuarios

Te recomendamos ignorar algunas solicitudes iniciadas por el usuario si no son necesarias, como varios gestos de "deslizar hacia abajo para actualizar" durante un período breve para verificar datos nuevos mientras los datos actuales aún están actualizados. Actuar en función de cada solicitud podría desperdiciar una cantidad significativa de energía al mantener la radio activa. Un enfoque más eficiente es limitar las solicitudes iniciadas por el usuario, de modo que solo se pueda realizar una solicitud durante un período, lo que reduce la frecuencia con la que se usa la radio.

Cómo usar una caché

Cuando almacenas en caché los datos de tu app, creas una copia local de la información a la que tu app necesita hacer referencia. Luego, la app puede acceder a la misma copia local de la información varias veces sin tener que abrir una conexión de red para realizar nuevas solicitudes.

Debes almacenar en caché los datos de la manera más agresiva posible, incluidos los recursos estáticos y las descargas a pedido, como las imágenes de tamaño completo. Puedes usar encabezados de caché HTTP para asegurarte de que tu estrategia de almacenamiento en caché no muestre datos inactivos en tu app. Para obtener más información sobre cómo almacenar respuestas de red en caché, consulta Cómo evitar descargas redundantes.

En Android 11 y versiones posteriores, tu app puede usar los mismos conjuntos de datos grandes que otras apps usan para casos de uso como aprendizaje automático y reproducción de contenido multimedia. Cuando tu app necesita acceder a un conjunto de datos compartido, primero puede buscar una versión almacenada en caché antes de intentar descargar una copia nueva. Para obtener más información sobre los conjuntos de datos compartidos, consulta Accede a conjuntos de datos compartidos.

Cómo usar mayor ancho de banda para descargar más datos con menos frecuencia

Cuando se conecta mediante una radio inalámbrica, un ancho de banda mayor suele tener el mismo costo de batería, lo que significa que la conectividad 5G suele consumir más energía que el LTE, que a su vez es más costosa que la 3G.

Esto significa que, si bien el estado de radio subyacente varía según la tecnología de radio, en general, el impacto relativo de la batería del tiempo de cola de cambio de estado es mayor en radios con ancho de banda más alto. Para obtener más información sobre el tiempo de cola, consulta La máquina de estado de radio.

Al mismo tiempo, un mayor ancho de banda significa que puedes realizar cargas previas de forma más activa y descargar más datos al mismo tiempo. Quizás de manera menos intuitiva, debido a que el costo de la batería del tiempo de cola es relativamente más alto, también es más eficiente mantener la radio activa durante períodos más largos durante cada sesión de transferencia para reducir la frecuencia de las actualizaciones.

Por ejemplo, si una radio LTE tiene el doble de ancho de banda y costo de energía de 3G, deberías descargar cuatro veces más datos durante cada sesión, o posiblemente hasta 10 MB. Cuando descargues esta cantidad de datos, es importante tener en cuenta el efecto de la carga previa en el almacenamiento local disponible y vaciar la caché de carga previa con regularidad.

Puedes usar ConnectivityManager para registrar un objeto de escucha en la red predeterminada y TelephonyManager para registrar un PhoneStateListener con el fin de determinar el tipo de conexión del dispositivo actual. Una vez que conozcas el tipo de conexión, podrás modificar las rutinas de carga previa según corresponda:

Kotlin

val cm = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val tm = getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager

private var hasWifi = false
private var hasCellular = false
private var cellModifier: Float = 1f

private val networkCallback = object : ConnectivityManager.NetworkCallback() {
    // Network capabilities have changed for the network
    override fun onCapabilitiesChanged(
            network: Network,
            networkCapabilities: NetworkCapabilities
    ) {
        super.onCapabilitiesChanged(network, networkCapabilities)
        hasCellular = networkCapabilities
    .hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)
        hasWifi = networkCapabilities
    .hasTransport(NetworkCapabilities.TRANSPORT_WIFI)
    }
}

private val phoneStateListener = object : PhoneStateListener() {
override fun onPreciseDataConnectionStateChanged(
    dataConnectionState: PreciseDataConnectionState
) {
  cellModifier = when (dataConnectionState.networkType) {
      TelephonyManager.NETWORK_TYPE_LTE or TelephonyManager.NETWORK_TYPE_HSPAP -> 4f
      TelephonyManager.NETWORK_TYPE_EDGE or TelephonyManager.NETWORK_TYPE_GPRS -> 1/2f
      else -> 1f

  }
}

private class NetworkState {
    private var defaultNetwork: Network? = null
    private var defaultCapabilities: NetworkCapabilities? = null
    fun setDefaultNetwork(network: Network?, caps: NetworkCapabilities?) = synchronized(this) {
        defaultNetwork = network
        defaultCapabilities = caps
    }
    val isDefaultNetworkWifi
        get() = synchronized(this) {
            defaultCapabilities?.hasTransport(TRANSPORT_WIFI) ?: false
        }
    val isDefaultNetworkCellular
        get() = synchronized(this) {
            defaultCapabilities?.hasTransport(TRANSPORT_CELLULAR) ?: false
        }
    val isDefaultNetworkUnmetered
        get() = synchronized(this) {
            defaultCapabilities?.hasCapability(NET_CAPABILITY_NOT_METERED) ?: false
        }
    var cellNetworkType: Int = TelephonyManager.NETWORK_TYPE_UNKNOWN
        get() = synchronized(this) { field }
        set(t) = synchronized(this) { field = t }
    private val cellModifier: Float
        get() = synchronized(this) {
            when (cellNetworkType) {
                TelephonyManager.NETWORK_TYPE_LTE or TelephonyManager.NETWORK_TYPE_HSPAP -> 4f
                TelephonyManager.NETWORK_TYPE_EDGE or TelephonyManager.NETWORK_TYPE_GPRS -> 1 / 2f
                else -> 1f
            }
        }
    val prefetchCacheSize: Int
        get() = when {
            isDefaultNetworkWifi -> MAX_PREFETCH_CACHE
            isDefaultNetworkCellular -> (DEFAULT_PREFETCH_CACHE * cellModifier).toInt()
            else -> DEFAULT_PREFETCH_CACHE
        }
}
private val networkState = NetworkState()
private val networkCallback = object : ConnectivityManager.NetworkCallback() {
    // Network capabilities have changed for the network
    override fun onCapabilitiesChanged(
            network: Network,
            networkCapabilities: NetworkCapabilities
    ) {
        networkState.setDefaultNetwork(network, networkCapabilities)
    }

    override fun onLost(network: Network?) {
        networkState.setDefaultNetwork(null, null)
    }
}

private val telephonyCallback = object : TelephonyCallback(), TelephonyCallback.PreciseDataConnectionStateListener {
    override fun onPreciseDataConnectionStateChanged(dataConnectionState: PreciseDataConnectionState) {
        networkState.cellNetworkType = dataConnectionState.networkType
    }
}

connectivityManager.registerDefaultNetworkCallback(networkCallback)
telephonyManager.registerTelephonyCallback(telephonyCallback)


private val prefetchCacheSize: Int
get() {
    return when {
        hasWifi -> MAX_PREFETCH_CACHE
        hasCellular -> (DEFAULT_PREFETCH_CACHE * cellModifier).toInt()
        else -> DEFAULT_PREFETCH_CACHE
    }
}

}

Java

ConnectivityManager cm =
 (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
TelephonyManager tm =
  (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE);

private boolean hasWifi = false;
private boolean hasCellular = false;
private float cellModifier = 1f;

private ConnectivityManager.NetworkCallback networkCallback = new ConnectivityManager.NetworkCallback() {
@Override
public void onCapabilitiesChanged(
    @NonNull Network network,
    @NonNull NetworkCapabilities networkCapabilities
) {
        super.onCapabilitiesChanged(network, networkCapabilities);
        hasCellular = networkCapabilities
    .hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR);
        hasWifi = networkCapabilities
    .hasTransport(NetworkCapabilities.TRANSPORT_WIFI);
}
};

private PhoneStateListener phoneStateListener = new PhoneStateListener() {
@Override
public void onPreciseDataConnectionStateChanged(
    @NonNull PreciseDataConnectionState dataConnectionState
    ) {
    switch (dataConnectionState.getNetworkType()) {
        case (TelephonyManager.NETWORK_TYPE_LTE |
            TelephonyManager.NETWORK_TYPE_HSPAP):
            cellModifier = 4;
            Break;
        case (TelephonyManager.NETWORK_TYPE_EDGE |
            TelephonyManager.NETWORK_TYPE_GPRS):
            cellModifier = 1/2.0f;
            Break;
        default:
            cellModifier = 1;
            Break;
    }
}
};

cm.registerDefaultNetworkCallback(networkCallback);
tm.listen(
phoneStateListener,
PhoneStateListener.LISTEN_PRECISE_DATA_CONNECTION_STATE
);

public int getPrefetchCacheSize() {
if (hasWifi) {
    return MAX_PREFETCH_SIZE;
}
if (hasCellular) {
    return (int) (DEFAULT_PREFETCH_SIZE * cellModifier);
    }
return DEFAULT_PREFETCH_SIZE;
}

Cómo optimizar las solicitudes iniciadas por la app

Las solicitudes iniciadas por la app suelen ocurrir según un programa, como una app que envía registros o estadísticas a un servicio de backend. Cuando trates con solicitudes iniciadas por la app, ten en cuenta la prioridad de esas solicitudes, si se pueden agrupar en lotes y si se pueden diferir hasta que el dispositivo se esté cargando o se conecte a una red no medida. Estas solicitudes se pueden optimizar con una programación cuidadosa y con bibliotecas como WorkManager.

Solicitudes de red por lotes

En un dispositivo móvil, el proceso de encender la radio, establecer una conexión y mantener la radio activa consume una gran cantidad de energía. Por este motivo, el procesamiento de solicitudes individuales en momentos aleatorios puede consumir una cantidad significativa de energía y reducir la duración de la batería. Un enfoque más eficiente es poner en cola un conjunto de solicitudes de red y procesarlas juntas. De esta manera, el sistema puede pagar el costo de energía de encender la radio una sola vez y seguir obteniendo todos los datos que solicita una app.

Cómo usar WorkManager

Puedes usar la biblioteca de WorkManager para realizar trabajos según un programa eficiente que considere si se cumplen condiciones específicas, como la disponibilidad de la red y el estado de batería. Por ejemplo, supongamos que tienes una subclase Worker llamada DownloadHeadlinesWorker que recupera los titulares de noticias más recientes. Se puede programar este trabajador para que se ejecute cada hora, siempre que el dispositivo esté conectado a una red no medida y que la batería del dispositivo no sea baja. También se puede programar una estrategia de reintento personalizada si surgen problemas para recuperar los datos, tal como se muestra a continuación:

Kotlin

val constraints = Constraints.Builder()
    .setRequiredNetworkType(NetworkType.UNMETERED)
    .setRequiresBatteryNotLow(true)
    .build()
val request =
    PeriodicWorkRequestBuilder<DownloadHeadlinesWorker>(1, TimeUnit.HOURS)
        .setConstraints(constraints)
        .setBackoffCriteria(BackoffPolicy.LINEAR, 1L, TimeUnit.MINUTES)
        .build()
WorkManager.getInstance(context).enqueue(request)

Java

Constraints constraints = new Constraints.Builder()
        .setRequiredNetworkType(NetworkType.UNMETERED)
        .setRequiresBatteryNotLow(true)
        .build();
WorkRequest request = new PeriodicWorkRequest.Builder(DownloadHeadlinesWorker.class, 1, TimeUnit.HOURS)
        .setBackoffCriteria(BackoffPolicy.LINEAR, 1L, TimeUnit.MINUTES)
        .build();
WorkManager.getInstance(this).enqueue(request);

Además de WorkManager, la plataforma de Android ofrece varias herramientas que te ayudan a crear una programación eficiente para completar tareas de red, como los sondeos. Si quieres obtener más información para usar estas herramientas, consulta la Guía para el procesamiento en segundo plano.

Cómo optimizar las solicitudes iniciadas por el servidor

Las solicitudes iniciadas por el servidor suelen ocurrir en respuesta a una notificación de un servidor. Por ejemplo, una app que se usa para leer los artículos de noticias más recientes puede recibir una notificación sobre un nuevo lote de artículos que se ajustan a las preferencias de personalización del usuario, que luego descarga.

Envía actualizaciones del servidor con Firebase Cloud Messaging

Firebase Cloud Messaging (FCM) es un mecanismo básico que se usa para transmitir datos de un servidor a una instancia de app en particular. Con FCM, el servidor puede notificar a la app en ejecución en un dispositivo en particular que hay nuevos datos disponibles para ella.

En comparación con el sondeo, en el que tu app debe hacer ping regularmente al servidor para buscar datos nuevos, este modelo basado en eventos permite que tu app cree una conexión nueva solo cuando sabe que hay datos para descargar. El modelo minimiza las conexiones innecesarias y reduce la latencia cuando actualiza la información dentro de la app.

FCM se implementa utilizando una conexión TCP/IP persistente. Esto minimiza la cantidad de conexiones persistentes y permite que la plataforma optimice el ancho de banda y minimice el impacto asociado en la duración de batería.