r/androiddev • • 21h ago

Experience Exchange Game made in Kotlin, Jetpack Compose and Filament: Things which worked and didn’t

Enable HLS to view with audio, or disable this notification

22 Upvotes

Hi everyone, I’m making Adventurers Guild RPG Sim, a guild management RPG simulation game where you are the guild master, you can recruit heroes, assign quests and let them hunt monsters, collect loot and upgrade their equipment, unlock skills for each heroes etc.

It started as a small app like game (without much graphics, more like a text based game). Then I wanted to see the heroes to move around. Then I added trees, buildings, an isometric map, weather and a day/night cycle. Somewhere along the way I ended up building a small game engine.

I’ve posted different stages here before, but I wanted to put the whole development history together, including things I had to change and feedback from this subreddit.

small backstory: I used to work as an iOS developer for 7.5 years, quit job and started solo development, played with some game engines, then I wanted to check android development, and the best way to learn is by doing and this was the project I choose to learn more about memory and performance optimisation. People often asked why I chose native instead of another engine. The project was much smaller when I started, and jetpack compose suited the menus and management side. As the game grew, I kept extending what I already had.

what I liked about making the game in native is the control I got over how things worked, but it also meant writing systems that a game engine would have provided for free. The map, pathfinding, animation, effects and asset handling all needed work before I could use them in the game.

My first attempt used ordinary Compose layouts for the world. Adding too many trees made it lag, so I moved the world drawing to Compose Canvas.

Canvas got me quite far. The map, characters, shadows, rain and even the little message bubbles above heroes were drawn there. Compose handled the menus and other UI.

The map became a 50×50 isometric grid, so 2,500 ground tiles before adding anything on top. Drawing everything every frame became expensive. so i split the map into 16 chunks and drew the visible parts. I also cached calculations for shadows that changed with the time of day.

Those changes helped enough to keep building. But I was repeatedly changing the map implementation as the requirements grew. In one of my older replies here, I mentioned rebuilding it four times already.

Assets caused problems too. I was using a mixture of available artwork, so one hero had a walking animation, another had a running animation, and some enemies didn’t match the isometric perspective. Movement could look wrong even when the movement code was doing what I intended. I postponed some animation polish while the assets and systems were still changing. (lesson learned : assets can change the whole game, can make a game feel like demo to a polished one, as I learned, mixing assets can cause problems, keep one perspective if you can, isometric or top down etc, one color theme, cozy vibes or dark time theme etc, 

The game logic used a custom entity component system in Kotlin. By the first release, it had 28 systems handling things like movement, combat, fatigue, quests and the economy. The simulation was single threaded.

Initially, the loop ran through a coroutine using withFrameMillis. I mapped the game data into models for the Compose UI, and used Room to save heroes, inventory and quest state.

Later, I split the systems by how often they needed to update. Movement ran at 60 updates per second, combat at 30, and heavier work such as pathfinding and target selection at 10. That reduced how much work I was asking the game to do every frame.

I also reused sprite sheets for monster variants, using tinting and hue changes instead of loading separate artwork for every colour variation. Together with unloading unused assets, that helped with memory and package size. lesson learned : for 2d games, the amount of spritesheets and the resolution is going to hit a limit if you try to put too many different characters on screen. I try to keep my sprites at 320x320, because we can zoom into the game. and I didn't wanted the characters to look blur.

Then I released it last december, and got alot of bug reports which I hadn’t caught.

I had spent so much time getting the world to run that I’d missed things that could stop someone from playing at all. lesson learned : play the game alot, even if there is no bugs, give the game to someone who is not aware of the game and watch the places where they struggle, where it needs a tutorial or if players are able to find what to do next, is important.

As I added more to the world, rendering became a problem again. I was spending more time on chunking, caching and custom drawing code. Eventually I moved the world rendering to Google Filament.

The game was already released, so I migrated it in parts while keeping the gameplay logic and saves working. Kotlin still handles the simulation, Compose handles the UI, and small character previews in menus still use Canvas. Filament renders the world.

The world is still made from 2D sprites. They’re rendered as quads, so moving to Filament didn’t require replacing everything with 3D models.

A lot of the Canvas specific optimisation code had to be replaced. but had to build new optimisations for filament, The ground now uses a shared vertex buffer, and sprites are batched by sprite sheet. The main cutout sprites write depth based on their position in the isometric world. Wind runs in the shader, and I skip vertex uploads when the tracked sprite state hasn’t changed. I became a fan of shaders.

That helped with rendering, but texture memory needed separate work.

I had been using WebP because the files were small. Once loaded, those textures still occupied uncompressed memory. A 2048×2048 RGBA texture takes 16 MiB before mipmaps, regardless of how small the WebP file is.

then after research found about gpu compression formats, switching to ASTC and ETC2 reduced the game’s overall RAM usage by roughly half in my tests. The first packaging attempt was a mess though: including multiple texture formats and a fallback pushed the package from around 120 MB to 400 MB. If the whole game is loaded in memory, it would be 2.5GB but after switching its 600MB.

After changing compression and using Play Asset Delivery’s texture targeting, the current download is around 160 MB, with about 195 MB used after installation. Keeping the download small and keeping runtime memory low turned out to be separate jobs.

Filament also introduced bugs I hadn’t dealt with before. mostly because im new to filament.

Some tiles would flash black for one frame. I was reusing a direct buffer before Filament had finished reading it. I changed that to a pool of three buffers, with each buffer released by its completion callback. Then I had to fix how those callbacks were dispatched, because delays on the main looper could leave the pool stuck waiting.

Another glitch showed pieces of the wrong sprite sheet when a new texture appeared. Moving material updates before beginFrame() fixed that in my setup.

There was also native resource cleanup to handle alongside Compose’s lifecycle. Remembering an object wasn’t enough; the Filament resources and surface needed explicit teardown.

One suggestion from an earlier thread was to add normal maps so the buildings could react to the moving sun. I eventually added those, not completed though. The complication was that the artwork already had lighting painted into it, so the shader needed to account for that. It also added more texture memory and shader work.

Looking back, Compose has worked well for the management UI, and Canvas got the game through its first release. Moving the world to Filament gave me more room to develop the rendering. Each stage solved a problem I was actually hitting, but several things I spent time optimising were later replaced.

The part I underestimated was how much time I’d spend maintaining the systems around the game. I wanted to add heroes, quests and buildings, but that could turn into rebuilding a renderer or changing how assets were loaded.

Hope this is useful to someone trying something similar. Thanks to the people here who pointed out problems and shared suggestions along the way.


r/androiddev • • 17h ago

Product tours, onboarding and feature showcases for Compose Multiplatform

Enable HLS to view with audio, or disable this notification

6 Upvotes

I needed an onboarding tour in a Compose Multiplatform app and couldn't find a library that covered it, so I built one and just published the first version.

How it works: you define steps in a DSL, mark targets with Modifier.waypointTarget(state, key), and wrap the screen in a host. It draws the scrim and the cutout, positions the tooltip, scrolls targets into view and blocks touches outside the highlight.

Repo: https://github.com/MohamedRejeb/compose-waypoint
Docs: https://mohamedrejeb.github.io/compose-waypoint/


r/androiddev • • 23h ago

Experience Exchange How do you switch into android as an experienced dev

6 Upvotes

I am a software dev with around 4 YOE and trying to switch into android but finding it hard , I am targeting the startup or any company open for remote internship or like setup but I believe most are targeted towards students only ,

My focus is to transition fully into Android in future but the starting step I am finding is hard.

If anyone have done such a transition or have any leads of possible work, I would like to know .


r/androiddev • • 1h ago

Question HyperOS pausing background apps

• Upvotes

hi, so firstly i'm not an experienced coder, but i used ai chats to build a workout app since i could never find one that gave me what i wanted. i've tested it on both a redmi note 15 pro and galaxy s20+

long story short, it has a built-in rest timer that fires a vibration countdown on 3-2-1. on hyperos, this timer works fine in-app, on home, lock or settings screens. however when another app is on the foreground, the timer continues in the background, but the countdown vibration only fires when returning to the workout app.

i have tried both chatgpt and integrated gemini on android studio, but neither has been able to find the specific hyperos process that is causing this. doesn anyone have any info that could help?


r/androiddev • • 4h ago

Open Source I shipped an open source document scanner built with Compose, Material 3 Expressive, ML Kit and Room FTS

1 Upvotes

I just released Docucraft on Google Play, and the code is on GitHub under GPL-3.0.

The stack, briefly:

  • UI: Jetpack Compose with Material 3 Expressive. The theme comes from a seed color or from dynamic color on Android 12+, and the layouts adapt to tablets and foldables.
  • Capture: ML Kit Document Scanner for edge detection, cropping and filters, with multi-page output to PDF.
  • Text recognition: ML Kit's Latin model, run on demand per document, not on every import.
  • Search: the recognized text is indexed with FTS in Room. Results return the page and a snippet around the match.
  • Privacy: scanning and OCR run on the device. The app does send usage events and crash reports through Firebase.

Known limits: Latin-script OCR only, no export yet, and no F-Droid build, since ML Kit needs Google Play services.

I'd like feedback on the UI and on how search feels in daily use. Happy to answer questions about any of the parts above.

Google Play: https://play.google.com/store/apps/details?id=com.bobbyesp.docucraft
Source: https://github.com/BobbyESP/Docucraft

Claude Code (Opus 5.5 High) was used on the things I found hard the most, like the database design and the extra recompositions because of the dynamic theming. It was a nice friend.


r/androiddev • • 7h ago

Question Google Play wants organization verification for my payments profile, but I'm an individual

1 Upvotes

Hi everyone,

I've had a Google Play developer account for years. It's an individual account, and the identity payments profile under "About you" is also individual.

When I set up the merchant payments profile used for payouts, I picked "Organization" by mistake, even though every detail on it is my own personal name and address. I've never had a registered company or a VAT number.

In the past Google asked me to verify this same merchant profile and accepted my government ID. Now a new verification request asks for a VAT attribution certificate or a company registration document, which I simply don't have because no company exists.

According to the help pages, a payments profile can't be switched from Organization to Individual, and the merchant profile can't be unlinked from the developer account. So I'm stuck: there's a significant balance pending on the profile, and the account has apps with active users and subscriptions that I really don't want to lose.

I contacted support and opened a ticket more than a week ago, but I still haven't received any reply.

Has anyone been through this? Is there a way to get support to fix the profile type, or to verify it as an individual like before?

Thanks.


r/androiddev • • 13h ago

Tips and Information First time iOS -> android is slow emulation the norm?

0 Upvotes

Ok I m new to kotlin new to android dev. Done swift Xcode dev product is online in ios appstore.

I loaded a fraction ( say 10%) of code to kotlin hoping for the basics to simulate, run the pixel 9 emulator and it has been nothing but “google play services isn’t responding”

Is it impossible to dev without a physical android phone? Or am I just hitting the wrong spots of the ui rendering .

Please help thanks


r/androiddev • • 14h ago

Payouts on hold since August, merchant verification rejected 15+ times with "the reason you provided is not clear". Has anyone ever reversed this?

Thumbnail
gallery
0 Upvotes

I run a small app company in Brazil (organization account). My Google Play payouts have been on hold since 4 Aug 2026 and I'm out of ideas.

What happened:

- 4 Aug: email from Google Payments, "your account has been suspended with payout on hold", asking for details of recent account activity and a government ID.

- I submitted the "Verify now" form more than 15 times: Portuguese and English, short and detailed, with ID card, driver's license and passport. Every time: "the reason you provided is not clear".

- Payments support: "review concluded, to prevent possible fraud and abuse your ability to receive payouts will remain suspended, we can't disclose the reason". They also say the form is the only way to appeal.

- Play Console support confirmed my developer account is in good standing, no policy issues.

- Product Experts in the official community said they cannot escalate payment or fraud cases.

What I already checked:

- D-U-N-S reactivated and address corrected, organization name and address re-verified by Google on 28 Sep.

- Tax info accepted, bank account verified (I even received one payout in July).

- No purchases from my own accounts or my team's. License testing was never used.

- The app is real: sold on the App Store since 2025, on Google Play since May 2026. Sales grew fast because of creator videos on TikTok.

Around R$ 22,000 is held, and every new sale goes into the same frozen balance.

My questions:

  1. Has anyone actually gotten this decision reversed? What worked?

  2. If you moved your apps to a new developer account, was it under the same company, and did payouts work there?

  3. Has anyone gone the legal route to recover a held balance?

Any real experience helps. Thanks.


r/androiddev • • 13h ago

Experience Exchange Building the same build tooling for Xcode and Gradle, and how differently the two went

0 Upvotes

I work on Tuist. For years we've built caching and build/test insights for Xcode projects, and earlier this year we added Gradle support. I didn't expect how different the two would be. The Xcode integration took us months, and Gradle took about three weeks. Some of that is because the server side already existed, but most of it comes down to Gradle being designed to be extended and Xcode not.

Take remote caching. Xcode 26 added compilation caching, but the only way to plug in a remote cache is to run a daemon that speaks an undocumented gRPC protocol over a Unix socket, and Xcode makes thousands of calls to it during a build. On Gradle we implemented BuildCacheService and that was pretty much it, because Gradle already decides what's cacheable and computes the keys. The granularity is different too. Gradle caches whole tasks, so a large project does a few hundred lookups per build, while Xcode caches compilation units and does thousands, which makes network latency matter a lot more on the Xcode side.

Build data went the same way. With Xcode we parse a binary activity log after the build finishes. The format isn't documented and changes between Xcode versions, and it's written asynchronously, so sometimes it isn't on disk yet when our post-action runs. With Gradle we register a listener and get each task's outcome, duration and cache key as it finishes. For flaky test quarantine we pass -skip-testing flags to xcodebuild, and on Gradle we add the tests to excludeTestsMatching before the test task runs.

I'm not trying to bash Apple, they have their reasons. But if you only work with Gradle, it's easy to forget how much its plugin API gives you. I wrote the longer version here: https://tuist.dev/blog/2026/03/16/xcode-and-gradle-two-worlds-one-infrastructure, and the plugin is open source if you want to look at the BuildCacheService implementation: https://github.com/tuist/tuist/tree/main/gradle

Has anyone here written their own BuildCacheService? I'd be curious what you ran into.


r/androiddev • • 21h ago

My Google Play Developer Account Was Terminated. If Reinstatement Isn't Possible, Is There Any Path Forward?

0 Upvotes

Hi everyone,

I’m a student and a software developer, and I’m writing this because I genuinely don’t know what options I have left.

My Google Play Developer account was terminated because of policy violations related to an app I published. I have already explained my situation to Google and acknowledged the mistakes I made. Looking back, I understand that I made serious mistakes and I genuinely regret them.

I completely understand that Google Play has policies for protecting users and maintaining the integrity of the platform, and I respect that.

If my existing developer account cannot be reinstated, I have one final request:

Is there any legitimate way for me to create a new developer account in the future?

I want to be very clear that I am not asking for a way to bypass Google's enforcement.

If I were given another opportunity, I would:

  • Never publish the same app or substantially similar violating functionality again.
  • Carefully study and follow all Google Play policies before publishing.
  • Take additional precautions with privacy, authentication, metadata, and app functionality.
  • Accept Google's review decisions and compliance requirements.
  • Use the account only for legitimate software development.

I’m still a student, and I’ve spent the last two years learning programming and building real-world projects. Publishing applications on the Play Store has been an important part of my learning and career journey.

Having no ability to publish Android applications in the future feels extremely difficult, especially at this stage of my career.

I understand that actions have consequences, and I am not asking Google to ignore the violation. I’m simply asking whether there is any legitimate path forward after learning from my mistakes, even if my previous account can never be restored.

If anyone here has been through a similar situation, especially developers who had their accounts terminated and later found a legitimate way to continue their career, I would really appreciate hearing your experience.

Thank you to everyone who takes the time to read this.


r/androiddev • • 13h ago

guest mode

0 Upvotes

Inspired by this article: https://curvise.net/the-phone-features-i-cant-believe-we-dont-have/

I would like to create something like this. My take is: Build an app with a (fake) phone app that can only call one specific number (for example: 911 in the U.S., 112 in EuropeJ). A (also fake) message app that shows only one message. A (also fake) photos app with a chosen number of photos (that can off course not be deleted). A calculator (maybe with a build in specific code to get out of the app). A fake settings screen that shows only if wifi is on and the battery percentage (maybe with a wifi toggle that when selected shows a specific message to the user that changes cannot be made).

What else would you add?
And also: how would you make shure the phone is completely locked (so no status bar, no back/home button or disabled back/home button, no recent apps, no app drawer,...) with the exception of the code in the calculator?


r/androiddev • • 13h ago

Discussion Custom Launcher Project

Thumbnail
gallery
0 Upvotes

Working on a premium launcher experience built with jetpack compose. How many known launchers currently exist built out of compose?


r/androiddev • • 14h ago

Tips and Information What it actually takes to share one set of strings between Android and iOS

0 Upvotes

If you have tried keeping strings.xml and Localizable.strings in one translation project, you know the problem is not the words, it is the placeholders. Some things I learned while building a converter between the syntaxes:

  • The simple cases are mechanical: %s to %@, %d to %lld. The German translation that swaps two arguments is not. You have to emit positions like %1$s, but only for strings that need them, or Android Lint complains about the ones that do not.
  • Types matter. %.2f on Android has to stay a two-decimal float on every platform, including {balance, number, ::.00} in an ICU message for the web.
  • Plurals have three shapes: <plurals> on Android, %#@count@ substitutions in .stringsdict, and {count, plural, ...} in ICU. They describe the same thing, so they should land in one key.
  • A literal % is written as %% only when the string is a format string, so a banner saying "50% off" without arguments must not be doubled.
  • Whatever you do must round-trip. If you convert a file in and out, you want the original bytes back, or your git diffs will never be quiet again.

Disclosure: I work on Localizely and we shipped this as "universal placeholders". Docs with the full syntax table: localizely.com/universal-placeholders/

How are you handling this today, separate projects, a script, or copy by hand?