diciembre 21, 2025
18 min de lectura

Preguntas de entrevista para Android senior: Kotlin, Compose y arquitectura

interview
career-advice
job-search
Preguntas de entrevista para Android senior: Kotlin, Compose y arquitectura
Milad Bonakdar

Milad Bonakdar

Autor

Prepárate para entrevistas Android senior con preguntas prácticas sobre corrutinas Kotlin, estado en Jetpack Compose, arquitectura, rendimiento, pruebas, datos offline-first y seguridad.


Introducción

En una entrevista Android senior, prepárate para explicar decisiones de arquitectura, concurrencia segura con el ciclo de vida, propiedad del estado en Compose, flujos de datos offline-first, diagnóstico de rendimiento, estrategia de pruebas y seguridad. Una buena respuesta no solo dice qué API usarías, sino por qué encaja con la restricción del producto y cómo la depurarías en producción.

Usa esta guía como una lista práctica de preparación. Prioriza corrutinas y Flow, ViewModel y gestión de estado, repositorios con Room, decisiones entre Compose y UI heredada, arranque y memoria, y ejemplos reales de tus propias apps.

Cómo usar estas preguntas

  • Practica cada respuesta como una decisión breve: contexto, trade-off, implementación y modo de fallo.
  • Conecta las decisiones técnicas con impacto en usuarios, como arranque más rápido, lecturas offline fiables, tokens más seguros o menos errores de ciclo de vida.
  • Prepara una historia real de proyecto para arquitectura, rendimiento, pruebas y seguridad.

Kotlin Avanzado y Características del Lenguaje (5 Preguntas)

1. Explica las Corrutinas de Kotlin y sus ventajas sobre los hilos.

Respuesta: Las corrutinas son primitivas de concurrencia ligeras que permiten escribir código asíncrono de forma secuencial.

  • Ventajas sobre los Hilos:
    • Ligereza: Se pueden crear miles de corrutinas sin problemas de rendimiento
    • Concurrencia Estructurada: La relación padre-hijo garantiza una limpieza adecuada
    • Soporte de Cancelación: Propagación de cancelación incorporada
    • Manejo de Excepciones: Manejo de excepciones estructurado
  • Componentes Clave:
    • CoroutineScope: Define el ciclo de vida
    • Dispatchers: Controlan el contexto de ejecución (Main, IO, Default)
    • suspend functions: Se pueden pausar y reanudar
class UserRepository(private val api: ApiService) {
    suspend fun getUser(id: Int): Result<User> = withContext(Dispatchers.IO) {
        try {
            val user = api.fetchUser(id)
            Result.success(user)
        } catch (e: Exception) {
            Result.failure(e)
        }
    }
}

class UserViewModel : ViewModel() {
    private val _user = MutableLiveData<User>()
    val user: LiveData<User> = _user
    
    fun loadUser(id: Int) {
        viewModelScope.launch {
            when (val result = repository.getUser(id)) {
                is Result.Success -> _user.value = result.data
                is Result.Error -> handleError(result.exception)
            }
        }
    }
}

Rareza: Muy Común Dificultad: Difícil


2. ¿Qué son las Sealed Classes y cuándo deberías usarlas?

Respuesta: Las sealed classes representan jerarquías de clases restringidas donde todas las subclases se conocen en tiempo de compilación.

  • Beneficios:
    • Expresiones when exhaustivas
    • Gestión de estado con seguridad de tipos
    • Mejor que los enums para datos complejos
  • Casos de Uso: Representación de estados, resultados, eventos de navegación
sealed class UiState<out T> {
    object Loading : UiState<Nothing>()
    data class Success<T>(val data: T) : UiState<T>()
    data class Error(val message: String) : UiState<Nothing>()
}

class UserViewModel : ViewModel() {
    private val _uiState = MutableStateFlow<UiState<User>>(UiState.Loading)
    val uiState: StateFlow<UiState<User>> = _uiState.asStateFlow()
    
    fun loadUser() {
        viewModelScope.launch {
            _uiState.value = UiState.Loading
            try {
                val user = repository.getUser()
                _uiState.value = UiState.Success(user)
            } catch (e: Exception) {
                _uiState.value = UiState.Error(e.message ?: "Unknown error")
            }
        }
    }
}

// In UI
when (val state = uiState.value) {
    is UiState.Loading -> showLoading()
    is UiState.Success -> showData(state.data)
    is UiState.Error -> showError(state.message)
    // Compiler ensures all cases are handled
}

Rareza: Común Dificultad: Media


3. Explica Kotlin Flow y en qué se diferencia de LiveData.

Respuesta: Flow es el stream asíncrono frío de Kotlin que emite valores secuencialmente.

  • Flow vs LiveData:
    • Flow: Stream frío, soporta operadores, no es consciente del ciclo de vida, más flexible
    • LiveData: Stream caliente, consciente del ciclo de vida, específico de Android, más simple para la UI
  • Tipos de Flow:
    • Flow: Stream frío (comienza al ser recolectado)
    • StateFlow: Stream caliente con estado actual
    • SharedFlow: Stream caliente para eventos
class UserRepository {
    // Cold Flow - starts when collected
    fun getUsers(): Flow<List<User>> = flow {
        val users = api.fetchUsers()
        emit(users)
    }.flowOn(Dispatchers.IO)
    
    // StateFlow - hot, holds state
    private val _userState = MutableStateFlow<List<User>>(emptyList())
    val userState: StateFlow<List<User>> = _userState.asStateFlow()
    
    // SharedFlow - hot, for events
    private val _events = MutableSharedFlow<Event>()
    val events: SharedFlow<Event> = _events.asSharedFlow()
}

class UserViewModel : ViewModel() {
    val users = repository.getUsers()
        .map { it.filter { user -> user.isActive } }
        .catch { emit(emptyList()) }
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5000),
            initialValue = emptyList()
        )
}

// Collect in Activity/Fragment
lifecycleScope.launch {
    repeatOnLifecycle(Lifecycle.State.STARTED) {
        viewModel.users.collect { users ->
            updateUI(users)
        }
    }
}

Rareza: Muy Común Dificultad: Difícil


4. ¿Qué son las Inline Functions y cuándo deberías usarlas?

Respuesta: Las inline functions copian el cuerpo de la función al sitio de la llamada, evitando la sobrecarga de la llamada a la función.

  • Beneficios:
    • Elimina la sobrecarga de la asignación de lambdas
    • Permite retornos no locales desde lambdas
    • Mejor rendimiento para funciones de orden superior
  • Casos de Uso: Funciones de orden superior con parámetros lambda
  • Compromiso: Aumenta el tamaño del código
// Without inline - creates lambda object
fun measureTime(block: () -> Unit) {
    val start = System.currentTimeMillis()
    block()
    val end = System.currentTimeMillis()
    println("Time: ${end - start}ms")
}

// With inline - no lambda object created
inline fun measureTimeInline(block: () -> Unit) {
    val start = System.currentTimeMillis()
    block()
    val end = System.currentTimeMillis()
    println("Time: ${end - start}ms")
}

// noinline - prevents specific parameter from being inlined
inline fun performOperation(
    inline operation: () -> Unit,
    noinline logger: () -> Unit
) {
    operation()
    saveLogger(logger)  // Can store noinline lambda
}

// crossinline - allows inline but prevents non-local returns
inline fun runAsync(crossinline block: () -> Unit) {
    thread {
        block()  // Can't return from outer function
    }
}

Rareza: Media Dificultad: Difícil


5. Explica la Delegación en Kotlin.

Respuesta: La delegación permite que un objeto delegue algunas de sus responsabilidades a otro objeto.

  • Delegación de Clases: Palabra clave by
  • Delegación de Propiedades: Lazy, observable, delegates
  • Beneficios: Reutilización de código, composición sobre herencia
// Class delegation
interface Repository {
    fun getData(): String
}

class RemoteRepository : Repository {
    override fun getData() = "Remote data"
}

class CachedRepository(
    private val remote: Repository
) : Repository by remote {
    private var cache: String? = null
    
    override fun getData(): String {
        return cache ?: remote.getData().also { cache = it }
    }
}

// Property delegation
class User {
    // Lazy initialization
    val database by lazy {
        Room.databaseBuilder(context, AppDatabase::class.java, "db").build()
    }
    
    // Observable property
    var name: String by Delegates.observable("Initial") { prop, old, new ->
        println("$old -> $new")
    }
    
    // Custom delegate
    var token: String by PreferenceDelegate("auth_token", "")
}

class PreferenceDelegate(
    private val key: String,
    private val defaultValue: String
) {
    operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
        return sharedPreferences.getString(key, defaultValue) ?: defaultValue
    }
    
    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
        sharedPreferences.edit().putString(key, value).apply()
    }
}

Rareza: Media Dificultad: Media


Patrones de Arquitectura (6 Preguntas)

6. Explica la arquitectura MVVM y sus beneficios.

Respuesta: MVVM (Model-View-ViewModel) separa la lógica de la UI de la lógica de negocio.

Loading diagram...
  • Model: Capa de datos (repositories, data sources)
  • View: Capa de UI (Activities, Fragments, Composables)
  • ViewModel: Lógica de presentación, sobrevive a los cambios de configuración
  • Beneficios: Testeable, separación de responsabilidades, consciente del ciclo de vida
// Model
data class User(val id: Int, val name: String, val email: String)

// Repository
class UserRepository(
    private val api: ApiService,
    private val dao: UserDao
) {
    suspend fun getUser(id: Int): User {
        return dao.getUser(id) ?: api.fetchUser(id).also {
            dao.insertUser(it)
        }
    }
}

// ViewModel
class UserViewModel(
    private val repository: UserRepository
) : ViewModel() {
    private val _uiState = MutableStateFlow<UiState<User>>(UiState.Loading)
    val uiState: StateFlow<UiState<User>> = _uiState.asStateFlow()
    
    fun loadUser(id: Int) {
        viewModelScope.launch {
            _uiState.value = UiState.Loading
            try {
                val user = repository.getUser(id)
                _uiState.value = UiState.Success(user)
            } catch (e: Exception) {
                _uiState.value = UiState.Error(e.message ?: "Error")
            }
        }
    }
}

// View (Fragment)
class UserFragment : Fragment() {
    private val viewModel: UserViewModel by viewModels()
    
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        
        viewLifecycleOwner.lifecycleScope.launch {
            viewModel.uiState.collect { state ->
                when (state) {
                    is UiState.Loading -> showLoading()
                    is UiState.Success -> showUser(state.data)
                    is UiState.Error -> showError(state.message)
                }
            }
        }
    }
}

Rareza: Muy Común Dificultad: Media


7. ¿Qué es la Arquitectura Limpia y cómo la implementas en Android?

Respuesta: La Arquitectura Limpia separa el código en capas con dependencias claras.

Loading diagram...
  • Presentation: UI, ViewModels
  • Domain: Use Cases, Business Logic, Entities
  • Data: Repositories, Data Sources (API, Database)
  • Dependency Rule: Las capas internas no conocen las capas externas
// Domain Layer - Entities
data class User(val id: Int, val name: String, val email: String)

// Domain Layer - Repository Interface
interface UserRepository {
    suspend fun getUser(id: Int): Result<User>
}

// Domain Layer - Use Case
class GetUserUseCase(private val repository: UserRepository) {
    suspend operator fun invoke(id: Int): Result<User> {
        return repository.getUser(id)
    }
}

// Data Layer - Repository Implementation
class UserRepositoryImpl(
    private val remoteDataSource: UserRemoteDataSource,
    private val localDataSource: UserLocalDataSource
) : UserRepository {
    override suspend fun getUser(id: Int): Result<User> {
        return try {
            val localUser = localDataSource.getUser(id)
            if (localUser != null) {
                Result.success(localUser)
            } else {
                val remoteUser = remoteDataSource.fetchUser(id)
                localDataSource.saveUser(remoteUser)
                Result.success(remoteUser)
            }
        } catch (e: Exception) {
            Result.failure(e)
        }
    }
}

// Presentation Layer - ViewModel
class UserViewModel(
    private val getUserUseCase: GetUserUseCase
) : ViewModel() {
    fun loadUser(id: Int) {
        viewModelScope.launch {
            when (val result = getUserUseCase(id)) {
                is Result.Success -> handleSuccess(result.data)
                is Result.Failure -> handleError(result.exception)
            }
        }
    }
}

Rareza: Común Dificultad: Difícil


8. Explica la Inyección de Dependencias y Dagger/Hilt.

Respuesta: La Inyección de Dependencias proporciona dependencias a las clases en lugar de crearlas internamente.

  • Beneficios: Testeabilidad, bajo acoplamiento, reutilización
  • Dagger: Framework de DI en tiempo de compilación
  • Hilt: Dagger simplificado para Android
// Define dependencies with Hilt
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
    @Provides
    @Singleton
    fun provideRetrofit(): Retrofit {
        return Retrofit.Builder()
            .baseUrl("https://api.example.com/")
            .addConverterFactory(GsonConverterFactory.create())
            .build()
    }
    
    @Provides
    @Singleton
    fun provideApiService(retrofit: Retrofit): ApiService {
        return retrofit.create(ApiService::class.java)
    }
}

@Module
@InstallIn(SingletonComponent::class)
object DatabaseModule {
    @Provides
    @Singleton
    fun provideDatabase(@ApplicationContext context: Context): AppDatabase {
        return Room.databaseBuilder(context, AppDatabase::class.java, "app_db")
            .build()
    }
    
    @Provides
    fun provideUserDao(database: AppDatabase): UserDao {
        return database.userDao()
    }
}

// Repository with injected dependencies
@Singleton
class UserRepository @Inject constructor(
    private val apiService: ApiService,
    private val userDao: UserDao
) {
    suspend fun getUser(id: Int): User {
        return userDao.getUser(id) ?: apiService.fetchUser(id).also {
            userDao.insertUser(it)
        }
    }
}

// ViewModel with injected repository
@HiltViewModel
class UserViewModel @Inject constructor(
    private val repository: UserRepository
) : ViewModel() {
    // ViewModel logic
}

// Activity/Fragment
@AndroidEntryPoint
class UserActivity : AppCompatActivity() {
    private val viewModel: UserViewModel by viewModels()
}

Rareza: Muy Común Dificultad: Difícil


9. ¿Qué es el patrón Repository y por qué usarlo?

Respuesta: El patrón Repository abstrae las fuentes de datos, proporcionando una API limpia para el acceso a los datos.

  • Beneficios:
    • Única fuente de verdad
    • Lógica de datos centralizada
    • Fácil de cambiar las fuentes de datos
    • Testeable
  • Implementación: Coordina entre múltiples fuentes de datos
interface UserRepository {
    suspend fun getUsers(): Flow<List<User>>
    suspend fun getUser(id: Int): User?
    suspend fun saveUser(user: User)
    suspend fun deleteUser(id: Int)
}

class UserRepositoryImpl @Inject constructor(
    private val remoteDataSource: UserRemoteDataSource,
    private val localDataSource: UserLocalDataSource,
    private val cacheDataSource: UserCacheDataSource
) : UserRepository {
    
    override suspend fun getUsers(): Flow<List<User>> = flow {
        // Emit cached data first
        val cachedUsers = cacheDataSource.getUsers()
        if (cachedUsers.isNotEmpty()) {
            emit(cachedUsers)
        }
        
        // Fetch from local database
        val localUsers = localDataSource.getUsers()
        if (localUsers.isNotEmpty()) {
            emit(localUsers)
            cacheDataSource.saveUsers(localUsers)
        }
        
        // Fetch from remote
        try {
            val remoteUsers = remoteDataSource.fetchUsers()
            localDataSource.saveUsers(remoteUsers)
            cacheDataSource.saveUsers(remoteUsers)
            emit(remoteUsers)
        } catch (e: Exception) {
            // If remote fails, we already emitted cached/local data
        }
    }
    
    override suspend fun getUser(id: Int): User? {
        return cacheDataSource.getUser(id)
            ?: localDataSource.getUser(id)
            ?: remoteDataSource.fetchUser(id)?.also {
                localDataSource.saveUser(it)
                cacheDataSource.saveUser(it)
            }
    }
}

Rareza: Muy Común Dificultad: Media


10. Explica la arquitectura de Single Activity.

Respuesta: La arquitectura de Single Activity utiliza una Activity con múltiples Fragments, gestionados por el Navigation Component.

  • Beneficios:
    • Navegación simplificada
    • ViewModels compartidos entre fragments
    • Mejores animaciones
    • Deep linking más fácil
  • Navigation Component: Maneja las transacciones de fragmentos, la pila de retroceso, los argumentos
// Navigation graph (nav_graph.xml)
<navigation>
    <fragment
        android:id="@+id/homeFragment"
        android:name="com.app.HomeFragment">
        <action
            android:id="@+id/action_home_to_detail"
            app:destination="@id/detailFragment" />
    </fragment>
    
    <fragment
        android:id="@+id/detailFragment"
        android:name="com.app.DetailFragment">
        <argument
            android:name="userId"
            app:argType="integer" />
    </fragment>
</navigation>

// MainActivity - single activity
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        
        val navController = findNavController(R.id.nav_host_fragment)
        setupActionBarWithNavController(navController)
    }
}

// HomeFragment - navigate to detail
class HomeFragment : Fragment() {
    fun navigateToDetail(userId: Int) {
        val action = HomeFragmentDirections.actionHomeToDetail(userId)
        findNavController().navigate(action)
    }
}

// DetailFragment - receive arguments
class DetailFragment : Fragment() {
    private val args: DetailFragmentArgs by navArgs()
    
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        val userId = args.userId
        viewModel.loadUser(userId)
    }
}

Rareza: Común Dificultad: Media


11. ¿Qué es la arquitectura MVI (Model-View-Intent)?

Respuesta: MVI es una arquitectura de flujo de datos unidireccional inspirada en Redux.

  • Componentes:
    • Model: Representa el estado de la UI
    • View: Renderiza el estado, emite intents
    • Intent: Acciones/eventos del usuario
  • Beneficios: Estado predecible, depuración más fácil, depuración de viaje en el tiempo
// State
data class UserScreenState(
    val isLoading: Boolean = false,
    val user: User? = null,
    val error: String? = null
)

// Intent (user actions)
sealed class UserIntent {
    data class LoadUser(val id: Int) : UserIntent()
    object RetryLoading : UserIntent()
}

// ViewModel
class UserViewModel : ViewModel() {
    private val _state = MutableStateFlow(UserScreenState())
    val state: StateFlow<UserScreenState> = _state.asStateFlow()
    
    fun processIntent(intent: UserIntent) {
        when (intent) {
            is UserIntent.LoadUser -> loadUser(intent.id)
            is UserIntent.RetryLoading -> retryLoading()
        }
    }
    
    private fun loadUser(id: Int) {
        viewModelScope.launch {
            _state.value = _state.value.copy(isLoading = true, error = null)
            try {
                val user = repository.getUser(id)
                _state.value = _state.value.copy(
                    isLoading = false,
                    user = user
                )
            } catch (e: Exception) {
                _state.value = _state.value.copy(
                    isLoading = false,
                    error = e.message
                )
            }
        }
    }
}

// View
class UserFragment : Fragment() {
    private val viewModel: UserViewModel by viewModels()
    
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        
        // Send intent
        viewModel.processIntent(UserIntent.LoadUser(123))
        
        // Observe state
        viewLifecycleOwner.lifecycleScope.launch {
            viewModel.state.collect { state ->
                render(state)
            }
        }
    }
    
    private fun render(state: UserScreenState) {
        when {
            state.isLoading -> showLoading()
            state.error != null -> showError(state.error)
            state.user != null -> showUser(state.user)
        }
    }
}

Rareza: Media Dificultad: Difícil


Rendimiento y Optimización (5 Preguntas)

12. ¿Cómo optimizas el rendimiento de RecyclerView?

Respuesta: Múltiples estrategias mejoran el rendimiento del desplazamiento de RecyclerView:

  1. ViewHolder Pattern: Reutiliza vistas (incorporado)
  2. DiffUtil: Actualizaciones de listas eficientes
  3. Stable IDs: Sobrescribe getItemId() y setHasStableIds(true)
  4. Prefetching: Aumenta la distancia de prefetch
  5. Image Loading: Utiliza bibliotecas como Glide/Coil con el tamaño adecuado
  6. Avoid Heavy Operations: No realices cálculos costosos en onBindViewHolder
  7. Nested RecyclerViews: Establece setRecycledViewPool() y setHasFixedSize(true)
class UserAdapter : ListAdapter<User, UserViewHolder>(UserDiffCallback()) {
    
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserViewHolder {
        val binding = ItemUserBinding.inflate(
            LayoutInflater.from(parent.context),
            parent,
            false
        )
        return UserViewHolder(binding)
    }
    
    override fun onBindViewHolder(holder: UserViewHolder, position: Int) {
        holder.bind(getItem(position))
    }
    
    override fun getItemId(position: Int): Long {
        return getItem(position).id.toLong()
    }
}

class UserDiffCallback : DiffUtil.ItemCallback<User>() {
    override fun areItemsTheSame(oldItem: User, newItem: User): Boolean {
        return oldItem.id == newItem.id
    }
    
    override fun areContentsTheSame(oldItem: User, newItem: User): Boolean {
        return oldItem == newItem
    }
}

// In Fragment/Activity
recyclerView.apply {
    setHasFixedSize(true)
    adapter = userAdapter
    // Increase prefetch
    (layoutManager as? LinearLayoutManager)?.initialPrefetchItemCount = 4
}

// Update list efficiently
userAdapter.submitList(newUsers)

Rareza: Muy Común Dificultad: Media


13. ¿Cómo detectas y corriges las fugas de memoria en Android?

Respuesta: Las fugas de memoria ocurren cuando los objetos se mantienen en la memoria más tiempo del necesario.

  • Causas Comunes:
    • Fugas de contexto (referencias a Activity/Fragment)
    • Referencias estáticas
    • Clases internas anónimas
    • Listeners no registrados
    • Corrutinas no canceladas
  • Herramientas de Detección:
    • Librería LeakCanary
    • Android Studio Memory Profiler
    • Heap dumps
// BAD - Memory leak
class MyActivity : AppCompatActivity() {
    companion object {
        var instance: MyActivity? = null  // Static reference leaks
    }
    
    private val handler = Handler()  // Implicit reference to Activity
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        instance = this
        
        // Anonymous class holds Activity reference
        handler.postDelayed({
            // Activity might be destroyed
        }, 10000)
    }
}

// GOOD - No memory leak
class MyActivity : AppCompatActivity() {
    private val handler = Handler(Looper.getMainLooper())
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        // Use weak reference if needed
        val weakRef = WeakReference(this)
        handler.postDelayed({
            weakRef.get()?.let { activity ->
                // Safe to use activity
            }
        }, 10000)
    }
    
    override fun onDestroy() {
        super.onDestroy()
        handler.removeCallbacksAndMessages(null)  // Clean up
    }
}

// ViewModel - survives configuration changes
class MyViewModel : ViewModel() {
    private val job = Job()
    private val scope = CoroutineScope(Dispatchers.Main + job)
    
    override fun onCleared() {
        super.onCleared()
        job.cancel()  // Cancel coroutines
    }
}

Rareza: Muy Común Dificultad: Media


14. ¿Cómo optimizas el tiempo de inicio de la app?

Respuesta: Un inicio más rápido mejora la experiencia del usuario:

  1. Lazy Initialization: Inicializa los objetos solo cuando sea necesario
  2. Avoid Heavy Work in Application.onCreate():
    • Mueve al hilo de fondo
    • Aplaza la inicialización no crítica
  3. Content Providers: Minimiza o carga de forma lazy
  4. Reduce Dependencies: Menos librerías = inicio más rápido
  5. App Startup Library: Inicialización estructurada
  6. Baseline Profiles: Sugerencias de compilación Ahead-of-time
class MyApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        
        // Critical initialization only
        initCrashReporting()
        
        // Defer non-critical work
        lifecycleScope.launch {
            delay(100)
            initAnalytics()
            initImageLoader()
        }
        
        // Background initialization
        CoroutineScope(Dispatchers.IO).launch {
            preloadData()
        }
    }
}

// App Startup library
class AnalyticsInitializer : Initializer<Analytics> {
    override fun create(context: Context): Analytics {
        return Analytics.getInstance(context).apply {
            initialize()
        }
    }
    
    override fun dependencies(): List<Class<out Initializer<*>>> {
        return emptyList()
    }
}

// Lazy initialization
class MyActivity : AppCompatActivity() {
    private val database by lazy {
        Room.databaseBuilder(this, AppDatabase::class.java, "db").build()
    }
    
    // Only created when accessed
    private val heavyObject by lazy {
        createHeavyObject()
    }
}

Rareza: Común Dificultad: Media


15. ¿Cómo manejas la carga y el almacenamiento en caché de bitmaps de forma eficiente?

Respuesta: El manejo eficiente de imágenes es crucial para el rendimiento:

  • Libraries: Glide, Coil (manejan el almacenamiento en caché automáticamente)
  • Manual Optimization:
    • Downsampling (carga imágenes más pequeñas)
    • Memory cache (LruCache)
    • Disk cache
    • Bitmap pooling
// Using Glide
Glide.with(context)
    .load(imageUrl)
    .placeholder(R.drawable.placeholder)
    .error(R.drawable.error)
    .diskCacheStrategy(DiskCacheStrategy.ALL)
    .override(width, height)  // Resize
    .into(imageView)

// Manual implementation with LruCache
class ImageCache {
    private val maxMemory = (Runtime.getRuntime().maxMemory() / 1024).toInt()
    private val cacheSize = maxMemory / 8
    
    private val memoryCache = object : LruCache<String, Bitmap>(cacheSize) {
        override fun sizeOf(key: String, bitmap: Bitmap): Int {
            return bitmap.byteCount / 1024
        }
    }
    
    fun getBitmap(key: String): Bitmap? {
        return memoryCache.get(key)
    }
    
    fun putBitmap(key: String, bitmap: Bitmap) {
        if (getBitmap(key) == null) {
            memoryCache.put(key, bitmap)
        }
    }
}

// Downsample large images
fun decodeSampledBitmap(file: File, reqWidth: Int, reqHeight: Int): Bitmap {
    return BitmapFactory.Options().run {
        inJustDecodeBounds = true
        BitmapFactory.decodeFile(file.path, this)
        
        inSampleSize = calculateInSampleSize(this, reqWidth, reqHeight)
        inJustDecodeBounds = false
        
        BitmapFactory.decodeFile(file.path, this)
    }
}

fun calculateInSampleSize(options: BitmapFactory.Options, reqWidth: Int, reqHeight: Int): Int {
    val (height: Int, width: Int) = options.run { outHeight to outWidth }
    var inSampleSize = 1
    
    if (height > reqHeight || width > reqWidth) {
        val halfHeight: Int = height / 2
        val halfWidth: Int = width / 2
        
        while (halfHeight / inSampleSize >= reqHeight && halfWidth / inSampleSize >= reqWidth) {
            inSampleSize *= 2
        }
    }
    
    return inSampleSize
}

Rareza: Común Dificultad: Difícil


16. ¿Qué es ANR y cómo lo previenes?

Respuesta: ANR (Application Not Responding) ocurre cuando el hilo principal se bloquea durante demasiado tiempo.

  • Causas:
    • Computación pesada en el hilo principal
    • Llamadas de red en el hilo principal
    • Operaciones de base de datos en el hilo principal
    • Deadlocks
  • Prevention:
    • Mueve el trabajo pesado a hilos de fondo
    • Utiliza corrutinas con dispatchers adecuados
    • Evita bloques synchronized en el hilo principal
    • Utiliza WorkManager para tareas en segundo plano
// BAD - Blocks main thread
class BadActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        // ANR risk!
        val data = fetchDataFromNetwork()  // Blocks UI thread
        processLargeFile()  // Blocks UI thread
    }
}

// GOOD - Background processing
class GoodActivity : AppCompatActivity() {
    private val viewModel: MyViewModel by viewModels()
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        viewModel.loadData()  // Handled in ViewModel with coroutines
    }
}

class MyViewModel : ViewModel() {
    fun loadData() {
        viewModelScope.launch {
            // Automatically on Main dispatcher
            showLoading()
            
            // Switch to IO dispatcher for network
            val data = withContext(Dispatchers.IO) {
                fetchDataFromNetwork()
            }
            
            // Switch to Default dispatcher for heavy computation
            val processed = withContext(Dispatchers.Default) {
                processData(data)
            }
            
            // Back to Main for UI update
            updateUI(processed)
        }
    }
}

Rareza: Común Dificultad: Fácil


Pruebas (3 Preguntas)

17. ¿Cómo escribes pruebas unitarias para ViewModels?

Respuesta: Los ViewModels deben probarse de forma aislada con dependencias mockeadas.

class UserViewModelTest {
    @get:Rule
    val instantExecutorRule = InstantTaskExecutorRule()
    
    @get:Rule
    val mainDispatcherRule = MainDispatcherRule()
    
    private lateinit var viewModel: UserViewModel
    private lateinit var repository: UserRepository
    
    @Before
    fun setup() {
        repository = mockk()
        viewModel = UserViewModel(repository)
    }
    
    @Test
    fun `loadUser success updates state`() = runTest {
        // Given
        val user = User(1, "John", "[email protected]")
        coEvery { repository.getUser(1) } returns Result.success(user)
        
        // When
        viewModel.loadUser(1)
        
        // Then
        val state = viewModel.uiState.value
        assertTrue(state is UiState.Success)
        assertEquals(user, (state as UiState.Success).data)
    }
Newsletter subscription

Consejos de carrera semanales que realmente funcionan

Recibe las últimas ideas directamente en tu bandeja de entrada

Tu Próxima Entrevista Está a Solo un Currículum de Distancia

Crea un currículum profesional y optimizado en minutos. No se necesitan habilidades de diseño, solo resultados comprobados.

Crea mi currículum

Compartir esta publicación

Supera la Tasa de Rechazo del 75% de los ATS

3 de cada 4 currículums nunca llegan a un ojo humano. Nuestra optimización de palabras clave aumenta tu tasa de aprobación hasta en un 80%, asegurando que los reclutadores realmente vean tu potencial.