De Flutter a Android Nativo: Cómo Cambiar Tu Chip de Programador

De Flutter a Android Nativo: Cómo Cambiar Tu Chip de Programador

Guía profunda sobre el cambio de mentalidad necesario para pasar de Flutter a Android nativo con Kotlin y Jetpack Compose. Rompe hábitos, aprende nuevas formas de pensar, y abraza la cultura Android.

Por Omar Flores

De Flutter a Android Nativo: Cómo Cambiar Tu Chip de Programador

No Es Aprender Un Lenguaje. Es Cambiar Tu Forma de Pensar.


🧠 Introducción: Por Qué No Es Solo Sintaxis

Cuando migras de Flutter a Android, no es lo mismo que aprender Java después de C++. Es un cambio fundamental en cómo piensas sobre problemas, cómo estructuras soluciones, y cómo interactúas con el sistema operativo.

Muchos Flutter developers se acercan a Android con este mindset:

“OK, necesito la misma app funcionando en Android. Rápidamente. Con los mismos patterns que usé en Flutter.” performance-profiling-flutter-lento Eso es un error.

Android no es “Flutter but slower with more code”. Android es una plataforma con:

  • Una filosofía diferente
  • Convenciones establecidas hace 15 años
  • Expectativas diferentes sobre cómo debería funcionar tu código
  • Un ecosistema que funciona mejor si lo sigues, y peor si lo combates

Este post no es sobre sintaxis de Kotlin o cómo usar Compose. Es sobre cómo reprogramar tu mente de Flutter developer a Android developer.


🚀 Parte 1: El Mindset de Flutter vs El Mindset de Android

El Mundo de Flutter: “Escriba Una Vez, Ejecuta En Todas Partes”

En Flutter, tu mentalidad es:

1. Escribe el código UNA VEZ
2. Ejecuta en iOS
3. Ejecuta en Android
4. Ejecuta en Web
5. Profit

Las consecuencias de este mindset:

// Flutter way of thinking
// "Ignoro completamente la plataforma"
Container(
  width: 100,
  height: 100,
  child: Text("Botón"),
)

// No me importa:
// - Si es iOS o Android
// - Qué convenciones existen
// - Cómo otros apps funcionan
// - Material Design vs iOS HIG

El problema: Este mindset funciona para proyectos pequeños. Para proyectos grandes, ignorar la plataforma te sabotea.

El Mundo de Android: “Eres Parte de Un Ecosistema”

En Android, el mindset es opuesto:

1. Aprende cómo Android espera que hagas cosas
2. Sigue los patterns establecidos
3. Usa las APIs diseñadas para tu caso
4. Cuando algo no funciona, NO combates el sistema
5. Profit

Las consecuencias de este mindset:

// Android way of thinking
// "Sigo las convenciones de Android"

Button(
    onClick = { /* ... */ },
    modifier = Modifier.fillMaxWidth()
) {
    Text("Botón")
}

// Automáticamente tengo:
// - Material Design correcto
// - Accesibilidad
// - Tamaño correcto de touch target
// - Animaciones correctas
// - Ripple effects
// - Todas las convenciones Android

La diferencia es sutil pero profunda: No estás creando UI en un vacío. Estás participando en un sistema que otros 3 millones de developers entienden.


🎯 Parte 2: Cambios de Mentalidad Principales

Cambio 1: De “Escribe Una Vez” a “Escribe Bien Una Vez”

Mentalidad Flutter:

"La velocidad es todo. Hago que funcione rápido.
 Si necesito iOS después, duplico el código."

Mentalidad Android:

"Tomo el tiempo ahora para hacerlo bien.
 Si necesito cross-platform después, tengo arquitectura sólida."

Ejemplo práctico:

// ❌ Flutter thinking (no, en Android también es malo)
class TaskViewModel {
    var tasks = mutableListOf<Task>()  // Mutation everywhere
    var isLoading = false
    var error = ""

    fun loadTasks() {
        isLoading = true
        // Load...
        tasks = newTasks
        isLoading = false
    }
}

// ✅ Android thinking
data class TaskUiState(
    val tasks: List<Task> = emptyList(),
    val isLoading: Boolean = false,
    val error: String? = null
)

class TaskViewModel : ViewModel() {
    private val _uiState = MutableStateFlow(TaskUiState())
    val uiState: StateFlow<TaskUiState> = _uiState.asStateFlow()

    fun loadTasks() {
        viewModelScope.launch {
            _uiState.update { it.copy(isLoading = true) }
            try {
                val tasks = repository.getTasks()
                _uiState.update {
                    it.copy(
                        tasks = tasks,
                        isLoading = false,
                        error = null
                    )
                }
            } catch (e: Exception) {
                _uiState.update {
                    it.copy(
                        error = e.message,
                        isLoading = false
                    )
                }
            }
        }
    }
}

El código Android toma más tiempo escribir, pero:

  • Es testeable
  • Es predecible
  • Es mantenible
  • Es observable
  • Escala a 100+ screens sin degradación

Cambio 2: De “Controlar Todo” a “Confiar en El Framework”

Mentalidad Flutter:

"Implemento manualmente cada comportamiento.
 setState() cuando cambio algo.
 Provider para estado.
 BLoC si quiero estar fancy."

Mentalidad Android:

"Android tiene ViewModels, Flows, StateFlow.
 Tengo Lifecycle, Configuration Changes, Saved State.
 No lucho contra estas cosas, las uso."

Ejemplo: Configuration Changes (Rotaciones de pantalla)

// ❌ Flutter developer approach (NO en Android)
class TaskListViewModel {
    var tasks = mutableListOf<Task>()

    fun loadTasks() {
        // Cargar
        // Si el usuario rota... PERDEMOS LOS DATOS
    }
}

// ✅ Android developer approach
class TaskListViewModel : ViewModel() {
    // ViewModel SOBREVIVE configuration changes
    // No pierdes datos cuando el usuario rota

    private val _uiState = MutableStateFlow(TaskListUiState())
    val uiState: StateFlow<TaskListUiState> = _uiState.asStateFlow()

    init {
        loadTasks()
        // Si Android destruye la UI pero mantiene el ViewModel,
        // los datos persisten automáticamente
    }
}

@Composable
fun TaskListScreen(
    viewModel: TaskListViewModel = hiltViewModel()
    // hiltViewModel() automáticamente:
    // - Crea el ViewModel si no existe
    // - Sobrevive configuration changes
    // - Se destruye cuando sales del screen
    // - Inyecta dependencias
) {
    val uiState by viewModel.uiState.collectAsState()
    // ...
}

Flutter developer: “¿Qué? ¿El ViewModel sobrevive cambios de configuración? ¿Automáticamente?”

Android developer: “Sí. Eso es lo que hace.”

Cambio 3: De “Escribo Todo Yo” a “El Sistema Maneja Cosas Por Mí”

Mentalidad Flutter:

"Necesito:
 - Navegación: Implemento rutas
 - Dependency Injection: GetIt o manual
 - Temas: Lo hago todo en MaterialApp
 - Deep linking: Lo manejo en Firebase"

Mentalidad Android:

"Android proporciona:
 - Navegación: Navigation Compose
 - Dependency Injection: Hilt
 - Temas: Material3 + AppCompat
 - Deep linking: Intents y NavDeepLink

 Esto NO es opcional.
 Es así como funciona Android."

Ejemplo: Dependency Injection

// ❌ Flutter way
class TaskRepository {
    // Implemento todo manualmente
}

class TaskViewModel {
    val repository = TaskRepository()  // Direct instantiation

    fun loadTasks() {
        repository.getTasks()
    }
}

// ❌ Android way (viejo, no recomendado)
// Ahora Android espera Hilt
@HiltViewModel
class TaskViewModel @Inject constructor(
    private val taskRepository: TaskRepository
) : ViewModel() {
    fun loadTasks() {
        taskRepository.getTasks()
    }
}

// ✅ Configuración una vez
@Module
@InstallIn(SingletonComponent::class)
object TaskModule {
    @Provides
    fun provideTaskRepository(
        apiService: ApiService
    ): TaskRepository = TaskRepository(apiService)
}

// En cualquier lado
@HiltViewModel
class TaskViewModel @Inject constructor(
    private val taskRepository: TaskRepository
) : ViewModel() {
    // taskRepository inyectado automáticamente
}

Cambio 4: De “UI Reactiva” a “UI Observables”

Mentalidad Flutter:

"setState() para actualizar la UI
 Provider/BLoC si necesito state management
 Rebuild todo, dart es rápido"

Mentalidad Android:

"StateFlow/Flow para state management
 Collectores que reaccionan a cambios
 Recomposición optimizada automáticamente
 Menos rebuilds = mejor performance"

Ejemplo:

// ❌ Pensar en Flutter
class TaskListViewModel {
    var tasks: List<Task> = emptyList()

    fun loadTasks() {
        tasks = newTasks
        // Notify all listeners (setState implícito)
    }
}

// ✅ Pensar en Android
class TaskListViewModel : ViewModel() {
    private val _tasks = MutableStateFlow<List<Task>>(emptyList())
    val tasks: StateFlow<List<Task>> = _tasks.asStateFlow()

    fun loadTasks() {
        viewModelScope.launch {
            val newTasks = repository.getTasks()
            _tasks.value = newTasks
            // Solo los observers de tasks reaccionan
            // No afecta isLoading, error, etc.
        }
    }
}

// En el composable
@Composable
fun TaskListScreen(
    viewModel: TaskListViewModel = hiltViewModel()
) {
    val tasks by viewModel.tasks.collectAsState()
    val isLoading by viewModel.isLoading.collectAsState()

    // Si solo tasks cambió, isLoading no rebuilda
    // En Flutter, setState() rebuildaría todo
}

💡 Parte 3: Hábitos de Flutter Que Debes Romper

Hábito 1: “GetIt Para Everything”

// ❌ Flutter habit
GetIt.I<TaskRepository>().getTasks()
GetIt.I<ThemeService>().switchTheme()

// ✅ Android way
// Inyectar en constructor
class TaskViewModel @Inject constructor(
    private val taskRepository: TaskRepository,
    private val themeService: ThemeService
) : ViewModel() {
    // Ya lo tienes, sin necesidad de GetIt
}

Por qué: Hilt te da:

  • Inyección automática
  • Scopes (Singleton, Activity, ViewModel)
  • Verificación de tipos en tiempo de compilación
  • Testing simplificado

Hábito 2: “setState() Everywhere”

// ❌ Flutter habit (NO hagas esto en Android)
var tasks by mutableStateOf(emptyList<Task>())
var isLoading by mutableStateOf(false)

// Cuando cambias cosas:
tasks = newTasks  // Toda la UI rebuilda
isLoading = false

// ✅ Android way
data class TaskListUiState(
    val tasks: List<Task> = emptyList(),
    val isLoading: Boolean = false
)

private val _uiState = MutableStateFlow(TaskListUiState())

_uiState.update {
    it.copy(tasks = newTasks, isLoading = false)
}

Por qué: UiState hace explícito qué cambió. Las recomposiciones son precisas.

Hábito 3: “Provider/BLoC Anidado”

// ❌ Flutter habit
MultiProvider(
  providers: [
    ChangeNotifierProvider(create: (_) => TaskProvider()),
    ChangeNotifierProvider(create: (_) => ThemeProvider()),
    ChangeNotifierProvider(create: (_) => AuthProvider()),
  ],
  child: Consumer(
    builder: (context, taskProvider, _) {
      return Consumer2(
        builder: (context, themeProvider, authProvider, _) {
          // Nesting hell
        }
      )
    }
  )
)

// ✅ Android way
// No hay providers. Inyectas el ViewModel, listo.
@Composable
fun TaskListScreen(
    viewModel: TaskListViewModel = hiltViewModel(),
    themeViewModel: ThemeViewModel = hiltViewModel(),
    authViewModel: AuthViewModel = hiltViewModel()
) {
    // Cada ViewModel es independiente
    // No hay nesting
}

Hábito 4: “Ignorar Lifecycle”

// ❌ Flutter thinking
// "No hay lifecycle en Flutter, solo build()"

// ✅ Android thinking
// Android lifecycle es fundamental
@Composable
fun TaskListScreen(
    viewModel: TaskListViewModel = hiltViewModel()
) {
    LaunchedEffect(Unit) {  // onCompose
        viewModel.loadTasks()
    }

    DisposableEffect(Unit) {  // onDispose
        onDispose {
            viewModel.cleanupResources()
        }
    }
}

🏗️ Parte 4: Nuevos Conceptos Que Necesitas Abrazar

Concepto 1: Lifecycle

Android tiene un concepto central: Lifecycle. Los componentes nacen, viven, y mueren.

onCreate() → onStart() → onResume() → onPause() → onStop() → onDestroy()
// El ViewModel sobrevive onCreate() → onDestroy()
// Pero muere con onDestroy()

class TaskListViewModel : ViewModel() {
    init {
        println("ViewModel creado")  // Imprime una vez
    }

    override fun onCleared() {
        println("ViewModel destruido")  // Imprime cuando sales del screen
    }
}

// Composables reaccionan al lifecycle del ViewModel
@Composable
fun TaskListScreen(
    viewModel: TaskListViewModel = hiltViewModel()
) {
    LaunchedEffect(Unit) {
        // Esto corre cuando TaskListScreen entra en composición
        // Se cancela automáticamente cuando sale
    }
}

Concepto 2: Configuration Changes

Cuando el usuario rota la pantalla, Android:

1. Destruye el Activity/Composable
2. Crea uno nuevo con la configuración new
3. El ViewModel sobrevive automáticamente
4. Los datos persisten
class TaskListViewModel : ViewModel() {
    private val _tasks = MutableStateFlow<List<Task>>(emptyList())

    init {
        loadTasks()  // Corre una vez
        // Si el usuario rota, esto NO corre de nuevo
        // El ViewModel persiste
    }
}

En Flutter, debes manejar rotación explícitamente. En Android, es automático.

Concepto 3: Scopes en Hilt

// Singleton: Una instancia en toda la app
@Module
@InstallIn(SingletonComponent::class)
object AppModule {
    @Provides
    @Singleton
    fun provideApiService(): ApiService = ApiService()
}

// Activity scope: Una instancia por Activity
@Module
@InstallIn(ActivityComponent::class)
object ActivityModule {
    @Provides
    fun provideActivityHelper(): ActivityHelper = ActivityHelper()
}

// ViewModel scope: Una instancia por ViewModel
@Module
@InstallIn(ViewModelComponent::class)
object ViewModelModule {
    @Provides
    fun provideTaskRepository(api: ApiService): TaskRepository {
        return TaskRepository(api)
    }
}

Flutter developer: “¿Debo realmente preocuparme por dónde crear las instancias?”

Android developer: “Sí. Los scopes incorrectos causan memory leaks o comportamientos inesperados.”

Concepto 4: Material Design No Es Opcional

// ❌ Flutter thinking
// "Diseño mis propios componentes"

// ✅ Android thinking
@Composable
fun TaskScreen() {
    Scaffold(
        topBar = { TopAppBar(title = { Text("Tasks") }) },
        floatingActionButton = { FloatingActionButton(...) },
        content = { padding ->
            LazyColumn(contentPadding = padding) {
                // Content
            }
        }
    )
}

En Android, seguir Material Design no es una preferencia. Es la expectativa.


🎓 Parte 5: Nuevas Prácticas Que Debes Adoptar

Práctica 1: Test-Driven Development

// En Flutter: "Testeo al final"
// En Android: "Testeo mientras desarrollo"

class TaskViewModelTest {
    @get:Rule
    val instantExecutorRule = InstantTaskExecutorRule()

    @Test
    fun loadTasks_setsCorrectState() {
        val viewModel = TaskListViewModel(fakeRepository)

        viewModel.loadTasks()

        val state = viewModel.uiState.value
        assertThat(state.tasks).isEqualTo(expectedTasks)
        assertThat(state.isLoading).isFalse()
    }
}

Práctica 2: Composition Over Inheritance

// ❌ Flutter way (inheritance)
class TaskDetailViewModel extends BaseViewModel {
    // Hereda loadData(), handleError(), etc.
}

// ✅ Android way (composition)
class TaskDetailViewModel(
    private val loadTaskUseCase: LoadTaskUseCase,
    private val deleteTaskUseCase: DeleteTaskUseCase
) : ViewModel() {
    fun loadTask(id: Int) {
        loadTaskUseCase(id)
    }
}

Práctica 3: Clean Architecture Layers

presentation/
├── ui/
│   ├── screens/
│   └── composables/
└── viewmodels/

domain/
├── models/
├── usecases/
└── repositories/

data/
├── remote/
├── local/
└── repositories/

No es una sugerencia. Es cómo se organiza Android.

Práctica 4: Coroutines, No Callbacks

// ❌ Callback hell (Android viejo)
repository.getTasks(object : Callback {
    override fun onSuccess(tasks: List<Task>) {
        api.getUser(userId, object : Callback {
            override fun onSuccess(user: User) {
                // Callback hell
            }
        })
    }
})

// ✅ Coroutines (Android moderno)
viewModelScope.launch {
    val tasks = repository.getTasks()
    val user = api.getUser(userId)

    _uiState.update {
        it.copy(tasks = tasks, user = user)
    }
}

🚨 Parte 6: Errores Comunes de Flutter Developers en Android

Error 1: Crear Composables Con Lógica Pesada

// ❌ Malo
@Composable
fun TaskListScreen(
    onNavigateToDetail: (Int) -> Unit
) {
    val tasks = remember { mutableStateOf(emptyList<Task>()) }

    LaunchedEffect(Unit) {
        val apiService = ApiService()
        val repo = TaskRepository(apiService)
        val newTasks = repo.getTasks()
        tasks.value = newTasks
    }

    LazyColumn {
        items(tasks.value) { task ->
            TaskItem(task = task, onClick = {
                onNavigateToDetail(task.id)
            })
        }
    }
}

// ✅ Bueno
@Composable
fun TaskListScreen(
    viewModel: TaskListViewModel = hiltViewModel(),
    onNavigateToDetail: (Int) -> Unit
) {
    val uiState by viewModel.uiState.collectAsState()

    TaskListContent(
        uiState = uiState,
        onTaskClick = { viewModel.onTaskClick(it) }
    )

    LaunchedEffect(Unit) {
        viewModel.event.collect { event ->
            when (event) {
                is NavigateToDetail -> onNavigateToDetail(event.taskId)
            }
        }
    }
}

Error 2: No Usar SavedStateHandle Para Persistencia

// ❌ Flutter thinking
class TaskDetailViewModel(
    private val taskId: Int  // Perdido después de configuration change
) : ViewModel() {
    // Si el usuario rota, taskId se pierde
}

// ✅ Android way
@HiltViewModel
class TaskDetailViewModel @Inject constructor(
    private val savedStateHandle: SavedStateHandle,
    private val repository: TaskRepository
) : ViewModel() {
    val taskId: Int = savedStateHandle.get<Int>("taskId") ?: 0

    // Si el usuario rota, savedStateHandle persiste automáticamente
}

Error 3: Mutation Everywhere

// ❌ Malo (Flutter style)
class TaskListViewModel : ViewModel() {
    var tasks = listOf<Task>()
    var isLoading = false

    fun loadTasks() {
        tasks = newTasks
        isLoading = false
        // Nadie sabe cuándo cambió qué
    }
}

// ✅ Bueno (Android style)
class TaskListViewModel : ViewModel() {
    private val _uiState = MutableStateFlow(TaskListUiState())
    val uiState: StateFlow<TaskListUiState> = _uiState.asStateFlow()

    fun loadTasks() {
        _uiState.update { it.copy(
            tasks = newTasks,
            isLoading = false
        )}
        // Explícitamente un nuevo state
    }
}

Error 4: Resource Leaks

// ❌ Malo
class TaskDetailViewModel : ViewModel() {
    private val channel = Channel<Event>()  // NUNCA se cierra

    fun someFunction() {
        viewModelScope.launch {
            channel.send(Event.ShowMessage)
        }
    }
}

// ✅ Bueno
class TaskDetailViewModel : ViewModel() {
    private val _event = Channel<Event>(capacity = Channel.BUFFERED)
    val event: Flow<Event> = _event.receiveAsFlow()

    override fun onCleared() {
        _event.cancel()
        super.onCleared()
    }
}

🎯 Parte 7: Roadmap de Transición Práctica

Semana 1-2: Mentalidad Básica

  • Entiende ViewModel y StateFlow
  • Aprende sobre Android Lifecycle
  • Haz pequeños proyectos con MVVM
  • Lee Google’s Architecture Guide

Semana 3-4: Fundamentos

  • Domina Hilt y Dependency Injection
  • Aprende Navigation Compose
  • Entiende Configuration Changes
  • Testing con Compose

Semana 5-6: Patterns

  • Clean Architecture layers
  • Repository pattern
  • UseCase pattern
  • State management avanzado

Semana 7-8: Real World

  • Integra con API real
  • Manejo de errores robusto
  • Caching y persistencia
  • Deep linking

Semana 9+: Mastery

  • Optimizaciones de performance
  • Testing exhaustivo
  • Código review desde perspectiva Android
  • Contribuye a proyectos open source

💬 Parte 8: Conversación Mental Que Debes Tener

Cuando Piensas: “Esto Es Mucho Boilerplate”

Tu mente: “En Flutter escribía 10 líneas, aquí son 50”

Verdad: Ese boilerplate es:

  • Explicitness (qué cambió)
  • Testability (puedes inyectar)
  • Maintainability (otros lo entienden)
  • Type safety (compiler te ayuda)

Es mejor que simplicidad superficial.

Cuando Piensas: “¿Por Qué No Puedo Hacer Esto De Forma Simple?”

Tu mente: “En Flutter lo haría así…”

Verdad: Android tiene constraints que Flutter ignora:

  • Lifecycle management
  • Memory constraints
  • Configuration changes
  • System integration

Trabajar DENTRO de los constraints es más fácil que contra ellos.

Cuando Piensas: “Este Patrón Parece Excesivo”

Tu mente: “¿Necesito realmente un UseCase aquí?”

Verdad: Los patterns existen porque:

  • Apps crecen (10 screens → 50 → 100+)
  • Requisitos cambian
  • Los developers turnover
  • El código debe ser comprensible en 6 meses

Invierte en patrones ahora, ahorra tiempo después.


✅ Checklist: Has Realmente Cambiado Tu Chip

  • Escribes UiState data classes automáticamente
  • Piensas en ViewModels antes de UI
  • Entiendes y usas Lifecycle sin pensarlo
  • Sabes dónde poner cada cosa (presentation/domain/data)
  • Inyectas dependencias, no las creas
  • Tus composables son puros (sin lógica de negocio)
  • Testeas tu código regularmente
  • Entendes Configuration Changes sin cuestionar
  • Usas StateFlow/Flow naturalmente
  • Eres crítico con el código “Flutter style”

🏁 Conclusión: De Visitante a Nativo

Cuando cambias de Flutter a Android, pasas de ser un visitante en la plataforma a un nativo.

Como visitante:

  • Sigues las mapas turísticas (documentación)
  • No entiendes las convenciones locales
  • Cometes errores culturales
  • Eres ineficiente

Como nativo:

  • Sabes cómo funciona todo
  • Tus decisiones alinean con la cultura
  • Eres productivo
  • Tu código es mantenible

El tiempo invertido en cambiar tu mentalidad no es tiempo perdido. Es la mejor inversión que puedes hacer.

Los patterns, las arquitecturas, los frameworks vienen y van. Pero una mentalidad sólida sobre cómo funcionan los sistemas te sirve para toda tu carrera.

Bienvenido a Android.

Tags

#flutter #android #kotlin #jetpack-compose #mindset-shift #developer-journey #cross-platform-vs-native #career