# Stacked Architecture Document
---
This document includes the suggestions and steps to add features, dependencies, views and other changes that we can do in the application that uses stacked architecture.
For example in the fork of your [project](https://github.com/saileshbro/seems) we will be adding the dependencies, views, repositories, services, api calling guide, error handling and so on.
## Things that are to be discussed
We will discuss the various things that are listed below
- [x] [Folder Structure](#Folder-Structure)
- [x] [Stacked Architecture Layers](#Stacked-Architecture-Layers)
- [x] [Adding a new view](#Adding-a-View)
- [x] [Adding a service](#Adding-a-Service)
- [x] [Adding a new dependency](#Adding-a-Dependency)
- [x] [Adding a new viewmodel](#Adding-new-ViewModel)
- [x] [Types of ViewModels](#Types-of-ViewModels)
- [x] [Localization](#Localization)
- [ ] Making a new datamodel
- [ ] Making a api request/response model
- [ ] Making a data model
- [x] PR for language selector
- [x] [Working of `KDataTable` widget](#Working-of-KDataTable-widget)
### Folder Structure
```
|-app
|-common
|---extensions
|---helpers
|---error
|---ui
|-----widgets
|-----functions
|-constants
|-datamodels
|---api_models
|---models
|-repository
|-services
|-theme
|-features
```
1. `app` directory
The app directory contains the files that are used through out the app. Some of the files that it may contain are `app.dart` which houses the core functionality of the application including the dependency injection, routing, logging e.t.c. The files that are using in our app are `app.locator.dart`, `app.logger.dart`, `app.router.dart` e.t.c.
Since the `app.dart` file will hold all the dependency registration and routes registration, this file may be very big and difficult to navigate. To help us navigate much better, we have seperated the dependencies and routes registration to seperate files using `part` and `part of`.
2. `common` directory
This directory contains the common helper methods, widgets that are not related to any views and can be used anywhere, ui helpers that are useful for doing the ui, error classes, helpers methods and dart extensions.
Some of the folders that are listed in the directory tree are not implemented in our project. We can easily refer to the structure in this document and extend the directory. - `extensions` directory
This directory will hold all the dart extensions that are being used in the project. So that if will be easier for us to import from a single location. - `helpers` directory
This directory will hold the helper methods like validation, password hashing and so on. This directory are for those helper methods that are not used to build ui. - `error` directory
This directory is for registering the custom error object. In out case its `NetworkFailure`. This can also include the functions and callbacks which will be invoked whenever an error has occured. - `ui` directory
This directory will hold all the common ui related code. - `functions` directory
In this directory we will add the helper methods that are related to ui. For example like screen responsibe helpers, empty space returing functions and so on. - `widgets` directory
This directory will hold all the widgets that are used throughout the apps and that are not related to only a single view. For example custom made input form field, custom made button, custom made popup and so on.
3. `constants` directory
This directory contains the files that holds the constants and when changed, affects the entire application.
4. `datamodels` directory
This directory contains the data models that are used to serialize and deserialize the JSON response from the API. These models are immutable and are mainly used for serialization and deserialization purposes. - `api_models` directory
This folder only stores those models that only represend the data response from and to the api. This can also be called as data transfer objects. For example this can store `login_response_model.dart`. Here in this response model, we may get the details of the user. This user details will be the field in this response model. - `models` directory
This directory contains those models that can be or cannot be used in the `api_models` directory. For example this directory will contain `user_model.dart` containing a class `UserModel`. Here this `UserModel` will be used in `LoginResponseModel` class for serialization and deserialization.
We will be using [json_serializable](https://pub.dev/packages/json_serializable) package to generate serialize and deserialize the models.
> To run the generator to generate the codes that are required for serialization and deserialization use `flutter pub run build_runner watch`. This will run the build runner in watch mode and re-generate the codes once it detects the changes.
> If we just want to get the code and dont want it to watch the changes, we can instead run this command `flutter pub run build_runner build`.
> If the `build_runner` shows error related to files conflict then use `--delete-conflicting-outputs` to delete the conflicting outputs
5. `repository` directory
This directory contains the classes which act as the data source which consumes the services and returns the data. The files in this directory are injected with the services and they use services to get and send data. The repositories can be seperated based on the feature and it is better to have different repository for each feature.
6. `services` directory
This directory contains the files that do a single purpose job. For example a camera service is responsible for opening the camera, capturing a picture, getting information of that picture, and so on.
The services are used inside the viewemodels, repository and so on.
7. `themes` directory
The `themes` directory contains the files that are used to set multiple themes for the application. This directory may also include the files that are related to the global style like typography, decorations e.t.c.
8. `features` directory
This directory contains all the feature, for example `auth`, `home`, `profile` e.t.c. Each of feature contains the ui, widgets and viewmodels that are related only to this feature and are not related to any of the other features.
---
### Stacked Architecture Layers
In the big picture, this architecture mainly contains 3 layers:
1. `View`
This layer shows the ui to the user. A view consumes the state from the `ViewModel` and renders it.
2. `ViewModels`
This layer manages the state of the view, business logic and any other logic that are required for the user interaction. This is achieved by making use of the services.
3. `Services`
The service is a wrapper of single functionality of a feature. This is commonly used to wrap things like showing a dialog, wrapping database functionality, integrating an API e.t.c.
Some of the rules that are to be followed while working with this architecture:
- Views should never make use of the services directly. The view should only make use of the service through `ViewModel`.
- Views should not contain any logic. If the logic is from UI-only items, minimal logic is done in the view and rest is passed to the `ViewModel`.
- View are the function of ViewModel. What it means is that the view rendered only depends on the `ViewModel` state.
- ViewModels can be reused if some other view requires similar functionality.
- ViewModel should not depend on other ViewModel. If any two ViewModel needs to communicate, then this communication is done via a service.
---
### Adding a View
Suppose we need to add a new view to the application. We need to handle two cases here.
1. If the view is static
If the view is static, that is, if the view doesnot have any state and only shows a single view. Lets say this view displays the information of the app, say its `about_us_view`.
For this we need to create this file inside `features/about_us/about_us_view.dart`. Since this view is static, we dont need to create a `ViewModel`.
Then ceate a stateless widget in this file which displays the about us information.
```dart=
import 'package:flutter/material.dart';
class AboutUsView extends StatelessWidget {
@override
Widget build(BuildContext context) {
return const Scaffold(
body: Center(
child: Text('About Us'),
),
);
}
}
```
Now after creating a view, we need to register this view in the `app/routes.dart` file so that the code generator can generate the code for registering this route in `onGeneratedRoute` method, and for named routing.
> After adding it to the `app/routes.dart` file you need to run the build runner command to generate the code.
2. If the view is dynamic
If the view is dynamic, that is, if the view depends on the state, then we need to create a `ViewModel`.
Lets say that this `AboutUsView` depends on the information that is fetched from the API, then we will be needing a `AboutUsViewModel`. Create `about_us_viewmodel.dart` inside `app/about_us`.
```dart=
import 'package:stacked/stacked.dart';
class AboutUsViewModel extends BaseViewModel {}
```
After creating this viewmodel we need to register this in the dependencies section.
Go to dependencies array inside `app/dependencies.dart` and add the entry as a `Factory`
```dart=
Factory(
asType: AboutUsViewModel,
classType: AboutUsViewModel,
)
```
And then run `flutter pub run build_runner watch --delete-conflicting-outputs`
This will generate the code required for the registrstion of this viewmodel in the locator.
After registering the view and viewmodel, we can navigate to this view from the `NavigationService`
Inject this `NavigationService` in the viewmodel and you can use `navigateTo()` method to navigate to this view.
```dart=
final NavigationService _navigationService = locator<NavigationService>();
void toAboutUs(){
_navigationService.navigateTo(Routes.aboutUsView);
}
```
Now you can call this `toAboutUs` method from the view via viewmodel doing something like this.
```dart=
GestureDetector(
onTap: model.toAboutUs
child:Text("To About Us"),
),
```
---
### Adding a Service
To understand this better lets create a new service that handles the permissions. We will say this service called `PermissionHandlerService`. This service will be responsible for handling the permissions i.e. to ask for the permission and to keep track if the permission has been granted.
```dart=
import 'package:permission_handler/permission_handler.dart'
class PermissionService{
Future<bool> _requestPermission(Permission permission)async{
final status = await permission.request();
return status == PermissionStatus.granted;
}
Future<bool> requestLocationPermission(){
return _requestPermission(Permission.locationWheninUse);
}
}
```
Lets say this is the service. Now this service can be injected anywhere and can be used anywhere in the viewmodel. To see how the sercice is injected, [goto](#Adding-a-Dependency)
---
### Adding a Dependency
To add a dependency, we need to add it to the dependencies array inside `app/dependencies.dart`
Lets add the `PermissionService` that we created above. To add this, we can add a entry in dependencies array.
```dart=
LazySingleton(
asType: PermissionService,
classType: PermissionService,
)
```
And then run `flutter pub run build_runner watch --delete-conflicting-outputs` which will generate the code that is required to register this dependency.
---
### Adding new ViewModel
To add a new ViewModel we just need to extend a class with `BaseViewModel` and add it in the dependency array.
---
### Types of ViewModels
There are multiple types of viewmodels in stacked architecture. Some of them are listed below with their purpose.
1. `BaseViewModel`
This is the basic viewmodel from which all the other special viewmodels are extended.
2. `FutureViewModel`
This is the special viewmodel which is used to handle the futures. This viewmodel is extended from the `BaseViewModel` class and has some helper methods which handles the busy states of the future internally. While using this viewmodel we need to override the `futureToRun()` method which returns the furure. This handles the busy state of the future internally and also sets the error if the error occurs.
```dart=
class ExampleFutureViewModel extends FutureViewModel<String> {
@override
Future<String> futureToRun() => getDataFromServer();
Future<String> getDataFromServer() async {
await Future.delayed(const Duration(seconds: 3));
return 'this is the random data from the server';
}
}
```
3. `StreamViewModel`
This is the special viewmodel which is used to handle the streams. This viewmodel is also extended from the `BaseViewModel` and contains the helper methods to handle the stream. Some of them are listed below.
* `stream`: this is the getter which is required to be overridden which retutns the stream that is to be listened to
* `onData`: this is the listener which can be overriden to listen to the data in the stream
* `onCancel`: This is the callback which fires whenever the stream that the viewmodel is listening to closes.
* `onSubscribed`: this is the method that runs whenever the stream is subscribed to
* `onError`: this method is run whenever the error is appeared in the stream
<br>
```dart=
class StreamCounterViewModel extends StreamViewModel<int>{
String get title=>'this is the title';
@override
Stream<int> get stream => locator<CounterService>().counterStream;
}
```
4. `MultipleFutureViewModel`
This viewmodel lets run multiple futures. We need to provide the map of type strings along with the function that returns a future that will be executed after the viewmodel instance has been created.
```dart=
class ExampleMultipleViewModel extends MultipleFutureViewModel{
static const String dateFuture='DateFuture';
static const String timeFuture='TimeFuture';
String get fetchedDate => dataMap[dateFuture];
String get fetchedTime => dataMap[timeFuture];
bool get fetchingDate => busy(dateFuture);
bool get fetchingTime => busy(timeFuture);
@override
Map<String,Future Function()> get futuresMap => {
dateFuture : getDate,
timeFuture : getTime,
}
Future<String> getDate() async {
await Future.delayed(Duration(seconds:3));
return "Date";
}
Future<String> getTime() async {
await Future.delayed(Duration(seconds:3));
return "Time";
}
}
```
5. `MultipleStreamViewModel`
This allows us to handle multiple streams from a single viewmodel through string stream pairing. For every value emitted from the stream the `notifyListeners()` will be called and the UI will be built again.
```dart=
class MultipleStreamExampleViewModel extends MultipleStreamViewModel{
static const String dateStream = "dateStream";
static const String timeStream = "timeStream";
@override
Map<String,StreamData> get streamsMap => {
dateStream : StreamData<String>(dateStream()),
timeStream : StreamData<String>(timeStream()),
}
Stream<String> dateStream() async*{
Random random = Random();
while(true){
await Future.delayed(Duration(milliseconds:random.nextInt(2000)));
yield generateRandomString(random.nextInt(999));
}
}
Stream<String> timeStream() async* {
Random random = Random();
while(true){
await Future.delayed(Duration(milliseconds:random.nextInt(2000)));
yield generateRandomString(random.nextInt(999));
}
}
String generateRandomString(int len) {
final r = Random();
return String.fromCharCodes(List.generate(len, (index) => r.nextInt(33) + 89));
}
}
```
6. `ReactiveViewModel`
This viewmodel is used to listen to the services that are being used in the viewmodel.
7. `IndexTrackingViewModel`
This viewmodel is handy to track the index of the widgets sucn as bottom navbar, page view, side drawer e.t.c.
---
### Localization
To add the localization we will have to split all the locales inthe app to different files. Inside `assets/lang` create the locale files `en_US.json` and `zh_CN.json` file.
After adding these assets we need to update the assets in the `pubspec.yaml` file.
We will have to add `flutter_localizations` package inside `pubspec.yaml` file.
```yaml=
...
cupertino_icons: ^1.0.2
flutter_localizations:
sdk: flutter
...
```
Inside `lib/localizations` create a new file called `app_localizations.dart`. This file will have the logic that is required to get the strings from the json file and supply it down the widget tree.
Create a class called `AppLocalizations` which takes a locale from the constructor. This class is necessary because we will be getting the locale data from the json file and pass it to this class.
```dart=
class AppLocalizations{
final Locale locale;
late Map<String,dynamic> _localizedStrings;
AppLocalizations({
required this.locale,
});
static AppLocalizations? of(BuildContext context) =>
Localizations.of<AppLocalizations>(context,AppLocalizations);
String translate(String key) => _localizedStrings[key] as String? ?? key;
Future<bool> load() async {
final jsonString = await rootBundle.loadString("assets/lang/$locale.json");
final jsonMap = json.decode(jsonString) as Map<String,dynamic>;
_localizedStrings = jsonMap.map((key,value) => MapEntry(key,value));
return true;
}
static const LocalizationsDelegate<AppLocalizations>() delegate => _AppLocalizations();
}
class _AppLocalizationDelegate extends LocalizationDelegate<AppLocalizations>{
const _AppLocalizationDelegate();
@override
bool isSupported(Locale locale){
return Constants.languages.contains(locale);
}
@override
Future<AppLocalizations> load(Locale locale) async {
final AppLocalizations localizations = AppLocalizations(locale: locale);
await localizations.load();
return localizations;
}
@override
bool shouldReload(covariant LocalizationsDelegate<AppLocalizations> old) => false;
}
```
Now we can use this `AppLocalizations` class to get the locale data. But first we need to register the delegate in the `MaterialApp`.
```dart=
MaterialApp(
debugShowCheckedModeBanner: false,
...
locale: model.appLocale,
supportedLocales: Constants.languages,
localizationsDelegates: const [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
],
...
);
```
Now we can get the locale information via `AppLocalizations.of(context).translate("jsonKey")`. But just to make things easier, we will create a extension on BuildContext.
Inside `common/extensions` create a new `context.dart` file.
```dart=
extension BuildContextExtensions on BuildContext {
String tr(String s) {
return AppLocalizations.of(this)!.translate(s);
}
}
```
Now we can just import this extension and do `context.tr("jsonKey")`
Now for dynamic switching of the locale we will create a service called `LanguageService`
Inside `services` create a new file called `language_service.dart`
```dart=
class LanguageService{
Locale _locale = Constants.languages.first;
final _sharedPreferences = locator<SharedPreferencesSerice>();
Locale get locale => _locale;
final StreamController<Locale> _localeStream = StreamController<Locale>.broadcast();
Stream<Locale> get localeStream => _localeStream.stream;
LanguageService(){
_locale = _sharedPreferencesService.locale;
_localeStream.add(locale);
}
Future<void> changeLocale(Locale locale) async{
await _sharedPreferences.setLocale(locale);
_locale = locale;
_localeStream.add(locale);
}
void dispose(){
_localeStream.close();
}
}
```
After creating the service we will register this service in the dependencies array as `LazySingleton`
Now we will have to consume this service in a viewmodel. Since this viewmodel is not associated to any feature, we will add this inside `localizations` folder. Create a new file called `localization_viewmodel.dart`.
We will make use of the `StreamViewModel` since we will be listening to the `localeStream` from the `LanguageService`.
```dart=
class LocalizationViewModel extends StreamViewModel{
final _languageService = locator<LanguageService>();
@override
Stream get stream => _languageServices.localeStream;
Locale get appLocale => _languageService.locale;
@override
void dispose(){
_languageService.dispose();
super.dispose();
}
}
```
Now we can use this viewmodel in `main.dart` file by wrapping the `MaterialApp` widget with `ViewModelBuilder.reactive<LocalizationViewModel>()`
```dart=
return ViewModelBuilder<LocalizationViewModel>.reactive(
viewModelBuilder: () => locator<LocalizationViewModel>(),
builder: (context, model, child) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'SEEMS Mobile App',
theme: kDarkTheme,
locale: model.appLocale,
supportedLocales: Constants.languages,
localizationsDelegates: const [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
],
onGenerateRoute: StackedRouter().onGenerateRoute,
navigatorKey: StackedService.navigatorKey,
);
},
);
```
Now anywhere we need to change the locale, we will inject the `LanguageService` from the `locator` and call the `changeLocale()` method.
---
### Working of `KDataTable` widget
Lets see how we can use this widget by creating a sample view. In `features` create a new feature called `data_table_example` and then create a new view inside it.
```dart=
import 'package:flutter/material.dart';
class DataTableExample extends StatefulWidget {
const DataTableExample({Key? key}) : super(key: key);
@override
_DataTableExampleState createState() => _DataTableExampleState();
}
class _DataTableExampleState extends State<DataTableExample> {
@override
Widget build(BuildContext context) {
return Scaffold();
}
}
```
Now we will make use of the `KDataTable` widget in scaffold body. The `KDataTable` widget takes following arguments.
```dart=
required this.columns,
required this.rowCount,
required this.cellBuilder,
```
The `columns` list is a list of columns, which will always stay at the top of the list.
The `rowCount` is the count for all the data rows, just like in list view.
The `cellBuilder` is the builder function which is used to build each cell for given row index and column index.
The example usage of this widget is shown below.
```dart=
class _DataTableExampleViewState extends State<DataTableExampleView> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: KDataTable(
columns: List.generate(
6,
(index) => Text(
'COL${index + 1}',
style: const TextStyle(color: Colors.black, fontSize: 24),
),
),
rowCount: 200,
backgroundColor: Colors.white,
cellBuilder: (BuildContext context, int i, int j) =>
Text('R${i + 1}:C:${j + 1}'),
),
);
}
}
```
which produces the output as
