Building Your First Android App with Kotlin

Building Your First Android App with Kotlin

Kotlin is Google’s preferred language for Android development—it is concise, null-safe, and fully interoperable with Java. Modern Android development uses Jetpack Compose for UI, the Navigation component for screen routing, and ViewModel + Room for data management. This article walks through building a simple note-taking app from scratch, covering the essential components every Android app needs.

Project Setup and Dependencies

Android Studio creates new projects with Gradle as the build system. The build.gradle.kts file declares dependencies and configuration. A modern Android project includes Jetpack Compose (UI framework), Navigation Compose (screen routing), and optional libraries like Room (local database) and Retrofit (networking). The minimum SDK version determines which Android versions your app supports—API 24 (Android 7.0) covers over 95% of active devices. Using version catalogs (libs.versions.toml) keeps dependency versions organized.

// build.gradle.kts (app module)
plugins {
    id("com.android.application")
    id("org.jetbrains.kotlin.android")
    id("org.jetbrains.kotlin.plugin.compose")
}

android {
    namespace = "com.example.notepad"
    compileSdk = 35
    defaultConfig {
        applicationId = "com.example.notepad"
        minSdk = 24
        targetSdk = 35
        versionCode = 1
        versionName = "1.0"
    }
}

dependencies {
    implementation(platform("androidx.compose:compose-bom:2025.01.00"))
    implementation("androidx.compose.ui:ui")
    implementation("androidx.compose.material3:material3")
    implementation("androidx.navigation:navigation-compose:2.8.0")
    implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.0")
}

Building the UI with Compose

Jetpack Compose uses composable functions to declare UI. Unlike the old View system with XML layouts, Compose is declarative—you describe what the UI should look like for a given state, and Compose handles updating the screen when state changes. A composable function annotated with @Composable takes optional parameters and emits UI elements. Material 3 provides Modern design components: Scaffold (screen structure), TopAppBar, FloatingActionButton, and themed surfaces.

@Composable
fun NoteListScreen(
    notes: List<Note>,
    onAddNote: () -> Unit,
    onNoteClick: (Int) -> Unit
) {
    Scaffold(
        topBar = { TopAppBar(title = { Text("My Notes") }) },
        floatingActionButton = {
            FloatingActionButton(onClick = onAddNote) {
                Icon(Icons.Default.Add, contentDescription = "Add")
            }
        }
    ) { padding ->
        LazyColumn(modifier = Modifier.padding(padding)) {
            items(notes, key = { it.id }) { note ->
                NoteCard(note = note, onClick = { onNoteClick(note.id) })
            }
        }
    }
}

@Composable
fun NoteCard(note: Note, onClick: () -> Unit) {
    Card(
        modifier = Modifier.fillMaxWidth().padding(8.dp).clickable(onClick = onClick),
        elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
    ) {
        Column(modifier = Modifier.padding(16.dp)) {
            Text(note.title, style = MaterialTheme.typography.titleMedium)
            Spacer(Modifier.height(4.dp))
            Text(note.content, maxLines = 2, style = MaterialTheme.typography.bodyMedium)
        }
    }
}

ViewModel and State Management

ViewModel holds UI state and survives configuration changes (screen rotation). It exposes state as StateFlow or Compose MutableState, and the UI observes this state to recompose when data changes. Never store state directly in composables—they are recreated frequently. The ViewModel uses viewModelScope for coroutines and integrates with Room via Repository pattern. Navigation between screens uses NavHost with a route string, and data is passed via savedStateHandle or shared ViewModel.

class NoteViewModel(private val dao: NoteDao) : ViewModel() {
    private val _notes = MutableStateFlow<List<Note>>(emptyList())
    val notes: StateFlow<List<Note>> = _notes.asStateFlow()

    init {
        viewModelScope.launch {
            dao.getAllNotes().collect { _notes.value = it }
        }
    }
    fun addNote(title: String, content: String) {
        viewModelScope.launch {
            dao.insert(Note(title = title, content = content))
        }
    }
}

Testing Android apps uses Compose UI testing (createComposeRule) for UI tests and JUnit + MockK for ViewModel tests. Room databases can be tested with an in-memory instance. Modern Android development emphasizes clean architecture: UI layer (Compose), domain layer (use cases), and data layer (Room + Retrofit), with dependency injection via Hilt or Koin to wire them together.

Dependency Injection with Hilt

Hilt is Google’s dependency injection library for Android, built on Dagger. It provides a standard way to provide dependencies (ViewModels, Repositories, database instances) throughout the app without manual construction. Hilt annotations (@HiltAndroidApp, @AndroidEntryPoint, @Inject, @Module, @Provides) reduce boilerplate compared to manual DI. The ViewModel is provided through @HiltViewModel and injected into composables with hiltViewModel(). Hilt modules define how to create dependencies like Room databases or Retrofit API clients, with scoping (@Singleton, @ViewModelScoped, @FragmentScoped) controlling their lifecycle. Using Hilt makes the code more testable—you can replace real dependencies with mocks in tests by providing a test module.

@Module
@InstallIn(SingletonComponent::class)
object DatabaseModule {
    @Provides @Singleton
    fun provideDatabase(@ApplicationContext ctx: Context): AppDatabase {
        return Room.databaseBuilder(ctx, AppDatabase::class.java, "app.db").build()
    }
    @Provides fun provideNoteDao(db: AppDatabase) = db.noteDao()
}

Publishing to Google Play

Releasing an Android app on Google Play involves: creating a developer account ($25 one-time fee), preparing a signed release bundle (AAB format), creating a store listing, and defining the testing track (internal testing, closed alpha, open beta, production). Google Play’s Managed Publishing enables phased rollouts for monitoring crash rates before full release. The Play Console provides crash reports, performance data, and user ratings with sentiment analysis. For pre-release testing, Firebase Test Lab runs automated tests on a range of physical devices across API levels 24-35. Android App Bundles reduce download size by generating APKs specific to each device configuration, typically saving 30-50% download size.

Android Jetpack Compose vs Traditional Views

Android Jetpack Compose vs Traditional Views

Jetpack Compose is Google’s modern UI toolkit for Android, replacing the traditional View system with a declarative, Kotlin-first approach. Understanding the differences between Compose and Views helps teams decide whether to migrate, and how to approach new projects. This article compares both systems across key dimensions: development speed, performance, interoperability, and learning curve.

Declarative vs Imperative UI

Traditional Views use an imperative approach: you build a tree of View objects (defined in XML layout files), then programmatically modify them using findViewById() and setters (setText(), setVisibility(), etc.). State changes require manually updating each affected View. Compose is declarative: you define what the UI looks like for every possible state, and Compose automatically updates the screen when state changes. This eliminates a whole category of bugs where Views are out of sync with the underlying data.

// Traditional View: imperative
TextView textView = findViewById(R.id.greeting);
textView.setText("Hello, " + userName);
textView.setVisibility(showGreeting ? View.VISIBLE : View.GONE);

// Jetpack Compose: declarative
@Composable
fun Greeting(userName: String, showGreeting: Boolean) {
    if (showGreeting) {
        Text("Hello, $userName")
    }
    // Compose handles showing/hiding automatically when state changes
}

Layout Systems Compared

Traditional layouts use XML with LinearLayout, RelativeLayout, ConstraintLayout, and FrameLayout. ConstraintLayout is the most powerful, building flat view hierarchies with relative positioning rules. Compose uses composable functions: Row, Column, Box, and LazyColumn/LazyRow. The key difference is that Compose’s lazy lists recompose only visible items (like RecyclerView but simpler), while ScrollView in the View system loads all child views upfront. Compose’s Modifier system chains attributes (padding, clickable, background) in a fluent API, eliminating XML namespaces and attribute lookups.

State Management

Compose’s state management is its killer feature. With Views, you must manually store state, serialize it across configuration changes (onSaveInstanceState), and write logic to restore it. Compose uses remember (keep state across recompositions), rememberSaveable (survive process death), and ViewModel (survive configuration changes). State hoisting (lifting state to a parent composable) keeps components testable and reusable. Compose’s StateFlow and collectAsState() integration with ViewModel means state flows naturally from data layer to UI without manual wiring.

Interoperability

You can use Compose inside existing View-based apps (ComposeView) and embed Views inside Compose (AndroidView). Migration can be incremental—start with a single screen in Compose while keeping the rest of the app in Views. Google recommends new apps start with Compose, and existing apps gradually adopt it screen by screen. Compose 1.7+ (2025) has reached feature parity with the View system for most use cases. The Google Maps Compose library, Maps Compose, and Accompanist provide first-party Compose versions of popular libraries.

// Embedding Compose in an existing View-based Activity
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        findViewById<ComposeView>(R.id.compose_view).setContent {
            MaterialTheme {
                Text("Hello from Compose inside XML!")
            }
        }
    }
}

Performance benchmarks show Compose is comparable to Views for most screens, with Compose sometimes faster for complex animations (since the recomposition engine is highly optimized) and Views sometimes faster for very simple static screens. Compose’s draw phase is deferred and batched, reducing overdraw. The Compose compiler converts composable functions into efficient UI tree updates, and the layout system uses a single-pass measurement model (vs the View system’s two-pass measure/layout). For new projects, Compose is the clear recommendation—less code, fewer bugs, faster development, and full Google support going forward.

Animation and Gestures

Compose’s animation system is more intuitive than the View system’s Animation framework. Animations are state-driven: you define a target state, and Compose animates the transition automatically. Animatable holds a value that smooths between states. AnimatedVisibility handles enter/exit transitions (fade, slide, expand). Gesture detection uses Modifier.pointerInput() with awaitPointerEventScope for custom gestures, or higher-level modifiers like clickable, draggable, and swipeable. The animation framework integrates with the compose compiler to skip recompositions during animation frames where the layout has not changed, keeping animation performance smooth even on low-end devices. For complex gesture handling, the gesture navigation library provides standard swipe-back and bottom sheet interactions.

@Composable
fun AnimatedCounter(count: Int) {
    val animatable = remember { Animatable(0f) }
    LaunchedEffect(count) {
        animatable.animateTo(count.toFloat(), animationSpec = spring())
    }
    Text("Count: ${animatable.value.toInt()}")
}

Testing Compose UI

Compose UI testing uses semantics nodes rather than view IDs, making tests more robust to implementation changes. The createComposeRule() sets up a test environment, and SemanticsMatchers find elements by text, content description, state, or custom semantics properties. Compose’s onNodeWithText() and onNodeWithContentDescription() find elements by their displayed text, and performClick() simulates user interaction. Screenshot tests (Roborazzi or Paparazzi) capture composable snapshots and compare them against golden images. Compose testing is generally faster and more reliable than Espresso tests for View-based UI because there are no view hierarchies to traverse.

Optimizing Android App Performance

Optimizing Android App Performance

Android app performance directly impacts user retention, battery life, and app store ratings. Performance optimization covers three main areas: rendering (smooth 60/120fps UI), memory (avoiding leaks and excessive allocation), and battery (minimizing wake locks, network calls, and background work). This article covers profiling tools and optimization techniques for production Android apps.

Rendering Performance with the Profile GPU tool

The Android GPU Profiling overlay (Developer Options → Profile GPU rendering) shows a bar chart of how long each frame takes to render. If bars consistently exceed 16ms (for 60fps) or 8ms (for 120fps), the UI is janky. Common causes include: complex layouts with too many nested views, expensive draw operations (large bitmaps, complex shadows), and main thread blocking from disk I/O or network calls. Jetpack Compose’s layout inspector shows recomposition counts—excessive recomposition is a sign that state is being read at the wrong scope.

// Fix: Move state reads to the correct scope
@Composable
fun MyScreen() {
    // BAD: Reading state here recomposes the entire screen
    val scrollState = rememberScrollState()

    // GOOD: Use derivedStateOf for expensive computations
    val isAtBottom by remember {
        derivedStateOf { scrollState.value >= scrollState.maxValue }
    }

    LazyColumn(state = scrollState) {
        items(100) { index ->
            // Item content only recomposes when its own data changes
            ListItem(index = index)
        }
    }
}

Memory Management and Leak Detection

Android has limited memory, and the system kills apps that exceed their allocation. Memory leaks occur when an object holds a reference to an Activity or Context after the Activity is destroyed—common causes include anonymous inner classes, static references to Views, and unregistered listeners. LeakCanary is a memory leak detection library that automatically detects and reports leaks with the reference chain. Profile with Android Studio’s Memory Profiler: look for objects that should be garbage collected but aren’t, and use MAT (Memory Analyzer Tool) to analyze heap dumps for suspected leaks.

// Common leak pattern
class MyActivity : AppCompatActivity() {
    // BAD: Static reference to Activity context
    companion object {
        var sCallback: MyCallback? = null
    }
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        sCallback = MyCallback()  // Leaks activity on rotation!
    }
}

// FIX: Use application context or lifecycle-aware callbacks
class MyApplication : Application() {
    var callback: MyCallback? = null  // Application lives forever, no leak
}

Battery Optimization

Network requests are the #1 battery drain. Batch them using WorkManager for deferrable work, and use exponential backoff for retries. Reduce wake locks by using PendingIntent with FLAG_UPDATE_CURRENT instead of keeping the CPU awake. For location updates, use fused location provider with appropriate priority and interval rather than GPS directly. Background work should use WorkManager with constraints (network availability, idle status) rather than foreground services unless absolutely necessary.

// Efficient background work with WorkManager
val uploadWork = OneTimeWorkRequestBuilder<UploadWorker>()
    .setConstraints(
        Constraints.Builder()
            .setRequiredNetworkType(NetworkType.CONNECTED)
            .setRequiresBatteryNotLow(true)
            .build()
    )
    .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 30, TimeUnit.SECONDS)
    .build()
WorkManager.getInstance(context).enqueue(uploadWork)

Android Studio’s Energy Profiler visualizes power usage by component (CPU, network, GPS, wake locks). Profile on a physical device rather than an emulator for accurate battery readings, and always test on low-end devices (e.g., a 2019 Android Go phone) to ensure your app performs acceptably for the widest possible audience. Performance optimization is a continuous process—integrate benchmarking into your CI pipeline with Android Benchmark library and track regressions over time.

Startup Time Optimization

App startup time is critical for user retention—every 100ms delay reduces conversion by 1%. Android measures startup as cold (process created from scratch), warm (Activity recreated from cached process), and hot (Activity brought to foreground). The most impactful startup optimizations: defer non-critical initialization (lazy init with App Startup library), reduce content provider initialization (merge providers with CombineProviders), use baseline profiles (pre-compiled code paths shipped with the APK), and apply startup best practices from the Android Vitals dashboard. App Startup library initializes components on-demand rather than at application start, ensuring only the components needed for the current screen are loaded. Baseline profiles (generated with Jetpack Macrobenchmark) tell ART (Android Runtime) which code paths to ahead-of-time compile, reducing just-in-time compilation overhead on first launch.

// App Startup: defer non-critical initialization
@InitializationBug
class Initializer : Initializer<Unit> {
    override fun create(context: Context) {
        // Non-critical init moved here instead of Application.onCreate()
        Analytics.initialize(context)
    }
    override fun dependencies() = emptyList<Class<out Initializer<*>>>()
}

Network Optimization

Network requests are often the performance bottleneck in Android apps. Reduce network latency through: HTTP/2 multiplexing (single connection for multiple concurrent requests), response compression (gzip/deflate), client-side caching with OkHttp cache, optimistic UI updates (show results immediately while network requests run in background), and image caching with Coil or Glide (memory + disk cache). For API responses, pagination with cursor-based pagination limits payload size. GraphQL over REST reduces over-fetching. Use OkHttp’s event listener to monitor request timings (DNS lookup, TCP connect, TLS handshake, response body download) and identify bottlenecks.

Play PSP Games on Android: Emulation Guide

Play PSP Games on Android: Emulation Guide

Playing PlayStation Portable (PSP) games on Android devices is possible through emulation, software that mimics the PSP hardware to run original game ISOs. PPSSPP is the leading PSP emulator for Android, offering high compatibility, performance optimization, and features like upscaled resolution, save states, and texture filtering. This guide covers setting up PPSSPP, configuring it for optimal performance, and legal considerations for game ROMs.

Installing PPSSPP on Android

PPSSPP is available on the Google Play Store as both a free version (with ads) and a paid Gold version (supporting development). The free version is fully functional—ads only appear in the menu, not during gameplay. The Gold version removes ads and adds early access to experimental features. Download PPSSPP from the Play Store, or install the standalone APK from the official ppsspp.org website for the latest updates. No root access is required. After installation, place PSP game ISOs or CSOs (compressed ISOs) in a folder on your device’s internal storage or SD card, and PPSSPP will scan and display them in the game browser.

# Recommended folder structure on Android
/storage/emulated/0/PSP/GAME/  # For homebrew and DLC
/storage/emulated/0/PSP/ISO/   # For game ISOs and CSOs
/storage/emulated/0/PSP/SAVEDATA/  # Save files
/storage/emulated/0/PSP/PPSSPP_STATE/  # Save states

Performance Optimization Settings

PSP emulation is computationally intensive because the emulator must translate MIPS CPU instructions to ARM (or x86). Modern mid-range phones (Snapdragon 7xx or higher) run most PSP games at full speed. Key settings: enable “Hardware Transform” and “Vertex Cache” (on by default) for GPU acceleration. Set “Rendering Resolution” to 2x or 3x PSP (1080p or 1440p) for sharper graphics on high-resolution screens. Enable “Texture Scaling” (xBRZ or Hybrid) to smooth low-resolution game textures. For demanding games (God of War, GTA: Vice City Stories), try the “Vulkan” backend instead of OpenGL for better performance. Reduce “Rendering Resolution” to 1x PSP if frame rates drop. Enable “Frame Skipping” (1-2 frames) only as a last resort—it reduces visual smoothness.

Controller Support

PPSSPP supports Bluetooth controllers (PS4, PS5, Xbox, Razer Kishi, Backbone) and on-screen touch controls. Connect a controller via Bluetooth, and PPSSPP maps it automatically in most cases. For games that use the PSP’s analog stick and face buttons heavily (action games, shooters), a controller transforms the experience. The touchscreen overlay is customizable—you can resize buttons, adjust opacity, and reposition controls to avoid covering important screen areas. PPSSPP also supports per-game control profiles, so different games can have different button layouts and sensitivity settings saved and loaded automatically.

Enhancing Visual Quality

Beyond resolution scaling, PPSSPP offers several visual enhancements: anisotropic filtering (4x-16x improves texture appearance at angles), texture replacement (load high-resolution fan-made texture packs for games like Persona 3 Portable), post-processing shaders (FXAA anti-aliasing, scanlines for retro feel, cartoon effect), and geometry upscaling (smooths 3D model edges by increasing polygon count—dramatic improvement for early 3D PSP games). Cheat codes (CWCheat format) can unlock 60fps patches for games originally capped at 30fps, though this may require a device with strong single-core CPU performance. Save states allow saving anywhere, even in games that lack built-in save points, and quick-loading from the last save state takes under a second.

Game Compatibility Database

Not all PSP games run perfectly on PPSSPP. The PPSSPP Compatibility Database lists thousands of games with ratings from “Perfect” (full speed, no glitches) to “Nothing” (unplayable). Most popular titles run at “Playable” or better: God of War: Ghost of Sparta, Persona 3 Portable, Final Fantasy Tactics, GTA: Vice City Stories, Monster Hunter Freedom Unite, andMetal Gear Solid: Peace Walker all run exceptionally well on modern devices. Games that use the PSP’s Media Engine heavily (like Grand Theft Auto: Chinatown Wars) may need frame skipping enabled. Games with unique hardware requirements (camera peripheral, GPS accessory) will not function fully. The community actively maintains compatibility lists, and each new PPSSPP release improves support—check the list before purchasing a device specifically for PSP emulation to ensure your preferred games run at acceptable performance.

Performance Benchmarks by Device

PSP emulation performance varies significantly by device chipset. Snapdragon 8 Gen 2 and 8 Gen 3 devices (Samsung S23/S24, OnePlus 11/12) run every PSP game at 2x-3x resolution with stable 60fps. Snapdragon 7xx and 8xx Gen 1 (mid-range phones from 2022-2023) run most games at 1x-2x resolution. MediaTek Dimensity 8000+ series performs similarly to Snapdragon 8 Gen 1. Apple A13+ iPhones (iPhone 11 and newer) run PPSSPP via the App Store version with excellent performance—Apple’s single-core CPU performance is the best in the mobile market. Low-end devices (Snapdragon 4xx, MediaTek Helio) can run 2D games and less demanding 3D games at 1x resolution. Consider PPSSPP’s built-in performance display (Settings > Developer Options > Show FPS Counter) to monitor frame rates and identify bottlenecks. The emulator’s logging output helps diagnose specific game issues by recording emulation errors and warnings during gameplay.