In this article, I’m going to go into details about the code of this slide puzzle: the app architecture, the folder structure, the application structure, and so on.
Dashtronaut’s codebase is divided into 3 main layers: Data, Business Logic, and Presentation.
Contains the data sources, in this case, the local storage service using Hive and the service locator pattern to inject the service (more on that in a bit)
This layer also contains the models, which define the data format of the entities in the app: Puzzle, Tile, Position, Location, and Score. Most of those models also handle serialization into raw data (json) that is stored in/fetched from local storage. This is needed for the functionality of storing the user’s progress and score history in local storage.
This is where most of the business logic is. Aside from some logic inside the models, more complex application business logic is written in this layer inside ChangeNotifier providers.
For example, the PuzzleProvider has logic for generating the puzzle, updating tile locations, handling keyboard events, and so on. The StopWatchProvider handles the stop-watch logic. And the PhrasesProvider handles the logic for the phrases that Dash speaks in phrase-bubbles throughout the user flow.
What the user sees and interacts with. It contains the UI Widgets for the various application modules like the background, the puzzle, the app drawer, Dash, and the phrases along with the styles and layout delegates. The ChangeNotifier providers also belong to this layer as they receive user input from the UI and call notifyListeners() to update that UI accordingly.
Let’s see how that architecture is reflected in the folder structure. Inside the lib folder, you will find the following folders:
The service locator pattern is a design pattern in which an abstraction layer is introduced when obtaining a service, and there is a central registry, the “Service Locator” that is responsible for registering those services and in turn allows you to access them from anywhere in your app.
In practice, you create the interface of the service (the abstract class), and you implement this interface separately. Because the service locator returns the abstract interface, this separation allows for the ability to swap the implementation with a mock easily in testing, or swap it with an implementation that uses another package.
Let’s see this in action. For the local storage functionality in dashtronaut, you can find the abstract class lib/services/storage/storage_service.dart
abstract class StorageService {
Future<void> init();
Future<void> remove(String key);
dynamic get(String key);
Future<void> clear();
bool has(String key);
Future<void> set(String? key, dynamic data);
}
This interface is implemented in the lib/services/storage/hive_storage_service.dart file by populating the methods with Hive-specific functions.
But how is this service accessed throughout the app? Using the GetIt package as a service locator. This is the code for locating the storage service, found in the lib/services/service_locator.dart file:
final getIt = GetIt.instance;
setupServiceLocator() {
getIt.registerSingleton<StorageService>(HiveStorageService());
}
After calling the setupServiceLocator function before your runApp function, you can call this service anywhere in your app like so:
final StorageService storageService = getIt<StorageService>();
(This came in very handy for me when I migrated from the shared_preferences to the hive package in a previous project)