Gradle is the backbone of Android development, powering build automation, dependency management, and project configuration. As projects scale, module dependencies in Android Gradle become essential for keeping your codebase organized, improving reusability, and reducing build times. In this guide, we’ll break down: What module dependencies are in Android Gradle Different types of dependencies (implementation, api,...
If you’ve worked on an Android project, you’ve definitely dealt with Gradle dependencies. They help bring in external libraries, connect different parts of your project, and even let you add custom files. But not all dependencies work the same way. Some are used for linking modules, others for adding external projects, and some for including specific files. Choosing the right type can make your project more organized and easier to maintain.
In this blog, we’ll break down the different types of Gradle dependencies and when to use each one.
Types of Gradle dependencies
Gradle provides three main types of dependencies:
Module dependencies
Project dependencies
File dependencies
Each type serves a different purpose, and choosing the right one ensures better project organization, maintainability, and performance.
Module Dependencies: The Standard Approach
Module dependencies are the most commonly used in Android development. They allow you to connect different modules within the same project.
Example use case:
You have a core module that handles networking and database logic.
Your app module depends on core to access those functionalities.
In Gradle, this might look like:
Kotlin
implementation project(":core")
Why use module dependencies?
Encourages modularization, making projects easier to scale.
Improves build times by allowing Gradle to compile modules separately.
Keeps your code organized and avoids duplication.
Project Dependencies: Linking External Projects
Project dependencies come into play when you want to include another Gradle project that isn’t part of your main project by default.
Example use case:
You’re working on a shared internal library that’s used across multiple apps.
Instead of publishing it to Maven or JCenter every time, you directly link the project.
In Gradle:
Kotlin
implementation project(path: ':shared-library')
Why use project dependencies?
Great for internal library development.
Lets you work with multiple projects simultaneously without extra publishing steps.
Useful in large teams or enterprise-level apps.
File Dependencies: Adding Custom JAR or AAR Files
File dependencies allow you to include JAR or AAR files directly into your project.
Example use case:
You’re integrating a third-party SDK that isn’t available in a public Maven repository.
You have a legacy .jar file you need for backward compatibility.
In Gradle:
Kotlin
implementation files('libs/custom-library.jar')
Why use file dependencies?
Perfect for custom or private libraries.
Helps when working with offline builds or older dependencies.
Best practice: Use file dependencies sparingly. If a library is available via Maven Central or Google’s repository, prefer that method — it’s easier to update and maintain.
Best Practices for Managing Gradle Dependencies
Prefer remote repositories (Maven Central, Google) over file dependencies.
Modularize your project: keep reusable logic in separate modules.
Use version catalogs (Gradle 7+) to centralize dependency versions.
Keep dependencies updated to avoid security vulnerabilities.
Avoid duplication by consolidating commonly used libraries in shared modules.
Conclusion
Gradle dependencies may seem simple, but choosing the right type — module, project, or file — can have a huge impact on your Android project’s structure and maintainability.
Use module dependencies for modular apps.
Use project dependencies for shared libraries across projects.
Use file dependencies only when necessary.
By understanding these distinctions, you’ll write cleaner code, speed up build times, and set yourself up for long-term project success.
FAQ: Gradle Dependencies in Android
Q1: What’s the difference between implementation and api in Gradle?
implementation: The dependency is only available in the current module.
api: The dependency is exposed to modules that depend on your module.
Q2: When should I use file dependencies in Gradle?
Only when the library isn’t available in a Maven or Gradle repository. Otherwise, prefer remote dependencies.
Q3: Can I convert a file dependency into a module or project dependency later?
Yes. If you gain access to the source code or publish the library internally, you can switch to module/project dependencies for better maintainability.
Q4: Do Gradle dependencies affect build speed?
Yes. Modular dependencies can improve build times, while excessive file dependencies can slow things down.
When building Android apps with Jetpack Compose, state management is one of the most important pieces to get right. If you don’t handle state properly, your UI can become messy, tightly coupled, and hard to scale. That’s where State Hoisting in Jetpack Compose comes in.
In this post, we’ll break down what state hoisting is, why it matters, and how you can apply best practices to make your Compose apps scalable, maintainable, and easy to debug.
What Is State Hoisting in Jetpack Compose?
In simple terms, state hoisting is the process of moving state up from a child composable into its parent. Instead of a UI component directly owning and mutating its state, the parent holds the state and passes it down, while the child only receives data and exposes events.
This separation ensures:
Reusability: Components stay stateless and reusable.
Single Source of Truth: State is managed in one place, reducing bugs.
Scalability: Complex UIs are easier to extend and test.
A Basic Example of State Hoisting
Let’s say you have a simple text field. Without state hoisting, the child manages its own state like this:
Kotlin
@ComposablefunSimpleTextField() {var text byremember { mutableStateOf("") }TextField(value = text, onValueChange = { text = it } )}
This works fine for small apps, but the parent composable has no control over the value. It becomes difficult to coordinate multiple composables.
@ComposablefunParentComposable() {var text byremember { mutableStateOf("") }SimpleTextField( text = text, onTextChange = { text = it } )}
Here’s what changed
The parent owns the state (text).
The child only displays the state and sends updates back via onTextChange.
This is the core idea of State Hoisting in Jetpack Compose.
Why State Hoisting Matters for Scalable Apps
As your app grows, different UI elements will need to communicate. If each composable owns its own state, you’ll end up duplicating data or creating inconsistencies.
By hoisting state:
You centralize control, making it easier to debug.
You avoid unexpected side effects caused by hidden internal state.
You enable testing, since state management is separated from UI rendering.
Best Practices for State Hoisting in Jetpack Compose
1. Keep Composables Stateless When Possible
A good rule of thumb: UI elements should be stateless and only care about how data is displayed. The parent decides what data to provide.
Example: A button shouldn’t decide what happens when it’s clicked — it should simply expose an onClick callback.
2. Use remember Wisely in Parents
State is usually managed at the parent level using remember or rememberSaveable.
Use remember when state only needs to survive recomposition.
Use rememberSaveable when you want state to survive configuration changes (like screen rotations).
Kotlin
var text byrememberSaveable { mutableStateOf("") }
3. Follow the Unidirectional Data Flow Pattern
Compose encourages Unidirectional Data Flow (UDF):
Parent owns state.
State is passed down to child.
Child emits events back to parent.
This clear flow makes apps predictable and avoids infinite loops or messy side effects.
4. Keep State Close to Where It’s Used, But Not Too Close
Don’t hoist all state to the top-level of your app. That creates unnecessary complexity. Instead, hoist it just far enough up so that all dependent composables can access it.
For example, if only one screen needs a piece of state, keep it inside that screen’s parent composable rather than in the MainActivity.
5. Use ViewModels for Shared State Across Screens
For larger apps, when multiple screens or composables need the same state, use a ViewModel.
This pattern keeps your UI clean and separates business logic from presentation.
Common Mistakes to Avoid
Keeping state inside deeply nested children: This makes it impossible to share or control at higher levels.
Over-hoisting: Don’t hoist state unnecessarily if no other composable needs it.
Mixing UI logic with business logic: Keep state handling in ViewModels where appropriate.
Conclusion
State Hoisting in Jetpack Compose is more than just a coding pattern — it’s the backbone of building scalable, maintainable apps. By lifting state up, following unidirectional data flow, and keeping components stateless, you set yourself up for long-term success.
To summarize:
Keep state in the parent, not the child.
Pass data down, send events up.
Use ViewModels for shared or complex state.
By applying these best practices, you’ll build apps that are not only functional today but also easy to scale tomorrow.
In modern Android development, ViewModel has become an indispensable component for managing UI-related data in a lifecycle-conscious manner. One powerful application of ViewModels is sharing data between multiple fragments or activities. This guide provides a deep dive into shared ViewModels, explaining their purpose, implementation, and best practices for creating seamless data sharing in your Android apps.
The Concept of Shared ViewModels
A Shared ViewModel is a ViewModel instance that is accessible across multiple fragments or activities, enabling shared state management. This approach is ideal when:
Fragment Communication: Multiple fragments need to work with the same data, such as a user profile or settings.
Decoupling Logic: You want fragments to exchange information without creating brittle, tightly-coupled dependencies.
Navigation Component Scenarios: Sharing data across destinations within a navigation graph requires clean state management.
Unlike standalone ViewModels scoped to a single UI component, shared ViewModels can be scoped to an entire activity or a specific navigation graph, allowing seamless state sharing while respecting lifecycle boundaries.
Why Use Shared ViewModels?
Here are some compelling reasons to choose shared ViewModels:
Lifecycle Safety: Data stored in a ViewModel persists through configuration changes like screen rotations, avoiding unwanted resets.
Simplified Communication: Fragments don’t need to interact directly, reducing the risk of complex dependencies and bugs.
Consistent Data: A single source of truth ensures data integrity and synchronization across multiple components.
Modern Architecture: Shared ViewModels align perfectly with MVVM (Model-View-ViewModel) architecture, a best practice for building scalable Android apps.
Step-by-Step Implementation of Shared ViewModels
Setting Up Dependencies
Add the core libraries you’ll need (use the latest stable versions from AndroidX/Hilt):
Kotlin
// app/build.gradle.ktsdependencies {// Fragments & Activity KTXimplementation("androidx.fragment:fragment-ktx:<ver>")implementation("androidx.activity:activity-ktx:<ver>")// Lifecycle / ViewModel / coroutines supportimplementation("androidx.lifecycle:lifecycle-viewmodel-ktx:<ver>")implementation("androidx.lifecycle:lifecycle-runtime-ktx:<ver>")// Only if you still use LiveData:implementation("androidx.lifecycle:lifecycle-livedata-ktx:<ver>")// Jetpack Navigation (Fragment)implementation("androidx.navigation:navigation-fragment-ktx:<ver>")implementation("androidx.navigation:navigation-ui-ktx:<ver>")// (Optional) Hilt for DI + ViewModelsimplementation("com.google.dagger:hilt-android:<ver>")kapt("com.google.dagger:hilt-android-compiler:<ver>")implementation("androidx.hilt:hilt-navigation-fragment:<ver>")kapt("androidx.hilt:hilt-compiler:<ver>")}
If using Hilt, also apply the plugin in your module’s Gradle file:
Prefer a single source of UI state with immutable data classes and expose it via StateFlow. Keep side effects (like toasts or navigation) separate using a SharedFlow for one-off events.
Kotlin
// Shared ViewModel example (Kotlin)@HiltViewModel// Remove if you’re not using HiltclassProfileSharedViewModel@Injectconstructor(privateval repo: ProfileRepository, // Your data sourceprivateval savedStateHandle: SavedStateHandle// For process death & args) : ViewModel() {dataclassUiState(val user: User? = null,val isLoading: Boolean = false,val error: String? = null )privateval _uiState = MutableStateFlow(UiState())val uiState: StateFlow<UiState> = _uiState// One-off events (navigation, snackbar, etc.)privateval _events = MutableSharedFlow<Event>()val events: SharedFlow<Event> = _eventssealedinterfaceEvent { objectSaved : Event }funload(userId: String) {// Example of persisting inputs using SavedStateHandle savedStateHandle["lastUserId"] = userId viewModelScope.launch { _uiState.update { it.copy(isLoading = true, error = null) }runCatching { repo.fetchUser(userId) } .onSuccess { user -> _uiState.update { it.copy(user = user, isLoading = false) } } .onFailure { e -> _uiState.update { it.copy(isLoading = false, error = e.message) } } } }funupdateName(newName: String) { _uiState.update { state -> state.copy(user = state.user?.copy(name = newName)) } }funsave() {val current = _uiState.value.user ?: return viewModelScope.launch {runCatching { repo.saveUser(current) } .onSuccess { _events.emit(Event.Saved) } .onFailure { e -> _uiState.update { it.copy(error = e.message) } } } }}
SavedStateHandle survives process death when used with Navigation and lets you read nav arguments via savedStateHandle.get<T>("arg") or create StateFlows: savedStateHandle.getStateFlow("key", default).
Scoping Options (Activity vs. Nav Graph)
Activity scope — share across all fragments in the same activity:
Use the coroutine test utilities and a fake repository:
Kotlin
@OptIn(ExperimentalCoroutinesApi::class)classProfileSharedViewModelTest {@get:Ruleval mainDispatcherRule = MainDispatcherRule() // sets Dispatchers.Main to a TestDispatcherprivateval repo = FakeProfileRepository()privatelateinitvar vm: ProfileSharedViewModel@BeforefunsetUp() { vm = ProfileSharedViewModel(repo, SavedStateHandle()) }@Testfun`load populates user and clears loading`() = runTest { vm.load("42")val state = vm.uiState.first { !it.isLoading }assertEquals("42", state.user?.id)assertNull(state.error) }}
Implement MainDispatcherRule by swapping Dispatchers.Main with a StandardTestDispatcher. Keep repositories pure and synchronous in tests, or use runTest with fakes.
When to Choose Each Scope
Use Activity scope when:
Tabs/bottom navigation fragments need the same state.
Data lives for the whole activity session (e.g., cart, auth session).
Use Nav-graph scope when:
Data is local to a flow (onboarding, multi-step form).
You want the ViewModel cleared when the flow finishes (pop).
Best Practices
Expose immutable state (StateFlow, LiveData) and keep mutables private.
Don’t hold views/context inside ViewModels. Inject repositories/use cases instead.
Use viewLifecycleOwner when observing in fragments (not this), to avoid leaks.
Keep UI state small & serializable if you rely on SavedStateHandle.
Model errors in state and display them; don’t throw them up to the UI.
Avoid shared ViewModels across activities; share via repository or a data layer.
Prefer StateFlow for new code; LiveData is still fine if your app already uses it.
Common Pitfalls (and Fixes)
State replays on rotation (toast fires again): Use SharedFlow/Channel for events, not StateFlow/LiveData.
ViewModel not shared between fragments: Ensure both fragments use the same scope (activityViewModels() or the samenavGraphId).
ViewModel survives too long: You probably used activity scope where a nav-graph scope made more sense.
Collectors keep running off-screen: Wrap collect in repeatOnLifecycle(Lifecycle.State.STARTED).
Shared ViewModels let fragments share state safely without talking to each other directly. Scope them to the activity for app-wide state or to a navigation graph for flow-scoped state. Expose state with StateFlow, drive UI with lifecycle-aware collectors, use SavedStateHandle for resilience, and keep one-off events separate. Follow these patterns and you’ll get predictable, testable, and decoupled UI flows.
State management is one of the most critical aspects of building dynamic and interactive Android applications. With Jetpack Compose, Android’s modern UI toolkit, managing state becomes more intuitive, but it also introduces new paradigms that developers need to understand. In this blog, we’ll explore state management in Jetpack Compose in detail. We’ll break down essential...
In today’s digital world, security and trust are essential, especially when it comes to sensitive information exchanged over the internet. Two foundational technologies that play a critical role in ensuring online security are digital signatures and SSL certificates. If you’re an Android user or developer, understanding these concepts is crucial for protecting your data and securing communications.
This blog will explain what a digital signature is, how SSL certificates work on Android devices, and their importance.
What Is a Digital Signature?
A digital signature is a kind of electronic fingerprint — a unique code attached to digital documents or messages that proves their authenticity and integrity. Think of it like a handwritten signature but much more secure because it’s based on cryptography.
Why Are Digital Signatures Important?
Authentication: Verifies the sender’s identity.
Integrity: Ensures the message or document has not been altered after signing.
Non-repudiation: The sender cannot deny having sent the message.
Digital signatures use a pair of keys: a private key (known only to the signer) and a public key (shared with others). When you sign a document, your device uses your private key to create a unique signature. Others can use your public key to verify that signature’s authenticity.
How Digital Signatures Work
Let’s look at a simplified workflow using cryptographic functions in Android’s Java/Kotlin environment to understand the digital signature process.
Kotlin
// Generating a digital signature in Android using Javaimport java.security.KeyPair;import java.security.KeyPairGenerator;import java.security.PrivateKey;import java.security.PublicKey;import java.security.Signature;publicclassDigitalSignatureExample {public static void main(String[] args) throws Exception {// Step 1: Generate key pair (public and private keys) KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA"); keyGen.initialize(2048); KeyPair pair = keyGen.generateKeyPair(); PrivateKey privateKey = pair.getPrivate(); PublicKey publicKey = pair.getPublic();// Step 2: Sign data String data = "This is a message to sign"; Signature signature = Signature.getInstance("SHA256withRSA"); signature.initSign(privateKey); signature.update(data.getBytes()); byte[] digitalSignature = signature.sign();// Step 3: Verify signature Signature verifier = Signature.getInstance("SHA256withRSA"); verifier.initVerify(publicKey); verifier.update(data.getBytes()); boolean isVerified = verifier.verify(digitalSignature); System.out.println("Signature Verified: " + isVerified); }}
Here,
Step 1: We create a key pair using RSA, a popular cryptographic algorithm.
Step 2: Using the private key, we “sign” the data. The process hashes the data and encrypts it with the private key to create the digital signature.
Step 3: Anyone with the matching public key can verify the signature. They hash the original data and decrypt the signature to confirm both match, ensuring the data is authentic and untampered.
What Are SSL Certificates?
An SSL (Secure Sockets Layer) certificate is a digital certificate that authenticates a website’s identity and enables an encrypted connection. When you visit a website with HTTPS, the SSL certificate is what makes the communication between your device (like an Android phone) and the website secure.
Key Features of SSL Certificates
Encryption: They encrypt data sent between your browser and the web server.
Authentication: They confirm the website’s identity using a digital signature issued by a trusted Certificate Authority (CA).
Data Integrity: They ensure data is not altered during transmission.
How SSL Certificates Work on Android Devices
When your Android device connects to an HTTPS website, a process called the SSL/TLS handshake happens. This is a behind-the-scenes conversation between your device and the web server to establish a secure encrypted connection.
The SSL/TLS Handshake Steps Simplified
1. Client Hello: Your Android device sends a request to the server saying it wants to connect securely, including which encryption methods it supports.
2. Server Hello & Certificate: The server responds with its SSL certificate, which contains its public key and the digital signature from a CA to prove authenticity.
3. Verification: Your Android device verifies the certificate by checking:
Is the certificate issued by a trusted CA (Android maintains a list of trusted root certificates)?
Is the certificate valid and not expired or revoked?
Does the domain match the certificate?
4. Session Key Creation: Once verified, your device and the server create a shared secret key for encrypting data during the session.
5. Secure Communication: All data transferred is encrypted with this session key, keeping your information safe from eavesdroppers.
Why Are Digital Signatures Integral to SSL Certificates?
The digital signature within an SSL certificate is created by a trusted Certificate Authority (CA). This signature vouches for the authenticity of the certificate, confirming the server’s identity. Without this digital signature, an SSL certificate wouldn’t be trustworthy, and your Android device couldn’t be sure it’s communicating with the intended server.
Why You Should Care About Digital Signatures & SSL on Android
Digital signatures are essential for verifying identity and data integrity.
SSL certificates use digital signatures to secure websites.
Android devices use SSL certificates to ensure safe browsing and protect user data.
Developers should understand how to implement and verify digital signatures to build secure Android apps.
By grasping these concepts, you empower yourself to better protect your digital life, whether you’re surfing the web or developing mobile apps.
With Android’s continuous evolution, power management has become increasingly fine-tuned. Starting from Android 9 (API level 28), Android introduced App Standby Buckets, a dynamic classification system that governs how apps can access system resources based on their usage patterns.
These buckets are essential for developers who rely on background jobs, alarms, or network access to power their app’s core functionality.
In this post, we’ll explore what these buckets are, how they limit your app’s capabilities, and how you can optimize your app to function efficiently within these boundaries.
What Are App Standby Buckets?
App Standby Buckets categorize apps based on how frequently they are used. Android uses a combination of machine learning and user behavior analysis to dynamically assign apps to a bucket.
The buckets help Android prioritize system and battery resources without degrading the user experience.
Here are the five main buckets:
Active — App is currently in use or was used very recently.
Working Set — App used often, possibly running in the background.
Frequent — App used regularly but not daily.
Rare — App used infrequently.
Restricted — App is misbehaving or user has manually restricted it.
Resource Limits by Standby Bucket
Each bucket determines how much access an app has to jobs, alarms, and network activity. Below is a breakdown of the execution time windows for each resource type:
Active
Regular Jobs: Up to 20 minutes in a rolling 60-minute period
Expedited Jobs: Up to 30 minutes in a rolling 24-hour period
Alarms: No execution limits
Network Access: Unrestricted
Note (Android 16+): Prior to Android 16, apps in the Active bucket had no job execution limit.
Working Set
Regular Jobs: Up to 10 minutes in a rolling 4-hour period
Expedited Jobs: Up to 15 minutes in a rolling 24-hour period
Alarms: Limited to 10 per hour
Network Access: Unrestricted
Frequent
Regular Jobs: Up to 10 minutes in a rolling 12-hour period
Expedited Jobs: Up to 10 minutes in a rolling 24-hour period
Alarms: Limited to 2 per hour
Network Access: Unrestricted
Rare
Regular Jobs: Up to 10 minutes in a rolling 24-hour period
Expedited Jobs: Up to 10 minutes in a rolling 24-hour period
Alarms: Limited to 1 per hour
Network Access: Disabled
Restricted
Regular Jobs: Once per day for up to 10 minutes
Expedited Jobs: Up to 5 minutes in a rolling 24-hour period
Alarms: One per day (exact or inexact)
Network Access: Disabled
Regular vs. Expedited Jobs
Android distinguishes between two types of scheduled jobs:
Regular Jobs: Standard background tasks scheduled via JobScheduler or WorkManager.
Expedited Jobs: Urgent, high-priority jobs using setExpedited(true) or expedited workers in WorkManager.
Expedited jobs have separate quotas from regular jobs. Once those quotas are exhausted, they may still run under the regular job limits.
Best Practice: Use expedited jobs only for urgent tasks. For everything else, rely on regular job scheduling.
Alarm Limits
Starting with Android 12, alarm limits have tightened:
Apps in Working Set or below are subject to hourly or daily caps.
Apps in the Restricted bucket can schedule only one alarm per day (exact or inexact).
If your app depends on alarms, consider alternatives like JobScheduler or WorkManager, especially for non-critical tasks.
Network Access Limitations
Apps in the Rare and Restricted buckets cannot access the network unless they are running in the foreground.
This has big implications for features like:
Background syncing
Data uploads
Real-time updates
Make sure to test network-reliant tasks across all bucket conditions.
Android 13+ Update: FCM Quota Change
As of Android 13, the number of high-priority Firebase Cloud Messaging (FCM) messages an app can receive is no longer tied to the standby bucket.
This change benefits apps that rely on push messages (like messaging or ride-sharing apps), ensuring more consistent delivery.
Developer Tips for Bucket Optimization
Track App Usage Use UsageStatsManager to monitor your app’s current bucket status.
Leverage WorkManager It automatically handles job fallback between expedited and regular quotas.
Respect Background Limits Overusing background resources can land your app in the Restricted bucket.
Batch and Defer Tasks Reduce battery drain and stay in higher buckets longer by batching non-critical jobs.
Test Across Buckets Simulate different standby buckets with this ADB command:
App Standby Buckets are a key piece of Android’s power management strategy. By tailoring your background behavior to each bucket’s constraints, you not only improve performance and battery life but also ensure a smoother user experience.
Understanding how these limits work — and respecting them — helps you build apps that are efficient, resilient, and Play Store compliant.
FAQs
Q: Can I manually move my app to a different bucket? A: No. The system dynamically assigns apps based on usage. You can only simulate bucket placement during testing.
Q: Do background restrictions help battery life? A: Yes, but they can also restrict your app’s background capabilities. Design wisely.
Q: How do I test alarms or jobs under low buckets like Rare or Restricted? A: Use ADB to simulate conditions, monitor behavior, and fine-tune fallback strategies.
Encryption is powerful, but if you don’t manage keys securely or follow best practices, your data might still be at risk. Here’s what you should know when working with encryption in Kotlin, especially for Android apps.
Why Is Key Management So Important?
Think of encryption keys like the keys to your house. If someone steals your key, they can unlock everything — even if your door is super strong.
In encryption:
The secret key unlocks your encrypted data.
If keys are exposed or hard-coded in your app, attackers can decrypt your info easily.
So, secure key management means generating, storing, and using encryption keys safely.
Best Practices for Managing Encryption Keys in Kotlin/Android
1. Use Android’s Keystore System
Android provides a secure container called the Keystore, where you can safely generate and store cryptographic keys. Keys stored here are hardware-backed and cannot be extracted, making it extremely hard for attackers to steal them.
Here’s a quick way to generate and use a key in Android Keystore:
generateKeyInKeystore creates a new AES key stored securely inside the Android Keystore.
You specify the key’s purpose and encryption parameters.
getKeyFromKeystore fetches the stored key when you need it for encryption or decryption.
2. Never Hardcode Keys in Your App
Avoid placing keys as constants in your source code. Hardcoded keys are easily extracted through reverse engineering. Always generate keys at runtime or securely fetch them from the Keystore.
3. Use a Secure Initialization Vector (IV)
IVs should be random and unique for every encryption. Never reuse IVs with the same key. The IV is usually sent alongside the encrypted data, often as a prefix, because it’s needed for decryption.
Here’s how to generate a secure IV in Kotlin:
Kotlin
import java.security.SecureRandomfungenerateRandomIV(): ByteArray {val iv = ByteArray(16)SecureRandom().nextBytes(iv)return iv}
4. Authenticate Your Data
Encryption protects confidentiality, but attackers can still tamper with ciphertext if you don’t check data integrity. Use authenticated encryption modes like AES-GCM that combine encryption and integrity checks.
If you’re getting started with Android Automotive OS (AAOS), you’ll quickly run into something called Car Service in AOSP. It’s one of those essential components that makes Android work inside a car — not on your phone, but actually on the car’s infotainment system.
In this guide, we’ll break down Car Service in AOSP step-by-step, explain how it works, what it does, and walk through code examples so you can understand and start building with confidence.
What Is Car Service in AOSP?
In the world of Android Open Source Project (AOSP), Car Service is a system service designed specifically for the automotive version of Android. It’s what bridges the gap between car hardware (like sensors, HVAC, speed, fuel level) and Android apps or services that need that data.
Think of it as the middleman that manages and exposes car hardware features to Android applications safely and consistently.
Why Is Car Service Important in Android Automotive?
Access to Vehicle Data: It lets apps access data like speed, gear, HVAC status, fuel level, etc.
Security: Only authorized components can access sensitive vehicle data.
Abstraction: It hides the car’s hardware complexity behind clean Android APIs.
Interoperability: Developers can build apps that work across different car manufacturers.
Core Components of Car Service in AOSP
Let’s simplify the architecture. Here’s how the system flows:
When we talk about password security, the conversation usually goes straight to hashing algorithms — things like SHA-256, bcrypt, or Argon2. But there are two lesser-known players that can make or break your defenses: salts and pepper.
Think of them as seasoning for your password hashes — not for flavor, but for security.
Why Password Hashing Alone Isn’t Enough
Hashing is like putting your password through a one-way blender — you can’t (easily) get the original password back. But if attackers get your hashed password database, they can still use rainbow tables or brute-force attacks to figure out the original passwords.
That’s where salts and pepper come in. They make every hash unique and harder to crack — even if someone has your database.
Salts vs. Pepper: What’s the Difference?
Salts
A random value added to each password before hashing.
Stored alongside the hash in the database.
Makes it impossible for attackers to use precomputed hash tables.
Every user gets a unique salt.
Pepper
A secret value added to the password before hashing.
Not stored in the database — kept separately (e.g., in environment variables or secure key vaults).
Even if the attacker steals your database, they can’t crack hashes without the pepper.
In short:
Salt is public but unique per password
Pepper is secret and the same for all passwords (or sometimes per user, but still hidden).
Kotlin Example: Salting and Peppering Passwords
Let’s see this in Kotlin. We’ll use the MessageDigest API for hashing (for simplicity), though in real production you should use stronger libraries like BCrypt or Argon2.
Kotlin
import java.security.MessageDigestimport java.security.SecureRandomimport java.util.Base64objectPasswordHasher {// Generate a random salt for each passwordfungenerateSalt(length: Int = 16): String {val random = SecureRandom()val salt = ByteArray(length) random.nextBytes(salt)return Base64.getEncoder().encodeToString(salt) }// Your secret pepper - should be stored securely (e.g., env variable)privateconstval PEPPER = "SuperSecretPepperValue123!"// Hash with salt + pepperfunhashPassword(password: String, salt: String): String {val saltedPepperedPassword = password + salt + PEPPERval digest = MessageDigest.getInstance("SHA-256")val hashBytes = digest.digest(saltedPepperedPassword.toByteArray(Charsets.UTF_8))return Base64.getEncoder().encodeToString(hashBytes) }// Verify passwordfunverifyPassword(inputPassword: String, storedSalt: String, storedHash: String): Boolean {val inputHash = hashPassword(inputPassword, storedSalt)return inputHash == storedHash }}funmain() {val password = "MySecurePassword!"// 1. Generate saltval salt = PasswordHasher.generateSalt()// 2. Hash password with salt + pepperval hashedPassword = PasswordHasher.hashPassword(password, salt)println("Salt: $salt")println("Hash: $hashedPassword")// 3. Verifyval isMatch = PasswordHasher.verifyPassword("MySecurePassword!", salt, hashedPassword)println("Password match: $isMatch")}
Salt Generation
We create a random salt using SecureRandom.
This ensures no two hashes are the same, even if passwords are identical.
Pepper Usage
The pepper is stored outside the database, often in environment variables or secure vaults.
It’s the “secret ingredient” that attackers won’t see if they only have the database.
Hashing
We combine the password + salt + pepper before hashing with SHA-256.
In production, replace SHA-256 with bcrypt or Argon2 for better resistance against brute force.
Verification
When a user logs in, we retrieve the stored salt, hash the provided password with the same pepper, and compare the results.
Best Practices for Salts and Pepper
Always use a unique salt for each password. Never reuse salts.
Store salts with the hash in the database.
Keep pepper secret — in an environment variable, key management system, or hardware security module.
Use a slow, memory-hard hashing algorithm like bcrypt, scrypt, or Argon2.
Rotate peppers periodically for maximum security.
Never hard-code pepper in your source code for production.
Why Salts and Pepper Matters
Attackers thrive on shortcuts. Salts remove the shortcut of using rainbow tables. Pepper blocks attackers even if they have your entire password database. Together, they make your password security significantly harder to break.
Conclusion
When it comes to security, the little details — like salts and pepper — make a big difference. Hashing without them is like locking your front door but leaving the window wide open. So next time you store a password, make sure it’s seasoned with both.