Flutter: A Skeleton App w/ GetIt + GetItMixin

Recently we compared and contrasted some common state management packages. And while app architecture was not the focus of the article, we received some feedback that the examples were too simple to give a good picture of how the approaches would scale. In this post we’ll attempt to address that concern by building a simple yet scalable skeleton app using GetIt and GetItMixin!

Get What?

If you’re not familiar with GetIt, it is a robust implementation of the classic service locator pattern, which allows you to register objects by their type and then look them up from anywhere in the app. Recently it gained a sibling package named GetItMixin which provides reactive widget binding hooks. Combined these two packages make up one of the simplest and cleanest methods of managing state within a Flutter application. Let’s take a look!

NOTE: To follow along, the full demo is posted here:
https://github.com/esDotDev/flutter_experiments/tree/master/get_it_skeleton_app

Step 0. Setting the stage.

To really show off an architectures ability to scale, we need a somewhat complex app. Layout and navigation code are not the focus here, but lets assume that the app being built will have the following “features”:

  • List nearby Bluetooth devices
  • Access to location of users device (gps coords)
  • Persistent app settings
  • In-app purchases
  • Maps integration

In addition to the technical requirement above, this architecture should also:

  • Cleanly separate logic and ui code
  • Allow ui to easily use actions and bind to data reactively
  • Provide ability to override logic components for easy testing

Step 1. The “logic” layer:

Before diving into implementation, let’s look first at structure. Our logic layer will primarily consist of a set of manager and service classes to handle the various features of the app. managers are collections of state and some actions to work on that state. services exist to interact with the outside world, while providing an abstracted API for your managers to use. There can also be other supporting logical packages, like data or utils:

Keep it flat, or organize folder-by-feature, both should work nicely here.

Managers

GetItMixin is designed to work with mutable objects like ChangeNotifiers or ValueNotifiers. This means that each manager class that holds state, should usually be a ChangeNotifier or a collection of ValueNotifier fields. For this example, we’ll use the ValueNotifier approach as it is the most succinct.

Here is an example of how one of the managers might look:

class PurchasesManager {
  //State 
  final isProEnabled = ValueNotifier<bool>(false);
  // Actions
  Future<void> init() async { ...  }
  Future<void> upgradeToPro() async => ...
  Future<void> restorePurchases() async => ...
  // etc
}

You can see that we declare a mutable isProEnabled field, that ui will be able to bind to, and a set of actions that the ui can make use of. More on this below…

For more examples of managers, check out the BlueTooth Manager, Purchases Manager or Settings Manager classes.

Services

Services are just vanilla classes that interact with the world outside your application. Usually with async methods like init(), scanDevices(), saveFile(), getLocation() etc. They are used by managers to complete their actions. The main purpose of the services are to remove implementation details from the manager and provide a simple and clean API for it to consume. Not only does this make testing quite easy, but it allows you to change out implementations later with minimal refactoring.

For some examples of services, check out the LocationService or BlueToothService classes. Keep in mind that this is a skeleton app, and these are stub classes so there is really not a lot to look at here!

Disposal

Another common responsibility of the logic layer is to dispose of things when they are no longer in use. GetIt makes this easy with a Disposable interface you can add to your singletons, enabling you to dispose multiple services or managers with a single call to GetIt.I.reset(), or GetIt.I.popScope().

Step 2. Declaring your singletons (preferably in main.dart!)

With our logical structure defined, lets look at how we can make those objects globally accessible withGetIt:

A couple notes on the above:

  • We use lazy registration here so that any singleton can access any other, with worrying about order of instantiation. This works similarly to the late keyword in dart.
  • We can use the pushNewScope method to easily replace any singleton with a mock version. In this example, when testMode=true, anyone who requests a BlueToothService instance, will instead receive a BlueToothServiceMock.
  • It is a good idea to place this code in a conspicuous place like main.dart or /lib/singletons.dart. This way, new developers are likely to spot it, and will gain an immediate grounding in your architecture.

A spoonful of sugar…

Now that our singletons are all mapped, we could access them with getIt.I.get<Foo> and call it a day, but it is a nicer on your ui developers to add a little syntax sugar, and define shortcuts for our most commonly used instances:

That’s more like it!

To see the complete example, checkout the full main.dart file.

With the logic layer created, and singletons mapped, we’re ready to move on to the ui!

Step 3. Creating the “ui” layer

We have found that ui components for an app can generally be broken up by a few main types:

  • /pages
  • /modals
  • /common

Using that general structure, you can see an example of some of the components this example app might have here:

folder-by-feature structure is less obvious in the ui layer, so we organize by type instead.

As the app grows, many more sub-packages will need to be created, and organization will become critical as your file count will often grow to dozens or even hundreds. However, these three top-level packages should provide a solid set of high level buckets, within which most components or sub-packages will naturally fit.

From within any of these ui components, there are two things we will need to be able to do: trigger actions and bind to data.

Actions

Using actions is simple, we can just reach out to any of our managers, and call methods directly:

onPressed: () => bluetooth.ignoreDevice(...);
onPressed: () => app.showDialog(...);
onPressed: () => settings.darkMode.value = false;

The code above shows one of the core benefits of this approach: its directness. There is no context or ref required to trigger actions within your app, you just grab the appropriate manager and call a method. There is no need for big reducers methods, middleware layers, or for multiple classes or enums to be defined for message passing. If you want to pass some data to the action, just pass it to the method directly! This tight coupling keeps boilerplate to a minimum, and also makes debugging more straightforward.

Data Binding

To bind to some data within the logic layer add the GetItMixin to any StatelessWidget and call watchX to bind to any ValueNotifier.

For example, we could bind to the list of visible devices with:

class ListDevicesPage extends StatelessWidget with GetItMixin {
  ListDevicesPage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    List<BtDeviceInfo> devices = watchX((BlueToothManager m) => m.visibleDevices);
    return ListView(
      children: devices.map(_DeviceListTile.new).toList(),
    );
  }
}

This binding syntax is impressively succinct. It’s also nice that we do not need to extend a custom widget, or use a custom build method signature. Just adding the mixin is enough.

As another example, the Settings view might look something like this:

class SettingsDrawer extends StatelessWidget with GetItMixin {
  @override
  Widget build(BuildContext context) {
    // Bind to `darkMode` property, we will rebuild when it changes
    bool darkMode = watchX((SettingsManager m) => m.darkMode);
    return Column(children: [
      Checkbox(
        value: darkMode,
        // Change the darkMode value when pressed
        onChanged: (value) => settings.darkMode.value = value,
      ),
    ]);
  }
}

In this case we just use the .value field on the ValueNotifier as the “action”, since there is not much to be gained by creating a setDarkMode method just to set this value. However, you could certainly do that if you wished to provide a slightly more succinct API for your views, or to make it more obvious when side effects are occurring.

For more simple examples of view bindings, and actions being used, check out the MapsPage, ListDevicesPage or the MainApp.

watch___
In addition to watchX for ValueNotifier, GetItMixin supports a number of other use cases, these are just some of them:

  • watchOnly – Watch a specific field on a ChangeNotifier
  • watchStream – Watch a stream, provide a default value
  • watchFuture – Watch a future, provide a default value
  • registerHandler – attach ui side-effect to ValueNotifier
  • registerStreamHandler – attach ui side-effect to Stream
  • registerFutureHandler – attach ui side-effect to Future

For a list of all available methods, and more information about what they do, check out the GetItMixin docs page.

StatefulWidget
You can use all of these methods on StatefulWidgets as well. To do so, add the GetItStatefulWidgetMixin and GetItStateMixin mixins respectively. You can see a basic example of this in the MainApp class:

That’s it! At this point you have a scalable and testable architecture that can take you very far. You can bind to shared data and initiate actions from any level in the widget tree. Your logic and ui packages have plenty of room to scale, and organization should be easy and obvious for the life of the project. Finally, you can very easily mock or test any of your managers or services, either individually or as an integrated system.

A note on package structure

You may be wondering why this architecture is not organized by the classic folder-by-type, or folder-by-feature style. The answer is that while logical components like services, manager, models often group naturally by feature, many views will touch several features or data sources at once. This can make it difficult, or even impossible, to organize all logical components along with corresponding views (aka folder-by-feature).

For example, in the app above, it would be common for different views to access BlueToothManager, while also accessing SettingsManager and PurchasesManager, etc. It would not be totally clear where to put certain views in the first place, which leads to future developers being unsure where to find them. Which is the opposite of what we want!

Because of this, we’ve found it can be nice to group by the high-level packages of ui and logic making a clean high-level split at the top. And then organize packages a little differently within the two layers:

  • Within the logic package, folder-by-feature approach will work nicely, or you can just keep it mostly flat and rely on good naming to ensure things are grouped together. For instance, blue_tooth_service and blue_tooth_manager will naturally live next to each other. This works well here, because you typically don’t have more than 10 or 20 of these managers and services classes, organization does not need to be overly granular.
  • Within the ui package, you can organize by pages, modals, and any further groupings you need to keep things organized in a more folder-by-type approach. This additional organization is much more critical in the ui layer, as you’ll typically have dozens or even hundreds of custom widgets in your app and good organization is key to easily finding / discovering them later.

Hopefully you enjoyed this article, we plan to do another one on riverpod next. If you have any questions or comments, let us know below!

shawn.blais

Shawn has worked as programmer and product designer for over 20 years, shipping several games on Steam and Playstation and dozens of apps on mobile. He has extensive programming experience with Dart, C#, ActionScript, SQL, PHP and Javascript and a deep proficiency with motion graphics and UX design. Shawn is currently the Technical Director for gskinner.

@tree_fortress

12 Comments

  1. Wow! What a statement “Hopefully you enjoyed this article”, “Hopefully”? This is an excellent article, succinct, full of useful information, and explanatory. Thanks a mil and, please, keep them coming. As usual, your advice on architecture are a gem.
    BTW, Blaise, any plans to write an intermediate/advanced book on dart/flutter? Your writing prowess and ability to make things clear is great and I will buy your book in a flash!

  2. Wow! What a statement “Hopefully you enjoyed this article”, “Hopefully”? This is an excellent article, succinct, full of useful information, and explanatory. Thanks a mil and, please, keep them coming. As usual, your advice on architecture are a gem.
    BTW, Blaise, any plans to write an intermediate/advanced book on dart/flutter? Your writing prowess and ability to make things clear is great and I will buy your book in a flash!

  3. Wow, thanks for the kind words! No, no plans for a book at the moment.

  4. Shawn, thank for sharing your knowledge, I’m looking to adapt any architecture to use in my projects, so I think this is a light at the tunnel’s end.
    You could write about using GetIt and Riverpod, as an practical example. Thank you again, and happy new year.

  5. Hey Pedro, we’re working on some posts for riverpod now, but in the meantime you can view code for the riverpod version here: https://github.com/esDotDev/flutter_experiments/tree/master/riverpod_skeleton_app/lib

  6. why dont have getx ? can u talk about getx with this ?

  7. Truth is we prefer smaller more targeted libraries like `provider`, `riverpod`, `GetIt`, mainly because they solve the one specific thing that we can’t easily do ourselves. Once you have a good reactive DI system in place, the rest of the sugar provided by GetX is either fairly easy to implement, or better served by a dedicated library (like `go_router`).

    The biggest issue we have with large frameworks like GetX, is the additional complexity it imposes on the codebase, as each new developer now needs to understand an extra layer of abstractions, on top of the core abstractions. More lines of code to be understood, and more potential for bugs hiding in those lines of code. We prefer a more modular approach, where dedicated components are used to do specific jobs, but otherwise vanilla flutter and simple architectural design patterns are used as much as possible.

  8. Brilliant thank you very much for creating this gem.

  9. Love the article, I have been looking at a package called ‘cubes’ to try and fix my main gripe with GetItMixins, have you ever heard or used it before and if so, what are your thoughts? As petty as this sounds my main gripe with GetITMixins is the syntax of watching changes to fields. Im not a fan of how it is not explicit what controller and field you are watching, instead this is implied through the function param type and the return value of the watch methods. I have to say this confused me for a while to begin with, I was under the impression that watch ran every time the controller changed any of its fields due to the way its written. Another thing I’m not keen on with GetItMixins is how the entire widget is rebuilt whenever the value changes (I think) which can be expensive for large widgets. GetItMixins also don’t work with builder widgets. Then I came across cubes and their extra goodies and better syntax just makes so much more sense. In your example, the syntax for your checkbox would simply look like this ‘settings.darkMode.build((value)=> Checkbox(value:value, …))’. Not only will this ONLY rebuild the checkbox when the value changes, but the syntax is explicit and very readable. The only thing I’m not overly keen on with cubes is that their syntax to register dependencies (just built on top of GetIt) is slightly less readable that standard GetIT (when it comes to singletones anyway). I would be interested to hear your thoughts because I think cubes looks to be an even simpler version of GetItMixins

  10. The thing about GetIt (the core lib, not the mixin), is it already has a built in way to isolate rebuilds, AnimatedBuilder, ValueListenableBuilder, FutureBuilder, StreamBuilder.

    The `Mixin` is specifically for when you _do_ want the entire widget to rebuild. watchX, watchStream etc, are basically just single-line builder equivalents. If you do want to rebuild just a portion of the tree, you can use the various builders. This is similar to providers .watch and .select api + a Consumer widget.

    One turn off of Cubes at initial glance, is how you have to use a different build method, and it only accepts a single type of data. GetIt is quite a bit more flexible here, you can reach out and read or bind to multiple things, in any widget. There is no custom build method, just add the mixin.

    Regarding the `watchX` API, seems like it’s just a matter of learning the api. It’s always felt natural to me, as I came from provider, which would do something like: `bool isLoggedIn = context.select((UserModel m) => m.isLoggedIn)` to do a similar thing.

  11. Fair point, Im not sure why this did not occur to me earlier but I had not even thought of using the standard flutter builders combined with futures etc pulled from the controller/viewModel before, and your explanation of the GetItMixin has finally made me realise its purpose, so thanks for that I think you have converted me over.

    if im completely honest with you I’m new to flutter but an experienced developer so I’m struggling to find relevant content as its all either far too basic (stuff I can figure out on my own) or far too specific to app development and has the disclaimer that it does not work well on the web (not acceptable for my case). This blog is actually one of the first bits of content I have actually found insightful.

    I have decided to use flutter on a large, complex application I’m writing for a client (as it needs to be multi-platform) and I’m learning as I go coming from a web PHP/Laravel & JS background. Thats probably why the implied watch types look a bit foreign for me (and was what actually turned me off provider at first glance) but from what you have said there I think its clear I just need to get used to that.

    I used to be a solution architect before leaving to start working for myself so have been trying to find a simple, scalable architecture I’m happy with for the last 6 weeks (and 13 iterations into the product) as I know how important it is. However, each time “level up” so to speak with flutter & dart I end up finding a better way and moving over.

    I have been using GetX up until recently as I really liked its simplicity however something just felt a little icky about using it (especially because the documentation for it is so poor). upon finding issues with its navigation system and limitations when it came to keeping persistent sidebar navigation on large screens I jumped ship and started the search (for the 13th time now) for a new architecture. Your apps are no doubt impressive so when I found a blog post talking about your favourite packages I took a serious look into each one.

    I like GetIt, very similar to the GetX syntax and also very specific and good at what it does (no bloat). I took a look into GoRouter and despite finding an issue with it (which I have reported), Im pretty sure its going to solve the issues I had with GetX and also provides me with much more control than GetX did which is always a bonus. So GetIt and GoRouter cover dependency injection and routing, I have built my own adaptive layout building system with the help of your flextras recommendation which I’m happy with, and then finally when it comes to state management, I have tried Bloc, Provider, GetX and although GetX bindings have been my favourite so far, I think its becoming clear to me I’m probably better off coming up with my own state management system and then injecting the classes using GetIt because all the other options out there are so overcomplicated or require so much boilerplate that its not even worth the benefits.

    So considering your recommendations have been solid so far, before I start writing the 14th and hopefully last iteration of the product, would you recommend anything else for a complex business application that needs to be adaptable and responsive to all flutters supported platforms?

  12. Honestly, it doesn’t matter all that much what you use for SM, I would opt for something simple. It really is just a system to say, when ThingA changes rebuild WidgetB.

    I would just stick to Provider, or GetIt, both are simple, easy to use, minimal boilerplate, and they do they job. Or Cubes is fine as well really. Honestly it doesn’t matter a lot, more important is how you use it, how you divide up your models/controllers etc.

    Provider is probably your best bet, as it is extremely well known and robust, and will give you a nice foundation as you evaluate other solutions in the future. If you find provider is not enough it most likely points to an issue with your architecture, rather than an issue with provider.

Leave a Reply

Your email address will not be published. Required fields are marked *