{"id":27918148,"url":"https://github.com/misterjimson/beyond","last_synced_at":"2025-07-19T10:09:58.677Z","repository":{"id":56826539,"uuid":"221293873","full_name":"MisterJimson/beyond","owner":"MisterJimson","description":"An approach to scalable Flutter development","archived":false,"fork":false,"pushed_at":"2021-04-01T17:39:03.000Z","size":286,"stargazers_count":56,"open_issues_count":1,"forks_count":8,"subscribers_count":7,"default_branch":"master","last_synced_at":"2025-05-06T18:19:30.071Z","etag":null,"topics":["architecture","demo","example","flutter","state-management"],"latest_commit_sha":null,"homepage":"","language":"Dart","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/MisterJimson.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2019-11-12T19:19:29.000Z","updated_at":"2025-02-24T02:56:43.000Z","dependencies_parsed_at":"2022-09-20T22:02:09.956Z","dependency_job_id":null,"html_url":"https://github.com/MisterJimson/beyond","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/MisterJimson/beyond","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MisterJimson%2Fbeyond","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MisterJimson%2Fbeyond/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MisterJimson%2Fbeyond/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MisterJimson%2Fbeyond/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/MisterJimson","download_url":"https://codeload.github.com/MisterJimson/beyond/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MisterJimson%2Fbeyond/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":265915154,"owners_count":23848491,"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":["architecture","demo","example","flutter","state-management"],"created_at":"2025-05-06T18:19:22.591Z","updated_at":"2025-07-19T10:09:58.650Z","avatar_url":"https://github.com/MisterJimson.png","language":"Dart","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Beyond\n[![Actions Status](https://github.com/MisterJimson/beyond/workflows/Test/badge.svg)](https://github.com/MisterJimson/beyond/actions)\n\nAn example app that demonstrates an approach to scalable Flutter app development\n\nMost Flutter content that currently exists focuses on state management and I hope this can dig into other areas a bit more. _Beyond just state management_\n\nI am open to feedback, suggestions, and discussions on how this approach can be improved.\n\nIt's important to note that this is not meant to prescribe how everyone should write Flutter apps nor that this is the best option for every project.\n## Example App Setup\n1. Get a free API key from https://locationiq.com/\n2. Open `lib/service/config/config.dart` and enter your API key\n## Beyond Helpers\n[Beyond Helpers](https://github.com/MisterJimson/beyond/tree/master/beyond_helpers) is a collections of minor utilities to help with implementing this approach\n# Architecture Overview\nThe rest of this document describes how apps that use this approach are structured and why.\n## Goals\n- Simple\n- Clean\n- Testable\n- Consistent\n- Don't reinvent the wheel, use libraries when appropriate\n## Page\nA Widget that takes up the entire view of your application and can be navigated to and from. Pages contain other Widgets that are used to compose UI.\n## ViewModel\nA class that contains all the logic and local state for a Page or other component. Provides a way to interact with and react to that state. Every Page should have a ViewModel. Other UI components that are not Pages can also have ViewModels when needed. Powered by MobX. See more below about state management.\n## Service\nA class that provides a way for the app to interact with something outside of its control. Examples are: web APIs, native device APIs, SDKs, and databases. Ideally services should hold minimal amounts of state.\n\nLets look two examples of Services in this project\n### Service Example 1: ApiService\nThe [ApiService](https://github.com/MisterJimson/beyond/blob/master/lib/service/api_service.dart) provides a way for our app to interact with the [LocationIQ](https://locationiq.com/) REST Api. When interacting with a REST Api there are a few things you need to do. Create Api models, serialize/deserialize JSON, handle authentication, etc. The Service should encapsulate most of that, and expose an interface that is easy to use and understand to the rest of the app.\n\nLets look at the public interface of this Service\n```dart\nString token;\nFuture\u003cApiResponse\u003cString\u003e\u003e getAuthToken(String username, String password);\nFuture\u003cApiResponse\u003cPlace\u003e\u003e getPlace(double longitude, double latitude);\nFuture\u003cApiResponse\u003cList\u003cPointOfInterest\u003e\u003e\u003e getPointsOfInterest(double longitude, double latitude, String type, {int radius = 500});\nString getStaticMapImageUrl(String longitude, String latitude);\n```\nFrom the above its clear how to use this Service and its easy to know what data you get back. Anyone working on your app won't need to know the implementation details of the Service. \n\nAnother reason we abstract the API with a Service is for mocking during tests. It's very simple to use a mock that returns anything you want from the above methods and properties. This will be expanded on below in Testing.\n### Service Example 2: SharedPreferencesService\nThe [SharedPreferencesService](https://github.com/MisterJimson/beyond/blob/master/lib/service/shared_preferences_service.dart) provides a way to for our app to interact with the native SharedPreferences/NSUserDefaults APIs on Android and iOS.\n\nYou may be thinking \"Wait, isn't there an official package for this?\" and you would be right. We are using that package here and we are not redeveloping its functionality.\n\nHere is our SharedPreferencesService. We have only implemented what our app currently needs.\n```dart\nclass SharedPreferencesService implements Startable {\n  SharedPreferences prefs;\n\n  @override\n  Future start() async {\n    prefs = await SharedPreferences.getInstance();\n  }\n\n  Future\u003cbool\u003e setString(String key, String value) {\n    return prefs.setString(key, value);\n  }\n\n  String getString(String key) {\n    return prefs.getString(key);\n  }\n}\n```\nWhy wrap the SharedPreferences library in our own service? There are a few reasons.\n\nFirst, to allow for better mocking and testing. Anything that interacts with things outside of your control (Services) should be mocked in most tests. For native Apis especially as there is no Android or iOS when running Flutter tests. If you try to use SharedPreferences during a test you will get `MissingPluginException`. Thats no good. \n\nWhile you can use `setMockMethodCallHandler` to mock calls to the native platform, that requires you to have a pretty solid understanding of how the plugin makes native calls and what data it expects. These calls can change version to version without changes to the public API of the plugin.\n\nSecond, startup control. Some Services will require some asynchronous work before they are ready to be used. You can see that in the `SharedPreferencesService` above and also in the [PackageInfoService](https://github.com/MisterJimson/beyond/blob/master/lib/service/package_info_service.dart). Creating our own classes allows us to standardize the startup of all these Services with a common interface (`Startable`) and lets us know for sure when the Service is ready to be used by the rest of the app.\n## Manager\nA class that holds global state and provides ways to interact with and react to that state.\n\nTODO\n## State Management\nThere are many articles and discussions on state management for Flutter so I will only touch on this briefly.\n\nLets define what state management is when it comes to writing a Flutter app.\n\nState management is...\n- How the app interacts with data\n- How the app reacts to changes in data\n- How the app organizes code related to doing the above\n\nIf you look at the above statements and compare them to using `StatefulWidgets` and `setState` you will come to a few limitations when working on a large app.\n- App state and UI are tied closely together \n- Ties rebuilding UI to the same scope as your state\n- Makes it hard to separate UI code from \"business logic\"\n\nThe above can be challenging when building a large application. There are a few alternative approaches built into Flutter and many third party libraries that each have different ways of solving for this.\n\nMobX is the library used for state management here and the below section will expand on why.\n### Why MobX?\nThere are a few primary reasons why MobX is the preferred choice for state management.\n\nTODO\n## Service Location \u0026 Dependency Injection\nFor SL and DI we keep it simple. We have a [ServiceLocator](https://github.com/MisterJimson/beyond/blob/master/lib/infra/service_locator.dart) that contains all our singletons and prepares them for use by the rest of the app. \n\nWe also have a [ViewModelFactory](https://github.com/MisterJimson/beyond/blob/master/lib/infra/view_model_factory.dart) that is used to create instances of ViewModels and pass in parameters.\n\nThese 2 classes allow all our app's components to request what they need by constructor injection.\n\n### SL \u0026 DI Example 1: AuthManager\nTo understand what a class in our app depends on, just look at the final fields and constructor.\n```dart\nfinal ApiService _api;\nfinal SharedPreferencesService _sharedPreferencesService;\n\nAuthManager(this._api, this._sharedPreferencesService);\n```\nHere you can see the AuthManager requires the ApiService and the SharedPreferencesService. Easy. To provide these, just pass them in when the app starts up in our ServiceLocator.\n```dart\nAuthManager authManager;\n\nServiceLocator() {\n  configService = ConfigService();\n  packageInfoService = PackageInfoService();\n  apiService = ApiService(configService, packageInfoService);\n  sharedPreferencesService = SharedPreferencesService();\n  authManager = AuthManager(apiService, sharedPreferencesService);\n}\n```\nIf you ever need the AuthManager to use another class, just pass it in and add it as another final field in the AuthManager.\n### SL \u0026 DI Example 2: ViewModels\nViewModels are created on demand when the Widgets that require them are created. Same as the AuthManager above, looking at the constructor tells you what is required to create it.\n\nViewModels can also have parameters that are not from our ServiceLocator. Below is our ParkDetailViewModel that requires a Park object to be passed in as well.\n```dart\nclass ParkDetailViewModel {\n  final ApiService _apiService;\n  final Park _park;\n\n  String get parkName =\u003e _park.name;\n\n  String get distanceFrom =\u003e \"You are ${_park.distance} meters away\";\n\n  ParkDetailViewModel(this._park, this._apiService);\n}\n```\nAs mentioned before, our [ViewModelFactory](https://github.com/MisterJimson/beyond/blob/master/lib/infra/view_model_factory.dart) is responsible for creating these ViewModels. Add to it as you build out your app.\n```dart\nclass ViewModelFactory {\n  final ServiceLocator _locator;\n\n  ViewModelFactory(this._locator);\n\n  LoginViewModel login() =\u003e LoginViewModel(_locator.authManager);\n\n  HomeViewModel home() =\u003e HomeViewModel(\n      _locator.authManager,\n      _locator.apiService,\n      _locator.locationService,\n      _locator.navigationManager);\n\n  ParkDetailViewModel parkDetail(Park park) =\u003e\n      ParkDetailViewModel(park, _locator.apiService);\n}\n```\nYou can see above that the HomeViewModel and LoginViewModel don't need any extra arguments, while ParkDetailViewModel needs the Park passed in.\n## Testing\nTesting is very important in any app designed to scale. The below sections go into detail about the different types of testing I recommend for this approach.\n### Testing Setup\nBefore writing tests we setup mocks, stubs and configure our ServiceLocator for testing. [Mockito](https://github.com/dart-lang/mockito) is used for mocking, stubbing and validating interactions with mocks.\n#### Mocks \u0026 Basic Stubs\nCreate mock classes for each class you plan to mock. This should mainly be Services. \n\nMocking services is very important since things outside of your control can alter the results of your tests despite code not changing. The large majority of tests should only test the code in your project, not Web APIs or native device APIs.\n\nBy default mocks will let you call any function or access any field, always retuning null. Setup some basic stubs that allow simple tests to run without having to specify common behavior in every test.\n\nYou can start small, only stubbing what you need to, and expand over time.\n```dart\nclass MockSharedPreferencesService extends Mock implements SharedPreferencesService {}\n\nclass MockApiService extends Mock implements ApiService {}\n\nvoid setupApiStubs(ApiService api) {\n  when(api.getAuthToken(any, any))\n      .thenAnswer((_) =\u003e Future.value(ApiResponse(200, data: \"token\")));\n  when(api.getPlace(any, any)).thenAnswer(\n    (_) =\u003e Future.value(\n      ApiResponse(\n        200,\n        data: Place(\n          lat: \"42\",\n          lon: \"42\",\n          displayName: \"Place\",\n          address: Address(city: \"city\", road: \"road\", houseNumber: \"42\"),\n        ),\n      ),\n    ),\n  );\n  when(api.getPointsOfInterest(any, any, any)).thenAnswer(\n    (_) =\u003e Future.value(\n      ApiResponse(\n        200,\n        data: [\n          PointOfInterest(lat: \"42\", lon: \"42\", name: \"POI\", distance: 10)\n        ],\n      ),\n    ),\n  );\n}\n\nvoid setupSharedPreferencesStubs(SharedPreferencesService sharedPreferences) {\n  when(sharedPreferences.setString(any, any))\n      .thenAnswer((_) =\u003e Future.value(true));\n}\n```\n#### TestServiceLocator\nWhen integration or end to end testing it's handy to have your service locator setup with mocks. This lets you access components like ViewModels just as you do in your application, and takes care of passing around mock and real dependencies.\n\nHere you can see us setting up our ServiceLocator with mocks for our Services and real implementations for our other components.\n\nWe also setup some basic stubs that can be used in most tests, as noted before.\n```dart\nclass TestServiceLocator extends ServiceLocator {\n  TestServiceLocator() : super.empty() {\n    // Mocks\n    sharedPreferencesService = MockSharedPreferencesService();\n    apiService = MockApiService();\n    locationService = MockLocationService();\n\n    // Real\n    authManager = AuthManager(apiService, sharedPreferencesService);\n    viewModelFactory = ViewModelFactory(this);\n    navigationManager = NavigationManager(viewModelFactory, authManager);\n\n    // Basic stubbing useful for many tests\n    // Can be overridden in specific tests as needed\n    setupApiStubs(apiService);\n    setupSharedPreferencesStubs(sharedPreferencesService);\n    setupLocationStubs(locationService);\n  }\n}\n```\n### Unit Testing: Managers\nUnit testing is best suited to code you completely control and the more valuable code should be tested first. For this approach, Managers should be the priority to test.\n\nHere is an example of our AuthManager tests that demonstrate how we use our mocks and validate behavior.\n\nTake note of resetting the mocks per test and overriding the basic stubs with test specific stubs.\n\nThese tests are written as AAA (Arrange, Act, Assert), but feel free to structure the actual tests however you prefer.\n```dart\nvoid main() {\n  // Mock dependencies\n  MockApiService api;\n  MockSharedPreferencesService sharedPreferences;\n\n  // System under test\n  AuthManager auth;\n\n  // Create our mocks with basic stubs\n  // Do this before every test to ensure fresh mocks\n  setUp(() {\n    api = MockApiService();\n    sharedPreferences = MockSharedPreferencesService();\n\n    setupApiStubs(api);\n    setupSharedPreferencesStubs(sharedPreferences);\n  });\n\n  void createSystemUnderTest() {\n    auth = AuthManager(api, sharedPreferences);\n  }\n\n  test('loadSavedLogin with no saved login results in logged out AuthState',\n      () async {\n    // Arrange\n    createSystemUnderTest();\n\n    // Act\n    auth.loadSavedLogin();\n\n    // Assert\n    assert(auth.authState != null);\n    assert(!auth.authState.isLoggedIn);\n    assert(auth.authState.token == null);\n  });\n\n  // Other than the basic stubs, we can stub for a specific test as well\n  // This stub is removed at the end of the test as we recreate mocks in setUp\n  test('loadSavedLogin with saved login results in logged in AuthState',\n      () async {\n    // Arrange\n    createSystemUnderTest();\n    when(sharedPreferences.getString(\"loginToken\")).thenReturn(\"token\");\n\n    // Act\n    auth.loadSavedLogin();\n\n    // Assert\n    assert(auth.authState != null);\n    assert(auth.authState.isLoggedIn);\n    assert(auth.authState.token == \"token\");\n  });\n}\n```\n### UI Integration Testing: Pages, Widgets, ViewModels\n#### Pages \u0026 ViewModels\nTODO\n#### Widgets\nTODO\n### Integration Testing: Services\nTODO\n### End To End Testing\nTODO\n## Code Style\nOther than the structure mentioned in this document, [Pedantic](https://github.com/dart-lang/pedantic) is a library that help enforces best practices for Dart code.\n## Configuration\nTODO: Implement this section in example app\n\nWhen building client side applications its important to be able to support them remotely. After a user installs an app there is basically no way to force them to update. I always suggest adding a way to remotely configure your app without pushing an update.\n\nYou should be able to do the following without releasing an client side update.\n* Change URLs for Web APIs\n* Change API keys\n* Enforce a minimum supported version\n# Dependencies\nThese are the libraries used for this approach to Flutter development. There are other libraries used in the example app, but these are the ones that are important and recommended for this approach.\n * [Mobx](https://github.com/mobxjs/mobx.dart)\n * [Pedantic](https://github.com/dart-lang/pedantic)\n * [Mockito](https://github.com/dart-lang/mockito)","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmisterjimson%2Fbeyond","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmisterjimson%2Fbeyond","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmisterjimson%2Fbeyond/lists"}