Application architecture

To build a maintainable app you need to start right, here I will describe the structure I used to build e-commerce app.

I will talk about the main concepts and why I have chosen this way of building it.

The Flutter app

This is the simplest step, just run flutter create architecture_app, now open the project in you favourite editor and run it.

Add packaged

There are few packages to add to the dependencies and dev_dependencies open your ./pubspec.yaml and add the following

dependencies:
  bloc: ^3.0.0 # to manage state
  flutter_bloc: ^3.2.0 # to make bloc pattern fun
  graphql_flutter: ^3.0.0 # a graphql client
dev_dependencies:
  artemis: ^5.1.0 # to generate dart classes from graphql queries
  build_runner: ^1.5.0 # required by artemis
  json_serializable: ^3.0.0 # required by artemis

Don’t forget to run flutter pub get.

Setup build phase

get the schema

Go to https://api.graphqlplaceholder.com/ download the schema and put the file in the root of your project, it should be called schema.graphql.

setup the build.yaml

Create a new file in the project root build.yaml and paste the following

targets:
  $default:
    sources:
      - lib/**
      - graphql/**
      - schema.graphql
    builders:
      artemis:
        options:
          generate_helpers: true
          schema_mapping:
            - schema: schema.graphql
              queries_glob: graphql/*.graphql
              output: lib/graphql/graphql_api.dart

The settings are self explanatory, we tell the build script to look for files in ./graphql/*.graphql and the generated dart classes will be placed in ./lib/graphql/graphql_api.dart.

Now try the build is working by running flutter pub run build_runner build.

Once done you will get new files in ./lib/graphql, do not worry about the errors for now!

Write the query & automagically generate dart classes

Create a new file ./graphql/post.graphql and paste this simple query

query getPosts {
    posts {
        id
        title
        author {
            id
            name
        }
    }
}

And then run the build command flutter pub run build_runner build.

You should get the new generated classes in ./lib/graphql/graphql_api.graphql.dart have a look at the file, do not edit it.

Prepare GraphQL client

Create a file ./lib/client.dart

import 'package:flutter/foundation.dart';
import 'package:graphql_flutter/graphql_flutter.dart';

final HttpLink _httpLink = HttpLink(
  uri: 'https://api.graphqlplaceholder.com/',
);

final Link _link = _httpLink;

ValueNotifier<GraphQLClient> client = ValueNotifier(
  GraphQLClient(
    cache: InMemoryCache(),
    link: _link,
  ),
);

Post repository

This is the class that will handle all the networking, this will make your app modular, easy to test, and easy to reason about, create the file in ./lib/repositories/post.dart.

import 'package:flutter/foundation.dart';
import 'package:architecture_app/graphql/graphql_api.dart';
import 'package:graphql_flutter/graphql_flutter.dart';

class PostRepository {
  final GraphQLClient client;

  PostRepository({
    @required this.client,
  });

  Future<List<GetPosts$Query$Post>> getPosts() async {
    final results = await client.query(
      QueryOptions(
        documentNode: GetPostsQuery().document,
      ),
    );
    print(results);
    if (results.hasException) {
      throw results.exception;
    } else {
      return GetPosts$Query.fromJson(results.data).posts;
    }
  }
}

When I told you we will do magic I meant it, the Artemis package did what it says on the tin and now you do not have to even worry about parsing the results from json.

BLoC files

I will have a BLoC for each screen that will handle all the business logic, the UI part will just render according to the state of the BLoC.

Home events

./lib/screens/home/home_event.dart.

class HomeEvent {}

class HomeLoadEvent extends HomeEvent {}

We have just one event the home screen can send which is telling the BLoC to load the required data.

Home state

./lib/screens/home/home_state.dart.

import 'package:flutter/foundation.dart';
import 'package:architecture_app/graphql/graphql_api.dart';

class HomeState {}

class HomeInitialState extends HomeState {}

class HomeLoadingState extends HomeState {}

class HomeLoadedState extends HomeState {
  final List<GetPosts$Query$Post> posts;
  HomeLoadedState({
    @required this.posts,
  });
}

class HomeErrorState extends HomeState {}

Beside the initial state, we have three possible cases: loading, error, or loaded state.

Home bloc

./lib/screens/home_bloc.dart.

import 'package:flutter/foundation.dart';
import 'package:bloc/bloc.dart';

import 'bloc.dart';

import 'package:architecture_app/repositories/post.dart';

class HomeBloc extends Bloc<HomeEvent, HomeState> {
  final PostRepository postRepository;

  HomeBloc({
    @required this.postRepository,
  });

  @override
  get initialState => HomeInitialState();

  @override
  Stream<HomeState> mapEventToState(event) async* {
    if (event is HomeLoadEvent) {
      try {
        yield HomeLoadingState();
        final posts = await postRepository.getPosts();
        yield HomeLoadedState(
          posts: posts,
        );
      } catch (error) {
        yield HomeErrorState();
      }
    }
  }
}

The BLoC code is supper simple when using this way, just get the posts from the repository and yield a new state according to the results.

Home screen

Create one file in ./lib/screens/home_screen.dart.

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

import 'bloc.dart';

class HomeScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Architecture demo'),
      ),
      body: Container(
        child: BlocBuilder<HomeBloc, HomeState>(
          builder: (context, state) {
            if (state is HomeLoadingState) {
              return Container(
                child: Center(
                  child: CircularProgressIndicator(),
                ),
              );
            } else if (state is HomeLoadedState) {
              final posts = state.posts;
              return ListView.builder(
                itemCount: posts.length,
                itemBuilder: (context, index) {
                  final post = posts[index];
                  return ListTile(
                    key: Key(post.id),
                    title: Text(post.title),
                    subtitle: Text('By: ${post.author.name}'),
                  );
                },
              );
            } else {
              return Container(
                child: Center(
                  child: Text('You have an error'),
                ),
              );
            }
          },
        ),
      ),
    );
  }
}

Connect the home screen in main.dart

Open ./main.dart and change all the code to the following.

import 'package:architecture_app/client.dart';
import 'package:architecture_app/screens/home/home_screen.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:graphql_flutter/graphql_flutter.dart';

import 'package:architecture_app/screens/home/bloc.dart';
import 'package:architecture_app/repositories/post.dart';

GraphQLClient _client = client.value;
PostRepository postRepository = PostRepository(
  client: _client,
);

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Architecture demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: BlocProvider(
        create: (context) => HomeBloc(
          postRepository: postRepository,
        )..add(HomeLoadEvent()),
        child: HomeScreen(),
      ),
    );
  }
}

Conclusion

We have build a modular app, even thought it has one screen, you can add many screens and each screen will have its own BLoC. Using a repositories will separate the networking logic from the app logic, you can simply call different api endpoints, log every network request, stub the whole api when doing testing without touching any of the app code.

Full source code at https://github.com/agent3bood/Flutter-architecture-app/tree/starter-kit .