{"id":16319974,"url":"https://github.com/roubachof/falotier_riverpod","last_synced_at":"2025-03-20T22:31:04.687Z","repository":{"id":86503506,"uuid":"510312817","full_name":"roubachof/falotier_riverpod","owner":"roubachof","description":"The purpose of this PoC is to implement main real life app use cases and see if Riverpod elegantly supports all the needed mutations.","archived":false,"fork":false,"pushed_at":"2024-09-30T09:23:12.000Z","size":9148,"stargazers_count":42,"open_issues_count":0,"forks_count":2,"subscribers_count":3,"default_branch":"main","last_synced_at":"2024-10-11T22:28:57.930Z","etag":null,"topics":["design-system","flutter","flutter-examples","freezed","gradients","immutable-collections","proof-of-concept","riverpod","theme-ui"],"latest_commit_sha":null,"homepage":"","language":"Dart","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/roubachof.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2022-07-04T10:26:43.000Z","updated_at":"2024-09-30T09:23:16.000Z","dependencies_parsed_at":"2024-10-28T09:07:36.493Z","dependency_job_id":"b8522753-2684-4723-865d-6f008ec51c69","html_url":"https://github.com/roubachof/falotier_riverpod","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/roubachof%2Ffalotier_riverpod","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/roubachof%2Ffalotier_riverpod/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/roubachof%2Ffalotier_riverpod/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/roubachof%2Ffalotier_riverpod/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/roubachof","download_url":"https://codeload.github.com/roubachof/falotier_riverpod/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":244085007,"owners_count":20395523,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["design-system","flutter","flutter-examples","freezed","gradients","immutable-collections","proof-of-concept","riverpod","theme-ui"],"created_at":"2024-10-10T22:28:47.336Z","updated_at":"2025-03-20T22:31:04.681Z","avatar_url":"https://github.com/roubachof.png","language":"Dart","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cimg width=\"500\" alt=\"falotier long logo\" src=\"https://user-images.githubusercontent.com/596903/201747914-2f0c5052-ca0c-4896-86e9-15e1abd44f42.png\"\u003e\n\nThe purpose of this PoC is to implement all real life app scenarios and see if the selected state management library elegantly supports all the needed mutations.\n\nThe selected state management library: [Riverpod](https://riverpod.dev).\n\n\u003cimg width=\"860\" alt=\"providers_home\" src=\"docs/list_and_providers.png\"\u003e\n\n\nI am a `Riverpod` enthusiast, but coming from a MVVM oriented world, I had my share of doubts and interrogations.\nI read a lot of examples, but I couldn't find a sample app that covers the scenarios I always meet when I am building an app for a client.\n\n\u003cimg width=\"760\" alt=\"Capture d’écran 2022-11-11 à 20 58 33\" src=\"https://user-images.githubusercontent.com/596903/201429767-1493fbac-4791-45aa-806a-5afc06eeb2aa.png\"\u003e\n\n\nSo I decided to see if I could build an app that supports the most classic use-cases from real-life app with `Riverpod`.\nAnd here is the result.\n\n## Document in progress\n\nFollow me on twitter @Piskariov, [linkedin](https://www.linkedin.com/in/alfonsi/) or subscribe to my blog [sharpnado](https://www.sharpnado.com/#/portal/signup) to receive all future explanations of falotier's implementation!\n\n- [x] Showcase `falotier_riverpod` ([blog post](https://www.sharpnado.com/falotier-riverpod))\n- [x] Enumerate all the app loading states\n- [x] General architecture\n- [ ] Implementation details of `loading from scratch`\n- [ ] Implementation details of `refreshing`\n- [ ] Implementation details of `list update`\n- [ ] Implementation details of `item details update`\n\n## Supported use-cases\n\nIn Falotier, I try to support as much real-life use cases.\\\nYou have a list of streets with a street lamp which is lit or not.\n\nAs a Falotier:\n* You can assign new streets to you by taping to the `Add Street` button,\n* You can lighten your burden by taping on the cross of an item,\n* You must lit the street lamp when the night is coming by taping on the street lamp bulb,\n* You must turn off the street lamp when the night is over with the same gesture.\n\nhttps://user-images.githubusercontent.com/596903/202012055-60936a90-c675-4506-86a9-18c7fcb08900.mp4\n\n\n### Loading from scratch\n\nThe loading of an empty screen.\\\nIf the loading is successful, we display our result.\\\nOr we display an informative message to the user and we give him the possibility to retry.\n\n![loading home](docs/loading_home.jpg)\n\n### Riverpod implementation\n\n[1. Simplest use case: only one dependency](load_from_scratch.md)\n\n### Refreshing\n\nWhen an item is already loaded and we want to refresh it.\\\nTypically the `pull-to-refresh` of a list.\\\nIf the refresh is successful, we update our whole list.\\\nIt there is an error during the refresh, we show a `SnackBar`.\n\n![refreshing home](docs/refresh_home.jpg)\n\n### Riverpod implementation\n\nComing soon...For now feel free to browse the code.\n\n### List update \n\n#### Adding a new item in the list\nWe tap on the `FloatingActionButton`, it opens a modal displaying our available streets.\\\nWe select one, we call an `update` method on our server side.\\\nDuring the call, we show an overlay.\\\nIf the remote call is successful we update our list state and close the modal.\\\nIf there was a error during the call, we display a `SnackBar` to our user.\n\n#### Riverpod implementation\n\nComing soon...For now feel free to browse the code.\n\n![add item to a lits](docs/add_item.jpg)\n\n#### Removing an item from a list\n\nWe tap on an item cross.\\\nWe call a `remove` method on our server side.\\\nDuring the call, we transform our cross icon to a loading widget.\\\nIf the remote call is successful we update our list state and the item is removed.\\\nIf there was a error during the call, we display a `SnackBar` to our user.\n\nhttps://user-images.githubusercontent.com/596903/201427126-e86240b6-d567-4178-9adf-d65b2f532ba0.mp4\n\n#### Riverpod implementation\n\nComing soon...For now feel free to browse the code.\n\n### Item details update\n\nBy taping to a list item, we navigate to a street.\\\nBy taping on the street lamp, we toggle its light.\\\nThis is calling a method on the server side.\\\nDuring the remote call, we display an animation of the light which is growing or fading.\\\nIf it fails, the previous state is restored and we display a `SnackBar`.\n\n![update an item](docs/lamp_details_update.jpg)\n\nOur architecture must of course propagate the new immutable item to the item list.\n\nhttps://user-images.githubusercontent.com/596903/201427337-d67bbf4a-46ae-41df-b2ce-80f3d0b9cc94.mp4\n\n## The application architecture\n\nThere is a never-ending debate on how you should structure your application code.\nYou could use several architectures, but the selected architecture should be:\n\n1. Easy to understand\n\n* It means that an average developer shouldn't need a 2 weeks training to be able to understand it.\n* It's good to start from a well-known architecture, and adapt it to your needs. Well-known architecture have been adopted for a reason: they passed the test of time.\n\n2. Easy to modify\n\n* It means you don't need to implement 4 interfaces and 2 DTOs when you want to add a new entity type to your architecture. \n* It also mean you won't have to implement an abstract method from a grand parent object in order to fix a bug.\n* It probably means refrain yourself from using too much inheritance: always prefer composition vs inheritance.\n* Most importantly, it means that when a developer want to add a feature, he should know how to do it instantly. He shouldn't lost 3 days thinking about the best way to add the feature in the architecture.\n\n### A simplified DDD architecture\n\nFor nearly 10 years now I'm using a simplified `DDD` architecture that is widely adopted and understandable.\nIt means it uses concepts that everyone understand like: repositories, entities and services (providers).\n\n![collapsed-1](https://www.sharpnado.com/content/images/2022/12/collapsed-1.png)\n\nIt has 3 layers, from lowest to highest:\n\n1. `Infrastructure`\n\nThe is the lower technical stuff like logging, tools, storage...\n\n2. `Domain`\n\nThis is where you put all your business logic, your repositories, your entities, your services.\nThe difference with classic `DDD` is **that services are here implemented with providers**, achieving a global reactive caching system.\n\n3. `Presentation`\n\nThis is all your UI stuff, routes, pages, widget, animation, etc...\n\n`Presentation` depends on `Domain` which depends on `Infrastructure`.\nThis is the **only** way allowed.\nYour infrastructure **cannot** reference your domain layer for example.\n\n#### What about the Application layer ?\n\nWell, first people tend to misunderstand this layer coined in the blue book and you have as many interpretations as implementations. \nFor example, I saw people mixing the concept of domain services with application services.\n\nBut mostly: because you don't need another layer.\nKeep it simple.\n\n\n#### Why not a feature-driven architecture?\n\nLately, I saw some posts and articles promoting feature-oriented architecture.\nIt means that you will reproduce this DDD architecture for each feature, for example:\n\n```\n‣ lib\n  ‣ src\n    ‣ features\n      ‣ feature1\n        ‣ presentation\n        ‣ domain\n        ‣ infrastructure\n      ‣ feature2\n        ‣ presentation\n        ‣ domain\n        ‣ infrastructure\n```\n\nI already tried this approach in the past, and it will fail to meet the \"easy to modify\" rule.\n\nFirst you have to choose what a feature is:\\\nIs the feature related to the domain or the UI?\n\nIf you choose to split your code according to the domain, you will spend your time trying to find your UI code.\n\nIf you choose to split your code according to your UI pages, you are basically perverting the domain with UI design.\n\nYou should think domain first.\nDoing that you will duplicate domain logic across your features.\nFor example let's consider an `Order` entity, on which you will apply all actions regarding orders like:\n* `Cancel`\n* `Update`\n* `Add`\n* `Confirm`\n* `GetHistory`\n\nWe can clearly see the service presenting all those actions will span across various features of your application like:\n* `OrderHistoryPage`\n* `ShoppingCartPage`\n* `OrderDetailsPage`\n\nSo what will you do with this service?\nSplit it in multiple services across all features?\nCreate a unique service shared across all features?\n\nYou will also think very deeply to the concept of feature:\n* Is it UI related?\n* Is it domain related?\n* How do you split all those concepts?\n\nThis will not be straightforward, and it's very likely that you will have to move things around cause you didn't get it right the first time...\nProductivity will suffer.\n\nThe simple \"classic\" layer architecture has a very dig advantage: it's straightforward, and everyone will understand where to find the code he wants.\n\nIt's not ideal, not maybe the most elegant code organization, but it's the most efficient I know so far.\n\n### 1. Infrastructure\n\n![infrastructure](https://www.sharpnado.com/content/images/2022/12/infrastructure.png)\n\nNot much to say.\nJust the really basic technical thing unrelated to the domain or the UI.\n\nIf I had a local storage implementation, i'd put it here for example.\n\n### 2. Domain\n\n![domain](https://www.sharpnado.com/content/images/2022/12/domain.png)\n\n#### Entities\n\nEach domain is grouped around an entity.\nHere we have our `StreetLamp` which is our main entity.\n\n```dart\npart 'street_lamp.freezed.dart';\n\nint streetLampComparator(StreetLamp s1, StreetLamp s2) {\n  return streetComparator(s1.street, s2.street);\n}\n\n@freezed\nclass StreetLamp with _$StreetLamp {\n  const StreetLamp._();\n  const factory StreetLamp({\n    required String id,\n    required Street street,\n    required bool isLit,\n  }) = _StreetLamp;\n\n  factory StreetLamp.fromStreet(Street street) {\n    return StreetLamp(id: \"new\", street: street, isLit: false);\n  }\n\n  @override\n  String toString() {\n    return 'StreetLamp( id: $id, name: ${street.name}, isLit: $isLit )';\n  }\n}\n```\n\nAll the entities are immutable thanks to [freezed](https://pub.dev/packages/freezed).\n\n#### Repositories\n\nWe retrieve entities from repositories.\nA repository can be local or remote.\nA local repository is most of the time a database like `Hive` or `Isar`;\n\nThe interesting thing, is that the consumer of the repository shouldn't know what kind of repository it is. He is just interested in getting those entities.\n\nThis is why we use interfaces: to establish a contract.\nSo the consumer only knows about the interface and nothing about the implementation.\nDoing so, we can easily swap an implementation for another.\n\n```dart\nimport 'impl/remote_repository_mock.dart';\nimport 'street_lamp.dart';\n\nfinal streetLampRemoteRepositoryProvider = Provider\u003cStreetLampRemoteRepository\u003e(\n    (ref) =\u003e ref.watch(streetLampRemoteRepositoryMockProvider));\n\nabstract class StreetLampRemoteRepository {\n  Future\u003cStreetLamp\u003e get(String id);\n  Future\u003cIList\u003cStreetLamp\u003e\u003e getList(CityZone zone);\n  Future\u003cStreetLamp\u003e addOrUpdate(StreetLamp streetLamp);\n  Future remove(StreetLamp streetLamp);\n}\n```\n\n##### Mocks\n\nI always use interfaces when I want to mock a repository or a service.\n\nWith mocks: \n* You can deeply test multiple scenarios for your application,\n* you can also create a local demo of your app.\n\nWe retrieve our entities from a mock repository, emulating a remote call to a server.\n\n```dart\nimport '../interfaces.dart';\nimport '../street_lamp.dart';\n\nfinal streetLampRemoteRepositoryMockProvider =\n    Provider\u003cStreetLampRemoteRepositoryMock\u003e((ref) =\u003e\n        StreetLampRemoteRepositoryMock(\n            ref.read(cityZoneRemoteRepositoryMockProvider)));\n\nclass StreetLampRemoteRepositoryMock implements StreetLampRemoteRepository {\n  static final _log = LoggerFactory.logger('StreetLampRemoteRepositoryMock');\n\n  static int _nextId = 2001;\n\n  final CityZoneRemoteRepositoryMock _cityZoneRemoteRepositoryMock;\n  final _emulator = RemoteCallEmulator(exceptionProbability: 0);\n\n  Map\u003cCityZone, Map\u003cString, StreetLamp\u003e\u003e? _zoneLamps;\n\n  StreetLampRemoteRepositoryMock(this._cityZoneRemoteRepositoryMock);\n\n  @override\n  Future\u003cStreetLamp\u003e addOrUpdate(StreetLamp streetLamp) async {\n    _log.i('addOrUpdate( $streetLamp )');\n\n    await _emulator.makeRemoteCall();\n\n    if (streetLamp.id == 'new') {\n      streetLamp = streetLamp.copyWith(id: (_nextId++).toString());\n    }\n\n    _zoneLamps![streetLamp.street.zone]![streetLamp.id] = streetLamp;\n\n    return streetLamp;\n  }\n   \n  ...\n}\n```\n\n#### Providers as services\n\nIn the DDD terminology we normally have services that will deal with our entities actions and states. Since we are using `Riverpod`, and to unlock the full potential of the library, we implement our services as providers.\n\n```dart\nimport 'street_lamp.dart';\n\npart 'providers.g.dart';\n\n@Riverpod(keepAlive: true)\nclass ZoneStreetLamps extends _$ZoneStreetLamps {\n  static final _log = LoggerFactory.logger('ZoneStreetLampsProvider');\n\n  @override\n  Future\u003cIList\u003cStreetLamp\u003e\u003e build({required CityZone zone}) async {\n    _log.i('build( isRefreshing: ${state.isRefreshing}, '\n        'isReloading: ${state.isReloading}, '\n        'hasValue: ${state.hasValue} )');\n\n    final repository = ref.watch(streetLampRemoteRepositoryProvider);\n    final lamps = await repository.getList(zone);\n    return lamps.sort(streetLampComparator);\n  }\n\n  Future addOrUpdate(StreetLamp streetLamp) async {\n    _log.i('addOrUpdate( $streetLamp )');\n\n    final repository = ref.read(streetLampRemoteRepositoryProvider);\n    final updatedLamp = await repository.addOrUpdate(streetLamp);\n\n    await update((currentList) {\n      final updatedList =\n          currentList.updateById([updatedLamp], (item) =\u003e item.id);\n      return currentList.length != updatedList.length\n          ? updatedList.sort(streetLampComparator)\n          : updatedList;\n    });\n  }\n\n  Future remove(StreetLamp streetLamp) async {\n    _log.i('remove( $streetLamp )');\n\n    final repository = ref.read(streetLampRemoteRepositoryProvider);\n    await repository.remove(streetLamp);\n\n    await update((currentList) {\n      return currentList.removeWhere(\n        (element) =\u003e element.id == streetLamp.id,\n      );\n    });\n  }\n}\n\n```\n\nUsing providers also bring a caching feature. \n\nSo we don't need a local repository to cache our objects in memory.\nOf course, if we'd need a persistent caching solution, we would need one, maybe implemented with `Hive` or `Isar`.\n\nHere we can see I use the `keepAlive` option so that my provider will have the same lifetime that my app. Using this property brings us domain-level in-memory caching.\n\n#### What about dependency injection?\n\nAll the idea behind `Riverpod` is to have a reactive states of all provided objects.\nThis is why we always want to use the `ref.watch` method. It's kind like a reactive service locator. Injecting dependencies through constructor would defeat the reactive nature of our architecture. Would defeat what makes `Riverpod` so great.\n\n#### What about naming?\n\nI made the choice to regroup providers and interfaces in a simple file respectively named, `providers` and `interfaces.dart`.\n\nThanks to modern IDE, we can use such simplification that could bring some clarity to our folders.\n\n![tabs](https://www.sharpnado.com/content/images/2022/12/tabs.png)\n\nThis is just a detail, but I find it quite easy to navigate through the code.\n\n### 3. Presentation\n\n![presentation](https://www.sharpnado.com/content/images/2022/12/presentation.png)\n\nUsually I try the folders follow the navigation of the app.\nBut when I have a very simple app, I just prefer to have them accessible at the same level.\n\nFor example, if I had an app with 3 tabs, that would be my top folders.\n\n#### Views\n\nThe page you can navigate to have `Screen` suffix.\nThe widget that are local to the screen are located in the same folder.\nIf a widget if reused through the app, it would go to a widget folder. Normally.\n\nBut in that app, I used an external package that bring all the design to the app.\nIt is added as a submodule in the `packages` folder.\n\n![design_package](https://www.sharpnado.com/content/images/2022/12/design_package.png)\n\nAll my styles and semantics are in this package. \nIt frees the presentation layer from all constants regarding spacing, colors, or even text sizes:\n\n```dart\nWidget build(context, ref) {\n  final theme = AppTheme.of(context);\n  final safePaddingTop = MediaQuery.of(context).padding.top;\n\n  ...\n\n  CustomScrollView(\n    slivers: [\n      SliverToBoxAdapter(\n        child: Stack(\n          children: [\n            Padding(\n              padding: const AppEdgeInsets.regular()\n                  .toEdgeInsets(theme)\n                  .add(EdgeInsets.only(top: safePaddingTop)),\n              child: Column(\n                crossAxisAlignment: CrossAxisAlignment.start,\n                children: [\n                  AppText.pageTitle(\n                    'Falotier',\n                    color: theme.colors.accent,\n                  ),\n                  const Padding(\n                    padding: EdgeInsets.only(right: 100),\n                    child: AppText.subtitleLarge(\n                      'A state management PoC, with a top hat.',\n                      maxLines: 2,\n                    ),\n                  ),\n                  const AppGap(AppGapSize.semiBig),\n                  const AppText.paragraphLarge(\n                      'The purpose of this PoC is to implement all real life app '\n                      'scenarios and see if the selected state management library '\n                      'elegantly supports all the needed mutations.'),\n                  const AppGap(AppGapSize.regular),\n                  SizedBox(\n                    height: 50,\n                    child: Row(\n                      crossAxisAlignment: CrossAxisAlignment.center,\n                      children: [\n                        const AppText.subtitleMedium('City'),\n                        const AppGap(AppGapSize.regular),\n                        const CityDropDown(),\n                        const Spacer(),\n                        Chip(\n                            backgroundColor: theme.colors.accent,\n                            label: AppText.paragraphLarge(\n                              'Riverpod',\n                              color: theme.colors.onAccent,\n                            ))\n                      ],\n                    ),\n                  ),\n                ],\n              ),\n            ),\n```\n\nIn a company you're likely to have a design system created by your designers.\nThis package is simply the implementation of the design system.\nHaving a package allow you to quickly reuse this design system in another app.\n\nFor the implementation of a design system as a dart package, I followed [Aloïs Deniel](https://github.com/aloisdeniel) guidance.\nPlease have a look at [this great video](https://www.youtube.com/watch?v=lTy8odHcS5s).\n\n#### ViewModel, Controllers, Models, ...\n\nI came from the `C# WPF Xamarin MVVM` world, so, of course I tried to reproduce mvvm in `Flutter`. You could use mvvm in flutter, but I would advise you to use a mvvm oriented library for this like [stacked](https://pub.dev/packages/stacked) or even bloc or cubit.\n\nIf if you want to unleash the reactive power of riverpod, you need to use providers and watch the states of your objects. So you don't want an intermediate layer between your providers and your widgets.\n\nBut the good news is that you can still have providers at the presentation-level depending on those at the domain-level.\n\nLet's have a look at the `providers.dart` file in the `home` folder:\n\n```dart\nclass SelectedZone extends _$SelectedZone {\n  static final _log = LoggerFactory.logger('SelectedZoneProvider');\n\n  @override\n  Future\u003cCityZone\u003e build() async {\n    _log.i(\n        'build( isRefreshing: ${state.isRefreshing}, isReloading: ${state.isReloading}, hasValue: ${state.hasValue} )');\n\n    // Domain need to be initialized\n    await ref.watch(domainInitializerProvider.future);\n\n    final zones = await ref.watch(availableZonesProvider.future);\n    _log.listCount(zones);\n    return zones[0];\n  }\n\n  @override\n  bool updateShouldNotify(\n    AsyncValue\u003cCityZone\u003e previous,\n    AsyncValue\u003cCityZone\u003e next,\n  ) {\n    if (previous.hasValue \u0026\u0026 next.hasValue) {\n      bool shouldUpdate = previous.value!.id != next.value!.id;\n      _log.i('shouldUpdate: $shouldUpdate');\n      return shouldUpdate;\n    }\n\n    return super.updateShouldNotify(previous, next);\n  }\n\n  void reload() {\n    _log.i('reload()');\n\n    ref.invalidate(availableZonesProvider);\n  }\n\n  select(CityZone zone) {\n    _log.i('select( $zone )');\n    update((previousZone) =\u003e zone);\n  }\n}\n```\n\nThis provider keep the state of the selected city in the home page.\n\n![selected_zone](https://www.sharpnado.com/content/images/2022/12/selected_zone.png)\n\nBut it's just maintaining the state in the UI layer, so you can have different caching strategy or lifetime than your domain layer.\n\nYou can also use providers to transform a domain object to a UI object. If you like it that way. Nowadays I just put computed properties that will be displayed in my UI as late properties in my domain objects... \n\nDon't waste time with complexities that add no values.\n\n### Wrapping-up\n\n`Falotier` is following a very simplified DDD architecture.\nThe most important aspect of architecting apps, is the simplicity of it.\n\nDon't try to reinvent the wheel, just adapt a well-known architecture to your needs.\nNever forget that you're not alone in your team (well if you are, do whatever you want :), always think about your colleagues that will suffer from your incredible state of the art abstractions requiring DTO, adapters, mappers, Unit of Work and interceptors.\\\nMake all things obvious.\n\nRiverpod is the most impressive state management library I've seen for many years (I built a lot of C# architecture). To display its full power you have to adapt yourself, you have to implement its philosophy.\n\nDon't resist: embrace it :)\n\n\n## The design\n\nI implemented a theme with a separate flutter project called falotier_design.\\\nIt's like I had a design system and implemented it with a theme, giving semantics to colors, spaces, text, etc...\\\nI also have a collection of `Widgets` in this library that I use in my apps.\n\nFor this I followed [Aloïs Deniel](https://github.com/aloisdeniel) guidance.\\\nPlease have a look at [this great video](https://www.youtube.com/watch?v=lTy8odHcS5s).\n\nYou can also [check its repo](https://github.com/aloisdeniel/asgard_shop).\n\n### The street details composition\n\nThe street detail screen is built dynamically with composition of image and stacks of gradients.\n\nComing soon...For now feel free to browse the code.\n\n## The story behind\n\nComing soon...For now feel free to imagine it.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Froubachof%2Ffalotier_riverpod","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Froubachof%2Ffalotier_riverpod","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Froubachof%2Ffalotier_riverpod/lists"}