As a Software Engineering graduate with many side projects under my belt, I was no stranger to the complexities which awaited me in the working world. When the opportunity arose for me to be a part of the development for PraLoop, I eagerly accepted it, thinking that it would be a simple application to complete and that my prior Flutter experience would allow me to coast through all tasks. I was both right and wrong. To everyone reading this, here is what I learned as a Junior Developer working on an application that has been shipped to the Apple App Store and Google Play.
Tech Stack
The most important part of this journey I believe is the tech stack. Flutter is one of the most popular mobile application frameworks among developers, and it is not hard to see why. Support for virtually every single platform, easy-to-read syntax, blazing fast speeds, and massive community participation through packages published on pub.dev are some of the commonly touted accolades of Flutter and Dart.
Next, for anyone who has been remotely near the Flutter ecosystem lately, a state management solution must be chosen. Kind of like the Sorting Hat in Harry Potter, but the choices being Riverpod, Bloc and GetX. I personally did not have much of an opinion between the three, but our team went with Riverpod due to its simplicity and robustness. Using the codegen approach via @riverpod annotations, it eliminates manual provider wiring and gives compile-time safety, as the generator handles the base class and registration, so mistakes surface before runtime rather than during it. It also offers distinct provider types matched to your use case:
FutureProviderfor one-shot async work, such as for theapp_startup.StreamProviderfor live data, such as checking the user’s subscription status.Notifierfor all page-level state management, keeping intent explicit throughout the codebase.
On the widget side, Riverpod offers three base classes to cover different needs:
ConsumerWidgetfor simple reactive widgets.ConsumerStatefulWidgetwhen you need lifecycle methods likeinitStateordispose.HookConsumerWidgetfor form pages that combine Flutter Hooks with Riverpod.
Paired with Riverpod was Freezed, which gave us value equality, copyWith(), and union types out of the box. Its fromJson support also meant that domain models like User handle both immutability and JSON deserialization from the same annotation, removing what would otherwise be a separate mapping layer. The immutability Freezed enforces works directly in Riverpod’s favor as well, since comparing old and new state by value is what lets Riverpod skip unnecessary rebuilds in the first place.
Challenges Faced
As with any Flutter project, it was smooth sailing at first; setup, initial pages, all looking good. Before long however, I soon ran into several issues with the tasks I was assigned. For the sake of brevity, I will only document the main ones faced here.
YouTube Player Items / Player Page
The main feature which I had trouble completing was the player pages for each item. For illustration, PraLoop supports 4 item types: Audio, Video, YouTube and Recording. For 3 of the item types, it could well be handled by well-maintained existing packages such as audioplayers and video_player. For YouTube, there were existing solutions by Mr. Sarbagya, who authored both youtube_player_flutter and youtube_player_iframe. Not to be dismissive of the incredible accomplishment of publishing a well-known and popular package, but I was skeptical to use a package for something which was a core feature of our application. Research was conducted on how we could implement it on our own, using dart:ffi, native solutions, but we soon realized it would be much more work than was worth it. Coupled with the work needed to implement youtube_explode_dart into a functional search for YouTube videos (as we are not allowed to store YouTube videos on our backend, only the metadata), the player page was arguably the core feature of PraLoop. Shoutout as well to Mr. Oleksii, who created YoutubeExplode, and Miss Mattia, who created the Dart port.
Following that, the next biggest challenge was combining the ability to play all four of those items into a single page, which adheres to the GoF Adapter Pattern to keep the implementation clean and maintainable. Since the functionality for each item was going to be the same (speed control, screen mirroring, full screen, etc.), we needed a solution which worked for all players used. Fortunately, with a quick Google search, the answer was revealed: an existing package called chewie, which was highly customizable and straightforward to use. This was yet again another architectural decision we made to use existing, popular solutions instead of building one from scratch, to save a significant amount of time and resources.
Localizations
The next challenge I faced was to do with localizations. I had never given this part of the app development process much thought, so the initial setup and research took more time than I want to admit it did. Of course, I went straight for the official Flutter docs, but realized then it came with one pesky problem: the guide did not explicitly mention supporting localizations in places without BuildContext. That led me down a rabbit hole of considering many solutions, such as making a simple helper function, the Flutter Intl VS Code extension, and creating a Riverpod provider specifically for localizations.
In line with the project philosophy demonstrated so far, we decided to go for the simplest and least impactful solution: the helper function, as we only encountered this issue when displaying error dialogs from view models.
Timezone Detection
Quite relative to localizations, in the sense that if we have a large userbase, we have to account for users from different parts of the world. PraLoop detects and sets the user’s timezone automatically upon first app launch. Initially, I thought that the device timezone could be easily extracted via the built-in DateTime() object. However, I discovered that the timeZoneName property only returned the offset, such as +08. We were not able to reliably identify a timezone with this information alone, as many locations could use the same offset and be thousands of kilometers apart. Fortunately, there was an existing package which perfectly solved this situation: flutter_timezone, created by Mr. Thomas Järvstrand.
There was one last issue: certain devices may return a deprecated timezone. Therefore, we had to implement a helper function to link any potentially deprecated timezones to their canonical counterpart.
Compact UI
The final challenge I personally faced was ensuring that all functionalities were able to fit within the space that was allocated. I believe although PraLoop is a fairly simple app in theory, in reality it contains many more functionalities than most people would expect. For example, the player page controls, which at the time of writing has 7 features. Not only did I have to fit them all within one card, but I also had to ensure the design of each component was responsive for each screen dimension. That is when my heroes FittedBox, ConstrainedBox, Flexible and MediaQuery came to the rescue.
AI-Assisted Coding
Any project in 2026 would not be completed without the help of a multitude of AI coding tools. Without them, I believe that this project, which took roughly 6 months, would have taken twice the amount of time. Coding agents not only helped with actual development, but also with debugging, research and decision-making. A much needed disclaimer here is that any outputs MUST be verified by a human.
The two tools which I used mainly in this project are Claude Code and CodeRabbit, which I believe need no introduction.
For Claude Code specifically, there are many plugins and skills which can be implemented to significantly improve the performance of agents (or simply act as a convenience). Here are the ones I found useful, with their appropriate documentation:
Things I Wish I Took Part In
As a Junior Dev, there were many things which I was simply too unfamiliar with, and which would have slowed everything down had I been tasked with them, as I would have had to take time to learn before applying the knowledge.
CI/CD
The final piece of the puzzle when developing an app. CI/CD pipelines are both a pain to set up and manage, but are one of the most important parts of actually delivering the application to users. Checks need to be done before pushing to production and continuously throughout, to ensure no vulnerabilities are exposed and users are not experiencing bugs. In PraLoop, the delivery flow differs by platform: the web side is deployed automatically via GitHub Actions after lint and tests pass, while the API deployment is run manually due to various considerations such as vital database migrations. On the mobile side, parts of the release workflow such as generating store assets and localized app descriptions are also automated using local scripts.
App Store / Google Play Publishing
I did have experience publishing my very own app to Google Play, which simply involved signing and submitting, and in my experience it was approved relatively easily. However, I heard that the process for iOS apps is much more complicated and time-consuming, which is why we might have to introduce an OTA solution such as Shorebird to our pipeline, for cases where quick fixes need to be implemented.
Sync Functionality
Another core functionality offered by PraLoop is cross-device sync. You could have your items on any mobile device or tablet across Android and iOS. Basically, anything a user produces is synced. To my very brief understanding, it is built on a Go backend using Gin for HTTP routing, with MySQL for persistent storage and Redis handling sync event publishing via pub/sub. On the Flutter side, the client is hand-written rather than generated, which gives us full control over things like authentication token injection. Sync processing also runs in its own dedicated service, separate from the main API, so sync jobs never interfere with or slow down regular app requests.
Key Takeaways
All in all, this entire journey was extremely fruitful, as there were many, many things I learned that will contribute to my improvement as a Software Engineer.
My main technical takeaways were:
- Great architecture is the base for any successful project.
- AI-assisted coding is here to stay. However, our role as software developers has experienced a paradigm shift, and we need to take on an even broader ownership level, not only of the features we are working on, but the entire project.
- Do not blindly trust packages published on pub.dev. Number of installs, contributors and stars on GitHub are an indicator of whether a package is well-maintained.
On a separate note, my takeaways for someone who is aiming to be a Software Engineer but is perhaps stuck in tutorial hell, struggling to land a role, or is just looking to improve in general (definitely applies to myself as well):
- Keep learning. Take time out of each day to learn. Consistency is key.
- Stay focused. Keep the phone away, stop scrolling (fr).
- Stay healthy. A strong body leads to a sharp mind.