Enterprise Mobile Apps Development Principles

Oleksandr Leushchenko
Flutter Community
Published in
8 min readAug 6, 2024

Everything you say will be massively wrong in some scenario. Remember this and stay humble.
— Julie Zhuo

AI-generated picture of a barn with Notre Dame Cathedral elements

A small barn with elements of Notre Dame Cathedral looks ridiculous. Do you know what will be even more ridiculous? It’s maintenance cost. Probably, we all agree that when a client requests to build a barn, they don’t expect to get a Gothic masterpiece. It’s just nonsense. So why do we build mobile applications like that?

A single-screen calculator app built with Clean Architecture might be a nice tutorial or an arthouse project, but not a pragmatic decision. It is our job as professionals to keep the development as efficient as possible. It is easy to write complex projects, and it is hard to write simple ones.

In this article, I’ll share my basic principles for building complex mobile applications in Flutter.

Architecture

A complex system that works invariably evolved from a simple system that worked. A complex system designed from scratch never works and cannot be patched up to make it work.
— Gall’s Law

Architecture depends on the complexity of the project. The relationship is straightforward: the more complex the problem you are solving, the more comprehensive the architecture it requires. Thus it’s natural to assume that to write complex mobile applications, a complex architecture is needed. However, that’s not the only approach. Instead of complicating the architecture, you may simplify the problem! Take a big complex app and split it into sub-modules, then split sub-modules into sub-sub-modules. Continue doing that until you find yourself working on a small and simple portion of the big complex problem. Small and simple packages do not necessarily require complex architectures!

Simple does not mean primitive. Simple code is the code that’s easy to understand. Simple code may look suboptimal in many places, and most of the time, it will not be the most elegant or concise code. However, it will always be predictable and familiar. Simple code conforms to simple agreements. Once you learn these agreements, you will master the whole codebase, regardless of its scale.

Software design is like creating the architecture of a house, it’s about the big picture. On the other hand, code design is working on the details, like the location of a painting on a certain wall. Code design is also very important, but not as fundamental as software design. A code design mistake is usually more easily corrected, while software design errors are a lot more costly to repair.
— Domain-Drive Design Quickly book by Abel Avram & Floyd Marinescu

In a good architecture, it doesn’t matter what state management library you are using or whether you have migrated to Navigator 2.0 or kept the old one. What matters is whether you created good boundaries between modules and thought about the domain well enough. Do not try to predict the whole complexity of the project. Let the project evolve. Make basic simple agreements and scale them.

My advice: split complex apps into small isolated simple modules, and do not let implementation details (like, state management or navigation) rule the architecture of your app.
Practical reference: you may learn how to split a huge app into simple small bricks from the
Writing Flutter Apps in Lego style video.

Unified Solutions

If you ever talk to a great programmer, you’ll find they know their tools like an artist knows their paintbrushes.
— Bill Gates

Identify tools and patterns you and your team are comfortable with and use them across the whole solution. The unification not only makes the code easier to read and maintain. When you find a way to improve something in your project, you will be able to make improvements on the full scale.

Let’s imagine, you found it convenient to report analytics in bloc’s «onEvent» method. You may easily scale this decision to the whole solution if you used bloc. However, if part of the application uses redux, another part uses riverpod, and in some places, you decided to use «setState» instead of “proper state management”, you will come up with four different implementations for the same problem. That significantly limits your ability to make further improvements and creates silos in the codebase. In pathological cases, you’ll have a team of engineers who are afraid to propose improvements to unfamiliar parts of the codebase.

My advice: every problem should have a single solution in the scope of the project.
Practical reference: you may learn about technical decisions at Tide from the
Project Miniclient tutorial and Mobile Tech Stack at Tide video.

Tests

All software you write will be tested — if not by you and your team, then by the eventual users — so you might as well plan on testing it thoroughly.
— Andy Hunt

Do you need tests on your project? It depends on your plans for the code. If you are experimenting with the new feature, writing a demo for your talk, or a code snippet for a tutorial, then tests are not mandatory. However, if you treat your tutorial seriously, you may still want to add tests. As Robert Nystrom wrote in the repository that complements his beautiful «Crafting Interpreters» book:

I am a terribly forgetful, error-prone mammal, so I automated as much as I could… I have a full Lox test suite that I use to ensure the interpreters in the book do what they’re supposed to do.

I find this approach extremely professional. How many times have you seen misprints in code listings in books that in the best case make the sample app non-compilable, but in the worst case, make the app doing wrong things? If all authors write tests for their samples, the life of readers (especially, ones who are doing their first steps in programming) would become so much better.

Returning to software development. We are not writing tests to find bugs. We write them to validate that the application is behaving according to requirements. If you don’t feel confident in the requirements of the solution you are going to use, it might be fine to sacrifice some best practices, including tests. When prioritizing speed over quality you generate a tech debt. Don’t forget to plan time to work on it once confidence in the solution increases. Tests must be in the «definition of done» of the task. The functionality is incomplete without tests, and you can not start working on new functionality without adding tests.

Code without tests is bad code. It doesn’t matter how well written it is; it doesn’t matter how pretty or object-oriented or well-encapsulated it is. With tests, we can change the behavior of our code quickly and verifiably. Without them, we really don’t know if our code is getting better or worse.
— Michael Feathers

Tests are essential for refactoring. Without refactoring, you will never be able to scale the app without fearing that your next small improvement breaks something. If you think that testing is too expensive and your client doesn’t pay you for that, try writing widget tests in BDD style. Following this approach can easily bump test coverage to 90–100%, keeping the test writing process a breeze. As a pleasant bonus, you’ll get documentation for the app.

My advice: practice TDD or BDD; it will look inefficient till the first time you see a test that was supposed to fail pass. Use a sociable paradigm to keep the test suite less fragile.
Practical reference: watch this short
BDD in Flutter video series, and read the rationale behind the sociable paradigm.

Experiments

The most dangerous phrase in the language is: We’ve always done it this way.
— Grace Hopper

Once you have established patterns and, most importantly, written tests, you may start experimenting. Take one module and make something in a non-standard way there. Change state management, try not using use cases if you use them, and try to use use cases if you don’t. Implement an idea you do not feel fully comfortable with. Play devil’s advocate and protect this idea.

It is extremely important to keep experiments isolated in a single module. Do not scale before you are convinced that refactoring is worth it. Do not rush to change the code just because you’ve read that the new state management that was released two weeks ago solves every problem in your app. It won’t.

When you completed a successful experiment, plan the migration. Unification is important for maintainability, hence once you identify an improvement, make sure you scale it.

My advice: always have one experiment running, but do not rush to scale it until you are convinced that it improves anything. Don’t be afraid to revert the experimental code: a failed experiment is still a success.
Practical reference: read books, watch videos, read articles, learn from other technology stacks.

Documentation

Good code is its own best documentation. As you’re about to add a comment, ask yourself, ‘How can I improve the code so that this comment isn’t needed?’
— Steve McConnell

Your code should be self-explanatory already, don’t create useless documentation for methods or classes, unless you are working on an SDK.

For mobile apps, you may get the «how to use» kind of documentation if you adopt the BDD approach. It will simplify your QA work and make support more reliable. To get knowledge about how some functionality of the app works, every member of the team may just check BDD scenarios. If something is not covered in such scenarios, that means you have missed a requirement, and hence engineers must update tests.

If you need to create technical documentation for the project, put it in README files in the repository with the project, and not somewhere on Confluence. Keep the docs close to where they might be needed.

The same advice applies to module documentation. Describe the intention behind modules and high-level decisions. Keeping the documentation close to the code makes it easier to find and update once the code changes.

If you use the same unified approach in every package, you wouldn’t need to document technical details and instead, you’ll be able to focus on what is important — functionality and behaviors.

My advice: keep the documentation close to the context where it might be needed.
Practical reference: practice writing meaningful README files, and adopt the
BDD technique in your team.

Summary

  1. Architecture depends on the complexity of the project. Instead of complicating the architecture, simplify the problem.
  2. Simple does not mean primitive. Simple code will not be the most elegant or concise, however, it will always be predictable and familiar.
  3. Unify. When you find a way to improve something in your project, you will be able to make improvements on the full scale.
  4. Tests are essential for the maintenance. Consider using BDD.
  5. Always have one experiment running, but do not scale before you are convinced that refactoring is worth it.
  6. Placing the documentation close to where it is needed will help the team keep it up to date.

About the author

Oleksandr Leushchenko is a GDE in Flutter and Dart, and a seasoned mobile developer with more than a decade of experience in building cross-platform mobile applications. He is a Senior Staff Engineer in a UK fintech company called Tide where he helps a team of 60+ engineers build an international financial services platform.

Russia started an unfair and cruel war against my country. If you found this article interesting or useful, please, donate to Ukraine’s Armed Forces. I can recommend my friends volunteers — the “Yellow Tape”, you can be 100% sure that the money will support our victory. Thanks in advance to everyone who participated.
Glory to Ukraine! 🇺🇦

--

--