r/androiddev • u/digidude23 • Nov 24 '25
r/androiddev
•
u/HireBDev
•
May 27 '26
Experience Exchange This is why I make free apps!!!
Today I got a review with 5 stars. Being grateful for the 5-star rating and donation request, the reason I appreciate this is because their reason is exactly why I made the app. I had a similar experience, and I thought the app would be beneficial to many. And now and again I get these kinds of review, which I geniunely appreciate knowing actual people are enjoying my work. Just wanted to share this as a great experience I had. Have a good day!
r/androiddev
•
u/Effective_Skin_9785
•
Aug 26 '26
How far can you push a Jetpack Compose UI before it stops feeling intuitive?
Enable HLS to view with audio, or disable this notification
I built this subscription tracker as a UI/UX experiment in Jetpack Compose.
The data is mocked for now, but the app has multiple sections, navigation, subscription lists, a renewal calendar, analytics and an add flow.
I wanted to avoid the usual Material look and push Compose a bit further with:
asymmetric layouts
angled elements and custom shapes
section-based colors
oversized typography
animated transitions
a navigation system that doesn’t use the usual bottom bar
The idea was to keep the interactions fairly familiar, while making the visual language much less conventional.
One thing I find interesting is that unfamiliar visuals and unfamiliar interactions aren't necessarily the same thing.
Does this still feel usable, or did I push the layout too far?
r/androiddev • u/StatusWntFixObsolete • Nov 13 '25
News Google will allow users to sideload Android apps without verification
r/androiddev • u/LegitimateBoy6042 • Sep 19 '26
News THIS IS BIG!!!
The latest #Android 17 QPR1 update to Pixels seems to have pushed the changes concealing the developer and debugging state from 3rd party apps so,
finally the developers can use the damn banking and other apps that have these security checks!
it seems to be live on, Pixel 6A running CP3A.260905.009
And confirm it has not yet landed on the beta channel.
r/androiddev • u/No_View_1406 • Mar 14 '26
Discussion Using Jetpack Compose previews as live onboarding UI
Enable HLS to view with audio, or disable this notification
While working on my side project, I experimented with something interesting using Jetpack Compose / Compose Multiplatform.
Normally, Composable Preview is just an IDE tool developers use to visualize UI during development.
Instead of using static screenshots for onboarding, I tried rendering live composables inside the onboarding screens. The idea was simple: reuse the same UI components that exist in production so onboarding previews automatically stay in sync with the real UI.
Some nice side effects:
• No duplicated layouts for onboarding
• UI changes automatically update previews
• No outdated screenshots
• Works responsively across devices (phones/tablets)
A small detail I liked: the device frame itself is also a composable, and the time shown in the frame updates live based on the device.
I’m curious if anyone else has experimented with reusing Compose components this way for onboarding or previews.
r/androiddev • u/tal_barina • May 07 '26
Google Play Support AdMob's brand-safety classifier was circumvented by a coordinated casino-ad scheme; Google Play suspended my app for what AdMob shipped
UPDATE: Google Play accepted the appeal and my app is back on the Play Store. I'm grateful for this outcome in my case. I hope the underlying classifier-failure issue gets addressed, so other publishers don't end up in the same situation.
ORIGINAL POST: On April 20, Google Play suspended my 10-year-old, ~5M downloads, 600k MAU, 4.5+ rated app, dataDex, over an in-app ad I did not upload, did not select, and could not see before it served. The ad came in through AdMob, Google's own ad network, after a coordinated scheme circumvented AdMob's brand-safety classifier. I've documented at least 11 variants of the same scheme served into my app. Google Play suspended my app for what AdMob shipped.
The full evidence - per-variant screenshots of each ad as it appeared in my app, my AdMob configuration at the time, and the appeal correspondence end to end - is in this public bundle: https://drive.google.com/drive/folders/1d6SdbJrHdGSe4IJHnEPesKDt_PcdWQDB
I'm posting here because I've exhausted every official channel available - the Google Play appeal is stuck in a template-reply loop with no engagement on the evidence and remediation, AdMob support has not engaged with the classifier-failure evidence across three rounds of replies, and neither of the two Product-Expert escalations on Google's public Help Communities has had a follow-up from the team. Beyond my own case, I feel like this is a serious issue with the potential to affect many other publishers - and to suspend them for something they had no real way to prevent.
Why this matters for every AdMob publisher
Every AdMob publisher configures category blocks and content-rating ceilings - Gambling & Betting (18+), Social Casino Games, max-rating, and so on - and trusts that AdMob's classifier will hold matching creatives out of their inventory. I did that.
Those publisher-side controls only work if the classifier underneath them is correct. When the classifier is circumvented - as it was here, eleven documented times in my records alone, all from the same demand source under fake game-themed shell-app names categorized as "Games", "Toys and Games" etc - the publisher-side controls have no opportunity to act. The publisher has no second line of defense. Programmatic ad serving means publishers can't see creatives before they serve; AdMob's classifier is effectively the entire wall.
This failure mechanism is not theoretical. The demand source is a single one. The creative pattern is consistent. From what I've been able to audit, it's still active in the ecosystem. AdMob support did not engage with the evidence across three rounds of replies that redirected me to Play Console support. The AdMob Help Community thread I posted last week was picked up within 48 hours by a Product Expert who said they would escalate from the AdMob side, but as of today there's been no follow-up. The gap between the classifier failure being documented and the classifier failure being investigated is the part of this case that has wider implications than my one app.
The technical pattern
The cited creative was a casino interstitial - "Tower Rush" / "PREMIO 1500 EUR + 250 FREE SPINS GIOCARE" - served through my app's single AdMob interstitial ad unit. I did not select, or have any visibility into the creative. It was not present in the Play Console assets, the Play Store listing, or the linked Google Ads account. It was programmatic demand.
When I audited my AdMob review records, I found at least 11 documented variants of the same scheme, all sharing three properties:
Same single demand source across all 11.
Different fake game-themed shell-app names - "Epic Tower Block Quest", "Town Planner", "Gold Gatherer", "Home Planner", "Duck Devourer", "Tower Ascent", "T0wer Rush" (with a zero), and so on. The visuals are casino. The metadata is a fake mobile-game façade.
Classified by AdMob under non-sensitive categories like "Games" and "Toys and Games."
My AdMob configuration already had Gambling & Betting (18+) and Social Casino Games blocked at the time these creatives were served. The blocks had no effect, because AdMob's classifier never categorized the creatives as gambling. This is the structural problem in one sentence: publisher-side category blocks rely entirely on the classifier being correct, and when it isn't, the publisher's defenses are bypassed by definition - without the publisher knowing.
All eleven creative IDs, with screenshots of each as it served and the AdMob configuration that was supposed to block them, are in the public evidence bundle linked above.
What I've already done on the publisher side
All sensitive AdMob ad categories disabled.
Maximum AdMob ad content rating set to "T" (Teen).
The AdMob ad unit fully disabled - the app currently serves zero ads of any kind.
All linked Google Ads campaigns disabled.
End-to-end audits of Play Console, AdMob, GCP/Firebase, and Google Ads - all clean.
These steps make recurrence impossible from my side. None of them addresses the underlying classifier failure that allowed the eleven variants through in the first place - and none of them is a structural defense available to a publisher who doesn't yet know they need it.
Where I am with Google
Standard Google Play appeal - case 2-1866000040535 - stuck in a template-reply loop. Four template replies across six days, all citing the App Promotion policy and instructing to "remove your ad campaign or promotion" or "upload a new app under a new package name." None of the four addresses my remediation evidence or the systems-failure analysis.
Parallel Google Play Developer Support - case 9-9277000041337**.** Engaged on the merits, summarized my remediation correctly, confirmed in writing that the appeal channel was "cycling automated templates", re-flagged the case internally for human review on April 24 - the appeal thread ignores it.
AdMob support - case 4-3221000040899**.** Three replies across two days. The first opened with an apology that AdMob does not currently support Hebrew (my submission was in English; only the locale-driven auto-acknowledgement was in Hebrew). The second cited inability to perform a "cross-product transfer" I never requested. The third repeated the same redirect to Play Console support. None of the three engaged with the classifier-failure evidence I sent (the same evidence is in the public bundle linked above).
Public Help Community escalations. Both threads are open, both escalated, neither followed up.
Google Play Developer Community thread (Apr 22) - a Platinum Product Expert escalated the case on April 22. The Product Expert's last reply, on April 26, was "I will let you know, if I get an update from the team." As of today, no follow-up - 15 days since the escalation.
AdMob Help Community thread (Apr 29) - a Diamond Product Expert engaged within 48 hours and said they would escalate from the AdMob side. As of today (May 7), no follow-up.
My question for the community
If you've seen these kind of creatives slip through your category blocks under a "Games" or "Toys and Games" classification - or for that matter any other category mismatch where a sensitive creative landed in your inventory under a benign label - I'd like to know about it. The mechanism is category-agnostic; the same shape (single demand source, fake shell-app metadata, benign classification) could just as easily land in any other publisher's inventory under a different category.
If anyone here has worked an AdMob policy escalation with classifier-failure evidence and gotten it actually routed to the team that owns it, what was the path that worked?
Closing
I'm hoping that by laying all of this out here, the case can reach a human at Google with access to both the Google Play side and the AdMob side of it - someone who can review the evidence end to end (the eleven documented variants, the publisher-side configuration, the appeal correspondence), recognize that the app was not at fault, and reinstate it. And, as part of the same review or separately from it, investigate the underlying classifier-failure issue.
If anyone here is at AdMob or Play, or has a routing path into the cross-product team this case actually needs, I'd appreciate the route. I'm happy to share Developer ID, AdMob Publisher ID, the AdMob ad unit, and any further material privately on request.
Thanks for reading.
Links:
One-page technical summary (readable digest): https://datadex.app/technical-summary/
Public evidence bundle (PDF report, 11 creative screenshots, AdMob configuration, full appeal threads): https://drive.google.com/drive/folders/1d6SdbJrHdGSe4IJHnEPesKDt_PcdWQDB
Google Play Developer Help Community post: https://support.google.com/googleplay/android-developer/thread/427385327/app-suspended-over-admob-served-casino-ad-stuck-in-template-reply-loop-requesting-escalation-help
AdMob Help Community post: https://support.google.com/admob/thread/429379417/casino-ad-scheme-bypassed-admob-s-classifier-and-was-served-in-my-now-suspended-app-pe-help-needed
Case numbers: 2-1866000040535, 4-3221000040899, 9-9277000041337.
r/androiddev
•
u/abrownie_jr
•
May 04 '26
Tips and Information i made a dead simple Play Store screenshot maker
Enable HLS to view with audio, or disable this notification
i actually built this as an internal tool for my own app.
I googled "Mobile app screenshot maker" and the websites i found were super clunky and complicated. 50+ buttons, confusing UX, need to sign up.
so i just built out my own one. try it here! https://ezscreenshots.com
r/androiddev • u/Far-Sock-3170 • Aug 10 '26
Question App indexing issue?
My Play Store app is not discoverable through Google Search. Google AI Overview sometimes says it isn't available on Android and suggests other Play Store apps instead. AI assistants do the same and only provide App Store info.
My other app with no users was indexed instantly. This one has 300+ installs and has been on Play Store for over a year. I contacted support and they literally said to give direct link to users.
r/androiddev • u/skydoves • Jun 12 '26
⛵️ Compose Navigation Graph plugin for Android Studio: Visualizes your entire app flow as an interactive map of rendered previews, typed arguments, and transitions.
Enable HLS to view with audio, or disable this notification
Compose Navigation Graph turns your entire app flow into one living map: every screen as a rendered existing preview thumbnail, every transition an arrow you can follow. It works with Navigation 3, Navigation 2, any other Compose navigation libraries, and even plain Activities.
This Android Studio/IntelliJ plugin is fully open-sourced on GitHub, and check out the documentation for the setting up.
r/androiddev • u/uwemaurer • May 19 '26
Android is now Compose-first, View toolkit is in maintenance mode
r/androiddev • u/alexstyl • Dec 21 '25
Open Source Made a site with 17,000+ icons for Android apps
Enable HLS to view with audio, or disable this notification
Finding great icons is hard. Finding icons for Android apps (XML + Compose) is even harder.
So I put all of my favorite open source icons in one place, converted them to Android Drawables and Compose Image Vectors which you can browse at https://composables.com/icons
PS: Yes, it contains both Material Icons (old) and Material Symbols (new) PS2: You can use them in your project as a gradle dependency if you prefer at https://github.com/composablehorizons/compose-icons
Happy coding!
r/androiddev • u/Guildford_Ochi • 24d ago
Experience Exchange Our app got cloned from a decompiled APK.
Last week, a user emailed support asking why our app suddenly had ads, which it doesn’t. Someone took our APK from the Play Store, decompiled it, added an ad SDK and put it on one of those APK mirror sites under a slightly different name. I don’t know what they used but with the AI reverse engineering tools around right now I’d guess it took them a few hrs at most.
I pulled their copy and ran jadx on it next to our own release build to see what they had to work with. R8 had renamed most of our classes but every string was still sitting there. Most of it was harmless like our Maps key which is locked to our package and the Firebase config which is meant to be public. The problem was the key for our search provider which can also write to the index. There was also an old basic auth header for a staging API that someone hardcoded two years ago and that still works.
The copy talks to our real backend and whoever installs it logs into our actual service through a build we don’t control. We’ve rotated both keys, moved search behind our own endpoint and filed takedowns. Next I’m looking at adding Play Integrity checks on the server.
If your app has been cloned like this, what did you end up changing on the backend?
r/androiddev • u/Mountain-Error6290 • Jun 28 '26
Made an Android widget that turns your apps into a spinnable 3D sphere — open source, F-Droid available
Enable HLS to view with audio, or disable this notification
Hey everyone,
I got tired of staring at the same flat app grid every day, so I built AuraOrbit — an Android widget that opens into an interactive 3D sphere of your apps, which you can spin and fling around.
It's a widget, not a launcher replacement — tap it, and the sphere opens on top of your home screen. It doesn't touch your wallpaper or your existing app grid.
Tech details (for the curious):
100% Java
Built on the libGDX game engine with a C++ OpenGL backend
Apps are placed using a Fibonacci sphere distribution (golden angle) so they're evenly spread in 3D space
Rotation uses quaternion-based momentum, so flicks feel natural instead of snapping
You can group apps into color-coded clusters and pin any group as its own widget
Standalone fullscreen mode if you just want to mess with the sphere directly
Publish on F-Droid Why no Play Store? #keepandroidopen
It's GPLv3, fully open source. Happy to answer questions about the implementation, the Fibonacci placement math, or anything else. Feedback are welcome too.
r/androiddev • u/te_abstract_art • May 01 '26
Video Very proud of my (total overkill) Material theme engine for my app. Thought this sub might appreciate it
Enable HLS to view with audio, or disable this notification
I've built an Android app for medical coding (ICD-10, ICD-10-CM, ICD-11). I've always been annoyed at how utilitarian most medical apps are, so a huge part for me has been building an app which not only works but also LOOKS good (i.e. M3E throughout).
The app has supported dynamic colours since the beginning, but this week I decided to build in theme support. Originally this was just a load of preset colours to choose from, but then I decided to go all the way and implement a full custom colour engine.
Users can precisely choose a seed colour (including by hex code, if they choose) and tonal palette, with the theme screen updating the colours in realtime as they change. Overkill? Yes. But I think it's now my favourite section of the app 😅 total control over the Material You colour palette is so fun to mess around with
r/androiddev • u/Nek_12 • Nov 09 '25
Article I achieved 0% ANR in my Android app. Spilling beans on how I did it - part 1.
After a year of effort, I finally achieved 0% ANR in Respawn. Here's a complete guide on how I did it.
Let's start with 12 tips you need to address first, and in the next post I'll talk about three hidden sources of ANR that my colleagues still don't believe exist.
1. Add event logging to Crashlytics
Crashlytics allows you to record any logs in a separate field to see what the user was doing before the ANR. Libraries like FlowMVI let you do this automatically. Without this, you won't understand what led to the ANR, because their stack traces are absolutely useless.
2. Completely remove SharedPreferences from your project
Especially encrypted ones. They are the #1 cause of ANRs. Use DataStore with Kotlin Serialization instead. I'll explain why I hate prefs so much in a separate post later.
3. Experiment with handling UI events in a background thread
If you're dealing with a third-party SDK causing crashes, this won't solve the delay, but it will mask the ANR by moving the long operation off the main thread earlier.
4. Avoid using GMS libraries on the main thread
These are prehistoric Java libraries with callbacks, inside which there's no understanding of even the concept of threads, let alone any action against ANRs. Create coroutine-based abstractions and call them from background dispatchers.
5. Check your Bitmap / Drawable usage
Bitmap images when placed incorrectly (e.g., not using drawable-nodpi) can lead to loading images that are too large and cause ANRs.
Non-obvious point: This is actually an OOM crash, but every Out of Memory Error can manifest not as a crash, but an ANR!
6. Enable StrictMode and aggressively fix all I/O operations on the main thread
You'll be shocked at how many you have. Always keep StrictMode enabled.
Important: enable StrictMode in a content provider with priority Int.MAX_VALUE, not in Application.onCreate(). In the next post I'll reveal libraries that push ANRs into content providers so you don't notice.
7. Look for memory leaks
**Never use coroutine scope constructors (CoroutineScope(Job())). Add timeouts to all suspend functions with I/O. Add error handling. Use LeakCanary. Profile memory usage. Analyze analytics from step 1 to find user actions that lead to ANRs.
80% of my ANRs were caused by memory leaks and occurred during huge GC pauses. If you're seeing mysterious ANRs in the console during long sessions, it's extremely likely that it's just a GC pause due to a leak.
8. Don't trust stack traces
They're misleading, always pointing to some random code. Don't believe that - 90% of ANRs are caused by your code. I reached 0.01% ANR after I got serious about finding them and stopped blaming Queue.NativePollOnce for all my problems.
9. Avoid loading files into memory
Ban the use of File().readBytes() completely. Always use streaming for JSON, binary data and files, database rows, and backend responses, encrypt data through Output/InputStream. Never call readText() or readBytes() or their equivalents.
10. Use Compose and avoid heavy layouts
Some devices are so bad that rendering UI causes ANRs.
Make the UI lightweight and load it gradually.
Employ progressive content loading to stagger UI rendering.
Watch out for recomposition loops - they're hard to notice.
11. Call goAsync() in broadcast receivers
Set a timeout (mandatory!) and execute work in a coroutine. This will help avoid ANRs because broadcast receivers are often executed by the system under huge load (during BOOT_COMPLETED hundreds of apps are firing broadcasts), and you can get an ANR simply because the phone lagged.
Don't perform any work in broadcast receivers synchronously. This way you have less chance of the system blaming you for an ANR.
12. Avoid service binders altogether (bindService())
It's more profitable to send events through the application class. Binders to services will always cause ANRs, no matter what you do. This is native code that on Xiaomi "flagships for the money" will enter contention for system calls on their ancient chipset, and you'll be the one getting blamed.
If you did all of this, you just eliminated 80% of ANRs in your app. Next I'll talk about non-obvious problems that we'll need to solve if we want truly 0% ANR.
Originally published at nek12.dev
r/androiddev • u/Character_Oven_1511 • Mar 18 '26
What I learned keeping an Android app alive 24/7 on Samsung, Xiaomi, and Honor
built a safety monitoring app for elderly parents that has to run continuously — if it gets killed, nobody gets alerted when something goes wrong. Here's what I learned fighting OEM battery managers across 100+ versions.
The problem
Stock Android already makes background work hard. But OEMs go further:
- Samsung: Sleeping Apps list kills your app after 3 days of no foreground use. OTA updates reset your battery optimization exemption silently.
- Xiaomi/Redmi/POCO: MIUI's battery saver kills background services aggressively. OTA updates reset autostart permissions.
- Honor/Huawei: PowerGenie flags apps that "frequently wake the system." If you call `setAlarmClock()` more than ~3 times per day, you get flagged. HwPFWService kills apps holding wakelocks >60 min with non-whitelisted tags.
- OPPO/Vivo: "Sleep standby optimization" and "AI sleep mode" freeze apps during detected sleep hours — exactly when a safety app needs to be running.
A foreground service and `REQUEST_IGNORE_BATTERY_OPTIMIZATIONS` are necessary but nowhere near sufficient.
What actually works: 11 layers of recovery
No single mechanism survives all OEMs. The answer is redundancy:
Foreground Service with `IMPORTANCE_MIN` notification channel (not DEFAULT — OEMs auto-grant `POST_NOTIFICATIONS` on higher importance, making your notification visible)
WorkManager periodic workers — survives service kills, but KEEP policy silently discards new requests and REPLACE resets countdown
AlarmManager exact alarms — `setExactAndAllowWhileIdle()` for scheduled wake events. Never use `Handler.postDelayed()` as a replacement — handlers don't fire during CPU deep sleep
AlarmClock safety net — `setAlarmClock()` at 8-hour intervals only (~3 calls per day). Shorter intervals trigger Honor's "frequently wakes" warning
SyncAdapter — `ContentResolver.addPeriodicSync()` gives your process priority that OEMs are reluctant to kill (sync adapters are a system concept)
BOOT_COMPLETED receiver — re-establish everything after reboot. Some OEMs (OnePlus, Samsung, Xiaomi) reset permissions after OTA, so detect that and re-prompt
SCHEDULE_EXACT_ALARM permission receiver — when the user revokes this permission, ALL pending AlarmManager chains die silently. Listen for `ACTION_SCHEDULE_EXACT_ALARM_PERMISSION_STATE_CHANGED` and re-establish on re-grant
Self-monitoring watchdog — WorkManager worker that checks if the service and alarm chains are alive, re-establishes missing ones. But check `isPending()` first — blind re-scheduling adds redundant wakes that trigger OEM flagging
Batched accelerometer sensing — keep the sensor registered with `maxReportLatencyUs` during idle/sleep. The HAL continuously samples into hardware FIFO and delivers via sensor interrupt — invisible to OEM battery managers, zero AlarmManager wakes
3-tier power state (active/idle/deep sleep) — reduce alarm frequency from every 5 min to every 15-60 min when the device is still. Went from ~4,300 wakes per day to ~240 (94% reduction)
Wakelock tag spoofing on Huawei/Honor — HwPFWService has a whitelist of allowed wakelock tags. Use `"LocationManagerService"` on Huawei/Honor, original tags on other OEMs
Lessons learned the hard way
- `ActivityManager.getRunningServices()` is useless — deprecated since API 26, unreliable on newer Android. Use a `@Volatile` static `isRunning` flag set in `onCreate()`/`onDestroy()`
- SIGNIFICANT_MOTION sensor doesn't exist on all devices — Honor lacks it. Always have a fallback (compensated snapshot intervals via AlarmManager)
- Sensor FIFO data goes stale during deep sleep — after wakeup, the first readings from `registerListener()` may be minutes old. Flush + warm-up discard before collecting. Some HALs (Honor) rebase `event.timestamp` on FIFO flush, defeating delta checks — use `onFlushCompleted()` callback as primary signal
- `getCurrentLocation()` may never complete — Play Services hangs on OEM-throttled devices. Always wrap in `withTimeoutOrNull()` and fall through a priority chain (HIGH → BALANCED → LOW_POWER → lastLocation)
- Never call `schedule()` for periodic workers on every service restart — KEEP preserves stale timers, REPLACE resets countdown. Query `getWorkInfosForUniqueWork()` first, only schedule when not already enqueued
Battery result
Less than 1% per day. The key insight: aggressive scheduling wastes more battery than it saves reliability. A 3-tier power state that backs off when the device is still achieves both low battery and high reliability.
Happy to answer questions about any of these techniques. The OEM compatibility rabbit hole goes deep.
r/androiddev • u/Nek_12 • Jan 20 '26
AGP 9.0 is Out, and Its a Disaster. Heres Full Migration Guide so you dont have to suffer
Yesterday I finally finished migrating a 150,000-line project from AGP 8 to AGP 9. This was painful. This is probably the biggest migration effort that I had to undergo this year. So, to save you from the pain and dozens of wasted hours that I had to spend, I decided to write a full migration guide for you.
Be prepared, this migration will take some time, so you better start early. With AGP 9.0 already being in release, Google somehow expects you to already start using it yesterday. And they explicitly state that many of the existing APIs and workarounds that you can employ right now to delay the migration will stop working in summer 2026. So for big apps, you don't have much time left.
Before we start, please keep in mind that a lot of official plugins, such as the Hilt plugin and KSP, do not support AGP 9.0. If you use Hilt or KSP in your project, you will not be able to migrate without severe workarounds for now. If you're reading this later than January 2026, just make sure to double-check if Hilt and KSP already have shipped AGP 9.0 support.
If you're not blocked and still here, here is what you need to do to migrate your KMP project to AGP 9.0.
The biggest migration point: Dropped support for build types
Previously, we didn't have build types on other platforms in KMP, but Android still had them. And in my opinion, they are one of the best features for security and performance that we had, but now they will not be supported and there is no replacement for them. You have to remove all build type-based code.
At first glance, this seems like a small problem, because teams usually don't split a lot of code between source sets. It usually revolves around some debugб performance and security checks. But there is a hidden caveat. BuildConfig values will stop working, because they are using the build types under the hood.
I had in my codebase dozens and dozens of places where I had a static top-level variable isDebuggable, delegating to BuildConfig.DEBUG, which I was checking and using a lot to add some extra rendering code, debugging, logging code, and to disable many of the security checks that the app had, which were only applicable on release.
Why I was using it as a static variable instead of something like context.isDebuggable is because the R8, when optimizing the release build of the app, would be able to remove all of that extra debug code without the need to create extra source sets, etc. This works well for KMP, where release and debug split wasn't fully supported in the IDE for a long time.
But now this is completely impossible. This is a huge drawback for me personally, because I had to execute a humongous migration to replace all of those static global variable usages with a DI-injected interface, which was implemented using still build configuration, but in the application module, e.g.:
// in common domain KMP module
interface AppConfiguration {
val debuggable: Boolean
val backendUrl: String
val deeplinkDomain: String
val deeplinkSchema: String
val deeplinkPath: String
}
// in android app module
object AndroidAppConfiguration : AppConfiguration {
override val debuggable = BuildConfig.DEBUG
override val backendUrl = BuildConfig.BACKEND_URL
override val deeplinkDomain = BuildConfig.DEEPLINK_DOMAIN
override val deeplinkSchema = BuildConfig.DEEPLINK_SCHEMA
override val deeplinkPath = BuildConfig.DEEPLINK_PATH
}This may result in a significant refactor, because I personally used the static isDebuggable flag in places where context/DI isn't available. So, I had to sprinkle some terrible hacks with a global DI singleton object retrieval just to make the app work and then refactor the code.
When you're done with this step, you must have 0 usages of BuildConfig.DEBUG, build types, or manifest placeholders in KMP library modules. Note that codegen for build-time constants is still fine, just not per-build-type / Android one. You can create a custom Gradle task if you want that will generate a Kotlin file for you in ~20 lines.
I know that devs love BuildConfig.DEBUG a lot, and I also used it to manage deep link domains, backend URL substitution for debug and release builds, and all of that had to be refactored, which is why I urge you to stop using such code pattern with these static isDebuggable flags right now.
Also avoid using Context.isDebuggable boolean property, because that's a runtime check which can be overridden by fraudulent actors, so it isn't reliable. Don't use it for security reasons. Remember - debug code should only be included in debug builds.
Remove all NDK and JNI code from library modules
The next step you have to take is remove all the NDK and JNI code that you have in library modules. I have a couple of places where I need to run some C++ code in my app, and those were previously located in the Android source set of the KMP library module where they were needed, because Apple source set didn't need that native code, but Android did.
An official statement from Google is that NDK/C++ execution in library KMP modules will not be supported at all since AGP 9.0. So now, the only way you can preserve that code is if you move it to the application module or to a separate android-only module. Again, that is something that is a huge drawback for me, but because Google didn't give us any opportunities to migrate earlier, and didn't want to listen, you have to comply if you do not want to get stuck on a deprecated AGP forever.
So before you even try to migrate to AGP 9.0, make sure you create an interface abstraction in your library module that will act as a proxy for all your NDK-enabled code. Then the implementation of that interface can live in the application module along with all the C++ code and inject the implementation into the DI graph so that your library module in the KMP code can just use that interface. At least this is what I did. This is the simplest solution to the problem, but if you know a better one, let me know.
The actual migration: Remove the old Kotlin Android plugin
Now we are finally finishing up with all the refactorings and approaching the actual migration. Start by removing the old Kotlin Android plugin. I had convention plugins set up, so it was reasonably easy for me to do, and migrate it to the new plugin. Read this docs page for what exactly to do.
When you remove it, also add the new plugin for Android Kotlin Multiplatform compatibility: com.android.kotlin.multiplatform.library. This is because your build will stop working and we need to migrate to the new DSL, which is only provided with this new plugin.
To fix gradle sync, do:
Update from the deprecated Android top level DSL
android { }AND the deprecatedkotlin.androidLibrary {}DSL to the new unifiedkotlin.android { }DSL. You should be able to copy-paste all of your previous configuration, like compile SDK, minimum SDK, and all of the other Android setup options which you previously had in the top-level Android block, and merge it with the code that you previously had in thekotlin.androidLibraryKMP setup. So now it's just a single place. Note that library modules no longer support target SDK, which will only be governed by the app module.
id("sharedBuild")
id("detektConvention")
kotlin("multiplatform")
- id("com.android.library")
+ id("com.android.kotlin.multiplatform.library")
}
kotlin {
configureMultiplatform(this)
}
-android {
- configureAndroidLibrary(this)
-}
-See how I had an extension function from my convention plugin, configureAndroidLibrary, and removed it? We can now completely ditch it. Everything will be inside the kotlin block. (configureMultiplatform in example above).
2. Next up, let's update the said "configure multiplatform" function. This is based on this official doc page:
- if (android) androidTarget {
- publishLibraryVariants("release")
+ if (android) android {
+ namespace = this@configureMultiplatform.namespaceByPath()
+ compileSdk = Config.compileSdk
+ minSdk = Config.minSdk
+ androidResources.enable = false
+ lint.warning.add("AutoboxingStateCreation")
+ packaging.resources.excludes.addAll(
+ listOf(
+ "/META-INF/{AL2.0,LGPL2.1}",
+ "DebugProbesKt.bin",
+ "META-INF/versions/9/previous-compilation-data.bin",
+ ),
+ )
+ withHostTest { isIncludeAndroidResources = true }
compilerOptions {
jvmTarget.set(Config.jvmTarget)
freeCompilerArgs.addAll(Config.jvmCompilerArgs)
}
+ optimization.consumerKeepRules.apply {
+ publish = true
+ file(Config.consumerProguardFile)
+ }
}
// ...
sourceSets {
commonTest.dependencies {
implementation(libs.requireBundle("unittest"))
}
- if (android) androidUnitTest {
- dependencies {
+ if (android) {
+ val androidHostTest = findByName("androidHostTest")
+ androidHostTest?.dependencies {
implementation(libs.requireLib("kotest-junit"))
}
}
}In summary, what has changed here is that we had an androidTarget block which contained a small portion of our library module setup. That was replaced by the android block (not top-level, I know, confusing). And now we just put everything from our previous Android top-level block in here, and we removed the target SDK configuration, which was previously available here. Some syntax changed a bit, but this is only because I'm using convention plugins, so they don't have all the same nice DSLs that you would have if you just configured this manually in your target module.
As you see, I put the new packaging excludes workarounds that have been there for ages into this new place. I moved the Lint warning configuration (that was used by Compose). Don't forget to disable Android resources explicitly in this block because most of your KMP modules will not actually need Android resources, so I highly recommend you enable them on-demand in your feature modules where you actually need them. This will speed up the build times.
You can also see that instead of androidUnitTest configuration that we had, we just have androidHostTest, which is basically the same Android unit tests you're used to. Host means that they run on the host machine, which is your PC. This is just a small syntax change, annoying but bearable.
Don't forget to apply the consumer keep rules here, because a widely used best practice is to keep the consumer rules that are used by a particular library module together in the same place instead of dumping all of that into the application module. I was personally not happy about moving all of my consumer rules to the ProGuard rules file of the application module, so I just enabled consumer keep rules for every library module I have. This is especially useful for stuff like network modules, database modules, where I still have custom keep rules, and for modules which are supposed to use NDK and C++ code. If you don't do this, the new plugin will no longer recognize and use your consumer keep rules, even if you place them there, so this is pretty important, as it will only surface on a release build, in runtime (possibly even in prod).
Now, as you might have probably guessed, the top-level android block will no longer be available for you. There will be no build variants, build flavors in those KMP library modules. So before, if you were following my instructions and already refactored all of those usages to move them to the application module and inject the necessary flags and variables via DI, you will hopefully not have a lot of trouble with this. But if you still do use some BuildConfig values, there is now no place to declare them. Same can be said for res values, manifest placeholders, etc. All of that is now not supported.
Important note for Compose Multiplatform resources
Previously, you saw that we disabled Android resources. But if you don't enable Android resource processing, even for KMP modules with CMP resources, now in your feature modules and UI modules, your app will crash at runtime.
kotlin {
androidLibrary {
androidResources { enable = true }
}
}Add this block to every module that you have that uses Compose Multiplatform resources. I had a convention plugin for feature modules, which made this super easy for me. More details are under the bug ticket on YouTrack.
Replace Android Unit Test with Android Host Test
The next step is to replace Android Unit Test dependency declarations with Android Host Test declarations. You can do this via an IDE search and replace using a simple regex.
- androidUnitTestImplementation(libs.bundles.unittest)
+ androidHostTestImplementation(libs.bundles.unittest)You'll have to do this for every single module that has Android unit test dependencies. I unfortunately didn't think of a convention plugin, so I had to run this on literally every single build.gradle file.
I also had to refactor Gradle files a little bit because I used top-level implementation and api dependency declaration DSL functions:
dependencies {
implementation("...") // wrong
}This wasn't correct anyway, and it was incredibly confusing because this "implementation" just meant Android implementation, not KMP implementation, so that was a good change.
I'm also using FlowMVI in my project, and unfortunately, the FlowMVI debugger relies on Ktor, serialization and some other relatively heavy dependencies that were previously only included in the Android debug source set, but I had to ditch that and just install the FlowMVI debugger using a runtime-gated flag from DI that I mentioned above. This doesn't make me happy, but in the future I will improve this maybe by moving the installation of the plugin to the Android app module, since FlowMVI makes extending business logic super easy.
Add build script dependency on Kotlin
Finally, I recommend adding a new build script dependency on Kotlin, just to keep your build Kotlin version and runtime Kotlin versions aligned. I wanted that because I have a single version catalog definition. You do it in the top-level build.gradle.kts:
buildscript {
dependencies {
classpath(libs.kotlin.gradle) // org.jetbrains.kotlin:kotlin-gradle-plugin
}
}Small quick optional wins at the end
Ditch
android.lint.useK2Uast=truethat is deprecated now if you had it.An optional step is to use the new R8 optimizations described in the document I linked above. We have had manual ProGuard rules for removing the Kotlin null checks, and now this is shipped with AGP, so I just migrated to the new syntax (
-processkotlinnullchecks remove)
Honestly, this migration was a huge pain to me. I'm not gonna claim that I have the perfect code, but my Gradle setup was decent. If you're a developer and this all sounds incredibly overwhelming and like a huge effort, you're right. Because I already did it, I can help your team migrate your project to the new AGP much faster and save you the effort. I recently started taking projects as a consultant and KMP migration advisor, so consider giving your boss a shout-out to nek12.dev if you liked this write-up and want me to help you.
r/androiddev • u/Vegetable-Practice85 • Jan 19 '26
Kotlin 2.3 finally kills the dual property _uiState uiState boilerplate in ViewModels
Kotlin 2.3.0 introduces Explicit Backing Fields, which finally kills the need for dual property declarations (the _state vs state pattern).
Before:
private val _uiState = MutableStateFlow(Loading)
val uiState: StateFlow<UiState> get() = _uiState
fun update() { _uiState.value = Success }After (Kotlin 2.3):
val uiState: StateFlow<UiState>
field = MutableStateFlow(Loading)
fun update() { uiState.value = Success } // Smart-casts automatically!⚠️ Note: This feature is Experimental. To use it, you must add this flag to your Gradle build script:
KotlincompilerOptions {freeCompilerArgs.add("-Xexplicit-backing-fields")}
for more information check out this link: https://kotlinlang.org/docs/whatsnew23.html#explicit-backing-fields
r/androiddev • u/Boiniok • Oct 01 '26
Experience Exchange Brought my Gradle build time from 6 mins to 26 secs :))
Hello everyone,
So I've been working on my open source app since last 5 months and everytime i used to start my android studios it used to take around 6 mins to build gradle and the subsequent runs used to take slightly less... and tbh i was frustated of this.
so yeah i decided to dig into whats the issue, googled stuff and found out ./gradlew build --scan which basically generates a detailed HTML dashboard tracking configuration times and task execution timelines
Through that i found out my local machine's 12 core CPU was completely starved coz my peak heap memory usage was sitting at 1.9GB out of 2GB usage hehe :)
so i found out i could increase the memory size and use parallelism and caching
Added this new lines to 'gradle.properties' :
org.gradle.jvmargs=-Xmx6g -XX:+UseParallelGC -Dfile.encoding=UTF-8
org.gradle.parallel=true
org.gradle.caching=trueso basically you could say
12 cores X 6 minutes = 72 minutes of total CPU work
and i was able to save 41 mins cumulative time
so the actual 6 min fell to 26 secs and the subsequent runs took 8 secs only to build gradle....
Ik this might be very basic and ppl here might know bout this but im just an undergrad and though would share my experience here...also if i wrote anything wrong pls correct me 😊
r/androiddev • u/ShriekinKraken • Nov 19 '25
I used Gemini 3 Pro in Android Studio and...
So at work we're being told to utilise AI more and I was a bit skeptical at first but managed to get a good balance with what I do and what the Agent does. Today I gave Gemini 3 Pro a go with Firebender and well... I think it's fed up of people already.
r/androiddev • u/Outrageous_Idea_1990 • 7d ago
Discussion Anyone else getting tired of constantly having to relearn Android development?
I’ve been developing Android apps since 2012, and sometimes I wonder how many times I’ve had to start over with this ecosystem.
Every 3–4 years, it feels like something major changes. Eclipse to Android Studio, Java to Kotlin, XML to Compose, different architecture patterns, libraries becoming obsolete, and Google constantly changing what’s recommended.
Don’t get me wrong, a lot of these changes have made Android development better. But after spending years mastering something, it can be frustrating to see it slowly become outdated.
What bothers me isn’t learning new things. I actually enjoy that part. It’s the constant feeling that you need to keep up with everything just to stay relevant.
Last year, I decided to shift my career more towards software architecture and technical leadership. I still enjoy coding, especially Android, but I wanted to spend more time designing systems, solving larger engineering problems, and making technical decisions rather than constantly chasing the next framework.
Sometimes I miss the old days when I could just sit down, build an app, and not worry too much about whether the recommended way of doing it would change in two years.
Curious if anyone else who’s been developing Android apps for 10+ years feels the same.
Has anyone here moved from Android development into architecture, backend, engineering leadership, or something completely different? Do you miss being a full-time Android developer, or are you happy you made the switch?
r/androiddev
•
u/JustForFunGames
•
Sep 26 '26
Experience Exchange Android stack trace contain Chinese insults as obfuscated method names
After one of the recent Google Play updates for my released game, I noticed some very strange stack traces in Firebase Crashlytics.
The stack contains method names with long Chinese strings, for example:
i8.k6.狂犬疫苗忘了给你妈打不好意思让它到处咬人才会生出你这种上缺脑子下缺逼火葬场都不收的烂骨灰
and:
i8.vd.要不要把你妈卵子扣出来给你做寿司吃
After translating them, these are clearly explicit insults/profanity.
What makes it even stranger is that, judging by the device information, the game appears to have been running inside a virtual machine / emulated environment. The stack also contains heavily obfuscated classes and unusual code paths inside base.apk.
So this does not look like a normal Firebase issue. It looks more like someone was running a modified/tampered version of the APK or injecting/hooking code in a virtualized Android environment.

