Starting a new Flutter project with our template

Starting a new Flutter project with our template

Flutter template

In one of our previous blogs I explained how I wrote our icapps_translations plugin for Flutter with all the bells and whistles.

I explained to you that it is important to have a good foundation when starting with a new technology. You need to be ready for takeoff when clients ask for a new project. So after finishing our proof of concepts, I gathered all the information on how we should work with Flutter and what should be included in the template project. You can find the template I've built here. 

At icapps we have a couple of template projects to make sure we can start a new project without having to do the setup again every time. It took some time to get everything up and running but now we’re here...

 

What do we need?

A template project should contain all the main components for starting a new project. This is what we think should be included in every project.

  • Network Layer

  • Network logging

  • Json Parsing

  • Dependency Injection

  • Flavors (Alpha, Beta, Release)

  • Flavor Configs

  • Viewmodels

  • Repositories

  • Translations

  • Navigation

  • Linting

  • Theming

  • An example that binds everything together

 

Network layer

When building our proof of concept we used the simple HTTP library but we found that we needed to cancel our network calls sometimes. We also wanted a more reliable way for adding interceptors and implementing refresh tokens. That is where Dio comes into play. Dio is a powerful HTTP client for Dart that supports all of the above issues. It is also one of the top Dart packages on pub.dev.

 

Network Logging

Network logging is maybe one of the coolest parts of this blogpost. Nicola Verbeeck, one of our colleagues,  wrote a plugin for Android development called Niddler. It allows you to inspect your network traffic even after the calls have been finished. So let's say you had an unexpected behavior in your app, Niddler makes it possible to check what response you got from the API. 

This is very powerful because a lot of other network logging tools do not provide this functionality. During our proof of concept we felt the need for a Niddler plugin that worked with Flutter. But why stop there? Niddler should not only be available for Flutter, it could be available for Dart itself. Nicola wrote a version for Dart during our proof of concept to make sure we had the same tools available as we had during Android development. 

If you want to use this, please check out this awesome package at pub.dev -> https://pub.dev/packages/niddler_dart. The plugin for android studio is available for download in the plugins section in android studio itself, or here https://plugins.jetbrains.com/plugin/10347-niddler

 

Json parsing

While working with network requests you will also need a JSON parser with it. When I started with Flutter one of the best resources available was the boring show. So when I started Flutter development I used built_value. This was fine for basic response parsing, but was terrible at parsing requests to a JSON format. 

We used it in our proof of concept as well and felt the limitations very quickly. So when I started on the Flutter Template itself I found that there was a much better package available that did everything (almost everything) what we needed. We could provide nullable and non nullable fields, add enums to our JSON objects and parse everything we needed. The only minor problem we’ve faced at this moment with the package is that it does not support nested fields.

 

{
    "data" : {
        "field" : "field"
    }
}

While eventually, we want to end up with this:

 
@JsonKey(name: "data/field")
String field;
 

This is currently not supported and leads to creating extra objects for parsing everything correctly. This could be improved and is already been added to the issue tracker of the package itself: https://github.com/dart-lang/json_serializable/issues/490

 

Dependency injection

Coming from Android, Dagger2 will sound familiar to you. That is because it is the go-to dependency injection library for Android development. We wanted to use something like Dagger in Flutter to maintain all our dependencies without having to recreate them everywhere. We found that Google is currently building a Flutter version of Dagger called inject.dart -> https://github.com/google/inject.dart The problem here is that there is no documentation on how to implement it and no official issue tracker support. We want to use a package that is stable and could be used in production. That is when we found Kiwi. Kiwi is somewhat similar to Dagger2 but could be improved in some ways. It is not as smart as Dagger2. Kiwi does not know when to create which dependency first. So it is really important that everything is configured in the correct order. Other than that, it works perfectly fine.

 

Flavors

We wanted to have different versions of the app: Beta, Alpha, Release. We found that Flutter links to a lot of good Medium posts. We just created an extra widget that wraps around every screen that shows a banner on screen indicating what flavor you are using. This makes it easier for testing. Talking about Flavors and specific configs could be a seperate blog post I think. That is why I want to give you some links to the best documentation already out there.

Our code

 

Flavor config

When you have different Flavors you also want to have different values for each Flavor. That is why we used a Singleton FlavorConfig. This gives use the option to add new or other flavor values for each project. The flavor config itself are just some simple objects and an enum to specify which Flavors there are available.

 

 
enum Flavor {
  DEV,
  ALPHA,
  BETA,
  PRODUCTION
}
 
class FlavorValues {
  FlavorValues({@required this.baseUrl});
  final String baseUrl;
  //Add other flavor specific values, e.g database name
}
 
class FlavorConfig {
  final Flavor flavor;
  final String name;
  final Color color;
  final FlavorValues values;
  static FlavorConfig _instance;
 
  factory FlavorConfig({
      @required Flavor flavor,
      Color color: Colors.blue,
      @required FlavorValues values}) {
    _instance ??= FlavorConfig._internal(
        flavor, StringUtils.enumName(flavor.toString()), color, values);
    return _instance;
  }
 
  FlavorConfig._internal(this.flavor, this.name, this.color, this.values);
  static FlavorConfig get instance { return _instance;}
  static bool isProduction() => _instance.flavor == Flavor.PRODUCTION;
  static bool isDevelopment() => _instance.flavor == Flavor.DEV;
  static bool isAlpha() => _instance.flavor == Flavor.ALPHA;
  static bool isBeta() => _instance.flavor == Flavor.BETA;
}

Viewmodels

In Android we use the Architecture components with Jetpack. This combines a very clean architecture with fast development. We wanted to recreate it with Flutter to have some of the same principles. We found that using ScopedModel was the best solution during our proof of concept. After Google I/O we switched to provider, we are never going back. It gives us more control about the viewmodel itself and most important we can dispose our resources without the need of calling dispose itself on our viewmodel. We now combine the ChangeNotifier (very similar to the ScopedModel implementation) with streams and the bloc pattern.

If we want to call an action that can not be done with the build function we use a Navigator interface to propagate a function from our viewmodel to our screen (Widget). In our widgets we use the ChangeNotifierProvider and Consumer from the provider package to make it easier to rebuild certain parts of our widget tree. Because sometimes we use streams. We also use the StreamBuilder itself. Previously I told you we are using Kiwi for dependency injection. This is a perfect place for using Kiwi. When using the ChangeNotifierProvider it requires a builder function for creating the viewmodel. This usually looks like this:

 

 
builder: (context) => kiwi.Container().resolve()..init(this), //this is our Navigator interface

This init function is just used to init the viewmodel and maybe start a get call right after everything is build.

This is a simple example of a screen with viewmodels

 
import 'package:myapp/navigator/main_navigator.dart';
import 'package:myapp/viewmodel/splash/splash_viewmodel.dart';
import 'package:flutter/material.dart';
import 'package:kiwi/kiwi.dart' as kiwi;
import 'package:provider/provider.dart';
 
class SplashScreen extends Stateless  implements SplashNavigator {
 
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: ChangeNotifierProvider<SplashViewModel>(
        child: Consumer<SplashViewModel>(
          builder: (context, value, child) => Text(value.text),
        ),
        builder: (context) => kiwi.Container().resolve()..init(this),
      ),
    );
  }
 
  @override
  void goToHome() => MainNavigatorWidget.of(context).goToHome();
}

You can see here that the Text widget is updated every time the SplashViewModel triggers notifyListeners(); The causes a rebuild and will update the text in our Text widget.

 

Repositories

We are using viewmodel to binding our views to the data available in the app. But where the data comes from is chosen by the repositories. Let's say you want to get all the users in a list. You will need to call the UserRepository to getAll() users. This is typically where caching will be stored or accessed. A viewmodel will not handle the caching for us because a viewmodel can and will be destroyed. The Repositories will be created once and stay in memory all the time.

 

Translations

I don't think there is much to say about the translations anymore. You have 2 options. You can use the icapps_translations package if you need support for the icapps translations (https://pub.dev/packages/icapps_translations). Otherwise you could use the locale_gen package to have locale translations (https://pub.dev/packages/locale_gen). Both offer the same api and same autogenerated files. So you can switch at any point to the other one. The only difference is where the translations are coming from. For more info about this topic please check out our other blog about Translations (https://www.icapps.com/blog/icapps-translations).

 

Navigation

We also want to be using a typesafe way to navigate to other screen. If you use this MainNavigatorWidget you can provide your other developers with all the routes available. When using a master detail view you can also navigate to other screens with an extra navigator. So not all your routes should be defined here. But they could be.

 

import 'package:myapp/screen/home/home_screen.dart';
import 'package:myapp/screen/splash/splash_screen.dart';
import 'package:myapp/widget/general/flavor_banner.dart';
import 'package:flutter/material.dart';
 
class MainNavigatorWidget extends StatefulWidget {
  const MainNavigatorWidget({Key key}) : super(key: key);
 
  @override
  MainNavigatorWidgetState createState() => MainNavigatorWidgetState();
 
  static MainNavigatorWidgetState of(context, {rootNavigator = false, nullOk = false}) {
    final MainNavigatorWidgetState navigator = rootNavigator
        ? context.rootAncestorStateOfType(
            const TypeMatcher<MainNavigatorWidgetState>(),
          )
        : context.ancestorStateOfType(
            const TypeMatcher<MainNavigatorWidgetState>(),
          );
    assert(() {
      if (navigator == null && !nullOk) {
        throw FlutterError('MainNavigatorWidget operation requested with a context that does not include a MainNavigatorWidget.\n'
            'The context used to push or pop routes from the MainNavigatorWidget must be that of a '
            'widget that is a descendant of a MainNavigatorWidget widget.');
      }
      return true;
    }());
    return navigator;
  }
}
 
class MainNavigatorWidgetState extends State<MainNavigatorWidget> {
  final GlobalKey<NavigatorState> navigationKey = GlobalKey<NavigatorState>();
 
  @override
  Widget build(BuildContext context) {
    return WillPopScope(
      onWillPop: _willPop,
      child: Navigator(
        key: navigationKey,
        initialRoute: SplashScreen.routeName,
        onGenerateRoute: onGenerateRoute,
      ),
    );
  }
 
  Route onGenerateRoute(RouteSettings settings) {
    switch (settings.name) {
      case Screen.routeName:
        return MaterialPageRoute(builder: (context) => FlavorBanner(child: Screen()), settings: settings);
      default:
        return null;
    }
  }
 
  Future<bool> _willPop() async => !await navigationKey.currentState.maybePop();
 
  void goToScreen() => navigationKey.currentState.pushNamed(Screen.routeName);
 
  void closeDialog() => Navigator.of(context, rootNavigator: true).pop();
 
  void goBack<T>({result}) => navigationKey.currentState.pop(result);
}

Linting

You want to make sure that your code is always consistent and clean. That is why you should always be using a linter to make sure that you and your team write the same kind of code. We have specified our linting rules in this template project. So it will be the same for every project.

 

Theming

We wanted to use theming but the documentation about theming always required use to use Theme.of(context). We did not really like this idea. Because everything needs to be styled. When we set some default theming on the MaterialApp widget. But not everything can be done here. So we created some static classes that contains everything we need for Theming. Duration, Colors, Dimens, Assets,...

 

Example

Every good template needs a small example. So we combined all of this and created a simple example. All you will see is a get call to get some users. and a localization switch. All the other things I talked about in this blog are in the template project. But are not visible when building the app. Most of it is just to get a good architecture.

 

So thank you for keeping up with me in my Flutter journey at icapps. In case you are inspired and you want to talk about one of your business ideas, you are welcome at our next Innovation Talks. 

Thursday September 26, we organise a lunch for everyone who wants to know more about Flutter as a business opportunity. Read more about it here