dezembro 21, 2025
18 min de leitura

Perguntas de entrevista Android sênior: Kotlin, Compose e arquitetura

interview
career-advice
job-search
Perguntas de entrevista Android sênior: Kotlin, Compose e arquitetura
Milad Bonakdar

Milad Bonakdar

Autor

Prepare-se para entrevistas Android sênior com perguntas práticas sobre corrotinas Kotlin, estado no Jetpack Compose, arquitetura, desempenho, testes, dados offline-first e segurança.


Introdução

Em uma entrevista Android sênior, prepare-se para explicar trade-offs de arquitetura, concorrência segura em relação ao ciclo de vida, propriedade de estado no Compose, fluxo de dados offline-first, diagnóstico de desempenho, estratégia de testes e segurança. Uma boa resposta não diz apenas qual API usar, mas por que ela atende à restrição do produto e como você investigaria problemas em produção.

Use este guia como uma checklist prática. Priorize corrotinas Kotlin e Flow, ViewModel e gerenciamento de estado, repositórios com Room, decisões entre Compose e UI legada, tempo de inicialização, memória e exemplos que você consegue explicar a partir dos seus próprios apps.

Como usar estas perguntas

  • Pratique cada resposta como uma decisão curta: contexto, trade-off, implementação e modo de falha.
  • Conecte escolhas técnicas ao impacto para o usuário, como inicialização mais rápida, leituras offline confiáveis, armazenamento seguro de tokens ou menos bugs de lifecycle.
  • Prepare uma história real de projeto para arquitetura, desempenho, testes e segurança.

Kotlin Avançado e Recursos da Linguagem (5 Perguntas)

1. Explique as Coroutines do Kotlin e suas vantagens sobre as threads.

Resposta: Coroutines são primitivas de concorrência leves que permitem escrever código assíncrono de maneira sequencial.

  • Vantagens sobre Threads:
    • Leves: É possível criar milhares de coroutines sem problemas de desempenho
    • Concorrência Estruturada: O relacionamento pai-filho garante uma limpeza adequada
    • Suporte ao Cancelamento: Propagação de cancelamento integrada
    • Tratamento de Exceções: Tratamento de exceções estruturado
  • Componentes Principais:
    • CoroutineScope: Define o ciclo de vida
    • Dispatchers: Controla o contexto de execução (Main, IO, Default)
    • suspend functions: Podem ser pausadas e retomadas
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)
            }
        }
    }
}

Raridade: Muito Comum Dificuldade: Difícil


2. O que são Sealed Classes e quando você deve usá-las?

Resposta: Sealed classes representam hierarquias de classes restritas onde todas as subclasses são conhecidas em tempo de compilação.

  • Benefícios:
    • Expressões when exaustivas
    • Gerenciamento de estado com segurança de tipo
    • Melhor que enums para dados complexos
  • Casos de Uso: Representação de estados, resultados, eventos de navegação
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
}

Raridade: Comum Dificuldade: Média


3. Explique o Kotlin Flow e como ele difere do LiveData.

Resposta: Flow é o stream assíncrono frio do Kotlin que emite valores sequencialmente.

  • Flow vs LiveData:
    • Flow: Stream frio, suporta operadores, não tem reconhecimento do ciclo de vida, mais flexível
    • LiveData: Stream quente, tem reconhecimento do ciclo de vida, específico do Android, mais simples para a UI
  • Tipos de Flow:
    • Flow: Stream frio (inicia na coleta)
    • StateFlow: Stream quente com estado atual
    • SharedFlow: Stream quente 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)
        }
    }
}

Raridade: Muito Comum Dificuldade: Difícil


4. O que são Inline Functions e quando você deve usá-las?

Resposta: Inline functions copiam o corpo da função para o local da chamada, evitando a sobrecarga da chamada da função.

  • Benefícios:
    • Elimina a sobrecarga de alocação de lambda
    • Permite retornos não locais de lambdas
    • Melhor desempenho para funções de ordem superior
  • Casos de Uso: Funções de ordem superior com parâmetros lambda
  • Compromisso: Aumenta o tamanho do 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
    }
}

Raridade: Média Dificuldade: Difícil


5. Explique a Delegação no Kotlin.

Resposta: A delegação permite que um objeto delegue algumas de suas responsabilidades a outro objeto.

  • Delegação de Classe: palavra-chave by
  • Delegação de Propriedade: Lazy, observable, delegates
  • Benefícios: Reutilização de código, composição em vez de herança
// 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()
    }
}

Raridade: Média Dificuldade: Média


Padrões de Arquitetura (6 Perguntas)

6. Explique a arquitetura MVVM e seus benefícios.

Resposta: MVVM (Model-View-ViewModel) separa a lógica da UI da lógica de negócios.

Loading diagram...
  • Model: Camada de dados (repositórios, fontes de dados)
  • View: Camada de UI (Activities, Fragments, Composables)
  • ViewModel: Lógica de apresentação, sobrevive a mudanças de configuração
  • Benefícios: Testável, separação de preocupações, reconhecimento do 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)
                }
            }
        }
    }
}

Raridade: Muito Comum Dificuldade: Média


7. O que é Clean Architecture e como você a implementa no Android?

Resposta: Clean Architecture separa o código em camadas com dependências claras.

Loading diagram...
  • Presentation: UI, ViewModels
  • Domain: Use Cases, Business Logic, Entities
  • Data: Repositories, Data Sources (API, Database)
  • Dependency Rule: Camadas internas não conhecem as camadas 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)
            }
        }
    }
}

Raridade: Comum Dificuldade: Difícil


8. Explique a Injeção de Dependência e Dagger/Hilt.

Resposta: A Injeção de Dependência fornece dependências para as classes em vez de criá-las internamente.

  • Benefícios: Testabilidade, baixo acoplamento, reutilização
  • Dagger: Framework de DI em tempo de compilação
  • 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()
}

Raridade: Muito Comum Dificuldade: Difícil


9. O que é o padrão Repository e por que usá-lo?

Resposta: O padrão Repository abstrai as fontes de dados, fornecendo uma API limpa para acesso aos dados.

  • Benefícios:
    • Única fonte de verdade
    • Lógica de dados centralizada
    • Fácil de trocar as fontes de dados
    • Testável
  • Implementação: Coordena entre várias fontes de dados
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)
            }
    }
}

Raridade: Muito Comum Dificuldade: Média


10. Explique a arquitetura Single Activity.

Resposta: A arquitetura Single Activity usa uma Activity com vários Fragments, gerenciados pelo Navigation Component.

  • Benefícios:
    • Navegação simplificada
    • ViewModels compartilhados entre fragments
    • Melhores animações
    • Deep linking mais fácil
  • Navigation Component: Lida com transações de fragment, pilha de retorno, 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)
    }
}

Raridade: Comum Dificuldade: Média


11. O que é a arquitetura MVI (Model-View-Intent)?

Resposta: MVI é uma arquitetura de fluxo de dados unidirecional inspirada no Redux.

  • Componentes:
    • Model: Representa o estado da UI
    • View: Renderiza o estado, emite intents
    • Intent: Ações/eventos do usuário
  • Benefícios: Estado previsível, depuração mais fácil, depuração com viagem no tempo
// 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)
        }
    }
}

Raridade: Média Dificuldade: Difícil


Desempenho e Otimização (5 Perguntas)

12. Como você otimiza o desempenho do RecyclerView?

Resposta: Várias estratégias melhoram o desempenho de rolagem do RecyclerView:

  1. ViewHolder Pattern: Reutiliza views (integrado)
  2. DiffUtil: Atualizações de lista eficientes
  3. Stable IDs: Substitua getItemId() e setHasStableIds(true)
  4. Prefetching: Aumente a distância de prefetch
  5. Image Loading: Use bibliotecas como Glide/Coil com dimensionamento adequado
  6. Avoid Heavy Operations: Não execute cálculos dispendiosos em onBindViewHolder
  7. Nested RecyclerViews: Defina setRecycledViewPool() e 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)

Raridade: Muito Comum Dificuldade: Média


13. Como você detecta e corrige vazamentos de memória no Android?

Resposta: Os vazamentos de memória ocorrem quando os objetos são mantidos na memória por mais tempo do que o necessário.

  • Causas Comuns:
    • Vazamentos de contexto (referências de Activity/Fragment)
    • Referências estáticas
    • Classes internas anônimas
    • Listeners não removidos
    • Coroutines não canceladas
  • Ferramentas de Detecção:
    • Biblioteca 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
    }
}

Raridade: Muito Comum Dificuldade: Média


14. Como você otimiza o tempo de inicialização do aplicativo?

Resposta: Uma inicialização mais rápida melhora a experiência do usuário:

  1. Inicialização Lazy: Inicialize objetos somente quando necessário
  2. Evite Trabalho Pesado em Application.onCreate():
    • Mova para uma thread em segundo plano
    • Adie a inicialização não crítica
  3. Content Providers: Minimize ou carregue de forma lazy
  4. Reduza as Dependências: Menos bibliotecas = inicialização mais rápida
  5. App Startup Library: Inicialização estruturada
  6. Baseline Profiles: Dicas de compilação 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()
    }
}

Raridade: Comum Dificuldade: Média


15. Como você lida com o carregamento e o cache de bitmaps de forma eficiente?

Resposta: O tratamento eficiente de imagens é crucial para o desempenho:

  • Bibliotecas: Glide, Coil (lidam com o cache automaticamente)
  • Otimização Manual:
    • Downsampling (carregar imagens menores)
    • Cache de memória (LruCache)
    • Cache de disco
    • 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
}

Raridade: Comum Dificuldade: Difícil


16. O que é ANR e como você o impede?

Resposta: ANR (Application Not Responding) ocorre quando a thread principal é bloqueada por muito tempo.

  • Causas:
    • Computação pesada na thread principal
    • Chamadas de rede na thread principal
    • Operações de banco de dados na thread principal
    • Deadlocks
  • Prevenção:
    • Mova o trabalho pesado para threads em segundo plano
    • Use coroutines com dispatchers adequados
    • Evite blocos synchronized na thread principal
    • Use WorkManager para tarefas em 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)
        }
    }
}

Raridade: Comum Dificuldade: Fácil


Testes (3 Perguntas)

17. Como você escreve testes unitários para ViewModels?

Resposta: Os ViewModels devem ser testados isoladamente com dependências mockadas.

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

Dicas de carreira semanais que realmente funcionam

Receba as últimas ideias diretamente na sua caixa de entrada

Destaque-se para Recrutadores e Conquiste o Emprego dos Seus Sonhos

Junte-se a milhares que transformaram suas carreiras com currículos impulsionados por IA que passam no ATS e impressionam gerentes de contratação.

Comece a criar agora

Compartilhar esta publicação

Duplique Seus Retornos de Entrevista

Candidatos que adaptam seus currículos à descrição da vaga obtêm 2,5 vezes mais entrevistas. Use nossa IA para personalizar automaticamente seu CV para cada candidatura instantaneamente.