Clutterfree Flutter Architecture
FRIDAY FEBRUARY 19 2021 - 6 MIN
Flutter is a modern and popular framework for building mobile applications. While its easy-to-learn syntax and fast development time make it a great choice for developing mobile apps, many developers struggle with finding the right architecture that fits their needs. To help solve this problem, we will be discussing a clutter-free architecture setup using a combination of popular libraries such as Bloc, Chopper, Auto Route, Injectable, Freezed, and Screen Util.
We will start by building a simple vanilla Flutter application that has basic functionality such as displaying a list of items and navigating to a detail page.
I will be skipping some of the non-essential code to keep things concise.
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Clutterfree Flutter',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: HomePage(),
);
}
}
class HomePage extends StatefulWidget {
@override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
List<String> words = [];
@override
void initState() {
super.initState();
fetchWords();
}
void fetchWords() async {
var response = await http.get('https://random-word-api.herokuapp.com/word?number=10');
if (response.statusCode == 200) {
setState(() {
words = json.decode(response.body);
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Clutterfree Flutter'),
),
body: ListView.builder(
itemCount: words.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(words[index]),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DetailPage(word: words[index]),
),
);
},
);
},
),
);
}
}
class DetailPage extends StatelessWidget {
final String word;
DetailPage({this.word});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(word),
),
body: Center(
child: Text(word),
),
);
}
}
With this code, we have a basic two-page Flutter application that makes API calls and displays a list of data on the first page and the details of a selected item on the second page. However, as the application grows in complexity and size, it becomes increasingly difficult to maintain the codebase, especially when it comes to state management and API calls.
Write Code Not Boilerplate
This is where Flutter libraries such as Bloc, Chopper, Auto Route, Injectable, Freezed, and Screen Util come in. These libraries are designed to help you write scalable, maintainable, and efficient Flutter applications. Let's take a look at how we can refactor our previous application to make use of these libraries.
Freezed
Freezed is a code generation library for Dart that provides a set of annotations and code generators to make it easy to create immutable classes and data structures. Freezed makes it simple to create classes that are easy to understand and test, and that can be used throughout your application without having to worry about mutability.
BLoC
The Bloc library provides a powerful and flexible pattern for state management. With Freezed, we can easily generate data classes for our application's state, which helps to reduce boilerplate code and ensures that our data model remains in sync with our state. The generated data classes also allow us to easily add new properties and update existing ones, ensuring that our data model remains up-to-date.
First, we need to create a BLoC that will handle the logic for retrieving and updating the data. To do this, we'll create a new class WordBloc
that extends Bloc:
import 'package:flutter/material.dart';
import 'package:injectable/injectable.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:bloc/bloc.dart';
import 'word_repository.dart';
part 'word_event.dart';
part 'word_state.dart';
@injectable
class WordBloc extends Bloc<WordEvent, WordState> {
final WordRepository _wordRepository;
WordBloc(this._wordRepository) : super(WordState.initial());
@override
Stream<WordState> mapEventToState(WordEvent event) async* {
if (event is FetchWords) {
yield WordState.loading();
try {
final words = await _wordRepository.getWords();
yield WordState.loaded(words: words);
} catch (e) {
yield WordState.error(errorMessage: e.toString());
}
}
}
}
@freezed
abstract class WordEvent with _$WordEvent {
const factory WordEvent.fetchWords() = FetchWords;
}
@freezed
abstract class WordState with _$WordState {
const factory WordState.initial() = WordInitial;
const factory WordState.loading() = WordLoading;
const factory WordState.loaded({List<String> words}) = WordLoaded;
const factory WordState.error({String errorMessage}) = WordError;
}
Injectable
Injectable provides a simple and flexible way to manage dependencies in our Flutter application. With Injectable, we can easily manage the lifecycle of our dependencies and provide the required instances to the parts of our application that need them. This can greatly simplify the process of writing tests and making changes to our application.
Chopper
The Chopper library simplifies the process of making API calls and allows us to easily add and manage our API endpoints. This can be particularly useful when working on a large-scale application, where the number of API endpoints can become difficult to manage. Chopper also provides support for request and response interception, making it easy to add global error handling and logging.
Let's rewrite the data layer of this app using Chopper for networking and Freezed for data models.
import 'package:chopper/chopper.dart';
import 'package:clutterfree_flutter/data/models/word.dart';
import 'package:injectable/injectable.dart';
part 'word_repository.chopper.dart';
@ChopperApi(baseUrl: 'https://random-word-api.herokuapp.com')
abstract class WordRepository extends ChopperService {
@Get(path: 'word?number=10')
Future<Response<List<Word>>> getWords();
static WordRepository create() {
final client = ChopperClient(
baseUrl: 'https://random-word-api.herokuapp.com',
services: [
_$WordRepository(),
],
converter: JsonConverter(),
);
return _$WordRepository(client);
}
}
@freezed
abstract class Word with _$Word {
const factory Word({String word}) = _Word;
factory Word.fromJson(Map<String, dynamic> json) => _$WordFromJson(json);
}
@injectable
class ProductionWordRepository extends WordRepository {
final ChopperClient chopperClient;
ProductionWordRepository(this.chopperClient) : super(chopperClient);
}
Auto Route
Auto Route makes it easy to navigate between pages in our application and reduces the amount of boilerplate code required to manage navigation. With AutoRoute, we can define routes in a declarative way, reducing the risk of bugs and making it easier to maintain our application.
You can use the generateRoute
function from the AutoRouter
class to define the routes using the AutoRoute library. Here's an example of how you can define the routes for your app:
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:injectable/injectable.dart';
import 'package:clutterfree_flutter/ui/views/home_page.dart';
import 'package:clutterfree_flutter/ui/views/detail_page.dart';
@Injectable(as: Router)
class $Router extends RouterBase {
@override
Widget generateRoute(RouteSettings settings) {
switch (settings.name) {
case Routes.homePage:
return MaterialPageRoute<dynamic>(
builder: (_) => HomePage(),
);
case Routes.detailPage:
var word = settings.arguments as String;
return MaterialPageRoute<dynamic>(
builder: (_) => DetailPage(word: word),
);
default:
return MaterialPageRoute<dynamic>(
builder: (_) => Scaffold(
body: Center(
child: Text('No route defined for ${settings.name}'),
),
),
);
}
}
}
class Routes {
static const String homePage = '/';
static const String detailPage = '/detail';
}
Screen Util
Finally, the Screen Util library makes it easy to build responsive user interfaces in Flutter. With ScreenUtil, we can easily adjust the size and position of our UI elements based on the size of the screen, ensuring that our application looks great on all devices.
Let's rewrite the UI and bind all the components together. We will be using Screen Util for scaling and AutoRoute for routing.
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:screenutils/screenutils.dart';
import 'package:clutterfree_flutter/word_bloc.dart';
import 'package:clutterfree_flutter/models/word.dart';
import 'package:clutterfree_flutter/word_repository.dart';
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider<WordBloc>(
create: (context) => WordBloc(WordRepository()),
child: Scaffold(
appBar: AppBar(
title: Text('Clutterfree Flutter'),
),
body: BlocBuilder<WordBloc, WordState>(
builder: (context, state) {
return state.when(
initial: () => Center(
child: CircularProgressIndicator(),
),
loading: () => Center(
child: CircularProgressIndicator(),
),
loaded: (words) {
return ListView.builder(
itemCount: words.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(words[index].word, style: TextStyle(fontSize: 20.sp)),
onTap: () {
Navigator.pushNamed(context, 'word_detail', arguments: words[index].word);
},
);
},
);
},
error: () => Center(
child: Text('Error Occurred', style: TextStyle(fontSize: 20.sp)),
),
);
},
),
),
);
}
}
class DetailPage extends StatelessWidget {
final String word;
DetailPage({this.word});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(word),
),
body: Center(
child: Text(word, style: TextStyle(fontSize: 30.sp)),
),
);
}
}
In conclusion, by using these libraries, we can build clean, scalable and maintainable Flutter applications that are easier to develop, test, and maintain. By using a combination of Bloc, Freezed, Chopper, AutoRoute, Injectable and ScreenUtil, we can take advantage of the latest and greatest in Flutter development and ensure that our applications are ready for the future.
For suggestions and queries, just contact me.