Android Development Fundamentals
itMobile and client application development
Android Development Fundamentals
Android development means building software for a platform that controls your process, components, resources, permissions, and device integration. Your code does not own one permanent process from start to finish. The system starts components when needed and can reclaim the process later.
That platform relationship is the first mental model to learn. A reliable Android app cooperates with lifecycle changes, reconstructs disposable user interface state, protects persistent data, and treats device resources as constrained.
Modern Android development uses Kotlin, Android Studio, Gradle, Jetpack libraries, and Jetpack Compose. Google recommends Kotlin for new Android apps. Compose is the recommended modern user interface toolkit.
Android is a good fit when you need a native application across phones, tablets, foldables, watches, televisions, cars, or other Android devices. It is not automatically the best fit for every client. A web application may be enough when installation, offline behavior, platform integration, and native distribution add little value.
Think in system entry points
An Android app has four framework component types:
- An activity is an entry point for user interaction.
- A service performs work without a user interface.
- A broadcast receiver responds to broadcast messages.
- A content provider exposes structured data through a controlled interface.
The system can enter your app through any declared component. There is no required application-level main function that owns the whole lifetime.
Most current applications use one activity as a host for several screens. Compose renders those screens, while a navigation library tracks destinations and back-stack state. Other component types remain important when the app integrates with system or cross-app behavior.
Do not create a service for ordinary asynchronous work. A coroutine can handle work tied to an active screen or process. WorkManager handles deferrable work that must survive the visible screen. A foreground service is reserved for suitable user-noticeable work and carries stricter platform rules.
The manifest tells the system what exists
Every app project includes an AndroidManifest.xml file. The manifest declares essential information to the build tools, Android, and Google Play.
It can declare:
- Application components and their classes
- Intent filters that describe supported entry points
- Permissions needed for protected data or capabilities
- Permissions required to call exported components
- Required hardware and software features
The manifest is a security and compatibility boundary. An exported component is available outside your application under its declared rules. A required feature can remove incompatible devices from Play distribution. A permission declaration expands what the app may ask the user or system to grant.
Gradle can merge several manifests from the app, build variant, and libraries. Inspect the merged manifest when behavior differs from the source file you opened.
The activity lifecycle is a state machine
The system moves an activity through lifecycle states and invokes callbacks around those transitions.
| Callback | Working meaning |
|---|---|
onCreate | Initialize the activity instance and compose the initial interface |
onStart | The activity becomes visible |
onResume | The activity becomes interactive in the foreground |
onPause | The activity is losing foreground focus |
onStop | The activity is no longer visible |
onDestroy | The activity instance is being destroyed |
Treat callbacks as state transitions, not a script that always runs in one fixed sequence. A configuration change can recreate the activity. The process can disappear while the app is not visible. Returning users expect their meaningful state to be restored.
Keep screen state in a ViewModel when it should survive activity recreation. Persist small restorable values through saved state when process recreation matters. Store durable application data in a database, DataStore, or files. These mechanisms solve different lifetimes.
Compose renders state
Jetpack Compose is declarative. A composable function describes the user interface for its current inputs. When observed state changes, Compose can run affected composables again and apply the required updates.
@Composable
fun Counter(
count: Int,
onIncrement: () -> Unit,
) {
Button(onClick = onIncrement) {
Text(text = "Count: " + count)
}
}
This function does not search for an existing button and mutate its label. It describes what the button should be now.
State is any value that can change over time. Compose observes supported state holders and schedules recomposition after changes. remember keeps an object across recompositions while that composable remains in the composition. It does not provide durable storage.
Prefer state hoisting for reusable components. Pass state down and send user actions up. A stateless composable is easier to preview, test, and reuse.
ViewModel state -> screen -> reusable component
ViewModel action <- screen <- user event
Recomposition can occur often, in a different order, or be skipped. Keep composables free of unguarded side effects. Use Compose effect APIs when work must follow the composition lifecycle.
Architecture separates policy from presentation
The recommended Android architecture separates at least two layers:
- The user interface layer displays application data and handles user interaction.
- The data layer contains application data and most business logic.
A repository exposes data to the rest of the app. It coordinates one or more data sources, such as a database, network client, sensor, or DataStore. The user interface layer should not reach directly into those data sources.
A ViewModel is a screen-level state holder. It receives actions, calls the data layer, and exposes user interface state. Modern guidance favors unidirectional data flow:
repository -> ViewModel -> user interface state -> Compose
repository <- ViewModel <- user action <- Compose
Keep one source of truth for each piece of data. Duplicate mutable copies drift. Derived presentation values can be calculated from source data.
Use a domain layer only when shared or complex business logic benefits from separate use cases. Small apps do not need every possible layer.
Resources adapt without branching everywhere
Android resources keep strings, images, colors, dimensions, and other values separate from code. Build tools generate identifiers in the R class.
The resource system selects alternatives for the current device configuration. You can provide different resources for languages, screen sizes, densities, orientations, and night mode while referring to one resource identifier.
Always provide suitable defaults. Use configuration qualifiers for alternatives. Do not scatter device checks through application logic when resource selection already models the variation.
Resource separation supports localization and adaptive layouts. It does not make an interface adaptive by itself. Your layout still needs to respond to available space and input modes.
Navigation models destinations and history
Navigation should represent user-visible destinations, their arguments, and the back stack. Pass stable identifiers between destinations rather than large mutable objects. Load current data from the source of truth at the destination.
A deep link is an external entry into a specific destination. Treat every deep link argument as untrusted input. Verify authorization after navigation when the destination accesses protected data.
Navigation state is not business state. The back stack answers where the user is. A repository answers what application data exists.
Store data by lifetime and sharing needs
Android provides several storage boundaries:
| Need | Starting point |
|---|---|
| Structured private data | Room database |
| Small preference-like data | DataStore |
| App-only files | App-specific internal storage |
| User-visible shared media | MediaStore or the appropriate system picker |
| Temporary recomposition state | Compose remember |
| Screen state across recreation | ViewModel |
Internal app storage is private to the app by default. Shared storage is intended for content that other apps or users need to access. Sensitive data does not belong in broadly accessible storage.
Caching and persistence are different decisions. A cache can be discarded and rebuilt. Persistent data must retain the user's committed intent.
Permissions are revocable capabilities
Declare only the permissions your feature needs. Prefer system APIs that complete the task without broad access.
For a runtime permission:
- Wait until the user invokes the feature.
- Check whether the permission is already granted.
- Explain the need when the platform indicates that a rationale is appropriate.
- Request the permission through the current activity result API.
- Handle denial without breaking unrelated parts of the app.
- Check again whenever performing the protected operation.
A permission can be denied or revoked. Code that assumes permanent access is incorrect.
Permission approval does not replace application authorization. A camera permission allows access to a device capability. It does not prove that a user may upload a photo to a particular account.
Background work must match user intent
Choose an execution mechanism from the required lifetime:
- Use a coroutine or another asynchronous mechanism for work that can stop with its owning lifecycle.
- Use WorkManager for deferrable work that must continue after the visible screen or process.
- Use a suitable foreground service only for supported user-noticeable work that must continue immediately.
- Use a specialized platform API when one matches the task.
Never block the main thread with network, storage, or expensive computation. The main thread handles user input and rendering. Blocking it causes jank and can lead to an application-not-responding failure.
Testing follows boundaries
Local tests run on the development machine or a server. They are usually fast and isolated. Instrumented tests run on a physical or emulated Android device and can exercise framework and user interface behavior.
Test the data layer and ViewModel with controlled dependencies. Test important Compose behavior through semantics, because the semantics tree is also what accessibility services use to understand the interface.
Use fakes when they provide a realistic, controllable implementation. Reserve broad end-to-end tests for critical flows. A test portfolio needs fast feedback and enough device coverage for platform-dependent behavior.
Accessibility is part of the component contract
Compose and Material components include useful accessibility semantics. Custom components need equivalent meaning and actions.
Label meaningful controls. Do not rely on color alone. Preserve readable contrast and touch targets. Support font scaling and changing window sizes. Test with TalkBack, Switch Access, accessibility inspection tools, and automated Compose tests.
The semantics tree connects accessibility and testing. A control with unclear semantics is harder for both a screen reader and a stable user interface test to identify.
Security starts with the sandbox
Android gives each app a separate identity and sandbox. The sandbox limits default access between apps. Your design must preserve that boundary.
- Keep sensitive files in internal storage.
- Export only components that need external access.
- Protect exported interfaces with appropriate permissions and validation.
- Minimize runtime permissions.
- Use secure network protocols.
- Treat intents, deep links, files, and network responses as untrusted input.
- Keep signing keys and service credentials out of the application package.
An app package can be inspected. A hard-coded secret is not secret.
Build, measure, and release deliberately
Gradle and the Android Gradle plugin build your source, resources, manifest, and dependencies into installable or publishable artifacts.
A build type represents a stage such as debug or release. A product flavor represents a product variation. Their cross-product creates build variants. Keep environment policy explicit instead of hiding it in source edits.
Measure performance on a production-like build. Debug instrumentation can distort results. Watch startup, rendering, main-thread work, memory, battery, crashes, and application-not-responding failures.
Google Play accepts an Android App Bundle and generates optimized APKs for supported device configurations. A release still needs versioning, signing, testing, store metadata, policy compliance, and monitoring after rollout.
A practical learning path
- Learn enough Kotlin to read nullability, functions, data classes, coroutines, and flows.
- Create a small Compose screen and hoist its state.
- Trace activity recreation and process-aware state restoration.
- Add navigation between two destinations.
- Move screen logic into a
ViewModel. - Put durable data behind a repository.
- Add one permission-gated feature with a denial path.
- Schedule one appropriate piece of persistent work.
- Test state logic and a critical Compose interaction.
- Review accessibility, security, release configuration, and measured performance.
