diciembre 21, 2025
18 min de lectura

Preguntas para Entrevistas de Desarrollador Senior Mobile (Android): Guía Completa

interview
career-advice
job-search
Preguntas para Entrevistas de Desarrollador Senior Mobile (Android): Guía Completa
Milad Bonakdar

Milad Bonakdar

Autor

Domina el desarrollo avanzado de Android con preguntas esenciales para entrevistas que cubren patrones de arquitectura, optimización del rendimiento, inyección de dependencias, pruebas, seguridad y diseño de sistemas para desarrolladores senior.


Introducción

Se espera que los desarrolladores Android Senior diseñen aplicaciones escalables y mantenibles, al tiempo que garantizan un alto rendimiento y calidad del código. Este rol exige una profunda experiencia en frameworks de Android, patrones arquitectónicos, inyección de dependencias, estrategias de pruebas y la capacidad de tomar decisiones técnicas informadas.

Esta guía completa cubre las preguntas esenciales de la entrevista para Desarrolladores Android Senior, que abarcan conceptos avanzados de Kotlin, patrones arquitectónicos, optimización del rendimiento, inyección de dependencias, pruebas y diseño de sistemas. Cada pregunta incluye respuestas detalladas, evaluación de rareza y niveles de dificultad.


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.