Flutter Community
Published in

Flutter Community

Flutter Shopping App prototype: Lessons learned

Some insights on how to implement a production-quality app using Dart + Flutter

Flutter Shopping App prototype: Lessons learned

Table of Contents

  • Overview
  • App architecture
  • State management
  • Reinventing the wheel
  • Test, test and then test once more
  • Summary
  • Your contribution

Overview

Flutter Shopping App prototype screens
Flutter Shopping App prototype
  • Immutable state, models;
  • Lazy loading implemented as an infinite list;
  • Loaded products’ caching;
  • Material Design UI (not great, not terrible);
  • Clean code structure: code is separated into modules by feature, commonly used components, utils, and constants are extracted;
  • Using a real-world Best Buy API client, which is implemented as a separate Dart package;
  • Unit/widget tests covering pretty much the whole code.
Flutter Shopping App prototype dependencies
Flutter Shopping App prototype dependencies
Flutter Shopping App prototype DEV dependencies
Flutter Shopping App prototype DEV dependencies

App architecture

Modular Application Architecture (source)
  • In Flutter, the architecture of your app strongly depends on the state management solution you will use. For instance, if you choose BLoC, you know that your business logic will be separated from the UI and extracted to the BLoC classes, you will also need to create the related event and state classes, etc. Then, if you choose something like a change notifier/provider, it makes sense to structure your app similarly as in MVVM. Therefore, structure your app having a specific state management solution in mind, it would eventually save you some time in the future.
  • Consistency is the key. If you or/and your team decides on the specific app architecture, please, stick to it. Consistent code and file structure help to find a specific component, widget or any other file in the project faster which results in easier maintenance. I would also recommend defining a decent set of static analysis rules in your project by adding the analysis_options.yaml file. If you are not sure of which rules should be included, you can use the pedantic package to enable the list of linter rules that Google uses in its own Dart code. More info about the static analysis options could be found here.

File structure

As far as I know, there are no official guidelines on how to structure the project files for your application. Usually, Flutter developers choose the layered architecture, hence they structure their project files like this:

Layered code structure (source)
  • Layers could become overwhelming. Since you put all the UI logic in one layer and all the business logic in another one, the number of files in a single layer starts to grow very fast while implementing one feature after another. Later, for instance, you could end up having a dozen different services in the same services folder which makes it difficult to find the specific file, and maintain a clear structure.
├───android
├───assets
│ └───fonts
├───ios
├───lib
│ ├───config
│ ├───constants
│ ├───modules
│ │ ├───cart
│ │ │ ├───bloc
│ │ │ ├───models
│ │ │ ├───pages
│ │ │ └───widgets
│ │ ├───products
│ │ │ ├───bloc
│ │ │ ├───models
│ │ │ ├───pages
│ │ │ ├───repositories
│ │ │ └───widgets
│ │ └───product_details
│ │ ├───pages
│ │ └───widgets
│ ├───utils
│ └───widgets
├───packages
│ └───best_buy_api
└───test
├───config
├───helpers
├───modules
│ ├───cart
│ │ └───widgets
│ ├───products
│ │ └───widgets
│ └───product_details
│ └───widgets
└───widgets
  • lib/config stores general configuration files like theme data, routes, and environment configuration;
  • lib/constants contain all the general-purpose constants, e.g. colours, common layout spacing values, texts, paths to files, etc.;
  • lib/utils are used for common helpers used across the app, for instance, helper functions, and extension methods;
  • lib/widgets contain Flutter widgets used in more than one module (common Widgets).
  • models — contains all the model classes, and entities representing the business-related data, like product, cart, cart item, etc.;
  • pages — stores Flutter Widgets which represent the actual pages that have a specific route to them;
  • repositories — in simple words, if the module needs to load some kind of data from the database, third-party API, internal storage or any other data source, we store those classes here. Also, this directory could be named as, or you could even have both, repositories and services, directories if you see a need for that. Repositories should provide high-level endpoints/functions to retrieve the data without exposing the implementation details on how this data was retrieved and/or formatted. This way, you just simply use these functions in your BLoC classes to get the data you need. This is called a Repository Pattern, you should definitely learn more about it;
  • widgets — all the UI components, which belong to this module, should be stored here.

State management

https://bloclibrary.dev/
  • Caching. By using the flutter_bloc library, it is very easy to extend it with the logic from the hydrated_bloc — this package automatically persists and restores BLoC and cubit states. As a result, the application’s state is stored locally and could be restored e.g. after the application is turned off and turned on again or the internet connection is lost.
  • Testability. In the BLoC packages ecosystem, there is another one called bloc_test. This package makes testing BLoCs and cubits easy — you just simply choose the BLoC to test, if needed, seed it with the required data, trigger some events and verify the transitions between states or just the final state. This way, BLoC classes could be tested independently, and you could verify their behaviour even before building any UI components.

Reinventing the wheel

Wheel v18.0 (source)

Code generation

Another thing to consider is code generation. In Flutter, if you want to compare one class object to another not by their reference, but by their internal values, you have to override the equality operator (==). Also, another common use case is when you want to serialize and deserialize the data from/to JSON, e.g. while making HTTP requests and later mapping the response to your custom models. If you have a lot of such classes, there is A LOT of boilerplate code you need to write. Thus, using code generators like json_serializable or freezed is a way to go in this case. By using them, you will write less boilerplate code as well as avoid some frustrating bugs which could be overlooked by writing the same code over and over again or copy-pasting it from another class — it happens once in a while, doesn't it?

Test, test and then test once more

That is the question (source)
  1. Code quality. One of the easiest ways to identify architectural code flaws is to cover them with tests. If you could not manage to mock a specific dependency or you could not reach a specific part of the code, usually the problem is not that you are bad at writing tests, but the code itself is poorly written.
  2. Time. This one is pretty straightforward — if you cover your code with tests, you save your time. That could sound like a paradox, but it’s actually true. There is nothing worse than adding a new feature to the app and later finding out that the old features are not working properly. You could avoid that by writing some tests, so why not just simply do that?

I will do it later…

Better late than never, right? If you do not follow the idea of TDD, there is nothing wrong to add tests after implementing the code, but it should be done iteratively, e.g. after implementing a single screen or feature.

Target — 100% code coverage

In the beginning, it could sound like a utopia, but it is very achievable. Flutter offers a lot of tools to write unit, widget and integration tests. While building the prototype, I haven’t used anything to measure the actual code coverage with tests (e.g. the test_coverage package should do the trick), but it’s a good practice to set the specific code coverage goal, measure and never go below it. As always, start low (like 60–70%, which isn’t that low, if you think about it) and after getting comfortable with writing tests in Flutter, you will notice that more and more code is covered in tests and it becomes easier to do that. Then, raise the bar and thrive for that 100%.

Summary

These were the main insights and ideas behind this (not so) simple Flutter Shopping App prototype. Now, I would recommend diving deeper into the code, seeing how different patterns were applied, how the state management is implemented, what classes were generated and so on. If you have any questions — do not hesitate to ask me directly, I am more than happy to answer them. See you next time!

Your contribution

👏 Press the clap button below to show your support and motivate me to write better!
💬 Leave a response to this article by providing your insights, comments or requests for future articles.
📢 Share this article with your friends, colleagues on social media.
➕ Follow me on Medium and check other articles.
⭐ Star the Github repository.

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store