Flutter: Deep dive into the new `skeleton` app template

For many years Flutter has provided the basic “Counter App” each time you created a new project. While that example provides a nice introduction to the foundational concepts of StatefulWidget and basic font/asset management, it does not really resemble the structure of a “complete app”. This has left developers without guidance for anything beyond a basic single page app.

To fix this issue the Flutter team have been hard at work on a new template with the stated goals:

  • make it easier for developers to continue their learning journey beyond the Counter app.
  • co-exist with the original Counter app template. It is not a replacement.

The new template was merged into master in June 2021, and you can create it with this command:

   flutter create -t skeleton new_flutter_template   

Tip: If you would like to check out the code without creating a new project, you can inspect the template definition files on the Flutter repository.

What’s in it?

The template attempts to provides some best practices, while avoiding weighing in on others. Lets dive into some of the specific topics.

Package Structure

The skeleton template is substantially larger than the Counter app, weighing in at 9 files spread across 3 packages:

The template organizes code by feature and not type, so all settings views, controllers and services are grouped in the same package:

This is a common approach, but also an age old debate in programming. The general consensus seems to be that organizing by type is effective with a smaller number of files (say, <10 per type), but as things grow, organizing by feature tends to be more effective.

State Management

To some degree the Flutter team have avoided commenting on state management with this template. No state management library is used. There is no real reactive data binding or data injection. Instead a simple ChangeNotifier is used to create a SettingsController and that controller is manually passed to the SettingsView via constructor arguments.

Roughly speaking, the app is configured as a simple MVC app with a Service layer (MVC+S). SettingsController is a combined model/controller object, that takes an instance of SettingsService in its constructor:

  // Set up the SettingsController, which will glue user settings to multiple
  // Flutter Widgets.
  final settingsController = SettingsController(SettingsService());

To rebuild the views when settings changes, the template simply binds the ChangeNotifier to the top of the app tree using the somewhat confusingly named AnimatedBuilder:

return AnimatedBuilder(
  animation: settingsController,
  builder: (BuildContext context, Widget? child) {
    return MaterialApp(...);

Don’t be thrown off by the AnimatedBuilder name here, there is no animation going on, this widget would more correctly be called ListenableBuilder. It just runs its builder once each time settingsController calls notifyListeners. This is the same idea as doing settingsController.addListener(() => setState((){})) but does not require using a StatefulWidget.

This is not very optimized as it builds the entire tree any time anything changes, however it does show the basics of how you can easily bind a ChangeNotifier to the tree and trigger rebuilds in a very simple way.


Many developers are likely waiting direction on where to go with the new Router API (aka Nav 2), but similarly to state management, this is a bit of a “no comment” from the Flutter team.

The template uses a classic style of routing, with simple named routes and the onGenerateRoute handler:

return MaterialApp(
// Define a function to handle named routes in order to support
// Flutter web url navigation and deep linking.
onGenerateRoute: (RouteSettings routeSettings) {
  return MaterialPageRoute<void>(
    settings: routeSettings,
    builder: (BuildContext context) {
      switch (routeSettings.name) {
        case SettingsView.routeName:
          return SettingsView(controller: settingsController);
        case SampleItemDetailsView.routeName:
          return const SampleItemDetailsView();
        case SampleItemListView.routeName:
          return const SampleItemListView();

As noted in the comments, this structure will get you basic deeplinking support on the web and in your apps. It will not, however, get you proper back/forward support on web, as it does not use the new Router api.

It’s understandable why the team decided to stick with classic Navigation here, otherwise the size and complexity of the example would have been inflated substantially. This is bound to disappoint some developers who were looking for some direction here.


The template nicely demonstrates how to perform localization using a l10n.yaml file and the flutter_localizations package:

// Provide the generated AppLocalizations to the MaterialApp. This
// allows descendant Widgets to display the correct translations
// depending on the user's locale.
localizationsDelegates: const [
supportedLocales: const [
  Locale('en', ''), // English, no country code

// Use AppLocalizations to configure the correct application title
// depending on the user's locale.
// The appTitle is defined in .arb files found in the localization directory.
onGenerateTitle: (BuildContext context) =>

This is great example as it shows how to create custom strings, as well as enabling localization for the built in flutter widgets. According to the Flutter docs, after adding the code above:

The Material and Cupertino packages should now be correctly localized in one of the 78 supported locales. Widgets should be adapted to the localized messages, along with correct left-to-right and right-to-left layout.

You can check the Flutter docs for more information on this topic.


The template does some interesting things when it comes to settings.

To begin with, it performs a load, before rendering the MaterialApp. This is quite a common use case in real apps, as you often want to fetch the theme before showing any styled pages, so it’s nice to see this added:

  // Set up the SettingsController, which will glue user settings to multiple
  // Flutter Widgets.
  final settingsController = SettingsController(SettingsService());

  // Load the user's preferred theme while the splash screen is displayed.
  // This prevents a sudden theme change when the app is first displayed.
  await settingsController.loadSettings();

  // Run the app and pass in the SettingsController. The app listens to the
  // SettingsController for changes, then passes it further down to the
  // SettingsView.
  runApp(MyApp(settingsController: settingsController));

Then it shows how you can switch between light and dark modes, using a saved enum value:

// Define a light and dark color theme. Then, read the user's
// preferred ThemeMode (light, dark, or system default) from the
// SettingsController to display the correct theme.
          theme: ThemeData(),
          darkTheme: ThemeData.dark(),
          themeMode: settingsController.themeMode,


The template provides an initial example of how to use the new Restoration API with your MaterialApp:

// Providing a restorationScopeId allows the Navigator built by the
// MaterialApp to restore the navigation stack when a user leaves and
// returns to the app after it has been killed while running in the
// background.
restorationScopeId: 'app',

There is not much explained about restoration beyond that one comment, but it is good to see it there to at least put this issue on the radar of new developers.

Closing Thoughts

In general the new template is a very nice next step for developers coming from the CounterApp but it will probably not be enough for more experienced developers looking for some real-world guidance.

For example, the state-management approach shown here is not really scalable or workable in a complete app. It would have been nice to see some sort of data injection, even if was built as a simple InheritedWidget or the new SharedAppData API, and some attention paid to optimizing rebuild regions when specific things change.

Navigation is another disappointing absence here. The fact that a simple demo of the new Router API can not be produced says a lot about the learning curve of the API. With that said, having experimented extensively with the new Router API myself, I fully understand the dilemma here and can’t really fault them for the decision. Things are still evolving, so while developers need guidance sooner rather than later, this is likely not the time or place to try and provide it.

While this serves as a clear stepping stone from Counter app for new developers and provides a solid foundational structure for how your app might be put together, it feels like there is room for a skeleton_2 some time in the not-to-distant future. Something that demonstrate a more robust state management approach along with a clear example of routing that works fully on the web.

Let us know below if you have any questions or comments about the new template!


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.



  1. Thanks for the write-up! I completely agree that it’s missing a few things here and there. However, I’m very happy to hear you think this is a good “intermediary” template, since our target audience was in fact intermediate Flutter devs.

    I spent a lot of time coding up different variations of this template, using techniques like InheritedWidget or Navigator 2.0 or ValueNotifier so we could directly compare the impact of what we include or exclude. If folks are at all interested in the decisions we made along the way, I put together a detailed document here: https://docs.google.com/document/d/1sXAPfPaaaskbzRoA4fbAoZjohcd6jZXYYJJQd0lXcuA

    Thanks again 🙂

  2. The author himself! Thanks for stopping by Brian, and sharing that link, there is a ton of info there and I found it a very interesting read, it’s great to see some insights into what customers did and did not request, and the rationale behind the decisions.

  3. I enjoyed this site. It was very easy to use and functional. Thanks.

Leave a Reply

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