React Native 0.86 em 2026: Edge-to-Edge Android, DevTools e Perguntas de Entrevista

Domine o React Native 0.86 com suporte edge-to-edge para Android 15+, melhorias no DevTools e perguntas práticas de entrevista. Guia completo com exemplos de código.

Diagrama do fluxo de desenvolvimento edge-to-edge Android com React Native 0.86

O React Native 0.86 introduz suporte completo edge-to-edge para Android 15+, sendo a segunda versão consecutiva sem mudanças que quebram compatibilidade. Lançada em junho de 2026, esta versão corrige as APIs críticas de layout e medição que a aplicação obrigatória do edge-to-edge no Android estava quebrando silenciosamente.

Zero Mudanças que Quebram Compatibilidade

O React Native 0.86 mantém compatibilidade total com a versão 0.85. A atualização não requer modificações de código para a maioria das aplicações, tornando-a uma das transições de versão mais suaves na história do React Native.

Entendendo Edge-to-Edge no Android 15

Os layouts edge-to-edge permitem que o conteúdo se estenda sob os elementos de interface do sistema, como a barra de status e a barra de navegação. O Android 15 impõe esse comportamento por padrão, independentemente da configuração do aplicativo. O React Native 0.86 garante que todas as APIs de medição e layout funcionem corretamente nessas condições.

A função measureInWindow agora retorna coordenadas precisas quando o modo edge-to-edge está ativo. Anteriormente, as medições podiam estar deslocadas pela altura dos elementos de interface do sistema, causando bugs de posicionamento em overlays, tooltips e botões de ação flutuantes.

SafeAreaHandler.tsxtypescript
import React, { useRef, useCallback } from 'react';
import { View, TouchableOpacity, Text, StyleSheet } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';

interface TooltipPosition {
  x: number;
  y: number;
}

export function SafeAreaHandler() {
  const buttonRef = useRef<View>(null);
  const insets = useSafeAreaInsets();

  const showTooltip = useCallback(() => {
    // measureInWindow now returns correct coordinates on Android 15+
    buttonRef.current?.measureInWindow((x, y, width, height) => {
      const position: TooltipPosition = {
        x: x + width / 2,
        // Coordinates are accurate even with edge-to-edge enabled
        y: y + height + 8,
      };
      console.log('Tooltip position:', position);
    });
  }, []);

  return (
    <View style={[styles.container, { paddingTop: insets.top }]}>
      <View ref={buttonRef} collapsable={false}>
        <TouchableOpacity onPress={showTooltip} style={styles.button}>
          <Text style={styles.buttonText}>Show Tooltip</Text>
        </TouchableOpacity>
      </View>
    </View>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1, alignItems: 'center', justifyContent: 'center' },
  button: { backgroundColor: '#007AFF', padding: 16, borderRadius: 8 },
  buttonText: { color: '#FFFFFF', fontWeight: '600' },
});

A prop collapsable={false} garante que a View mantenha uma referência nativa para medições. Sem isso, o React Native pode otimizar a view nativa, fazendo com que measureInWindow falhe.

Correções do KeyboardAvoidingView para Android 15

O componente KeyboardAvoidingView calculava incorretamente o espaço disponível quando o modo edge-to-edge está ativo. A versão 0.86 corrige isso levando em conta os insets de interface do sistema no cálculo da altura disponível.

ChatInput.tsxtypescript
import React, { useState } from 'react';
import {
  KeyboardAvoidingView,
  TextInput,
  Platform,
  StyleSheet,
  View,
} from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';

export function ChatInput() {
  const [message, setMessage] = useState('');
  const insets = useSafeAreaInsets();

  return (
    <KeyboardAvoidingView
      // behavior="padding" works correctly on Android 15+ edge-to-edge
      behavior={Platform.OS === 'ios' ? 'padding' : 'padding'}
      style={styles.container}
      // keyboardVerticalOffset accounts for headers and safe areas
      keyboardVerticalOffset={Platform.select({ ios: 88, android: 0 })}
    >
      <View style={styles.content}>
        {/* Chat messages would render here */}
      </View>
      <View style={[styles.inputContainer, { paddingBottom: insets.bottom }]}>
        <TextInput
          value={message}
          onChangeText={setMessage}
          placeholder="Type a message..."
          style={styles.input}
          multiline
        />
      </View>
    </KeyboardAvoidingView>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1 },
  content: { flex: 1 },
  inputContainer: { borderTopWidth: 1, borderTopColor: '#E5E5E5', padding: 8 },
  input: { minHeight: 40, padding: 12, backgroundColor: '#F5F5F5', borderRadius: 20 },
});

No Android 15, usar behavior="height" frequentemente causa saltos de layout. O behavior padding fornece animações mais suaves e posicionamento mais previsível com edge-to-edge habilitado.

Atualizações da StatusBar Durante a Exibição de Modais

Um bug persistente impedia alterações de estilo e visibilidade da StatusBar enquanto um Modal estava aberto. O React Native 0.86 resolve isso, permitindo theming dinâmico da barra de status em fluxos modais.

ThemedModal.tsxtypescript
import React, { useState, useEffect } from 'react';
import { Modal, View, StatusBar, Text, Pressable, StyleSheet } from 'react-native';

interface ThemedModalProps {
  visible: boolean;
  onClose: () => void;
  darkMode?: boolean;
}

export function ThemedModal({ visible, onClose, darkMode = false }: ThemedModalProps) {
  // StatusBar changes now apply correctly while modal is visible
  useEffect(() => {
    if (visible) {
      StatusBar.setBarStyle(darkMode ? 'light-content' : 'dark-content', true);
      StatusBar.setBackgroundColor(darkMode ? '#1A1A1A' : '#FFFFFF', true);
    }
    return () => {
      // Restore original status bar when modal closes
      StatusBar.setBarStyle('dark-content', true);
      StatusBar.setBackgroundColor('#FFFFFF', true);
    };
  }, [visible, darkMode]);

  return (
    <Modal visible={visible} animationType="slide" onRequestClose={onClose}>
      <View style={[styles.modalContent, darkMode && styles.darkBackground]}>
        <StatusBar
          barStyle={darkMode ? 'light-content' : 'dark-content'}
          backgroundColor={darkMode ? '#1A1A1A' : '#FFFFFF'}
        />
        <Text style={[styles.title, darkMode && styles.lightText]}>
          Modal Content
        </Text>
        <Pressable onPress={onClose} style={styles.closeButton}>
          <Text style={styles.closeButtonText}>Close</Text>
        </Pressable>
      </View>
    </Modal>
  );
}

const styles = StyleSheet.create({
  modalContent: { flex: 1, justifyContent: 'center', alignItems: 'center', backgroundColor: '#FFFFFF' },
  darkBackground: { backgroundColor: '#1A1A1A' },
  title: { fontSize: 24, fontWeight: 'bold', color: '#000000' },
  lightText: { color: '#FFFFFF' },
  closeButton: { marginTop: 20, padding: 12, backgroundColor: '#007AFF', borderRadius: 8 },
  closeButtonText: { color: '#FFFFFF', fontWeight: '600' },
});

Essa correção é particularmente importante para aplicações que implementam alternadores de modo escuro em fluxos modais, como telas de configurações ou visualizadores de imagens.

Pronto para mandar bem nas entrevistas de React Native?

Pratique com nossos simuladores interativos, flashcards e testes tecnicos.

Emulação de Modo Claro e Escuro no DevTools

O React Native DevTools introduz suporte para Emulation.setEmulatedMedia, permitindo troca instantânea de tema sem modificar a configuração do dispositivo ou emulador. Essa funcionalidade é acessada através da Paleta de Comandos com Cmd+Shift+P (macOS) ou Ctrl+Shift+P (Windows/Linux).

ThemeProvider.tsxtypescript
import React, { createContext, useContext, useState, useEffect } from 'react';
import { useColorScheme, Appearance } from 'react-native';

type Theme = 'light' | 'dark';

interface ThemeContextValue {
  theme: Theme;
  toggleTheme: () => void;
  colors: typeof lightColors;
}

const lightColors = {
  background: '#FFFFFF',
  surface: '#F5F5F5',
  text: '#000000',
  textSecondary: '#666666',
  primary: '#007AFF',
  border: '#E5E5E5',
};

const darkColors = {
  background: '#1A1A1A',
  surface: '#2D2D2D',
  text: '#FFFFFF',
  textSecondary: '#A0A0A0',
  primary: '#0A84FF',
  border: '#3D3D3D',
};

const ThemeContext = createContext<ThemeContextValue | null>(null);

export function ThemeProvider({ children }: { children: React.ReactNode }) {
  // DevTools emulation triggers colorScheme changes via setEmulatedMedia
  const systemColorScheme = useColorScheme();
  const [theme, setTheme] = useState<Theme>(systemColorScheme ?? 'light');

  useEffect(() => {
    // Listen for DevTools theme emulation changes
    const subscription = Appearance.addChangeListener(({ colorScheme }) => {
      if (colorScheme) {
        setTheme(colorScheme);
      }
    });
    return () => subscription?.remove();
  }, []);

  const toggleTheme = () => setTheme(prev => (prev === 'light' ? 'dark' : 'light'));
  const colors = theme === 'dark' ? darkColors : lightColors;

  return (
    <ThemeContext.Provider value={{ theme, toggleTheme, colors }}>
      {children}
    </ThemeContext.Provider>
  );
}

export function useTheme(): ThemeContextValue {
  const context = useContext(ThemeContext);
  if (!context) {
    throw new Error('useTheme must be used within ThemeProvider');
  }
  return context;
}

O estado de emulação de mídia é reiniciado quando o DevTools se desconecta, garantindo que as builds de produção não sejam afetadas pelos testes de tema realizados durante o desenvolvimento.

Perguntas de Entrevista: React Native 0.86 e Edge-to-Edge

As entrevistas técnicas em 2026 estão cada vez mais focadas na compatibilidade com Android 15 e implementação edge-to-edge. Dominar esses conceitos demonstra compreensão das práticas modernas de desenvolvimento móvel. Para uma preparação completa de entrevistas, é possível explorar o módulo perguntas de entrevista sobre debugging em React Native.

O que é o modo edge-to-edge e por que o Android 15 o impõe?

O modo edge-to-edge estende o conteúdo do aplicativo sob os elementos de interface do sistema (barra de status, barra de navegação, indicadores de gestos). O Android 15 impõe esse comportamento por padrão para fornecer experiências imersivas e interface consistente em toda a plataforma. Os aplicativos devem lidar com os insets de área segura para evitar que o conteúdo seja ocultado pela interface do sistema.

Como o comportamento do measureInWindow muda com edge-to-edge habilitado?

Antes do React Native 0.86, measureInWindow retornava coordenadas relativas à janela visível, que excluía as áreas de interface do sistema. Com edge-to-edge, a janela se estende até as bordas da tela, mas as medições estavam incorretamente deslocadas. A versão 0.86 garante que as coordenadas sejam precisas independentemente do estado edge-to-edge.

Qual é o behavior do KeyboardAvoidingView recomendado no Android 15?

Recomenda-se usar behavior="padding" no Android 15 com edge-to-edge habilitado. O behavior height pode causar saltos de layout porque o cálculo da altura disponível muda quando o teclado aparece. O behavior padding fornece transições mais suaves e posicionamento mais previsível.

Como os insets de área segura diferem entre iOS e Android edge-to-edge?

No iOS, os insets de área segura existem desde o iPhone X para o notch e indicador de início. No Android edge-to-edge, os insets incluem a altura da barra de status, altura da barra de navegação e áreas de indicador de gestos. A biblioteca react-native-safe-area-context fornece valores de insets multiplataforma através de useSafeAreaInsets().

Pressable android_ripple com PlatformColor

O React Native 0.86 adiciona suporte de PlatformColor à prop android_ripple, permitindo cores de ripple dinâmicas que respeitam as configurações de tema do sistema.

ThemedButton.tsxtypescript
import React from 'react';
import { Pressable, Text, PlatformColor, Platform, StyleSheet } from 'react-native';

interface ThemedButtonProps {
  title: string;
  onPress: () => void;
}

export function ThemedButton({ title, onPress }: ThemedButtonProps) {
  return (
    <Pressable
      onPress={onPress}
      style={({ pressed }) => [
        styles.button,
        pressed && styles.pressed,
      ]}
      android_ripple={{
        // PlatformColor adapts to system theme automatically
        color: Platform.OS === 'android'
          ? PlatformColor('?attr/colorControlHighlight')
          : undefined,
        borderless: false,
        foreground: true,
      }}
    >
      <Text style={styles.text}>{title}</Text>
    </Pressable>
  );
}

const styles = StyleSheet.create({
  button: {
    padding: 16,
    backgroundColor: Platform.select({
      android: PlatformColor('?attr/colorPrimary'),
      default: '#007AFF',
    }),
    borderRadius: 8,
    alignItems: 'center',
  },
  pressed: { opacity: 0.8 },
  text: { color: '#FFFFFF', fontWeight: '600', fontSize: 16 },
});

Usar ?attr/colorControlHighlight garante que a cor do ripple corresponda às diretrizes do Material Design e se adapte aos temas claro/escuro configurados no nível do sistema.

Image.getSize Retorna Dimensões Precisas

Os métodos Image.getSize e Image.getSizeWithHeaders agora retornam as dimensões originais da imagem em vez de valores subamostrados. Essa correção é essencial para implementar cálculos corretos de proporção de aspecto e layouts de imagem responsivos.

ResponsiveImage.tsxtypescript
import React, { useState, useEffect } from 'react';
import { Image, View, useWindowDimensions, StyleSheet } from 'react-native';

interface ResponsiveImageProps {
  uri: string;
  maxHeight?: number;
}

export function ResponsiveImage({ uri, maxHeight = 400 }: ResponsiveImageProps) {
  const { width: screenWidth } = useWindowDimensions();
  const [dimensions, setDimensions] = useState({ width: screenWidth, height: 200 });

  useEffect(() => {
    // getSize now returns true source dimensions, not downsampled values
    Image.getSize(
      uri,
      (width, height) => {
        const aspectRatio = width / height;
        let calculatedHeight = screenWidth / aspectRatio;
        
        // Constrain to maxHeight while maintaining aspect ratio
        if (calculatedHeight > maxHeight) {
          calculatedHeight = maxHeight;
        }
        
        setDimensions({
          width: screenWidth,
          height: calculatedHeight,
        });
      },
      (error) => {
        console.error('Failed to get image size:', error);
      }
    );
  }, [uri, screenWidth, maxHeight]);

  return (
    <View style={styles.container}>
      <Image
        source={{ uri }}
        style={{ width: dimensions.width, height: dimensions.height }}
        resizeMode="cover"
      />
    </View>
  );
}

const styles = StyleSheet.create({
  container: { width: '100%' },
});

Essa correção elimina a necessidade de soluções alternativas que obtinham as dimensões da imagem através de módulos nativos ou processamento de imagens do lado do servidor.

Tratamento de Grandes Respostas HTTP Sem OutOfMemoryError

O NetworkingModule anteriormente lançava OutOfMemoryError ao tratar grandes respostas HTTP. A versão 0.86 implementa processamento em chunks para respostas que excedem os limites de memória.

LargeFileDownload.tsxtypescript
import { useState, useCallback } from 'react';

interface DownloadProgress {
  loaded: number;
  total: number;
  percentage: number;
}

export function useLargeFileDownload() {
  const [progress, setProgress] = useState<DownloadProgress | null>(null);
  const [error, setError] = useState<string | null>(null);

  const downloadFile = useCallback(async (url: string): Promise<Blob | null> => {
    try {
      setError(null);
      setProgress({ loaded: 0, total: 0, percentage: 0 });

      const response = await fetch(url);
      
      if (!response.ok) {
        throw new Error(`HTTP ${response.status}: ${response.statusText}`);
      }

      const contentLength = response.headers.get('Content-Length');
      const total = contentLength ? parseInt(contentLength, 10) : 0;

      // NetworkingModule now handles large responses without OutOfMemoryError
      const reader = response.body?.getReader();
      if (!reader) {
        throw new Error('Response body is not readable');
      }

      const chunks: Uint8Array[] = [];
      let loaded = 0;

      while (true) {
        const { done, value } = await reader.read();
        if (done) break;
        
        chunks.push(value);
        loaded += value.length;
        
        setProgress({
          loaded,
          total,
          percentage: total > 0 ? Math.round((loaded / total) * 100) : 0,
        });
      }

      const blob = new Blob(chunks);
      setProgress({ loaded, total, percentage: 100 });
      return blob;
    } catch (err) {
      const message = err instanceof Error ? err.message : 'Download failed';
      setError(message);
      return null;
    }
  }, []);

  return { downloadFile, progress, error };
}

Para downloads de arquivos que excedam 50MB, recomenda-se usar react-native-blob-util ou bibliotecas similares de manipulação de arquivos nativos para melhor gerenciamento de memória.

O header Cookie passado através do construtor WebSocket estava sendo removido antes de chegar ao servidor. Isso impedia a autenticação baseada em sessões para conexões WebSocket.

AuthenticatedWebSocket.tstypescript
type MessageHandler = (data: string) => void;
type ErrorHandler = (error: Event) => void;

interface WebSocketOptions {
  url: string;
  sessionCookie: string;
  onMessage: MessageHandler;
  onError?: ErrorHandler;
}

export function createAuthenticatedWebSocket({
  url,
  sessionCookie,
  onMessage,
  onError,
}: WebSocketOptions): WebSocket {
  // Cookie header now reaches the server correctly in 0.86
  const ws = new WebSocket(url, undefined, {
    headers: {
      Cookie: sessionCookie,
      // Additional headers as needed
      'X-Client-Version': '1.0.0',
    },
  });

  ws.onopen = () => {
    console.log('WebSocket connected with session cookie');
  };

  ws.onmessage = (event) => {
    if (typeof event.data === 'string') {
      onMessage(event.data);
    }
  };

  ws.onerror = (error) => {
    console.error('WebSocket error:', error);
    onError?.(error);
  };

  ws.onclose = (event) => {
    console.log(`WebSocket closed: ${event.code} ${event.reason}`);
  };

  return ws;
}

Essa correção é crítica para aplicações que usam gerenciamento de sessões baseado em cookies junto com funcionalidades em tempo real. O módulo perguntas de entrevista sobre networking em React Native cobre em profundidade os padrões de implementação WebSocket.

Conclusão

  • O React Native 0.86 entrega as correções edge-to-edge obrigatórias para Android 15 sem mudanças que quebram compatibilidade
  • measureInWindow, KeyboardAvoidingView e Dimensions funcionam corretamente com edge-to-edge habilitado
  • As atualizações da StatusBar agora se aplicam enquanto os componentes Modal estão visíveis
  • A emulação de tema no DevTools acelera o desenvolvimento e testes do modo escuro
  • O suporte de PlatformColor no android_ripple permite feedback tátil adaptado ao tema
  • Image.getSize retorna dimensões originais precisas para layouts responsivos
  • Grandes respostas HTTP não causam mais OutOfMemoryError
  • Os headers Cookie do WebSocket chegam ao servidor como esperado

Comece a praticar!

Teste seus conhecimentos com nossos simuladores de entrevista e testes tecnicos.

Tags

#react-native
#android
#mobile-development
#edge-to-edge
#devtools

Compartilhar

Artigos relacionados