Dezember 21, 2025
17 Min. Lesezeit

Interviewfragen für erfahrene Mobile Entwickler (Android): Der ultimative Leitfaden

interview
career-advice
job-search
Interviewfragen für erfahrene Mobile Entwickler (Android): Der ultimative Leitfaden
MB

Milad Bonakdar

Autor

Meistern Sie die fortgeschrittene Android-Entwicklung mit wichtigen Interviewfragen zu Architekturmustern, Leistungsoptimierung, Dependency Injection, Tests, Sicherheit und Systemdesign für erfahrene Entwickler.


Einführung

Von erfahrenen Android-Entwicklern wird erwartet, dass sie skalierbare, wartbare Anwendungen entwerfen und gleichzeitig hohe Leistung und Codequalität gewährleisten. Diese Rolle erfordert fundierte Kenntnisse der Android-Frameworks, Architekturmuster, Dependency Injection, Teststrategien und die Fähigkeit, fundierte technische Entscheidungen zu treffen.

Dieser umfassende Leitfaden behandelt wichtige Interviewfragen für erfahrene Android-Entwickler, die fortgeschrittene Kotlin-Konzepte, Architekturmuster, Leistungsoptimierung, Dependency Injection, Tests und Systemdesign umfassen. Jede Frage enthält detaillierte Antworten, eine Seltenheitseinschätzung und Schwierigkeitsgrade.


Fortgeschrittene Kotlin- und Sprachfunktionen (5 Fragen)

1. Erläutern Sie Kotlin-Coroutinen und ihre Vorteile gegenüber Threads.

Antwort: Coroutinen sind leichtgewichtige Nebenläufigkeitsprimitive, die es ermöglichen, asynchronen Code sequentiell zu schreiben.

  • Vorteile gegenüber Threads:
    • Leichtgewichtig: Kann Tausende von Coroutinen ohne Leistungsprobleme erstellen
    • Strukturierte Nebenläufigkeit: Eltern-Kind-Beziehung gewährleistet ordnungsgemäße Bereinigung
    • Unterstützung für Abbruch: Integrierte Abbruchweiterleitung
    • Ausnahmebehandlung: Strukturierte Ausnahmebehandlung
  • Schlüsselkomponenten:
    • CoroutineScope: Definiert den Lebenszyklus
    • Dispatchers: Steuern den Ausführungskontext (Main, IO, Default)
    • suspend functions: Können pausiert und fortgesetzt werden
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)
            }
        }
    }
}

Seltenheit: Sehr häufig Schwierigkeit: Schwer


2. Was sind Sealed Classes und wann sollten Sie sie verwenden?

Antwort: Sealed Classes stellen eingeschränkte Klassenhierarchien dar, in denen alle Unterklassen zur Kompilierzeit bekannt sind.

  • Vorteile:
    • Erschöpfende when-Ausdrücke
    • Typsichere Zustandsverwaltung
    • Besser als Enums für komplexe Daten
  • Anwendungsfälle: Darstellung von Zuständen, Ergebnissen, Navigationsereignissen
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 ?: "Unbekannter Fehler")
            }
        }
    }
}

// In UI
when (val state = uiState.value) {
    is UiState.Loading -> showLoading()
    is UiState.Success -> showData(state.data)
    is UiState.Error -> showError(state.message)
    // Compiler stellt sicher, dass alle Fälle behandelt werden
}

Seltenheit: Häufig Schwierigkeit: Mittel


3. Erläutern Sie Kotlin Flow und wie es sich von LiveData unterscheidet.

Antwort: Flow ist Kotlin's kalter asynchroner Stream, der Werte sequentiell ausgibt.

  • Flow vs LiveData:
    • Flow: Kalter Stream, unterstützt Operatoren, nicht lebenszyklusabhängig, flexibler
    • LiveData: Heißer Stream, lebenszyklusabhängig, Android-spezifisch, einfacher für UI
  • Flow-Typen:
    • Flow: Kalter Stream (beginnt bei Sammlung)
    • StateFlow: Heißer Stream mit aktuellem Zustand
    • SharedFlow: Heißer Stream für Ereignisse
class UserRepository {
    // Cold Flow - beginnt, wenn gesammelt
    fun getUsers(): Flow<List<User>> = flow {
        val users = api.fetchUsers()
        emit(users)
    }.flowOn(Dispatchers.IO)
    
    // StateFlow - heiß, hält Zustand
    private val _userState = MutableStateFlow<List<User>>(emptyList())
    val userState: StateFlow<List<User>> = _userState.asStateFlow()
    
    // SharedFlow - heiß, für Ereignisse
    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)
        }
    }
}

Seltenheit: Sehr häufig Schwierigkeit: Schwer


4. Was sind Inline Functions und wann sollten Sie sie verwenden?

Antwort: Inline-Funktionen kopieren den Funktionskörper an die Aufrufstelle und vermeiden so den Overhead von Funktionsaufrufen.

  • Vorteile:
    • Eliminiert den Overhead der Lambda-Zuordnung
    • Ermöglicht nicht-lokale Rückgaben von Lambdas
    • Bessere Leistung für Funktionen höherer Ordnung
  • Anwendungsfälle: Funktionen höherer Ordnung mit Lambda-Parametern
  • Trade-off: Erhöht die Codegröße
// 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
    }
}

Seltenheit: Mittel Schwierigkeit: Schwer


5. Erläutern Sie Delegation in Kotlin.

Antwort: Delegation ermöglicht es einem Objekt, einige seiner Verantwortlichkeiten an ein anderes Objekt zu delegieren.

  • Klassendelegation: by-Schlüsselwort
  • Eigenschaftsdelegation: Lazy, observable, delegates
  • Vorteile: Codewiederverwendung, Komposition statt Vererbung
// 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()
    }
}

Seltenheit: Mittel Schwierigkeit: Mittel


Architekturmuster (6 Fragen)

6. Erläutern Sie die MVVM-Architektur und ihre Vorteile.

Antwort: MVVM (Model-View-ViewModel) trennt die UI-Logik von der Geschäftslogik.

Loading diagram...
  • Model: Datensicht (Repositories, Datenquellen)
  • View: UI-Schicht (Activities, Fragments, Composables)
  • ViewModel: Präsentationslogik, überlebt Konfigurationsänderungen
  • Vorteile: Testfähig, Trennung von Belangen, lebenszyklusabhängig
// 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)
                }
            }
        }
    }
}

Seltenheit: Sehr häufig Schwierigkeit: Mittel


7. Was ist Clean Architecture und wie implementieren Sie sie in Android?

Antwort: Clean Architecture trennt Code in Schichten mit klaren Abhängigkeiten.

Loading diagram...
  • Presentation: UI, ViewModels
  • Domain: Use Cases, Business Logic, Entities
  • Data: Repositories, Data Sources (API, Database)
  • Dependency Rule: Innere Schichten kennen die äußeren Schichten nicht
// 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)
            }
        }
    }
}

Seltenheit: Häufig Schwierigkeit: Schwer


8. Erläutern Sie Dependency Injection und Dagger/Hilt.

Antwort: Dependency Injection stellt Klassen Abhängigkeiten bereit, anstatt sie intern zu erstellen.

  • Vorteile: Testbarkeit, lose Kopplung, Wiederverwendbarkeit
  • Dagger: Compile-time DI-Framework
  • Hilt: Vereinfachtes Dagger für 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()
}

Seltenheit: Sehr häufig Schwierigkeit: Schwer


9. Was ist das Repository-Muster und warum sollte man es verwenden?

Antwort: Das Repository-Muster abstrahiert Datenquellen und bietet eine saubere API für den Datenzugriff.

  • Vorteile:
    • Single Source of Truth
    • Zentralisierte Datenlogik
    • Einfaches Wechseln von Datenquellen
    • Testfähig
  • Implementierung: Koordiniert zwischen mehreren Datenquellen
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)
            }
    }
}

Seltenheit: Sehr häufig Schwierigkeit: Mittel


10. Erläutern Sie die Single Activity-Architektur.

Antwort: Die Single Activity-Architektur verwendet eine Activity mit mehreren Fragments, die von der Navigationskomponente verwaltet werden.

  • Vorteile:
    • Vereinfachte Navigation
    • Gemeinsame ViewModels zwischen Fragments
    • Bessere Animationen
    • Einfacheres Deep Linking
  • Navigationskomponente: Behandelt Fragmenttransaktionen, Back Stack, Argumente
// 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)
    }
}

Seltenheit: Häufig Schwierigkeit: Mittel


11. Was ist die MVI-Architektur (Model-View-Intent)?

Antwort: MVI ist eine unidirektionale Datenflussarchitektur, die von Redux inspiriert ist.

  • Komponenten:
    • Model: Stellt den UI-Zustand dar
    • View: Rendert den Zustand, gibt Intents aus
    • Intent: Benutzeraktionen/Ereignisse
  • Vorteile: Vorhersagbarer Zustand, einfacheres Debugging, Time-Travel-Debugging
// 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)
        }
    }
}

Seltenheit: Mittel Schwierigkeit: Schwer


Leistung und Optimierung (5 Fragen)

12. Wie optimieren Sie die RecyclerView-Leistung?

Antwort: Mehrere Strategien verbessern die Scrollleistung von RecyclerView:

  1. ViewHolder-Muster: Wiederverwendung von Views (eingebaut)
  2. DiffUtil: Effiziente Listenaktualisierungen
  3. Stabile IDs: Überschreiben Sie getItemId() und setHasStableIds(true)
  4. Prefetching: Erhöhen Sie die Prefetch-Distanz
  5. Image Loading: Verwenden Sie Bibliotheken wie Glide/Coil mit korrekter Größe
  6. Vermeiden Sie schwere Operationen: Führen Sie keine teuren Berechnungen in onBindViewHolder durch
  7. Nested RecyclerViews: Setzen Sie setRecycledViewPool() und 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)

Seltenheit: Sehr häufig Schwierigkeit: Mittel


13. Wie erkennen und beheben Sie Speicherlecks in Android?

Antwort: Speicherlecks treten auf, wenn Objekte länger als nötig im Speicher gehalten werden.

  • Häufige Ursachen:
    • Kontextlecks (Activity/Fragment-Referenzen)
    • Statische Referenzen
    • Anonyme innere Klassen
    • Nicht abgemeldete Listener
    • Nicht abgebrochene Coroutinen
  • Erkennungswerkzeuge:
    • LeakCanary-Bibliothek
    • 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
    }
}

Seltenheit: Sehr häufig Schwierigkeit: Mittel


14. Wie optimieren Sie die App-Startzeit?

Antwort: Schnellerer Start verbessert die Benutzererfahrung:

  1. Lazy Initialization: Initialisieren Sie Objekte nur bei Bedarf
  2. Vermeiden Sie schwere Arbeit in Application.onCreate():
    • Verschieben Sie sie in den Hintergrund-Thread
    • Verschieben Sie die nicht kritische Initialisierung
  3. Content Providers: Minimieren oder Lazy-Load
  4. Reduzieren Sie Abhängigkeiten: Weniger Bibliotheken = schnellerer Start
  5. App Startup Library: Strukturierte Initialisierung
  6. Baseline Profiles: Ahead-of-Time-Kompilierungshinweise
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()
    }
}

Seltenheit: Häufig Schwierigkeit: Mittel


15. Wie behandeln Sie das Laden und Caching von Bitmaps effizient?

Antwort: Eine effiziente Bildverarbeitung ist entscheidend für die Leistung:

  • Bibliotheken: Glide, Coil (behandeln das Caching automatisch)
  • Manuelle Optimierung:
    • Downsampling (kleinere Bilder laden)
    • 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
}

Seltenheit: Häufig Schwierigkeit: Schwer


16. Was ist ANR und wie verhindern Sie es?

Antwort: ANR (Application Not Responding) tritt auf, wenn der Haupt-Thread zu lange blockiert ist.

  • Ursachen:
    • Schwere Berechnungen im Haupt-Thread
    • Netzwerkaufrufe im Haupt-Thread
    • Datenbankoperationen im Haupt-Thread
    • Deadlocks
  • Prävention:
    • Verschieben Sie schwere Arbeit in Hintergrund-Threads
    • Verwenden Sie Coroutinen mit geeigneten Dispatchern
    • Vermeiden Sie synchronisierte Blöcke im Haupt-Thread
    • Verwenden Sie WorkManager für Hintergrundaufgaben
// 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)
        }
    }
}

Seltenheit: Häufig Schwierigkeit: Einfach


Testen (3 Fragen)

17. Wie schreiben Sie Unit Tests für ViewModels?

Antwort: ViewModels sollten isoliert mit simulierten Abhängigkeiten getestet werden.

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.
Newsletter subscription

Wöchentliche Karrieretipps, die wirklich funktionieren

Erhalten Sie die neuesten Einblicke direkt in Ihr Postfach

Decorative doodle

Ihr nächstes Vorstellungsgespräch ist nur einen Lebenslauf entfernt

Erstellen Sie in wenigen Minuten einen professionellen, optimierten Lebenslauf. Keine Designkenntnisse erforderlich—nur bewährte Ergebnisse.

Meinen Lebenslauf erstellen

Diesen Beitrag teilen

Nutzen Sie Ihre 6 Sekunden Optimal

Recruiter scannen Lebensläufe durchschnittlich nur 6 bis 7 Sekunden. Unsere bewährten Vorlagen sind darauf ausgelegt, sofort Aufmerksamkeit zu erregen und sie weiterlesen zu lassen.