Physical Address
304 North Cardinal St.
Dorchester Center, MA 02124
Physical Address
304 North Cardinal St.
Dorchester Center, MA 02124


Android app development has evolved dramatically over the past few years. As someone who has been building Android applications since the early days of Eclipse and migrated through Android Studio’s evolution, I can tell you that modern Android development is more powerful and developer-friendly than ever before. This comprehensive guide covers everything you need to know about creating robust, scalable Android applications.
Whether you’re a beginner starting your mobile development journey or an experienced developer looking to modernize your skills, this guide will serve as your roadmap to mastering Android app development in 2025.
With over 2.5 billion active Android devices worldwide, Android continues to dominate the mobile operating system market. The platform offers developers unprecedented reach, flexibility, and monetization opportunities. Google’s commitment to modern development practices, including Jetpack Compose and Kotlin-first approach, makes Android development more intuitive and productive than ever.
| Android Market Stats | 2025 Numbers |
|---|---|
| Global Market Share | 71.2% |
| Active Devices | 2.5+ Billion |
| Google Play Downloads | 100+ Billion Annually |
| Developer Revenue | $47+ Billion |
Setting up your development environment correctly is crucial for a smooth development experience. Android Studio, Google’s official IDE, provides everything you need to build, test, and debug Android applications. The integrated Android SDK includes essential tools, libraries, and emulators.
The foundation of successful Android development starts with proper environment configuration. After years of working with different setups, I’ve learned that investing time in initial configuration saves countless hours later.
Setting up your development environment correctly is crucial for a smooth experience. Android Studio, Google’s official IDE, provides everything you need to build, test, and debug apps. If you’re new, check out our step-by-step Android Studio setup guide
// Example: Basic Android app structure in build.gradle.kts
android {
compileSdk 34
defaultConfig {
applicationId "com.example.myapp"
minSdk 24
targetSdk 34
versionCode 1
versionName "1.0"
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
}
dependencies {
implementation("androidx.core:core-ktx:1.12.0")
implementation("androidx.appcompat:appcompat:1.6.1")
implementation("com.google.android.material:material:1.11.0")
}
Key topics covered:
Modern Android development requires understanding the build system, dependency management, and project configuration. The Android Gradle Plugin handles compilation, packaging, and deployment processes automatically, but knowing how to configure it properly makes the difference between a smooth development experience and constant frustration.
| Component | Purpose | When to Use |
|---|---|---|
| Platform Tools | ADB, Fastboot | Debugging, device communication |
| Build Tools | AAPT, DX compiler | App packaging, resource compilation |
| Android Platforms | API level libraries | Target specific Android versions |
| System Images | Emulator OS | Testing without physical devices |
| Google APIs | Maps, Play Services | Location, authentication services |
Every Android developer must understand the core building blocks of Android applications. The Android framework provides a rich set of components that work together to create seamless user experiences.
Android applications consist of four main component types, each serving specific purposes in the app ecosystem. Understanding these components and their lifecycles prevents common architectural mistakes that lead to memory leaks and poor performance.
// Example: Basic Activity with lifecycle methods
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// Initialize UI components
// Set up click listeners
// Restore saved state if available
}
override fun onStart() {
super.onStart()
// Activity is becoming visible
// Start location updates, register receivers
}
override fun onResume() {
super.onResume()
// Activity is in foreground
// Resume animations, camera preview
}
override fun onPause() {
super.onPause()
// Activity is losing focus
// Pause animations, save user input
}
override fun onDestroy() {
super.onDestroy()
// Activity is being destroyed
// Release resources, unregister listeners
}
}
Essential concepts:
The Android application architecture follows specific patterns that ensure apps work reliably across different devices and Android versions. Understanding these fundamentals prevents common mistakes that lead to crashes, memory leaks, and poor performance.
The AndroidManifest.xml file serves as your app’s blueprint, declaring components, permissions, and system requirements:
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Permissions -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CAMERA" />
<!-- Feature requirements -->
<uses-feature
android:name="android.hardware.camera"
android:required="true" />
<application
android:name=".MyApplication"
android:theme="@style/Theme.MyApp"
android:allowBackup="true">
<!-- Main Activity -->
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<!-- Background Service -->
<service
android:name=".MyBackgroundService"
android:exported="false" />
</application>
</manifest>
Android offers two main approaches for building user interfaces: the traditional View system and the modern Jetpack Compose framework. Both have their place in modern Android App Development, and understanding when to use each approach is crucial for successful projects.
The View-based system has been Android’s UI foundation for over a decade. While Compose is the future, understanding Views remains important for maintaining existing codebases and working with third-party libraries that haven’t migrated to Compose yet.
<!-- Example: Traditional XML layout -->
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/titleText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Welcome to Android"
android:textSize="24sp"
android:textStyle="bold"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginTop="32dp" />
<Button
android:id="@+id/actionButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Get Started"
app:layout_constraintTop_toBottomOf="@id/titleText"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginTop="24dp" />
</androidx.constraintlayout.widget.ConstraintLayout>
// Corresponding Activity code
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val titleText = findViewById<TextView>(R.id.titleText)
val actionButton = findViewById<Button>(R.id.actionButton)
actionButton.setOnClickListener {
titleText.text = "Button Clicked!"
// Handle button click
}
}
}
Core concepts:
Jetpack Compose represents the future of Android UI development. This declarative UI toolkit simplifies interface creation and makes animations, theming, and state management more intuitive. After migrating several production apps to Compose, I can confidently say it reduces UI-related bugs and speeds up development significantly.
// Example: Jetpack Compose UI
@Composable
fun WelcomeScreen(
onGetStartedClick: () -> Unit = {}
) {
var buttonText by remember { mutableStateOf("Get Started") }
Column(
modifier = Modifier
.fillMaxSize()
.padding(32.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(
text = "Welcome to Android",
fontSize = 24.sp,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(bottom = 24.dp)
)
Button(
onClick = {
buttonText = "Button Clicked!"
onGetStartedClick()
}
) {
Text(text = buttonText)
}
}
}
// Using the composable in an Activity
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyAppTheme {
WelcomeScreen(
onGetStartedClick = {
// Handle button click
}
)
}
}
}
}
Compose fundamentals:
| Aspect | Traditional Views | Jetpack Compose |
|---|---|---|
| Paradigm | Imperative | Declarative |
| Layout Files | XML required | Pure Kotlin |
| State Management | Manual updates | Automatic recomposition |
| Animation | Complex XML/code | Built-in animation APIs |
| Testing | Espresso | Compose testing |
| Learning Curve | Moderate | Steep initially, easier long-term |
| Performance | Optimized over years | Improving rapidly |
Having worked with both systems extensively, I recommend learning Compose for new projects while maintaining View system knowledge for legacy code maintenance.
Activities serve as entry points for user interaction, while navigation determines how users move through your application. Modern navigation patterns focus on single-activity architectures with fragment-based navigation, which improves performance and enables better animation transitions.
The Navigation Component has revolutionized how we handle app navigation, providing compile-time safety and visual navigation graphs that make complex navigation flows manageable.
// Example: Navigation with Safe Args
// First, define navigation graph in XML
// nav_graph.xml
/*
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res/auto"
android:id="@+id/nav_graph"
app:startDestination="@id/homeFragment">
<fragment
android:id="@+id/homeFragment"
android:name="com.example.HomeFragment">
<action
android:id="@+id/action_home_to_detail"
app:destination="@id/detailFragment" />
</fragment>
<fragment
android:id="@+id/detailFragment"
android:name="com.example.DetailFragment">
<argument
android:name="itemId"
app:argType="string" />
</fragment>
</navigation>
*/
// Navigation in fragment
class HomeFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val itemButton = view.findViewById<Button>(R.id.itemButton)
itemButton.setOnClickListener {
val action = HomeFragmentDirections
.actionHomeToDetail(itemId = "123")
findNavController().navigate(action)
}
}
}
// Receiving arguments in destination
class DetailFragment : Fragment() {
private val args: DetailFragmentArgs by navArgs()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val itemId = args.itemId
// Use the itemId to load and display data
}
}
Navigation essentials:
Deep linking allows users to navigate directly to specific screens in your app from external sources:
// Deep link handling in Activity
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
handleDeepLink(intent)
}
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
handleDeepLink(intent)
}
private fun handleDeepLink(intent: Intent?) {
val uri = intent?.data
uri?.let {
when (it.pathSegments.firstOrNull()) {
"product" -> {
val productId = it.getQueryParameter("id")
navigateToProduct(productId)
}
"user" -> {
val userId = it.lastPathSegment
navigateToUserProfile(userId)
}
}
}
}
}
Effective data management forms the backbone of any successful Android application. Android provides multiple storage options, each optimized for different use cases and data types. Choosing the right storage solution impacts app performance, user experience, and development complexity.
Room database, part of Android Jetpack, offers compile-time SQL validation and seamless integration with other architecture components. It’s become the gold standard for local data persistence in Android apps.
// Entity definition
@Entity(tableName = "users")
data class User(
@PrimaryKey val id: String,
@ColumnInfo(name = "name") val name: String,
@ColumnInfo(name = "email") val email: String,
@ColumnInfo(name = "created_at") val createdAt: Long = System.currentTimeMillis()
)
// Data Access Object (DAO)
@Dao
interface UserDao {
@Query("SELECT * FROM users ORDER BY name ASC")
fun getAllUsers(): Flow<List<User>>
@Query("SELECT * FROM users WHERE id = :userId")
suspend fun getUserById(userId: String): User?
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertUser(user: User)
@Update
suspend fun updateUser(user: User)
@Delete
suspend fun deleteUser(user: User)
@Query("DELETE FROM users WHERE id = :userId")
suspend fun deleteUserById(userId: String)
}
// Database class
@Database(
entities = [User::class],
version = 1,
exportSchema = false
)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
companion object {
@Volatile
private var INSTANCE: AppDatabase? = null
fun getDatabase(context: Context): AppDatabase {
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
AppDatabase::class.java,
"app_database"
).build()
INSTANCE = instance
instance
}
}
}
}
DataStore replaces SharedPreferences with better performance, type safety, and coroutine support:
// Preferences DataStore
class UserPreferences(private val context: Context) {
companion object {
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(
name = "user_preferences"
)
val USERNAME_KEY = stringPreferencesKey("username")
val IS_LOGGED_IN_KEY = booleanPreferencesKey("is_logged_in")
val THEME_KEY = intPreferencesKey("theme_mode")
}
val userPreferences: Flow<UserPrefs> = context.dataStore.data
.catch { exception ->
if (exception is IOException) {
emit(emptyPreferences())
} else {
throw exception
}
}
.map { preferences ->
UserPrefs(
username = preferences[USERNAME_KEY] ?: "",
isLoggedIn = preferences[IS_LOGGED_IN_KEY] ?: false,
themeMode = preferences[THEME_KEY] ?: 0
)
}
suspend fun updateUsername(username: String) {
context.dataStore.edit { preferences ->
preferences[USERNAME_KEY] = username
}
}
suspend fun updateLoginStatus(isLoggedIn: Boolean) {
context.dataStore.edit { preferences ->
preferences[IS_LOGGED_IN_KEY] = isLoggedIn
}
}
}
data class UserPrefs(
val username: String,
val isLoggedIn: Boolean,
val themeMode: Int
)
Storage solutions:
| Storage Type | Use Case | Performance | Complexity | Data Size |
|---|---|---|---|---|
| Room Database | Structured data, complex queries | High | Medium | Large datasets |
| DataStore | User preferences, settings | High | Low | Small key-value pairs |
| SharedPreferences | Legacy preference storage | Medium | Low | Small data |
| Internal Storage | Private app files | High | Low | Any size |
| External Storage | Shared media, documents | Medium | High | Large files |
Proper architecture separates concerns, improves testability, and makes your codebase maintainable as it grows. Android Architecture Components provide tools that work together to create robust, lifecycle-aware applications. After working on numerous Android projects, I’ve seen how proper architecture prevents technical debt and enables teams to scale effectively.
The Model-View-ViewModel pattern, combined with Android Architecture Components, creates a robust foundation for scalable applications:
// Repository pattern for data management
class UserRepository(
private val userDao: UserDao,
private val apiService: ApiService
) {
fun getUsers(): Flow<Resource<List<User>>> = flow {
emit(Resource.Loading())
try {
// Emit cached data first
val localUsers = userDao.getAllUsers().first()
emit(Resource.Success(localUsers))
// Fetch fresh data from API
val remoteUsers = apiService.getUsers()
// Update local database
userDao.deleteAllUsers()
userDao.insertUsers(remoteUsers)
// Emit updated data
emit(Resource.Success(remoteUsers))
} catch (exception: Exception) {
emit(Resource.Error(
message = exception.localizedMessage ?: "Unknown error occurred",
data = userDao.getAllUsers().first()
))
}
}
suspend fun getUserById(userId: String): Resource<User> {
return try {
val user = apiService.getUserById(userId)
userDao.insertUser(user)
Resource.Success(user)
} catch (exception: Exception) {
val localUser = userDao.getUserById(userId)
if (localUser != null) {
Resource.Success(localUser)
} else {
Resource.Error(exception.localizedMessage ?: "User not found")
}
}
}
}
// ViewModel with StateFlow
class UserListViewModel(
private val userRepository: UserRepository
) : ViewModel() {
private val _uiState = MutableStateFlow(UserListUiState())
val uiState: StateFlow<UserListUiState> = _uiState.asStateFlow()
init {
loadUsers()
}
fun loadUsers() {
viewModelScope.launch {
userRepository.getUsers().collect { resource ->
_uiState.value = when (resource) {
is Resource.Loading -> {
_uiState.value.copy(isLoading = true)
}
is Resource.Success -> {
_uiState.value.copy(
isLoading = false,
users = resource.data ?: emptyList(),
error = null
)
}
is Resource.Error -> {
_uiState.value.copy(
isLoading = false,
error = resource.message,
users = resource.data ?: emptyList()
)
}
}
}
}
}
fun refreshUsers() {
loadUsers()
}
}
// UI State data class
data class UserListUiState(
val users: List<User> = emptyList(),
val isLoading: Boolean = false,
val error: String? = null
)
// Resource wrapper for network states
sealed class Resource<T>(
val data: T? = null,
val message: String? = null
) {
class Success<T>(data: T) : Resource<T>(data)
class Error<T>(message: String, data: T? = null) : Resource<T>(data, message)
class Loading<T>(data: T? = null) : Resource<T>(data)
}
class UserListFragment : Fragment() {
private lateinit var binding: FragmentUserListBinding
private lateinit var userAdapter: UserAdapter
private val viewModel: UserListViewModel by viewModels {
UserListViewModelFactory(
UserRepository(
userDao = AppDatabase.getDatabase(requireContext()).userDao(),
apiService = RetrofitInstance.apiService
)
)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = FragmentUserListBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupRecyclerView()
observeUiState()
setupRefreshListener()
}
private fun setupRecyclerView() {
userAdapter = UserAdapter { user ->
// Handle user click
findNavController().navigate(
UserListFragmentDirections.actionUserListToUserDetail(user.id)
)
}
binding.recyclerView.apply {
adapter = userAdapter
layoutManager = LinearLayoutManager(requireContext())
}
}
private fun observeUiState() {
viewLifecycleOwner.lifecycleScope.launch {
viewModel.uiState.collect { uiState ->
binding.progressBar.isVisible = uiState.isLoading
binding.errorText.isVisible = uiState.error != null
binding.errorText.text = uiState.error
userAdapter.submitList(uiState.users)
}
}
}
private fun setupRefreshListener() {
binding.swipeRefreshLayout.setOnRefreshListener {
viewModel.refreshUsers()
binding.swipeRefreshLayout.isRefreshing = false
}
}
}
Architecture patterns:
ViewModel survives configuration changes and provides a clear separation between UI and business logic. StateFlow offers lifecycle-aware data observation with better performance than LiveData in many scenarios.
Dependency injection improves code modularity, testability, and maintainability. Hilt, built on top of Dagger, provides compile-time dependency injection specifically designed for Android applications. After migrating multiple projects from manual DI to Hilt, I can attest to its effectiveness in reducing boilerplate and preventing dependency-related bugs.
// Application class with Hilt
@HiltAndroidApp
class MyApplication : Application()
// Database module
@Module
@InstallIn(SingletonComponent::class)
object DatabaseModule {
@Provides
@Singleton
fun provideAppDatabase(@ApplicationContext context: Context): AppDatabase {
return Room.databaseBuilder(
context,
AppDatabase::class.java,
"app_database"
).build()
}
@Provides
fun provideUserDao(database: AppDatabase): UserDao {
return database.userDao()
}
}
// Network module
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Provides
@Singleton
fun provideOkHttpClient(): OkHttpClient {
return OkHttpClient.Builder()
.addInterceptor(HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY
})
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.build()
}
@Provides
@Singleton
fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit {
return Retrofit.Builder()
.baseUrl("https://api.example.com/")
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.build()
}
@Provides
@Singleton
fun provideApiService(retrofit: Retrofit): ApiService {
return retrofit.create(ApiService::class.java)
}
}
// Repository module
@Module
@InstallIn(SingletonComponent::class)
object RepositoryModule {
@Provides
@Singleton
fun provideUserRepository(
userDao: UserDao,
apiService: ApiService
): UserRepository {
return UserRepository(userDao, apiService)
}
}
// Fragment with Hilt injection
@AndroidEntryPoint
class UserListFragment : Fragment() {
private val viewModel: UserListViewModel by viewModels()
// Field injection if needed
@Inject
lateinit var userPreferences: UserPreferences
// Rest of fragment implementation...
}
// ViewModel with Hilt injection
@HiltViewModel
class UserListViewModel @Inject constructor(
private val userRepository: UserRepository,
private val userPreferences: UserPreferences
) : ViewModel() {
// ViewModel implementation...
}
DI concepts:
| Hilt Component | Android Class | Scope | Lifetime |
|---|---|---|---|
| SingletonComponent | Application | @Singleton | App lifetime |
| ActivityRetainedComponent | ViewModel | @ActivityRetainedScoped | Activity retained |
| ActivityComponent | Activity | @ActivityScoped | Activity lifetime |
| FragmentComponent | Fragment | @FragmentScoped | Fragment lifetime |
| ViewComponent | View | @ViewScoped | View lifetime |
| ServiceComponent | Service | @ServiceScoped | Service lifetime |
Hilt reduces the boilerplate code traditionally associated with Dagger while providing the same performance benefits and compile-time safety.
Reactive programming handles asynchronous operations and data streams elegantly. Kotlin Coroutines and Flow provide powerful tools for managing concurrent operations and reactive data streams. The transition from RxJava to Coroutines has simplified Android development significantly.
// API service with suspend functions
interface ApiService {
@GET("users")
suspend fun getUsers(): List<User>
@GET("users/{id}")
suspend fun getUserById(@Path("id") userId: String): User
@POST("users")
suspend fun createUser(@Body user: CreateUserRequest): User
}
// Repository with Flow operations
class UserRepository(
private val userDao: UserDao,
private val apiService: ApiService,
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
) {
fun getUsersStream(): Flow<List<User>> {
return userDao.getAllUsers()
.flowOn(ioDispatcher)
}
suspend fun refreshUsers(): Result<Unit> {
return withContext(ioDispatcher) {
try {
val users = apiService.getUsers()
userDao.insertUsers(users)
Result.success(Unit)
} catch (exception: Exception) {
Result.failure(exception)
}
}
}
fun searchUsers(query: String): Flow<List<User>> {
return flow {
val localResults = userDao.searchUsers("%$query%")
emit(localResults)
try {
val remoteResults = apiService.searchUsers(query)
userDao.insertUsers(remoteResults)
emit(remoteResults)
} catch (exception: Exception) {
// Continue with local results if remote search fails
}
}.flowOn(ioDispatcher)
}
fun getUserUpdates(userId: String): Flow<User?> {
return combine(
userDao.getUserByIdFlow(userId),
fetchUserUpdatesFromServer(userId)
) { localUser, remoteUser ->
remoteUser ?: localUser
}.distinctUntilChanged()
}
private fun fetchUserUpdatesFromServer(userId: String): Flow<User?> {
return flow {
while (currentCoroutineContext().isActive) {
try {
val user = apiService.getUserById(userId)
emit(user)
} catch (exception: Exception) {
emit(null)
}
delay(30_000) // Poll every 30 seconds
}
}.flowOn(ioDispatcher)
}
}
// ViewModel with advanced Flow operations
class UserListViewModel(
private val userRepository: UserRepository
) : ViewModel() {
private val _searchQuery = MutableStateFlow("")
val searchQuery: StateFlow<String> = _searchQuery.asStateFlow()
private val _refreshTrigger = MutableSharedFlow<Unit>()
val users: StateFlow<List<User>> = combine(
userRepository.getUsersStream(),
_searchQuery.debounce(300)
) { users, query ->
if (query.isBlank()) {
users
} else {
users.filter { user ->
user.name.contains(query, ignoreCase = true) ||
user.email.contains(query, ignoreCase = true)
}
}
}.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = emptyList()
)
val isLoading = MutableStateFlow(false)
val error = MutableStateFlow<String?>(null)
init {
// Handle refresh triggers
_refreshTrigger
.onEach { refreshUsers() }
.launchIn(viewModelScope)
}
fun updateSearchQuery(query: String) {
_searchQuery.value = query
}
fun refreshUsers() {
viewModelScope.launch {
isLoading.value = true
error.value = null
userRepository.refreshUsers()
.onSuccess {
error.value = null
}
.onFailure { exception ->
error.value = exception.message
}
isLoading.value = false
}
}
fun triggerRefresh() {
viewModelScope.launch {
_refreshTrigger.emit(Unit)
}
}
}
Reactive programming tools:
| Operation | Purpose | Use Case |
|---|---|---|
| map | Transform emissions | Convert data types |
| filter | Filter emissions | Remove unwanted items |
| debounce | Delay emissions | Search input handling |
| distinctUntilChanged | Remove duplicates | Prevent unnecessary UI updates |
| combine | Merge multiple flows | Combine user input with data |
| merge | Combine flows concurrently | Multiple data sources |
| flatMapLatest | Switch to latest flow | Cancel previous operations |
Coroutines simplify asynchronous programming with sequential-looking code that handles threading automatically. Flow provides a cold, declarative stream API that integrates seamlessly with Android Architecture Components.
Most modern apps require network connectivity for data synchronization, user authentication, and content delivery. Retrofit combined with OkHttp provides a robust networking stack for Android applications. After implementing networking in dozens of production apps, I’ve learned that proper error handling and caching strategies make the difference between a reliable app and one that frustrates users.
// Data models
data class ApiResponse<T>(
val data: T?,
val message: String?,
val success: Boolean
)
data class User(
val id: String,
val name: String,
val email: String,
val avatarUrl: String?,
val createdAt: String
)
data class CreateUserRequest(
val name: String,
val email: String
)
// API service interface
interface ApiService {
@GET("users")
suspend fun getUsers(
@Query("page") page: Int = 1,
@Query("limit") limit: Int = 20
): ApiResponse<List<User>>
@GET("users/{id}")
suspend fun getUserById(
@Path("id") userId: String
): ApiResponse<User>
@POST("users")
suspend fun createUser(
@Body request: CreateUserRequest
): ApiResponse<User>
@PUT("users/{id}")
suspend fun updateUser(
@Path("id") userId: String,
@Body request: CreateUserRequest
): ApiResponse<User>
@DELETE("users/{id}")
suspend fun deleteUser(
@Path("id") userId: String
): ApiResponse<Unit>
@Multipart
@POST("users/{id}/avatar")
suspend fun uploadAvatar(
@Path("id") userId: String,
@Part avatar: MultipartBody.Part
): ApiResponse<User>
@GET("users/search")
suspend fun searchUsers(
@Query("q") query: String,
@Query("page") page: Int = 1
): ApiResponse<List<User>>
}
// Network interceptors
class AuthInterceptor(
private val tokenManager: TokenManager
) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val originalRequest = chain.request()
val token = tokenManager.getAccessToken()
if (token.isNullOrEmpty()) {
return chain.proceed(originalRequest)
}
val authenticatedRequest = originalRequest.newBuilder()
.header("Authorization", "Bearer $token")
.build()
val response = chain.proceed(authenticatedRequest)
// Handle token refresh if needed
if (response.code == 401) {
response.close()
val newToken = tokenManager.refreshToken()
if (newToken != null) {
val newRequest = originalRequest.newBuilder()
.header("Authorization", "Bearer $newToken")
.build()
return chain.proceed(newRequest)
}
}
return response
}
}
class NetworkConnectionInterceptor(
private val context: Context
) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
if (!isNetworkAvailable()) {
throw NoNetworkException("No network connection available")
}
return chain.proceed(chain.request())
}
private fun isNetworkAvailable(): Boolean {
val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE)
as ConnectivityManager
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val network = connectivityManager.activeNetwork
val capabilities = connectivityManager.getNetworkCapabilities(network)
capabilities?.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) == true
} else {
@Suppress("DEPRECATION")
connectivityManager.activeNetworkInfo?.isConnectedOrConnecting == true
}
}
}
// Custom exceptions
class NoNetworkException(message: String) : Exception(message)
class ApiException(
val code: Int,
message: String,
cause: Throwable? = null
) : Exception(message, cause)
// Network module with Hilt
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Provides
@Singleton
fun provideOkHttpClient(
@ApplicationContext context: Context,
authInterceptor: AuthInterceptor
): OkHttpClient {
return OkHttpClient.Builder()
.addInterceptor(HttpLoggingInterceptor().apply {
level = if (BuildConfig.DEBUG) {
HttpLoggingInterceptor.Level.BODY
} else {
HttpLoggingInterceptor.Level.NONE
}
})
.addInterceptor(NetworkConnectionInterceptor(context))
.addInterceptor(authInterceptor)
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.cache(
Cache(
directory = File(context.cacheDir, "http_cache"),
maxSize = 50L * 1024L * 1024L // 50 MB
)
)
.build()
}
@Provides
@Singleton
fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit {
return Retrofit.Builder()
.baseUrl(BuildConfig.BASE_URL)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.build()
}
@Provides
@Singleton
fun provideApiService(retrofit: Retrofit): ApiService {
return retrofit.create(ApiService::class.java)
}
}
// Network result wrapper
sealed class NetworkResult<T> {
data class Success<T>(val data: T) : NetworkResult<T>()
data class Error<T>(
val message: String,
val code: Int? = null,
val exception: Throwable? = null
) : NetworkResult<T>()
data class Loading<T>(val isLoading: Boolean = true) : NetworkResult<T>()
}
// Safe API call extension
suspend fun <T> safeApiCall(
apiCall: suspend () -> T
): NetworkResult<T> {
return try {
NetworkResult.Success(apiCall())
} catch (exception: Exception) {
when (exception) {
is NoNetworkException -> {
NetworkResult.Error("No internet connection")
}
is HttpException -> {
val errorMessage = try {
exception.response()?.errorBody()?.string() ?: "Unknown error"
} catch (e: Exception) {
"Network error occurred"
}
NetworkResult.Error(
message = errorMessage,
code = exception.code(),
exception = exception
)
}
is SocketTimeoutException -> {
NetworkResult.Error("Request timeout. Please try again.")
}
is UnknownHostException -> {
NetworkResult.Error("Unable to connect to server")
}
else -> {
NetworkResult.Error(
message = exception.localizedMessage ?: "Unknown error occurred",
exception = exception
)
}
}
}
}
// Repository with network error handling
class UserRepository(
private val apiService: ApiService,
private val userDao: UserDao
) {
suspend fun getUsers(forceRefresh: Boolean = false): Flow<NetworkResult<List<User>>> {
return flow {
emit(NetworkResult.Loading())
// Emit cached data first if available
if (!forceRefresh) {
val cachedUsers = userDao.getAllUsers().first()
if (cachedUsers.isNotEmpty()) {
emit(NetworkResult.Success(cachedUsers))
}
}
// Fetch from network
val networkResult = safeApiCall {
val response = apiService.getUsers()
if (response.success && response.data != null) {
// Cache successful response
userDao.deleteAllUsers()
userDao.insertUsers(response.data)
response.data
} else {
throw ApiException(400, response.message ?: "Unknown error")
}
}
emit(networkResult)
}.flowOn(Dispatchers.IO)
}
suspend fun createUser(request: CreateUserRequest): NetworkResult<User> {
return safeApiCall {
val response = apiService.createUser(request)
if (response.success && response.data != null) {
// Cache the new user
userDao.insertUser(response.data)
response.data
} else {
throw ApiException(400, response.message ?: "Failed to create user")
}
}
}
}
Networking essentials:
| Configuration | Development | Production | Testing |
|---|---|---|---|
| Timeout | 60 seconds | 30 seconds | 10 seconds |
| Logging | Full body | None | Headers only |
| Cache | Disabled | 50MB | Disabled |
| Retry | 3 attempts | 2 attempts | 1 attempt |
| Certificate Pinning | Disabled | Enabled | Mock |
Retrofit’s annotation-based approach makes API integration straightforward, while OkHttp’s interceptor system enables logging, authentication, and caching functionality.
Android’s background execution limits require careful planning for tasks that run outside the main application lifecycle. WorkManager provides a unified API for deferrable, guaranteed background work, while foreground services handle immediate, long-running operations.
// Work request data class
data class SyncWorkData(
val userId: String,
val syncType: String,
val priority: Int = 0
)
// Worker class for background sync
class DataSyncWorker(
context: Context,
params: WorkerParameters
) : CoroutineWorker(context, params) {
companion object {
const val KEY_USER_ID = "user_id"
const val KEY_SYNC_TYPE = "sync_type"
const val KEY_PRIORITY = "priority"
const val SYNC_TYPE_FULL = "full"
const val SYNC_TYPE_INCREMENTAL = "incremental"
private const val NOTIFICATION_ID = 1001
private const val CHANNEL_ID = "sync_channel"
}
override suspend fun doWork(): Result {
return try {
val userId = inputData.getString(KEY_USER_ID) ?: return Result.failure()
val syncType = inputData.getString(KEY_SYNC_TYPE) ?: SYNC_TYPE_INCREMENTAL
val priority = inputData.getInt(KEY_PRIORITY, 0)
// Show progress notification for long-running work
setForeground(createForegroundInfo())
// Perform the actual sync work
when (syncType) {
SYNC_TYPE_FULL -> performFullSync(userId)
SYNC_TYPE_INCREMENTAL -> performIncrementalSync(userId)
else -> return Result.failure()
}
// Return success with output data
val outputData = workDataOf(
"sync_completed_at" to System.currentTimeMillis(),
"items_synced" to 150
)
Result.success(outputData)
} catch (exception: Exception) {
if (runAttemptCount < 3) {
Result.retry()
} else {
Result.failure()
}
}
}
private suspend fun performFullSync(userId: String) {
// Simulate comprehensive data sync
val repository = (applicationContext as MyApplication).appContainer.userRepository
setProgress(workDataOf("progress" to 10))
repository.syncUserData(userId)
setProgress(workDataOf("progress" to 50))
repository.syncUserPreferences(userId)
setProgress(workDataOf("progress" to 80))
repository.syncUserContent(userId)
setProgress(workDataOf("progress" to 100))
}
private suspend fun performIncrementalSync(userId: String) {
// Simulate incremental sync
val repository = (applicationContext as MyApplication).appContainer.userRepository
repository.syncRecentChanges(userId)
}
private fun createForegroundInfo(): ForegroundInfo {
createNotificationChannel()
val notification = NotificationCompat.Builder(applicationContext, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_sync)
.setContentTitle("Syncing data")
.setContentText("Synchronizing your data in the background")
.setPriority(NotificationCompat.PRIORITY_LOW)
.setOngoing(true)
.build()
return ForegroundInfo(NOTIFICATION_ID, notification)
}
private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
CHANNEL_ID,
"Background Sync",
NotificationManager.IMPORTANCE_LOW
).apply {
description = "Notifications for background data synchronization"
}
val notificationManager = applicationContext.getSystemService(
Context.NOTIFICATION_SERVICE
) as NotificationManager
notificationManager.createNotificationChannel(channel)
}
}
}
// Work scheduler class
class BackgroundWorkScheduler(private val context: Context) {
private val workManager = WorkManager.getInstance(context)
fun schedulePeriodicSync(userId: String) {
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.setRequiresBatteryNotLow(true)
.build()
val syncWork = PeriodicWorkRequestBuilder<DataSyncWorker>(
repeatInterval = 6,
repeatIntervalTimeUnit = TimeUnit.HOURS
)
.setConstraints(constraints)
.setInputData(
workDataOf(
DataSyncWorker.KEY_USER_ID to userId,
DataSyncWorker.KEY_SYNC_TYPE to DataSyncWorker.SYNC_TYPE_INCREMENTAL
)
)
.setBackoffCriteria(
BackoffPolicy.EXPONENTIAL,
WorkRequest.MIN_BACKOFF_MILLIS,
TimeUnit.MILLISECONDS
)
.build()
workManager.enqueueUniquePeriodicWork(
"user_sync_$userId",
ExistingPeriodicWorkPolicy.KEEP,
syncWork
)
}
fun scheduleImmediateFullSync(userId: String) {
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
val fullSyncWork = OneTimeWorkRequestBuilder<DataSyncWorker>()
.setConstraints(constraints)
.setInputData(
workDataOf(
DataSyncWorker.KEY_USER_ID to userId,
DataSyncWorker.KEY_SYNC_TYPE to DataSyncWorker.SYNC_TYPE_FULL,
DataSyncWorker.KEY_PRIORITY to 1
)
)
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
.build()
workManager.enqueue(fullSyncWork)
}
fun observeWorkStatus(userId: String): LiveData<List<WorkInfo>> {
return workManager.getWorkInfosForUniqueWorkLiveData("user_sync_$userId")
}
fun cancelAllSyncWork(userId: String) {
workManager.cancelUniqueWork("user_sync_$userId")
}
}
// Foreground service for music playback or file downloads
class MediaPlaybackService : Service() {
companion object {
const val ACTION_START_PLAYBACK = "START_PLAYBACK"
const val ACTION_PAUSE_PLAYBACK = "PAUSE_PLAYBACK"
const val ACTION_STOP_PLAYBACK = "STOP_PLAYBACK"
const val EXTRA_MEDIA_URL = "media_url"
const val EXTRA_MEDIA_TITLE = "media_title"
private const val NOTIFICATION_ID = 2001
private const val CHANNEL_ID = "playback_channel"
}
private var mediaPlayer: MediaPlayer? = null
private var currentMediaTitle: String = ""
private val binder = MediaBinder()
inner class MediaBinder : Binder() {
fun getService(): MediaPlaybackService = this@MediaPlaybackService
}
override fun onCreate() {
super.onCreate()
createNotificationChannel()
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
when (intent?.action) {
ACTION_START_PLAYBOOK -> {
val mediaUrl = intent.getStringExtra(EXTRA_MEDIA_URL) ?: return START_NOT_STICKY
val mediaTitle = intent.getStringExtra(EXTRA_MEDIA_TITLE) ?: "Unknown"
startPlayback(mediaUrl, mediaTitle)
}
ACTION_PAUSE_PLAYBACK -> pausePlayback()
ACTION_STOP_PLAYBOOK -> stopPlayback()
}
return START_STICKY
}
override fun onBind(intent: Intent?): IBinder {
return binder
}
private fun startPlayback(mediaUrl: String, mediaTitle: String) {
currentMediaTitle = mediaTitle
try {
mediaPlayer?.release()
mediaPlayer = MediaPlayer().apply {
setDataSource(mediaUrl)
setOnPreparedListener { player ->
player.start()
updateNotification(isPlaying = true)
}
setOnCompletionListener {
stopPlayback()
}
setOnErrorListener { _, what, extra ->
stopPlayback()
true
}
prepareAsync()
}
startForeground(NOTIFICATION_ID, createNotification(isPlaying = false))
} catch (exception: Exception) {
stopSelf()
}
}
private fun pausePlayback() {
mediaPlayer?.pause()
updateNotification(isPlaying = false)
}
private fun stopPlayback() {
mediaPlayer?.stop()
mediaPlayer?.release()
mediaPlayer = null
stopForeground(true)
stopSelf()
}
private fun updateNotification(isPlaying: Boolean) {
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationManager.notify(NOTIFICATION_ID, createNotification(isPlaying))
}
private fun createNotification(isPlaying: Boolean): Notification {
val playPauseAction = if (isPlaying) {
NotificationCompat.Action.Builder(
R.drawable.ic_pause,
"Pause",
PendingIntent.getService(
this,
0,
Intent(this, MediaPlaybackService::class.java).setAction(ACTION_PAUSE_PLAYBACK),
PendingIntent.FLAG_IMMUTABLE
)
).build()
} else {
NotificationCompat.Action.Builder(
R.drawable.ic_play,
"Play",
PendingIntent.getService(
this,
0,
Intent(this, MediaPlaybackService::class.java).setAction(ACTION_START_PLAYBOOK),
PendingIntent.FLAG_IMMUTABLE
)
).build()
}
val stopAction = NotificationCompat.Action.Builder(
R.drawable.ic_stop,
"Stop",
PendingIntent.getService(
this,
0,
Intent(this, MediaPlaybackService::class.java).setAction(ACTION_STOP_PLAYBOOK),
PendingIntent.FLAG_IMMUTABLE
)
).build()
return NotificationCompat.Builder(this, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_music)
.setContentTitle(currentMediaTitle)
.setContentText(if (isPlaying) "Playing" else "Paused")
.addAction(playPauseAction)
.addAction(stopAction)
.setStyle(
androidx.media.app.NotificationCompat.MediaStyle()
.setShowActionsInCompactView(0, 1)
)
.setPriority(NotificationCompat.PRIORITY_LOW)
.setOngoing(isPlaying)
.build()
}
private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
CHANNEL_ID,
"Media Playback",
NotificationManager.IMPORTANCE_LOW
).apply {
description = "Controls for media playback"
setShowBadge(false)
}
val notificationManager = getSystemService(NotificationManager::class.java)
notificationManager.createNotificationChannel(channel)
}
}
}
Background processing:
| Work Type | Use Case | Execution | Battery Impact |
|---|---|---|---|
| WorkManager | Deferrable tasks | Guaranteed | Optimized |
| Foreground Service | User-aware tasks | Immediate | High |
| AlarmManager | Exact timing | System-dependent | Medium |
| JobScheduler | System-optimized | Batched | Low |
WorkManager automatically chooses the appropriate underlying technology (JobScheduler, AlarmManager, or immediate execution) based on device capabilities and Android version.
Android applications must perform network requests, database operations, and heavy computations off the main thread to maintain responsive user interfaces. Kotlin Coroutines provide structured concurrency that prevents common threading issues like memory leaks and race conditions.
// Custom dispatcher for heavy computations
class ComputationDispatcher {
companion object {
val Computation: CoroutineDispatcher = Dispatchers.Default.limitedParallelism(
parallelism = maxOf(1, Runtime.getRuntime().availableProcessors() - 1)
)
}
}
// Thread-safe repository with proper coroutine usage
class DataRepository(
private val apiService: ApiService,
private val database: AppDatabase,
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO,
private val computationDispatcher: CoroutineDispatcher = ComputationDispatcher.Computation
) {
private val _dataUpdates = MutableSharedFlow<DataUpdate>()
val dataUpdates: SharedFlow<DataUpdate> = _dataUpdates.asSharedFlow()
// Concurrent data processing
suspend fun processDataBatch(items: List<RawDataItem>): List<ProcessedDataItem> {
return withContext(computationDispatcher) {
items.chunked(100).map { chunk ->
async {
chunk.map { item ->
processItem(item)
}
}
}.awaitAll().flatten()
}
}
// Parallel network requests with proper error handling
suspend fun fetchUserDetails(userIds: List<String>): Map<String, User> {
return withContext(ioDispatcher) {
val deferredUsers = userIds.map { userId ->
async {
try {
userId to apiService.getUserById(userId)
} catch (exception: Exception) {
userId to null
}
}
}
deferredUsers.awaitAll()
.filter { it.second != null }
.associate { it.first to it.second!! }
}
}
// Combining multiple data sources with timeout
suspend fun getUserProfile(userId: String): Result<UserProfile> {
return withContext(ioDispatcher) {
try {
withTimeout(10_000) { // 10 second timeout
val userDeferred = async { apiService.getUserById(userId) }
val postsDeferred = async { apiService.getUserPosts(userId) }
val followersDeferred = async { apiService.getUserFollowers(userId) }
val user = userDeferred.await()
val posts = postsDeferred.await()
val followers = followersDeferred.await()
val profile = UserProfile(
user = user,
posts = posts,
followers = followers
)
// Cache the result
database.userProfileDao().insertUserProfile(profile)
Result.success(profile)
}
} catch (exception: TimeoutCancellationException) {
Result.failure(Exception("Request timed out"))
} catch (exception: Exception) {
// Try to get cached data
val cachedProfile = database.userProfileDao().getUserProfile(userId)
if (cachedProfile != null) {
Result.success(cachedProfile)
} else {
Result.failure(exception)
}
}
}
}
// Producer-consumer pattern with channels
fun startDataProcessingPipeline(): ReceiveChannel<ProcessedDataItem> {
return GlobalScope.produce(capacity = Channel.BUFFERED) {
val rawDataChannel = Channel<RawDataItem>(capacity = Channel.UNLIMITED)
// Producer coroutine
launch(ioDispatcher) {
try {
while (!rawDataChannel.isClosedForSend) {
val rawData = apiService.getRawData()
rawData.forEach { item ->
rawDataChannel.send(item)
}
delay(5000) // Poll every 5 seconds
}
} catch (exception: Exception) {
rawDataChannel.close(exception)
}
}
// Consumer coroutine
for (rawItem in rawDataChannel) {
val processedItem = withContext(computationDispatcher) {
processItem(rawItem)
}
send(processedItem)
_dataUpdates.emit(DataUpdate.ItemProcessed(processedItem))
}
}
}
private suspend fun processItem(item: RawDataItem): ProcessedDataItem {
// Simulate heavy computation
return withContext(computationDispatcher) {
// Complex data transformation
ProcessedDataItem(
id = item.id,
processedData = item.rawData.transform(),
timestamp = System.currentTimeMillis()
)
}
}
}
// ViewModel with proper scope management
class DataProcessingViewModel(
private val repository: DataRepository
) : ViewModel() {
private val _processingState = MutableStateFlow(ProcessingState.Idle)
val processingState: StateFlow<ProcessingState> = _processingState.asStateFlow()
private val _processedItems = MutableStateFlow<List<ProcessedDataItem>>(emptyList())
val processedItems: StateFlow<List<ProcessedDataItem>> = _processedItems.asStateFlow()
private var processingJob: Job? = null
fun startProcessing() {
processingJob?.cancel()
processingJob = viewModelScope.launch {
_processingState.value = ProcessingState.Loading
try {
val dataChannel = repository.startDataProcessingPipeline()
_processingState.value = ProcessingState.Processing
for (item in dataChannel) {
val currentItems = _processedItems.value.toMutableList()
currentItems.add(item)
_processedItems.value = currentItems
}
_processingState.value = ProcessingState.Completed
} catch (exception: Exception) {
_processingState.value = ProcessingState.Error(exception.message ?: "Unknown error")
}
}
}
fun stopProcessing() {
processingJob?.cancel()
_processingState.value = ProcessingState.Idle
}
override fun onCleared() {
super.onCleared()
stopProcessing()
}
}
sealed class ProcessingState {
object Idle : ProcessingState()
object Loading : ProcessingState()
object Processing : ProcessingState()
object Completed : ProcessingState()
data class Error(val message: String) : ProcessingState()
}
sealed class DataUpdate {
data class ItemProcessed(val item: ProcessedDataItem) : DataUpdate()
data class BatchCompleted(val count: Int) : DataUpdate()
data class ProcessingError(val error: String) : DataUpdate()
}
Concurrency management:
| Context | Purpose | Thread Pool | Use Case |
|---|---|---|---|
| Dispatchers.Main | UI updates | Main thread | View updates, user interactions |
| Dispatchers.IO | I/O operations | Shared thread pool | Network, database, file operations |
| Dispatchers.Default | CPU-intensive | Shared thread pool | Data processing, algorithms |
| Dispatchers.Unconfined | Testing | Current thread | Unit tests, debugging |
Structured concurrency ensures that background operations complete properly and don’t leak resources when activities or fragments are destroyed.
Comprehensive testing ensures your application works correctly across different devices, Android versions, and usage scenarios. Android provides testing frameworks for unit tests, integration tests, and UI tests. A well-tested app reduces crashes, improves user satisfaction, and enables confident refactoring.
// Test dependencies in build.gradle.kts
dependencies {
testImplementation("junit:junit:4.13.2")
testImplementation("org.mockito:mockito-core:4.11.0")
testImplementation("org.mockito.kotlin:mockito-kotlin:4.1.0")
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3")
testImplementation("androidx.arch.core:core-testing:2.2.0")
}
// Repository unit tests
class UserRepositoryTest {
@Mock
private lateinit var apiService: ApiService
@Mock
private lateinit var userDao: UserDao
private lateinit var repository: UserRepository
@get:Rule
val instantTaskExecutorRule = InstantTaskExecutorRule()
@OptIn(ExperimentalCoroutinesApi::class)
@get:Rule
val mainDispatcherRule = MainDispatcherRule()
@Before
fun setup() {
MockitoAnnotations.openMocks(this)
repository = UserRepository(apiService, userDao)
}
@Test
fun `getUsers should return cached data first then network data`() = runTest {
// Given
val cachedUsers = listOf(
User("1", "John", "john@email.com", null, "2023-01-01"),
User("2", "Jane", "jane@email.com", null, "2023-01-02")
)
val networkUsers = listOf(
User("1", "John Updated", "john@email.com", null, "2023-01-01"),
User("3", "Bob", "bob@email.com", null, "2023-01-03")
)
whenever(userDao.getAllUsers()).thenReturn(flowOf(cachedUsers))
whenever(apiService.getUsers()).thenReturn(
ApiResponse(data = networkUsers, message = null, success = true)
)
whenever(userDao.insertUsers(networkUsers)).thenReturn(Unit)
whenever(userDao.deleteAllUsers()).thenReturn(Unit)
// When
val result = repository.getUsers(forceRefresh = false).toList()
// Then
assertEquals(3, result.size)
assertTrue(result[0] is NetworkResult.Loading)
assertTrue(result[1] is NetworkResult.Success)
assertEquals(cachedUsers, (result[1] as NetworkResult.Success).data)
assertTrue(result[2] is NetworkResult.Success)
assertEquals(networkUsers, (result[2] as NetworkResult.Success).data)
verify(userDao).deleteAllUsers()
verify(userDao).insertUsers(networkUsers)
}
@Test
fun `createUser should handle network error gracefully`() = runTest {
// Given
val request = CreateUserRequest("Test User", "test@email.com")
val exception = HttpException(
Response.error<ApiResponse<User>>(
400,
"Bad Request".toResponseBody("text/plain".toMediaTypeOrNull())
)
)
whenever(apiService.createUser(request)).thenThrow(exception)
// When
val result = repository.createUser(request)
// Then
assertTrue(result is NetworkResult.Error)
assertEquals(400, (result as NetworkResult.Error).code)
verify(userDao, never()).insertUser(any())
}
@Test
fun `getUsersStream should emit database updates`() = runTest {
// Given
val users1 = listOf(User("1", "John", "john@email.com", null, "2023-01-01"))
val users2 = listOf(
User("1", "John", "john@email.com", null, "2023-01-01"),
User("2", "Jane", "jane@email.com", null, "2023-01-02")
)
val flow = flowOf(users1, users2)
whenever(userDao.getAllUsers()).thenReturn(flow)
// When
val result = repository.getUsersStream().toList()
// Then
assertEquals(2, result.size)
assertEquals(users1, result[0])
assertEquals(users2, result[1])
}
}
// ViewModel unit tests
@OptIn(ExperimentalCoroutinesApi::class)
class UserListViewModelTest {
@Mock
private lateinit var userRepository: UserRepository
private lateinit var viewModel: UserListViewModel
@get:Rule
val instantTaskExecutorRule = InstantTaskExecutorRule()
@get:Rule
val mainDispatcherRule = MainDispatcherRule()
@Before
fun setup() {
MockitoAnnotations.openMocks(this)
}
@Test
fun `init should load users automatically`() = runTest {
// Given
val users = listOf(
User("1", "John", "john@email.com", null, "2023-01-01"),
User("2", "Jane", "jane@email.com", null, "2023-01-02")
)
whenever(userRepository.getUsers(false)).thenReturn(
flowOf(NetworkResult.Success(users))
)
// When
viewModel = UserListViewModel(userRepository)
advanceUntilIdle()
// Then
assertEquals(users, viewModel.uiState.value.users)
assertFalse(viewModel.uiState.value.isLoading)
assertNull(viewModel.uiState.value.error)
}
@Test
fun `refreshUsers should update loading state correctly`() = runTest {
// Given
val users = listOf(User("1", "John", "john@email.com", null, "2023-01-01"))
whenever(userRepository.getUsers(any())).thenReturn(
flow {
emit(NetworkResult.Loading())
delay(100)
emit(NetworkResult.Success(users))
}
)
viewModel = UserListViewModel(userRepository)
// When
viewModel.refreshUsers()
// Then - Check loading state
assertTrue(viewModel.uiState.value.isLoading)
// Advance time and check final state
advanceUntilIdle()
assertFalse(viewModel.uiState.value.isLoading)
assertEquals(users, viewModel.uiState.value.users)
}
@Test
fun `search functionality should filter users correctly`() = runTest {
// Given
val allUsers = listOf(
User("1", "John Doe", "john@email.com", null, "2023-01-01"),
User("2", "Jane Smith", "jane@email.com", null, "2023-01-02"),
User("3", "Bob Johnson", "bob@email.com", null, "2023-01-03")
)
whenever(userRepository.getUsers(any())).thenReturn(
flowOf(NetworkResult.Success(allUsers))
)
viewModel = UserListViewModel(userRepository)
advanceUntilIdle()
// When
viewModel.updateSearchQuery("John")
advanceUntilIdle()
// Then
val filteredUsers = viewModel.uiState.value.users
assertEquals(2, filteredUsers.size)
assertTrue(filteredUsers.any { it.name.contains("John") })
}
}
// Custom test rule for main dispatcher
@ExperimentalCoroutinesApi
class MainDispatcherRule(
private val testDispatcher: TestDispatcher = UnconfinedTestDispatcher()
) : TestWatcher() {
override fun starting(description: Description) {
Dispatchers.setMain(testDispatcher)
}
override fun finished(description: Description) {
Dispatchers.resetMain()
}
}
// Room database integration test
@RunWith(AndroidJUnit4::class)
class UserDaoTest {
private lateinit var database: AppDatabase
private lateinit var userDao: UserDao
@Before
fun createDb() {
val context = ApplicationProvider.getApplicationContext<Context>()
database = Room.inMemoryDatabaseBuilder(context, AppDatabase::class.java)
.allowMainThreadQueries()
.build()
userDao = database.userDao()
}
@After
fun closeDb() {
database.close()
}
@Test
fun insertUser_and_getUserById() = runTest {
// Given
val user = User("1", "John Doe", "john@email.com", null, "2023-01-01")
// When
userDao.insertUser(user)
val retrievedUser = userDao.getUserById("1")
// Then
assertEquals(user, retrievedUser)
}
@Test
fun getAllUsers_returnsUsersInAlphabeticalOrder() = runTest {
// Given
val users = listOf(
User("3", "Charlie", "charlie@email.com", null, "2023-01-03"),
User("1", "Alice", "alice@email.com", null, "2023-01-01"),
User("2", "Bob", "bob@email.com", null, "2023-01-02")
)
// When
users.forEach { userDao.insertUser(it) }
val retrievedUsers = userDao.getAllUsers().first()
// Then
assertEquals(3, retrievedUsers.size)
assertEquals("Alice", retrievedUsers[0].name)
assertEquals("Bob", retrievedUsers[1].name)
assertEquals("Charlie", retrievedUsers[2].name)
}
@Test
fun updateUser_updatesExistingUser() = runTest {
// Given
val originalUser = User("1", "John", "john@email.com", null, "2023-01-01")
val updatedUser = originalUser.copy(name = "John Updated")
// When
userDao.insertUser(originalUser)
userDao.updateUser(updatedUser)
val retrievedUser = userDao.getUserById("1")
// Then
assertEquals("John Updated", retrievedUser?.name)
assertEquals(originalUser.email, retrievedUser?.email)
}
@Test
fun deleteUser_removesUserFromDatabase() = runTest {
// Given
val user = User("1", "John", "john@email.com", null, "2023-01-01")
// When
userDao.insertUser(user)
userDao.deleteUserById("1")
val retrievedUser = userDao.getUserById("1")
// Then
assertNull(retrievedUser)
}
}
// Compose UI testing
@RunWith(AndroidJUnit4::class)
class UserListScreenTest {
@get:Rule
val composeTestRule = createComposeRule()
@Test
fun userListScreen_displaysUsers_whenDataLoaded() {
// Given
val users = listOf(
User("1", "John Doe", "john@email.com", null, "2023-01-01"),
User("2", "Jane Smith", "jane@email.com", null, "2023-01-02")
)
val uiState = UserListUiState(
users = users,
isLoading = false,
error = null
)
// When
composeTestRule.setContent {
UserListScreen(
uiState = uiState,
onUserClick = { },
onRefresh = { },
onSearchQueryChange = { }
)
}
// Then
composeTestRule
.onNodeWithText("John Doe")
.assertIsDisplayed()
composeTestRule
.onNodeWithText("Jane Smith")
.assertIsDisplayed()
composeTestRule
.onNodeWithText("john@email.com")
.assertIsDisplayed()
}
@Test
fun userListScreen_showsLoadingIndicator_whenLoading() {
// Given
val uiState = UserListUiState(
users = emptyList(),
isLoading = true,
error = null
)
// When
composeTestRule.setContent {
UserListScreen(
uiState = uiState,
onUserClick = { },
onRefresh = { },
onSearchQueryChange = { }
)
}
// Then
composeTestRule
.onNodeWithTag("loading_indicator")
.assertIsDisplayed()
}
@Test
fun userListScreen_showsErrorMessage_whenError() {
// Given
val errorMessage = "Network error occurred"
val uiState = UserListUiState(
users = emptyList(),
isLoading = false,
error = errorMessage
)
// When
composeTestRule.setContent {
UserListScreen(
uiState = uiState,
onUserClick = { },
onRefresh = { },
onSearchQueryChange = { }
)
}
// Then
composeTestRule
.onNodeWithText(errorMessage)
.assertIsDisplayed()
}
@Test
fun userListScreen_callsOnUserClick_whenUserTapped() {
// Given
val users = listOf(
User("1", "John Doe", "john@email.com", null, "2023-01-01")
)
val uiState = UserListUiState(
users = users,
isLoading = false,
error = null
)
var clickedUserId: String? = null
// When
composeTestRule.setContent {
UserListScreen(
uiState = uiState,
onUserClick = { userId -> clickedUserId = userId },
onRefresh = { },
onSearchQueryChange = { }
)
}
composeTestRule
.onNodeWithText("John Doe")
.performClick()
// Then
assertEquals("1", clickedUserId)
}
@Test
fun userListScreen_filtersUsers_whenSearchQueryEntered() {
// Given
val users = listOf(
User("1", "John Doe", "john@email.com", null, "2023-01-01"),
User("2", "Jane Smith", "jane@email.com", null, "2023-01-02")
)
val uiState = UserListUiState(
users = users,
isLoading = false,
error = null
)
var searchQuery = ""
// When
composeTestRule.setContent {
UserListScreen(
uiState = uiState,
onUserClick = { },
onRefresh = { },
onSearchQueryChange = { query -> searchQuery = query }
)
}
composeTestRule
.onNodeWithTag("search_field")
.performTextInput("John")
// Then
assertEquals("John", searchQuery)
}
}
Testing approaches:
| Test Type | Scope | Speed | Maintenance | Coverage |
|---|---|---|---|---|
| Unit Tests | Single class/method | Fast | Low | Logic and algorithms |
| Integration Tests | Multiple components | Medium | Medium | Data flow and interactions |
| UI Tests | User interactions | Slow | High | User workflows |
| End-to-End Tests | Complete features | Very Slow | Very High | Critical user journeys |
Testing ViewModels and repository classes in isolation helps catch business logic errors early. UI tests verify that user interactions work correctly across different screen sizes and orientations.
App performance directly impacts user satisfaction and retention. Android provides profiling tools that help identify and resolve performance bottlenecks in CPU usage, memory allocation, and network operations. Performance optimization should be an ongoing process throughout development, not an afterthought.
// Memory-efficient image loading with Coil
class ImageManager @Inject constructor(
@ApplicationContext private val context: Context
) {
private val imageLoader by lazy {
ImageLoader.Builder(context)
.memoryCache {
MemoryCache.Builder(context)
.maxSizePercent(0.25) // Use 25% of available memory
.build()
}
.diskCache {
DiskCache.Builder()
.directory(context.cacheDir.resolve("image_cache"))
.maxSizeBytes(50 * 1024 * 1024) // 50 MB
.build()
}
.respectCacheHeaders(false)
.build()
}
fun loadImage(
imageView: ImageView,
url: String,
placeholder: Drawable? = null,
error: Drawable? = null
) {
val request = ImageRequest.Builder(context)
.data(url)
.target(imageView)
.placeholder(placeholder)
.error(error)
.memoryCacheKey(url)
.diskCacheKey(url)
.build()
imageLoader.enqueue(request)
}
fun preloadImages(urls: List<String>) {
urls.forEach { url ->
val request = ImageRequest.Builder(context)
.data(url)
.memoryCacheKey(url)
.diskCacheKey(url)
.build()
imageLoader.enqueue(request)
}
}
fun clearMemoryCache() {
imageLoader.memoryCache?.clear()
}
}
// Memory-efficient RecyclerView adapter
class UserAdapter(
private val onUserClick: (User) -> Unit
) : ListAdapter<User, UserAdapter.ViewHolder>(UserDiffCallback()) {
// Use object pool for view holders to reduce allocations
private val viewHolderPool = Pools.SimplePool<ViewHolder>(10)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val recycledViewHolder = viewHolderPool.acquire()
if (recycledViewHolder != null) {
return recycledViewHolder
}
val binding = ItemUserBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
return ViewHolder(binding, onUserClick)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(getItem(position))
}
override fun onViewRecycled(holder: ViewHolder) {
super.onViewRecycled(holder)
holder.unbind()
viewHolderPool.release(holder)
}
class ViewHolder(
private val binding: ItemUserBinding,
private val onUserClick: (User) -> Unit
) : RecyclerView.ViewHolder(binding.root) {
private var currentUser: User? = null
init {
binding.root.setOnClickListener {
currentUser?.let { user ->
onUserClick(user)
}
}
}
fun bind(user: User) {
currentUser = user
binding.nameText.text = user.name
binding.emailText.text = user.email
// Load avatar with proper memory management
if (user.avatarUrl != null) {
binding.avatarImage.load(user.avatarUrl) {
crossfade(true)
placeholder(R.drawable.placeholder_avatar)
error(R.drawable.error_avatar)
transformations(CircleCropTransformation())
memoryCacheKey(user.avatarUrl)
}
} else {
binding.avatarImage.setImageResource(R.drawable.default_avatar)
}
}
fun unbind() {
currentUser = null
// Clear image to prevent memory leaks
binding.avatarImage.setImageDrawable(null)
}
}
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
}
}
}
// Lifecycle-aware component to prevent leaks
class LocationTracker @Inject constructor(
@ApplicationContext private val context: Context
) : DefaultLifecycleObserver {
private var locationManager: LocationManager? = null
private var locationListener: LocationListener? = null
private val _location = MutableLiveData<Location>()
val location: LiveData<Location> = _location
override fun onStart(owner: LifecycleOwner) {
startLocationUpdates()
}
override fun onStop(owner: LifecycleOwner) {
stopLocationUpdates()
}
private fun startLocationUpdates() {
if (ContextCompat.checkSelfPermission(
context,
Manifest.permission.ACCESS_FINE_LOCATION
) != PackageManager.PERMISSION_GRANTED
) {
return
}
locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
locationListener = object : LocationListener {
override fun onLocationChanged(location: Location) {
_location.postValue(location)
}
override fun onStatusChanged(provider: String?, status: Int, extras: Bundle?) {}
override fun onProviderEnabled(provider: String) {}
override fun onProviderDisabled(provider: String) {}
}
locationManager?.requestLocationUpdates(
LocationManager.GPS_PROVIDER,
10000, // 10 seconds
100f, // 100 meters
locationListener!!
)
}
private fun stopLocationUpdates() {
locationListener?.let { listener ->
locationManager?.removeUpdates(listener)
}
locationManager = null
locationListener = null
}
}
// Optimized Room queries
@Dao
interface UserDao {
// Use indexes for frequently queried columns
@Query("SELECT * FROM users WHERE name LIKE :query ORDER BY name ASC LIMIT :limit")
suspend fun searchUsersByName(query: String, limit: Int = 50): List<User>
// Use specific columns instead of SELECT *
@Query("SELECT id, name, email FROM users WHERE active = 1")
suspend fun getActiveUserSummaries(): List<UserSummary>
// Batch operations for better performance
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertUsers(users: List<User>)
@Query("DELETE FROM users WHERE id IN (:userIds)")
suspend fun deleteUsersByIds(userIds: List<String>)
// Use Flow for reactive updates without polling
@Query("SELECT COUNT(*) FROM users WHERE active = 1")
fun getActiveUserCountFlow(): Flow<Int>
// Pagination with PagingSource
@Query("SELECT * FROM users ORDER BY created_at DESC")
fun getUsersPagingSource(): PagingSource<Int, User>
}
// Entity with proper indexing
@Entity(
tableName = "users",
indices = [
Index(value = ["email"], unique = true),
Index(value = ["name"]),
Index(value = ["active"]),
Index(value = ["created_at"])
]
)
data class User(
@PrimaryKey val id: String,
@ColumnInfo(name = "name") val name: String,
@ColumnInfo(name = "email") val email: String,
@ColumnInfo(name = "avatar_url") val avatarUrl: String?,
@ColumnInfo(name = "created_at") val createdAt: String,
@ColumnInfo(name = "active") val active: Boolean = true
)
// Data class for partial queries
data class UserSummary(
val id: String,
val name: String,
val email: String
)
// Optimized network configuration
class NetworkOptimizer @Inject constructor() {
fun createOptimizedOkHttpClient(): OkHttpClient {
return OkHttpClient.Builder()
.addInterceptor(createCompressionInterceptor())
.addInterceptor(createCacheInterceptor())
.addInterceptor(createRetryInterceptor())
.connectionPool(
ConnectionPool(
maxIdleConnections = 10,
keepAliveDuration = 5,
timeUnit = TimeUnit.MINUTES
)
)
.readTimeout(30, TimeUnit.SECONDS)
.connectTimeout(15, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.build()
}
private fun createCompressionInterceptor(): Interceptor {
return Interceptor { chain ->
val originalRequest = chain.request()
val compressedRequest = originalRequest.newBuilder()
.header("Accept-Encoding", "gzip, deflate")
.build()
chain.proceed(compressedRequest)
}
}
private fun createCacheInterceptor(): Interceptor {
return Interceptor { chain ->
val request = chain.request()
val response = chain.proceed(request)
// Cache GET requests for 5 minutes
if (request.method == "GET") {
response.newBuilder()
.header("Cache-Control", "public, max-age=300")
.build()
} else {
response
}
}
}
private fun createRetryInterceptor(): Interceptor {
return Interceptor { chain ->
val request = chain.request()
var response = chain.proceed(request)
var tryCount = 0
while (!response.isSuccessful && tryCount < 3) {
response.close()
tryCount++
Thread.sleep(1000 * tryCount) // Exponential backoff
response = chain.proceed(request)
}
response
}
}
}
// Request batching to reduce network calls
class BatchRequestManager @Inject constructor(
private val apiService: ApiService
) {
private val pendingRequests = mutableMapOf<String, CompletableDeferred<User>>()
private val batchJob = SupervisorJob()
private val scope = CoroutineScope(Dispatchers.IO + batchJob)
fun getUserAsync(userId: String): Deferred<User> {
return pendingRequests.getOrPut(userId) {
val deferred = CompletableDeferred<User>()
// Schedule batch execution
scope.launch {
delay(100) // Wait for more requests
executeBatch()
}
deferred
}
}
private suspend fun executeBatch() {
val currentRequests = pendingRequests.toMap()
pendingRequests.clear()
if (currentRequests.isEmpty()) return
try {
// Make batched API call
val userIds = currentRequests.keys.toList()
val users = apiService.getUsersBatch(userIds)
// Complete individual requests
users.forEach { user ->
currentRequests[user.id]?.complete(user)
}
// Handle missing users
currentRequests.forEach { (userId, deferred) ->
if (!deferred.isCompleted) {
deferred.completeExceptionally(
Exception("User not found: $userId")
)
}
}
} catch (exception: Exception) {
// Fail all requests in the batch
currentRequests.values.forEach { deferred ->
deferred.completeExceptionally(exception)
}
}
}
fun cleanup() {
batchJob.cancel()
}
}
Performance optimization areas:
| Metric | Target | Tool | Impact |
|---|---|---|---|
| App Startup Time | < 1.5s cold start | Method tracing | First impression |
| Memory Usage | < 200MB average | Memory profiler | System stability |
| CPU Usage | < 40% average | CPU profiler | Battery life |
| Network Efficiency | < 1MB/session | Network profiler | Data costs |
| APK Size | < 50MB | APK analyzer | Download rates |
| Frame Rate | 60 FPS | GPU profiler | User experience |
Proactive performance monitoring during development prevents performance regressions that are expensive to fix after release.
Mobile applications handle sensitive user data and must implement appropriate security measures. Android provides security APIs and best practices that protect user information and app integrity. Security should be built into the development process from the beginning, not added as an afterthought.
// Encrypted SharedPreferences implementation
class SecurePreferencesManager @Inject constructor(
@ApplicationContext private val context: Context
) {
private val sharedPreferences: SharedPreferences by lazy {
EncryptedSharedPreferences.create(
"secure_prefs",
getMasterKey(),
context,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
}
private fun getMasterKey(): MasterKey {
return MasterKey.Builder(context)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build()
}
fun storeSecureData(key: String, value: String) {
sharedPreferences.edit()
.putString(key, value)
.apply()
}
fun getSecureData(key: String, defaultValue: String = ""): String {
return sharedPreferences.getString(key, defaultValue) ?: defaultValue
}
fun removeSecureData(key: String) {
sharedPreferences.edit()
.remove(key)
.apply()
}
fun clearAllSecureData() {
sharedPreferences.edit()
.clear()
.apply()
}
}
// Android Keystore usage for encryption keys
class CryptoManager @Inject constructor() {
companion object {
private const val KEY_ALIAS = "MyAppSecretKey"
private const val ANDROID_KEYSTORE = "AndroidKeyStore"
private const val ENCRYPTION_TRANSFORMATION = "AES/GCM/NoPadding"
}
private val keyStore: KeyStore by lazy {
KeyStore.getInstance(ANDROID_KEYSTORE).apply {
load(null)
}
}
init {
generateSecretKey()
}
private fun generateSecretKey() {
if (!keyStore.containsAlias(KEY_ALIAS)) {
val keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEYSTORE)
val keyGenParameterSpec = KeyGenParameterSpec.Builder(
KEY_ALIAS,
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.setUserAuthenticationRequired(false)
.setRandomizedEncryptionRequired(true)
.build()
keyGenerator.init(keyGenParameterSpec)
keyGenerator.generateKey()
}
}
fun encrypt(data: String): EncryptedData {
val secretKey = keyStore.getKey(KEY_ALIAS, null) as SecretKey
val cipher = Cipher.getInstance(ENCRYPTION_TRANSFORMATION)
cipher.init(Cipher.ENCRYPT_MODE, secretKey)
val iv = cipher.iv
val encryptedBytes = cipher.doFinal(data.toByteArray(Charsets.UTF_8))
return EncryptedData(
encryptedData = Base64.encodeToString(encryptedBytes, Base64.DEFAULT),
iv = Base64.encodeToString(iv, Base64.DEFAULT)
)
}
fun decrypt(encryptedData: EncryptedData): String {
val secretKey = keyStore.getKey(KEY_ALIAS, null) as SecretKey
val cipher = Cipher.getInstance(ENCRYPTION_TRANSFORMATION)
val iv = Base64.decode(encryptedData.iv, Base64.DEFAULT)
val gcmSpec = GCMParameterSpec(128, iv)
cipher.init(Cipher.DECRYPT_MODE, secretKey, gcmSpec)
val encryptedBytes = Base64.decode(encryptedData.encryptedData, Base64.DEFAULT)
val decryptedBytes = cipher.doFinal(encryptedBytes)
return String(decryptedBytes, Charsets.UTF_8)
}
fun deleteKey() {
keyStore.deleteEntry(KEY_ALIAS)
}
}
data class EncryptedData(
val encryptedData: String,
val iv: String
)
// Biometric authentication implementation
class BiometricAuthManager @Inject constructor() {
fun isBiometricAvailable(context: Context): Boolean {
return when (BiometricManager.from(context).canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK)) {
BiometricManager.BIOMETRIC_SUCCESS -> true
BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE,
BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE,
BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> false
else -> false
}
}
fun authenticateWithBiometric(
activity: FragmentActivity,
title: String = "Biometric Authentication",
subtitle: String = "Use your biometric credential to authenticate",
description: String = "Place your finger on the sensor or look at the front camera",
onSuccess: () -> Unit,
onError: (String) -> Unit,
onFailure: () -> Unit
) {
val biometricPrompt = BiometricPrompt(
activity,
ContextCompat.getMainExecutor(activity),
object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
super.onAuthenticationSucceeded(result)
onSuccess()
}
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
super.onAuthenticationError(errorCode, errString)
onError(errString.toString())
}
override fun onAuthenticationFailed() {
super.onAuthenticationFailed()
onFailure()
}
}
)
val promptInfo = BiometricPrompt.PromptInfo.Builder()
.setTitle(title)
.setSubtitle(subtitle)
.setDescription(description)
.setNegativeButtonText("Cancel")
.build()
biometricPrompt.authenticate(promptInfo)
}
}
// SSL Certificate pinning
class CertificatePinner {
companion object {
private const val API_HOSTNAME = "api.example.com"
private const val SHA256_PIN_1 = "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="
private const val SHA256_PIN_2 = "sha256/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB="
}
fun createPinnedOkHttpClient(): OkHttpClient {
val certificatePinner = CertificatePinner.Builder()
.add(API_HOSTNAME, SHA256_PIN_1)
.add(API_HOSTNAME, SHA256_PIN_2) // Backup pin
.build()
return OkHttpClient.Builder()
.certificatePinner(certificatePinner)
.addInterceptor(createSecurityHeadersInterceptor())
.build()
}
private fun createSecurityHeadersInterceptor(): Interceptor {
return Interceptor { chain ->
val originalRequest = chain.request()
val secureRequest = originalRequest.newBuilder()
.addHeader("X-Requested-With", "XMLHttpRequest")
.addHeader("Cache-Control", "no-cache, no-store, must-revalidate")
.addHeader("Pragma", "no-cache")
.build()
chain.proceed(secureRequest)
}
}
}
// Token management with secure storage
class TokenManager @Inject constructor(
private val securePreferencesManager: SecurePreferencesManager,
private val cryptoManager: CryptoManager
) {
companion object {
private const val ACCESS_TOKEN_KEY = "access_token"
private const val REFRESH_TOKEN_KEY = "refresh_token"
private const val TOKEN_EXPIRY_KEY = "token_expiry"
}
fun saveTokens(accessToken: String, refreshToken: String, expiresIn: Long) {
val encryptedAccessToken = cryptoManager.encrypt(accessToken)
val encryptedRefreshToken = cryptoManager.encrypt(refreshToken)
securePreferencesManager.storeSecureData(
ACCESS_TOKEN_KEY,
"${encryptedAccessToken.encryptedData}:${encryptedAccessToken.iv}"
)
securePreferencesManager.storeSecureData(
REFRESH_TOKEN_KEY,
"${encryptedRefreshToken.encryptedData}:${encryptedRefreshToken.iv}"
)
val expiryTime = System.currentTimeMillis() + (expiresIn * 1000)
securePreferencesManager.storeSecureData(TOKEN_EXPIRY_KEY, expiryTime.toString())
}
fun getAccessToken(): String? {
if (isTokenExpired()) {
return null
}
val encryptedTokenData = securePreferencesManager.getSecureData(ACCESS_TOKEN_KEY)
if (encryptedTokenData.isEmpty()) return null
return try {
val parts = encryptedTokenData.split(":")
val encryptedData = EncryptedData(parts[0], parts[1])
cryptoManager.decrypt(encryptedData)
} catch (exception: Exception) {
null
}
}
fun getRefreshToken(): String? {
val encryptedTokenData = securePreferencesManager.getSecureData(REFRESH_TOKEN_KEY)
if (encryptedTokenData.isEmpty()) return null
return try {
val parts = encryptedTokenData.split(":")
val encryptedData = EncryptedData(parts[0], parts[1])
cryptoManager.decrypt(encryptedData)
} catch (exception: Exception) {
null
}
}
private fun isTokenExpired(): Boolean {
val expiryTimeString = securePreferencesManager.getSecureData(TOKEN_EXPIRY_KEY)
if (expiryTimeString.isEmpty()) return true
val expiryTime = expiryTimeString.toLongOrNull() ?: return true
return System.currentTimeMillis() >= expiryTime
}
fun clearTokens() {
securePreferencesManager.removeSecureData(ACCESS_TOKEN_KEY)
securePreferencesManager.removeSecureData(REFRESH_TOKEN_KEY)
securePreferencesManager.removeSecureData(TOKEN_EXPIRY_KEY)
}
suspend fun refreshToken(): String? {
val refreshToken = getRefreshToken() ?: return null
return try {
// Make refresh token API call
val response = apiService.refreshToken(RefreshTokenRequest(refreshToken))
if (response.isSuccessful && response.body() != null) {
val tokenResponse = response.body()!!
saveTokens(
tokenResponse.accessToken,
tokenResponse.refreshToken ?: refreshToken,
tokenResponse.expiresIn
)
tokenResponse.accessToken
} else {
clearTokens()
null
}
} catch (exception: Exception) {
clearTokens()
null
}
}
}
// Input validation utilities
class InputValidator {
companion object {
private val EMAIL_PATTERN = Patterns.EMAIL_ADDRESS
private val PHONE_PATTERN = Patterns.PHONE
// Prevent SQL injection and XSS
private val DANGEROUS_CHARACTERS = arrayOf(
"'", "\"", ";", "--", "/*", "*/", "xp_", "sp_",
"<script", "</script>", "<iframe", "</iframe>"
)
}
data class ValidationResult(
val isValid: Boolean,
val errorMessage: String? = null
)
fun validateEmail(email: String): ValidationResult {
return when {
email.isBlank() -> ValidationResult(false, "Email is required")
email.length > 254 -> ValidationResult(false, "Email is too long")
!EMAIL_PATTERN.matcher(email).matches() -> ValidationResult(false, "Invalid email format")
containsDangerousCharacters(email) -> ValidationResult(false, "Email contains invalid characters")
else -> ValidationResult(true)
}
}
fun validatePassword(password: String): ValidationResult {
return when {
password.isBlank() -> ValidationResult(false, "Password is required")
password.length < 8 -> ValidationResult(false, "Password must be at least 8 characters")
password.length > 128 -> ValidationResult(false, "Password is too long")
!hasUpperCase(password) -> ValidationResult(false, "Password must contain uppercase letter")
!hasLowerCase(password) -> ValidationResult(false, "Password must contain lowercase letter")
!hasDigit(password) -> ValidationResult(false, "Password must contain a number")
!hasSpecialCharacter(password) -> ValidationResult(false, "Password must contain special character")
containsDangerousCharacters(password) -> ValidationResult(false, "Password contains invalid characters")
else -> ValidationResult(true)
}
}
fun validatePhoneNumber(phone: String): ValidationResult {
return when {
phone.isBlank() -> ValidationResult(false, "Phone number is required")
phone.length > 15 -> ValidationResult(false, "Phone number is too long")
!PHONE_PATTERN.matcher(phone).matches() -> ValidationResult(false, "Invalid phone format")
containsDangerousCharacters(phone) -> ValidationResult(false, "Phone contains invalid characters")
else -> ValidationResult(true)
}
}
fun sanitizeInput(input: String): String {
var sanitized = input.trim()
// Remove dangerous characters
DANGEROUS_CHARACTERS.forEach { char ->
sanitized = sanitized.replace(char, "", ignoreCase = true)
}
// Encode HTML entities
sanitized = sanitized
.replace("&", "&")
.replace("<", "<")
.replace(">", ">")
.replace("\"", """)
.replace("'", "'")
return sanitized
}
private fun containsDangerousCharacters(input: String): Boolean {
return DANGEROUS_CHARACTERS.any { char ->
input.contains(char, ignoreCase = true)
}
}
private fun hasUpperCase(password: String): Boolean = password.any { it.isUpperCase() }
private fun hasLowerCase(password: String): Boolean = password.any { it.isLowerCase() }
private fun hasDigit(password: String): Boolean = password.any { it.isDigit() }
private fun hasSpecialCharacter(password: String): Boolean =
password.any { "!@#$%^&*()_+-=[]{}|;:,.<>?".contains(it) }
}
// Secure Room database queries
@Dao
interface SecureUserDao {
// Use parameterized queries to prevent SQL injection
@Query("SELECT * FROM users WHERE email = :email AND active = 1 LIMIT 1")
suspend fun getUserByEmail(email: String): User?
@Query("SELECT * FROM users WHERE name LIKE '%' || :searchTerm || '%' AND active = 1 ORDER BY name ASC LIMIT :limit")
suspend fun searchUsers(searchTerm: String, limit: Int = 50): List<User>
// Use IN clause with parameterized list
@Query("SELECT * FROM users WHERE id IN (:userIds) AND active = 1")
suspend fun getUsersByIds(userIds: List<String>): List<User>
// Avoid direct string concatenation in queries
@Query("SELECT COUNT(*) FROM users WHERE created_at >= :startDate AND created_at <= :endDate")
suspend fun getUserCountInDateRange(startDate: String, endDate: String): Int
}
Security considerations:
| Security Area | Implementation | Priority |
|---|---|---|
| Data Encryption | EncryptedSharedPreferences, Keystore | High |
| Network Security | Certificate pinning, TLS 1.3 | High |
| Authentication | Biometric, 2FA, secure tokens | High |
| Input Validation | Sanitization, parameterized queries | High |
| Code Protection | Obfuscation, anti-tampering | Medium |
| Runtime Security | Root detection, debugger detection | Medium |
The Android Security Model provides multiple layers of protection, but developers must implement application-level security measures correctly.
Android devices offer rich hardware capabilities that enhance user experiences. Camera integration, location services, and sensor data create engaging, context-aware applications. Proper hardware integration can differentiate your app from competitors.
// Camera manager with CameraX
class CameraManager @Inject constructor(
@ApplicationContext private val context: Context
) {
private var imageCapture: ImageCapture? = null
private var videoCapture: VideoCapture<Recorder>? = null
private var preview: Preview? = null
private var camera: Camera? = null
private var cameraProvider: ProcessCameraProvider? = null
private val orientationEventListener by lazy {
object : OrientationEventListener(context) {
override fun onOrientationChanged(orientation: Int) {
val rotation = when (orientation) {
in 45..134 -> Surface.ROTATION_270
in 135..224 -> Surface.ROTATION_180
in 225..314 -> Surface.ROTATION_90
else -> Surface.ROTATION_0
}
imageCapture?.targetRotation = rotation
videoCapture?.targetRotation = rotation
}
}
}
suspend fun initializeCamera(
lifecycleOwner: LifecycleOwner,
previewView: PreviewView,
lensFacing: Int = CameraSelector.LENS_FACING_BACK
): Boolean {
return try {
cameraProvider = ProcessCameraProvider.getInstance(context).await()
// Preview use case
preview = Preview.Builder()
.setTargetAspectRatio(AspectRatio.RATIO_16_9)
.setTargetRotation(previewView.display.rotation)
.build()
// Image capture use case
imageCapture = ImageCapture.Builder()
.setTargetAspectRatio(AspectRatio.RATIO_16_9)
.setTargetRotation(previewView.display.rotation)
.setCaptureMode(ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY)
.build()
// Video capture use case
val recorder = Recorder.Builder()
.setQualitySelector(QualitySelector.from(Quality.HIGHEST))
.build()
videoCapture = VideoCapture.withOutput(recorder)
val cameraSelector = CameraSelector.Builder()
.requireLensFacing(lensFacing)
.build()
// Unbind all use cases before rebinding
cameraProvider?.unbindAll()
// Bind use cases to camera
camera = cameraProvider?.bindToLifecycle(
lifecycleOwner,
cameraSelector,
preview,
imageCapture,
videoCapture
)
// Attach the viewfinder's surface provider to preview use case
preview?.setSurfaceProvider(previewView.surfaceProvider)
// Enable orientation listener
orientationEventListener.enable()
true
} catch (exception: Exception) {
Log.e("CameraManager", "Camera initialization failed", exception)
false
}
}
suspend fun captureImage(): Result<Uri> {
val imageCapture = imageCapture ?: return Result.failure(
Exception("Camera not initialized")
)
val name = SimpleDateFormat("yyyy-MM-dd-HH-mm-ss-SSS", Locale.US)
.format(System.currentTimeMillis())
val contentValues = ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, name)
put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg")
put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/MyApp")
}
val outputOptions = ImageCapture.OutputFileOptions.Builder(
context.contentResolver,
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
contentValues
).build()
return suspendCancellableCoroutine { continuation ->
imageCapture.takePicture(
outputOptions,
ContextCompat.getMainExecutor(context),
object : ImageCapture.OnImageSavedCallback {
override fun onError(exception: ImageCaptureException) {
continuation.resumeWith(Result.failure(exception))
}
override fun onImageSaved(output: ImageCapture.OutputFileResults) {
continuation.resumeWith(Result.success(output.savedUri!!))
}
}
)
}
}
suspend fun startVideoRecording(): Result<VideoRecordingHandle> {
val videoCapture = videoCapture ?: return Result.failure(
Exception("Camera not initialized")
)
val name = SimpleDateFormat("yyyy-MM-dd-HH-mm-ss-SSS", Locale.US)
.format(System.currentTimeMillis())
val contentValues = ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, name)
put(MediaStore.MediaColumns.MIME_TYPE, "video/mp4")
put(MediaStore.Video.Media.RELATIVE_PATH, "Movies/MyApp")
}
val mediaStoreOutputOptions = MediaStoreOutputOptions.Builder(
context.contentResolver,
MediaStore.Video.Media.EXTERNAL_CONTENT_URI
).setContentValues(contentValues).build()
val recording = videoCapture.output
.prepareRecording(context, mediaStoreOutputOptions)
.apply {
if (PermissionChecker.checkSelfPermission(
context,
Manifest.permission.RECORD_AUDIO
) == PermissionChecker.PERMISSION_GRANTED
) {
withAudioEnabled()
}
}
.start(ContextCompat.getMainExecutor(context)) { recordEvent ->
when (recordEvent) {
is VideoRecordEvent.Start -> {
Log.d("CameraManager", "Video recording started")
}
is VideoRecordEvent.Finalize -> {
if (!recordEvent.hasError()) {
Log.d("CameraManager", "Video saved: ${recordEvent.outputResults.outputUri}")
} else {
Log.e("CameraManager", "Video recording failed: ${recordEvent.error}")
}
}
}
}
return Result.success(VideoRecordingHandle(recording))
}
fun enableTorch(enabled: Boolean) {
camera?.cameraControl?.enableTorch(enabled)
}
fun setZoomRatio(zoomRatio: Float) {
camera?.cameraControl?.setZoomRatio(zoomRatio)
}
fun releaseCamera() {
orientationEventListener.disable()
cameraProvider?.unbindAll()
cameraProvider = null
}
}
data class VideoRecordingHandle(
private val recording: Recording
) {
fun stop() {
recording.stop()
}
fun pause() {
recording.pause()
}
fun resume() {
recording.resume()
}
}
// Comprehensive location manager
class LocationManager @Inject constructor(
@ApplicationContext private val context: Context
) {
private var fusedLocationClient: FusedLocationProviderClient? = null
private var locationCallback: LocationCallback? = null
private val _currentLocation = MutableLiveData<Location>()
val currentLocation: LiveData<Location> = _currentLocation
private val _locationUpdates = MutableSharedFlow<Location>()
val locationUpdates: SharedFlow<Location> = _locationUpdates.asSharedFlow()
init {
fusedLocationClient = LocationServices.getFusedLocationProviderClient(context)
}
suspend fun getCurrentLocation(): Result<Location> {
if (!hasLocationPermissions()) {
return Result.failure(SecurityException("Location permissions not granted"))
}
return try {
val location = fusedLocationClient?.getCurrentLocation(
Priority.PRIORITY_HIGH_ACCURACY,
CancellationTokenSource().token
)?.await()
if (location != null) {
Result.success(location)
} else {
Result.failure(Exception("Unable to get current location"))
}
} catch (exception: Exception) {
Result.failure(exception)
}
}
fun startLocationUpdates(
priority: Int = Priority.PRIORITY_HIGH_ACCURACY,
intervalMs: Long = 10000,
fastestIntervalMs: Long = 5000
): Boolean {
if (!hasLocationPermissions()) {
return false
}
val locationRequest = LocationRequest.Builder(priority, intervalMs)
.setWaitForAccurateLocation(false)
.setMinUpdateIntervalMillis(fastestIntervalMs)
.setMaxUpdateDelayMillis(intervalMs * 2)
.build()
locationCallback = object : LocationCallback() {
override fun onLocationResult(locationResult: LocationResult) {
locationResult.lastLocation?.let { location ->
_currentLocation.postValue(location)
CoroutineScope(Dispatchers.Main).launch {
_locationUpdates.emit(location)
}
}
}
}
try {
fusedLocationClient?.requestLocationUpdates(
locationRequest,
locationCallback!!,
Looper.getMainLooper()
)
return true
} catch (securityException: SecurityException) {
return false
}
}
fun stopLocationUpdates() {
locationCallback?.let { callback ->
fusedLocationClient?.removeLocationUpdates(callback)
}
locationCallback = null
}
suspend fun getAddressFromLocation(location: Location): Result<List<Address>> {
return try {
val geocoder = Geocoder(context, Locale.getDefault())
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
suspendCancellableCoroutine { continuation ->
geocoder.getFromLocation(
location.latitude,
location.longitude,
5
) { addresses ->
continuation.resumeWith(Result.success(addresses))
}
}
} else {
@Suppress("DEPRECATION")
val addresses = geocoder.getFromLocation(
location.latitude,
location.longitude,
5
) ?: emptyList()
Result.success(addresses)
}
} catch (exception: Exception) {
Result.failure(exception)
}
}
fun calculateDistance(
startLocation: Location,
endLocation: Location
): Float {
return startLocation.distanceTo(endLocation)
}
fun isLocationAccurate(location: Location, accuracyThresholdMeters: Float = 50f): Boolean {
return location.hasAccuracy() && location.accuracy <= accuracyThresholdMeters
}
private fun hasLocationPermissions(): Boolean {
return ContextCompat.checkSelfPermission(
context,
Manifest.permission.ACCESS_FINE_LOCATION
) == PackageManager.PERMISSION_GRANTED ||
ContextCompat.checkSelfPermission(
context,
Manifest.permission.ACCESS_COARSE_LOCATION
) == PackageManager.PERMISSION_GRANTED
}
fun cleanup() {
stopLocationUpdates()
fusedLocationClient = null
}
}
// Comprehensive sensor manager
class SensorManager @Inject constructor(
@ApplicationContext private val context: Context
) : SensorEventListener {
private val sensorManager: SensorManager =
context.getSystemService(Context.SENSOR_SERVICE) as SensorManager
private val _accelerometerData = MutableLiveData<SensorData>()
val accelerometerData: LiveData<SensorData> = _accelerometerData
private val _gyroscopeData = MutableLiveData<SensorData>()
val gyroscopeData: LiveData<SensorData> = _gyroscopeData
private val _magnetometerData = MutableLiveData<SensorData>()
val magnetometerData: LiveData<SensorData> = _magnetometerData
private var isListening = false
data class SensorData(
val x: Float,
val y: Float,
val z: Float,
val timestamp: Long,
val accuracy: Int
)
fun startListening() {
if (isListening) return
// Register accelerometer
sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)?.let { sensor ->
sensorManager.registerListener(
this,
sensor,
SensorManager.SENSOR_DELAY_NORMAL
)
}
// Register gyroscope
sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE)?.let { sensor ->
sensorManager.registerListener(
this,
sensor,
SensorManager.SENSOR_DELAY_NORMAL
)
}
// Register magnetometer
sensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD)?.let { sensor ->
sensorManager.registerListener(
this,
sensor,
SensorManager.SENSOR_DELAY_NORMAL
)
}
isListening = true
}
fun stopListening() {
sensorManager.unregisterListener(this)
isListening = false
}
override fun onSensorChanged(event: SensorEvent?) {
event?.let { sensorEvent ->
val sensorData = SensorData(
x = sensorEvent.values[0],
y = sensorEvent.values[1],
z = sensorEvent.values[2],
timestamp = sensorEvent.timestamp,
accuracy = sensorEvent.accuracy
)
when (sensorEvent.sensor.type) {
Sensor.TYPE_ACCELEROMETER -> _accelerometerData.postValue(sensorData)
Sensor.TYPE_GYROSCOPE -> _gyroscopeData.postValue(sensorData)
Sensor.TYPE_MAGNETIC_FIELD -> _magnetometerData.postValue(sensorData)
}
}
}
override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {
// Handle accuracy changes if needed
}
fun getDeviceOrientation(
accelerometerData: SensorData,
magnetometerData: SensorData
): DeviceOrientation {
val rotationMatrix = FloatArray(9)
val orientationAngles = FloatArray(3)
SensorManager.getRotationMatrix(
rotationMatrix,
null,
floatArrayOf(accelerometerData.x, accelerometerData.y, accelerometerData.z),
floatArrayOf(magnetometerData.x, magnetometerData.y, magnetometerData.z)
)
SensorManager.getOrientation(rotationMatrix, orientationAngles)
return DeviceOrientation(
azimuth = Math.toDegrees(orientationAngles[0].toDouble()).toFloat(),
pitch = Math.toDegrees(orientationAngles[1].toDouble()).toFloat(),
roll = Math.toDegrees(orientationAngles[2].toDouble()).toFloat()
)
}
fun detectShake(accelerometerData: SensorData, threshold: Float = 12.0f): Boolean {
val acceleration = sqrt(
accelerometerData.x.pow(2) +
accelerometerData.y.pow(2) +
accelerometerData.z.pow(2)
)
return acceleration > threshold
}
data class DeviceOrientation(
val azimuth: Float, // Rotation around Z-axis
val pitch: Float, // Rotation around X-axis
val roll: Float // Rotation around Y-axis
)
}
Hardware integration:
| Feature | API Level | Permission Required | Use Cases |
|---|---|---|---|
| Camera | All | CAMERA | Photo capture, video recording, QR scanning |
| Location | All | ACCESS_FINE_LOCATION | Navigation, geofencing, location-based services |
| Sensors | All | None | Fitness tracking, games, augmented reality |
| Bluetooth | All | BLUETOOTH | Device communication, IoT integration |
| NFC | 14+ | NFC | Payments, data transfer, smart tags |
| Microphone | All | RECORD_AUDIO | Voice commands, audio recording |
CameraX simplifies camera implementation while providing advanced features like image analysis and video recording across different device manufacturers.
Creating polished user experiences requires understanding advanced UI concepts like custom animations, gesture handling, and accessibility implementation. Modern users expect smooth, intuitive interfaces that respond naturally to their interactions.
// Advanced animation utilities for Compose
@Composable
fun AnimatedCard(
isExpanded: Boolean,
onClick: () -> Unit,
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
val animatedHeight by animateDpAsState(
targetValue = if (isExpanded) 200.dp else 80.dp,
animationSpec = spring(
dampingRatio = Spring.DampingRatioMediumBouncy,
stiffness = Spring.StiffnessLow
),
label = "height_animation"
)
val animatedElevation by animateDpAsState(
targetValue = if (isExpanded) 8.dp else 2.dp,
animationSpec = tween(300),
label = "elevation_animation"
)
val animatedCornerRadius by animateDpAsState(
targetValue = if (isExpanded) 16.dp else 8.dp,
animationSpec = tween(300),
label = "corner_animation"
)
Card(
modifier = modifier
.fillMaxWidth()
.height(animatedHeight)
.clickable { onClick() },
elevation = CardDefaults.cardElevation(defaultElevation = animatedElevation),
shape = RoundedCornerShape(animatedCornerRadius)
) {
AnimatedContent(
targetState = isExpanded,
transitionSpec = {
slideInVertically { fullHeight -> fullHeight } + fadeIn() with
slideOutVertically { fullHeight -> -fullHeight } + fadeOut()
},
label = "content_animation"
) { expanded ->
if (expanded) {
content()
} else {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text("Tap to expand")
}
}
}
}
}
// Complex list animations
@Composable
fun AnimatedList(
items: List<String>,
modifier: Modifier = Modifier
) {
LazyColumn(modifier = modifier) {
itemsIndexed(
items = items,
key = { _, item -> item }
) { index, item ->
val animationDelay = (index * 50).coerceAtMost(300)
AnimatedItemCard(
item = item,
animationDelay = animationDelay,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 8.dp)
.animateItemPlacement(
animationSpec = spring(
dampingRatio = Spring.DampingRatioMediumBouncy,
stiffness = Spring.StiffnessMedium
)
)
)
}
}
}
@Composable
fun AnimatedItemCard(
item: String,
animationDelay: Int,
modifier: Modifier = Modifier
) {
var isVisible by remember { mutableStateOf(false) }
LaunchedEffect(key1 = item) {
delay(animationDelay.toLong())
isVisible = true
}
AnimatedVisibility(
visible = isVisible,
enter = slideInHorizontally(
initialOffsetX = { fullWidth -> fullWidth },
animationSpec = tween(500)
) + fadeIn(animationSpec = tween(500)),
modifier = modifier
) {
Card(
modifier = Modifier.fillMaxWidth(),
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
) {
Text(
text = item,
modifier = Modifier.padding(16.dp),
style = MaterialTheme.typography.bodyLarge
)
}
}
}
// Custom loading animation
@Composable
fun PulsingLoadingIndicator(
modifier: Modifier = Modifier,
color: Color = MaterialTheme.colorScheme.primary
) {
val infiniteTransition = rememberInfiniteTransition(label = "pulse_transition")
val scale by infiniteTransition.animateFloat(
initialValue = 0.8f,
targetValue = 1.2f,
animationSpec = infiniteRepeatable(
animation = tween(1000),
repeatMode = RepeatMode.Reverse
),
label = "scale_animation"
)
val alpha by infiniteTransition.animateFloat(
initialValue = 0.4f,
targetValue = 1.0f,
animationSpec = infiniteRepeatable(
animation = tween(1000),
repeatMode = RepeatMode.Reverse
),
label = "alpha_animation"
)
Box(
modifier = modifier
.size(60.dp)
.scale(scale)
.alpha(alpha)
.background(
color = color,
shape = CircleShape
),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator(
modifier = Modifier.size(30.dp),
color = Color.White,
strokeWidth = 3.dp
)
}
}
// Custom gesture detector for complex interactions
@Composable
fun SwipeableCard(
onSwipeLeft: () -> Unit,
onSwipeRight: () -> Unit,
onSwipeUp: () -> Unit,
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
var offsetX by remember { mutableStateOf(0f) }
var offsetY by remember { mutableStateOf(0f) }
var rotation by remember { mutableStateOf(0f) }
val animatedOffsetX by animateFloatAsState(
targetValue = offsetX,
animationSpec = spring(dampingRatio = 0.8f),
label = "offset_x"
)
val animatedOffsetY by animateFloatAsState(
targetValue = offsetY,
animationSpec = spring(dampingRatio = 0.8f),
label = "offset_y"
)
val animatedRotation by animateFloatAsState(
targetValue = rotation,
animationSpec = spring(dampingRatio = 0.8f),
label = "rotation"
)
Box(
modifier = modifier
.offset { IntOffset(animatedOffsetX.roundToInt(), animatedOffsetY.roundToInt()) }
.rotate(animatedRotation)
.pointerInput(Unit) {
detectDragGestures(
onDragEnd = {
when {
offsetX > 300f -> {
onSwipeRight()
offsetX = 0f
offsetY = 0f
rotation = 0f
}
offsetX < -300f -> {
onSwipeLeft()
offsetX = 0f
offsetY = 0f
rotation = 0f
}
offsetY < -300f -> {
onSwipeUp()
offsetX = 0f
offsetY = 0f
rotation = 0f
}
else -> {
offsetX = 0f
offsetY = 0f
rotation = 0f
}
}
}
) { _, dragAmount ->
offsetX += dragAmount.x
offsetY += dragAmount.y
rotation = offsetX * 0.1f
}
}
) {
content()
}
}
// Multi-touch zoom and pan
@Composable
fun ZoomablePannable(
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
var scale by remember { mutableStateOf(1f) }
var offsetX by remember { mutableStateOf(0f) }
var offsetY by remember { mutableStateOf(0f) }
val animatedScale by animateFloatAsState(
targetValue = scale,
animationSpec = spring(dampingRatio = 0.9f),
label = "scale"
)
Box(
modifier = modifier
.fillMaxSize()
.clipToBounds()
.pointerInput(Unit) {
detectTransformGestures(
panZoomLock = false
) { centroid, pan, zoom, _ ->
scale = (scale * zoom).coerceIn(0.5f, 3.0f)
val maxX = (size.width * (scale - 1)) / 2
val maxY = (size.height * (scale - 1)) / 2
offsetX = (offsetX + pan.x).coerceIn(-maxX, maxX)
offsetY = (offsetY + pan.y).coerceIn(-maxY, maxY)
}
}
.graphicsLayer {
scaleX = animatedScale
scaleY = animatedScale
translationX = offsetX
translationY = offsetY
}
) {
content()
}
}
// Comprehensive accessibility support
@Composable
fun AccessibleUserCard(
user: User,
isSelected: Boolean,
onSelect: () -> Unit,
modifier: Modifier = Modifier
) {
Card(
modifier = modifier
.fillMaxWidth()
.semantics {
contentDescription = "User card for ${user.name}, ${user.email}" +
if (isSelected) ", selected" else ", not selected"
role = Role.Button
stateDescription = if (isSelected) "Selected" else "Not selected"
onClick {
onSelect()
true
}
}
.clickable(
onClickLabel = if (isSelected) "Deselect ${user.name}" else "Select ${user.name}"
) {
onSelect()
},
colors = CardDefaults.cardColors(
containerColor = if (isSelected)
MaterialTheme.colorScheme.primaryContainer
else
MaterialTheme.colorScheme.surface
)
) {
Row(
modifier = Modifier.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
AsyncImage(
model = user.avatarUrl,
contentDescription = "Profile picture of ${user.name}",
modifier = Modifier
.size(48.dp)
.clip(CircleShape)
.semantics {
contentDescription = "Avatar for ${user.name}"
},
placeholder = painterResource(R.drawable.placeholder_avatar),
error = painterResource(R.drawable.error_avatar)
)
Spacer(modifier = Modifier.width(16.dp))
Column(
modifier = Modifier.weight(1f)
) {
Text(
text = user.name,
style = MaterialTheme.typography.bodyLarge,
modifier = Modifier.semantics {
heading()
}
)
Text(
text = user.email,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.semantics {
contentDescription = "Email address: ${user.email}"
}
)
}
if (isSelected) {
Icon(
imageVector = Icons.Default.Check,
contentDescription = "Selected",
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.semantics {
contentDescription = "This user is selected"
}
)
}
}
}
}
// Accessible form with proper navigation
@Composable
fun AccessibleLoginForm(
onLogin: (String, String) -> Unit,
modifier: Modifier = Modifier
) {
var email by remember { mutableStateOf("") }
var password by remember { mutableStateOf("") }
var passwordVisible by remember { mutableStateOf(false) }
var emailError by remember { mutableStateOf<String?>(null) }
var passwordError by remember { mutableStateOf<String?>(null) }
Column(
modifier = modifier
.fillMaxWidth()
.padding(16.dp)
.semantics {
contentDescription = "Login form"
}
) {
Text(
text = "Login",
style = MaterialTheme.typography.headlineMedium,
modifier = Modifier
.padding(bottom = 16.dp)
.semantics {
heading()
}
)
OutlinedTextField(
value = email,
onValueChange = {
email = it
emailError = null
},
label = { Text("Email") },
placeholder = { Text("Enter your email address") },
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Email,
imeAction = ImeAction.Next
),
isError = emailError != null,
supportingText = emailError?.let { error ->
{
Text(
text = error,
color = MaterialTheme.colorScheme.error,
modifier = Modifier.semantics {
contentDescription = "Email error: $error"
liveRegion = LiveRegionMode.Polite
}
)
}
},
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 16.dp)
.semantics {
contentDescription = "Email input field" +
if (emailError != null) ", has error: $emailError" else ""
}
)
OutlinedTextField(
value = password,
onValueChange = {
password = it
passwordError = null
},
label = { Text("Password") },
placeholder = { Text("Enter your password") },
visualTransformation = if (passwordVisible)
VisualTransformation.None
else
PasswordVisualTransformation(),
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Password,
imeAction = ImeAction.Done
),
keyboardActions = KeyboardActions(
onDone = { onLogin(email, password) }
),
isError = passwordError != null,
supportingText = passwordError?.let { error ->
{
Text(
text = error,
color = MaterialTheme.colorScheme.error,
modifier = Modifier.semantics {
contentDescription = "Password error: $error"
liveRegion = LiveRegionMode.Polite
}
)
}
},
trailingIcon = {
IconButton(
onClick = { passwordVisible = !passwordVisible },
modifier = Modifier.semantics {
contentDescription = if (passwordVisible)
"Hide password"
else
"Show password"
}
) {
Icon(
imageVector = if (passwordVisible)
Icons.Filled.Visibility
else
Icons.Filled.VisibilityOff,
contentDescription = null
)
}
},
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 24.dp)
.semantics {
contentDescription = "Password input field" +
if (passwordError != null) ", has error: $passwordError" else "" +
if (passwordVisible) ", password is visible" else ", password is hidden"
}
)
Button(
onClick = { onLogin(email, password) },
enabled = email.isNotBlank() && password.isNotBlank(),
modifier = Modifier
.fillMaxWidth()
.semantics {
contentDescription = "Login button" +
if (email.isBlank() || password.isBlank())
", disabled, fill in all fields to enable"
else
", enabled"
}
) {
Text("Login")
}
}
}
Advanced UI topics:
| Technique | Impact | Implementation |
|---|---|---|
| Remember expensive calculations | High | Use remember and derivedStateOf |
| Lazy loading | High | LazyColumn, LazyRow, Pager |
| Image optimization | Medium | Coil with proper sizing and caching |
| Animation performance | Medium | Use hardware-accelerated properties |
| Recomposition optimization | High | Stable parameters, immutable data |
| View recycling | High | Proper ViewHolder patterns |
Accessibility isn’t optional in modern app development. Android’s accessibility APIs ensure your applications work for users with disabilities while improving the experience for all users.
Successfully launching an Android app requires understanding the Google Play Store’s requirements and monetization strategies to maximize reach and revenue. This section covers preparing your app for distribution, adhering to guidelines, and implementing monetization techniques.
// build.gradle (app level)
android {
...
signingConfigs {
release {
storeFile file("keystore.jks")
storePassword "your_store_password"
keyAlias "your_key_alias"
keyPassword "your_key_password"
}
}
buildTypes {
release {
signingConfig signingConfigs.release
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
android {
defaultConfig {
versionCode 1
versionName "1.0"
}
}
// Set up Firebase Remote Config for A/B testing
val remoteConfig = FirebaseRemoteConfig.getInstance()
remoteConfig.setDefaultsAsync(R.xml.remote_config_defaults)
remoteConfig.fetchAndActivate().addOnCompleteListener { task ->
if (task.isSuccessful) {
val buttonColor = remoteConfig.getString("button_color")
// Apply variant (e.g., button color) based on A/B test
}
}
// Log custom events FirebaseAnalytics.getInstance(context).logEvent(FirebaseAnalytics.Event.SELECT_ITEM) { param(FirebaseAnalytics.Param.ITEM_ID, itemId) param(FirebaseAnalytics.Param.ITEM_NAME, itemName) }
// build.gradle (app level) dependencies { implementation 'com.google.firebase:firebase-crashlytics:18.6.0' } // Log custom non-fatal exceptions FirebaseCrashlytics.getInstance().recordException(Exception("Non-fatal error occurred"))// Initialize BillingClient val billingClient = BillingClient.newBuilder(context) .setListener(purchasesUpdatedListener) .enablePendingPurchases() .build() // Query available products billingClient.querySkuDetailsAsync(params) { result, skuDetailsList -> if (result.responseCode == BillingClient.BillingResponseCode.OK) { // Display available products } }// Load banner ad val adView = AdView(context) adView.adUnitId = "your_ad_unit_id" adView.setAdSize(AdSize.BANNER) adView.loadAd(AdRequest.Builder().build())Key Takeaways:
Leveraging modern tools in Android Studio and build systems enhances productivity and code quality. This section explores essential tools and practices for efficient development.
// Custom Live Template for ViewModel // In Android Studio: File > Settings > Editor > Live Templates // Abbreviation: vm val $NAME$ = viewModel<${TYPE}ViewModel>()// gradle.properties org.gradle.caching=true// gradle.properties org.gradle.parallel=true# libs.versions.toml
[versions]
kotlin = "1.9.0"
androidx-core = "1.12.0"
[libraries]
kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" }
core-ktx = { module = "androidx.core:core-ktx", version.ref = "androidx-core" }
Usage in Gradle:
// build.gradle
dependencies {
implementation(libs.core.ktx)
implementation(libs.kotlin.stdlib)
}
# .github/workflows/ci.yml name: Android CI on: [push] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Set up JDK 11 uses: actions/setup-java@v3 with: java-version: '11' - name: Build with Gradle run: ./gradlew build - name: Run tests run: ./gradlew test./gradlew lint# detekt-config.yml style: WildcardImport: active: true excludeImports: ['java.util.*']// build.gradle plugins { id "org.sonarqube" version "4.0.0" }./gradlew ktlintFormat# .editorconfig root = true [*] indent_size = 4 indent_style = spaceKey Takeaways:
Cross-platform development allows code reuse across Android and other platforms, reducing development time and maintenance costs. This section compares native Android with cross-platform solutions.
// shared/src/commonMain/kotlin/Platform.kt expect class Platform { val name: String } // shared/src/androidMain/kotlin/Platform.kt actual class Platform { actual val name: String = "Android" }// shared/build.gradle.kts kotlin { android() ios() sourceSets { val commonMain by getting { dependencies { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.0") } } } }// Start FlutterActivity startActivity(FlutterActivity.createDefaultIntent(context))// Native module class CustomModule(reactContext: ReactContext) : ReactContextBaseJavaModule(reactContext) { override fun getName() = "CustomModule" @ReactMethod fun doSomething(value: String) { // Native implementation } }// WebView setup webView.settings.javaScriptEnabled = true webView.loadUrl("https://your-web-app.com")Key Takeaways:
Staying ahead in Android development requires keeping up with evolving APIs, privacy trends, and new form factors. This section explores emerging technologies and best practices.
// Apply dynamic colors DynamicColors.applyToActivitiesIfAvailable(application)// Check Privacy Sandbox availability if (BuildCompat.isAtLeastU()) { // Use Privacy Sandbox APIs }// build.gradle (feature module) apply plugin: 'com.android.dynamic-feature'context(AppComponent) fun performAction() { // Access AppComponent properties }// build.gradle android { baselineProfile { enabled true } }// Request permission with rationale if (shouldShowRequestPermissionRationale(Manifest.permission.CAMERA)) { // Show rationale dialog } else { requestPermissions(arrayOf(Manifest.permission.CAMERA), REQUEST_CODE) }// Adaptive layout in Compose val windowSizeClass = calculateWindowSizeClass() when (windowSizeClass.widthSizeClass) { WindowWidthSizeClass.Compact -> CompactLayout() else -> ExpandedLayout() }Key Takeaways:
Optimize for new form factors to reach broader audiences.
Stay updated with Android’s evolving APIs and privacy requirements.
Adopt modularization and Compose for future-proof apps.
No, beginners can start without coding knowledge, but learning Kotlin is highly recommended for building modern Android apps.
Kotlin is the official language supported by Google. Java is still widely used, but Kotlin is faster, safer, and future-focused.
Yes, but Android Studio is the official IDE and offers the best Android app development experience. Other editors exist for Android app development, but Android Studio is ideal for both beginners and experts.
For Android app development, you need at least 8 GB RAM (16 GB recommended), SSD storage, and a modern processor like Intel i5 or Ryzen 5. Around 10–20 GB free disk space is needed for Android app development SDKs and tools.
Learning Android app development usually takes 3–6 months to build simple apps if you practice consistently. Building advanced Android app development projects may take longer depending on complexity.
Yes, publishing Android app development projects on Google Play requires a one-time $25 developer account fee. After that, you can publish unlimited Android app development projects.
Yes, Jetpack Compose is the modern UI toolkit for Android app development. XML is still supported in Android app development, but Compose offers faster development and better integration with Kotlin.
Yes, Android app development is supported on Windows, macOS, and Linux through Android Studio. All platforms provide the same Android app development features in 2025.
You can test Android app development projects using Android Studio’s built-in emulator (AVD). However, testing Android app development projects on a real device is recommended for performance and hardware-specific behavior.
You can write and test apps offline, but you need internet access to download SDK updates, libraries, and dependencies.
Common Android app development challenges include slow emulator performance, Gradle build errors, and managing dependencies. Most Android app development issues can be fixed with proper setup and learning.
Yes, Android app development is highly valuable as Android powers the majority of smartphones worldwide. With the rise of Jetpack Compose, Kotlin Multiplatform, and AI features, Android app development demand is growing.
Yes, AI-powered coding assistants can help with Android app development by writing code, fixing bugs, and suggesting improvements. They won’t replace learning Android app development, but they can speed up the process.
Expect more focus on cross-platform development with Kotlin Multiplatform, advanced UI with Jetpack Compose, AI-driven apps, and support for foldable and wearable devices.
Beyond Android Studio, you’ll need the Android SDK, Git for version control, Gradle for build automation, and Firebase for backend services. Design tools like Figma or Adobe XD are also helpful for UI/UX planning in Android app development.
Android app development has unique challenges like device fragmentation and multiple screen sizes, but it offers more flexibility and customization options. The difficulty depends on your background and project requirements.
Android app development salaries vary by location and experience. Entry-level developers typically earn $60,000-$80,000, while senior Android app development specialists can earn $120,000+ annually.
Yes, Android app development offers multiple monetization strategies including in-app purchases, subscription models, advertisements, and premium app sales through Google Play Store.
Native Android app development with Kotlin/Java remains most popular. Cross-platform options include Flutter, React Native, and Xamarin, though native development typically offers better performance.
Material Design is crucial for Android app development as it provides consistent UI/UX guidelines. Following Material Design principles ensures your app feels native and intuitive to Android users.
Android app development supports various databases including SQLite (built-in), Room (recommended ORM), Firebase Realtime Database, and cloud solutions like MongoDB. Room is particularly popular for local data storage in modern Android app development.
Android app development requires responsive design using density-independent pixels (dp), flexible layouts with ConstraintLayout, and resource qualifiers for different screen densities and sizes.
Android app development security involves encrypting sensitive data, using HTTPS for network calls, implementing proper authentication, obfuscating code, and following Google’s security guidelines for app distribution.
Yes, Android app development now supports ML through TensorFlow Lite, ML Kit, and on-device processing. This enables features like image recognition, natural language processing, and predictive analytics in your apps.
APIs are essential in Android app development for connecting to web services, accessing device features, integrating third-party services, and enabling data synchronization between your app and backend servers.
Comprehensive Android app development testing includes unit tests, integration tests, UI tests with Espresso, and testing across multiple devices and API levels to ensure compatibility.
While not mandatory, understanding backend development enhances your Android app development skills. You can use Backend-as-a-Service platforms like Firebase or learn server technologies to build complete solutions.
Successful Android app development includes Google Play Console setup, following store guidelines, app store optimization (ASO), user feedback management, and marketing strategies to increase app visibility and downloads.