10 min read

How to Leverage BLoC for Code Sharing in Flutter and AngularDart

View all articles

Mid last year, I wanted to port an Android app to iOS and web. Flutter was the choice for mobile platforms, and I was thinking about what to choose for the web side.

While I fell in love with Flutter on first sight, I still had some reservations: While propagating state down the widget tree, Flutter’s InheritedWidget or Redux—with all its variations—will do the job, but with a new framework like Flutter, you would expect that the view layer would be a little more reactive, i.e., widgets would be stateless themselves, and change according to the state they’re fed from outside, but they aren’t. Aslo, Flutter only supports Android and iOS, but I wanted to publish to the web. I already have loads of business logic in my app and I wanted to reuse it as much as possible, and the idea of changing the code in at least two places for a single change in business logic was unacceptable.

I started looking around how to get over this and came across BLoC. For a quick intro, I recommend watching Flutter/AngularDart – Code sharing, better together (DartConf 2018) when you have the time.

BLoC Pattern

Diagram of the communications flow in the view, BLoC, repository, and data layers

BLoC is a fancy word invented by Google meaning “business logic components.” The idea of the BLoC pattern is to store as much of your business logic as possible in pure Dart code so it can be reused by other platforms. To achieve this, there are rules you must follow:

  • Communicate in layers. Views communicate with the BLoC layer, which communicates to the repositories, and the repositories talk to the data layer. Don’t skip layers while communicating.
  • Communicate over interfaces. Interfaces must be written in pure, platform-independent Dart code. For more information, see the documentation on implicit interfaces.
  • BLoCs only expose streams and sinks. The I/O of a BLoC will be discussed later.
  • Keep views simple. Keep the business logic out of the views. They should only display data and respond to user interaction.
  • Make BLoCs platform agnostic. BLoCs are pure Dart code, and so they should contain no platform-specific logic or dependencies. Do not branch into platform conditional code. BLoCs are logic implemented in pure Dart and are above dealing with the base platform.
  • Inject platform-specific dependencies. This may sound contradictory to the rule above, but hear me out. BLoCs themselves are platform agnostic, but what if they need to communicate with a platform-specific repository? Inject it. By ensuring communicating over interfaces and injecting these repositories, we can be sure that regardless of whether your repository is written for Flutter or AngularDart, the BLoC won’t care.

One last thing to keep in mind is that the input for a BLoC should be a sink, while the output is through a stream. These are both part of the StreamController.

If you strictly adhere to these rules while writing your web (or mobile!) app, creating a mobile (or web!) version can be as simple as making the views and platform-specific interfaces. Even if you’ve just begun using AngularDart or Flutter, it’s still easy to make views with basic platform knowledge. You may end up reusing more than half your codebase. The BLoC pattern keeps everything structured and easy to maintain.

Building an AngularDart and Flutter BLoC Todo App

I made a simple todo app in Flutter and AngularDart. The app uses Firecloud as a back-end and a reactive approach to view creation. The app has three parts:

  • bloc
  • todo_app_flutter
  • todoapp_dart_angular

You can choose to have more parts—for example, data interface, localization interface, etc. The thing to remember is that each layer should communicate with the other over an interface.

The BLoC Code

In the bloc/ directory:

  • lib/src/bloc: The BloC modules are stored here as pure Dart libraries containing the business logic.
  • lib/src/repository: The interfaces to data are stored in the directory.
  • lib/src/repository/firestore: The repository contains the FireCloud interface to data together with its model, and since this is a sample app, we only have one data model todo.dart and one interface to the data todo_repository.dart; however, in a real-world app, there will be more models and repository interfaces.
  • lib/src/repository/preferences contains preferences_interface.dart, a simple interface that stores successfully signed-in usernames to local storage on web or shared preferences on mobile devices.
//BLOC
abstract class PreferencesInterface{
//Preferences
 final DEFAULT_USERNAME = "DEFAULT_USERNAME";

 Future initPreferences();
 String get defaultUsername;
 void setDefaultUsername(String username);
}

Web and mobile implementations must implement this to the store and get the default username from local storage/preferences. The AngularDart implementation of this looks like:

// ANGULAR DART
class PreferencesInterfaceImpl extends PreferencesInterface {

 SharedPreferences _prefs;

 @override
 Future initPreferences() async => _prefs = await SharedPreferences.getInstance();

 @override
 void setDefaultUsername(String username) => _prefs.setString(DEFAULT_USERNAME, username);
 @override
 String get defaultUsername => _prefs.getString(DEFAULT_USERNAME);
}

Nothing spectacular here—it implements what it needs. You might notice the initPreferences() async method that returns null. This method needs to be implemented on the Flutter side since getting the SharedPreferences instance on mobile is async.

//FLUTTER
@override
Future initPreferences() async => _prefs = await SharedPreferences.getInstance();

Let’s stick a little bit with the lib/src/bloc dir. Any view that handles some business logic should have its BLoC component. In this dir, you’ll see view BLoCs base_bloc.dart, endpoints.dart, and session.dart. The last one is responsible for signing the user in and out and providing endpoints for repository interfaces. The reason the session interface exists is that the firebase and firecloud packages are not the same for web and mobile and must be implemented based on platform.

// BLOC
abstract class Session implements Endpoints {

 //Collections.
 @protected
 final String userCollectionName = "users";
 @protected
 final String todoCollectionName = "todos";
 String userId;

 Session(){
   _isSignedIn.stream.listen((signedIn) {
     if(!signedIn) _logout();
   });
 }

 final BehaviorSubject<bool> _isSignedIn = BehaviorSubject<bool>();
 Stream<bool> get isSignedIn => _isSignedIn.stream;
 Sink<bool> get signedIn => _isSignedIn.sink;

 Future<String> signIn(String username, String password);
 @protected
 void logout();

 void _logout() {
   logout();
   userId = null;
 }
}

The idea is to keep the session class global (singleton). Based on its _isSignedIn.stream getter, it handles the app switch between login/todo-list view and provides endpoints to repository implementations if the userId exists (i.e., the user is signed in).

base_bloc.dart is the base for all BLoCs. In this example, it handles load indicator and error dialog display as needed.

For the business logic example, we’ll take a look at todo_add_edit_bloc.dart. The file’s long name explains its purpose. It has a private void method _addUpdateTodo(bool addUpdate).

// BLOC
void _addUpdateTodo(bool addUpdate) {
 if(!addUpdate) return;
 //Check required.
 if(_title.value.isEmpty)
   _todoError.sink.add(0);
 else if(_description.value.isEmpty)
   _todoError.sink.add(1);
 else
   _todoError.sink.add(-1);

 if(_todoError.value >= 0)
   return;

 final TodoBloc todoBloc = _todo.value == null ? TodoBloc("", false, DateTime.now(), null, null, null) : _todo.value;
 todoBloc.title = _title.value;
 todoBloc.description = _description.value;

 showProgress.add(true);
 _toDoRepository.addUpdateToDo(todoBloc)
     .doOnDone( () => showProgress.add(false) )
     .listen((_) => _closeDetail.add(true) ,
     onError: (err) => error.add( err.toString()) );
}

The input for this method is bool addUpdate and it’s a listener of final BehaviorSubject<bool> _addUpdate = BehaviorSubject<bool>(). When a user clicks on the save button in the app, the event sends this subject sink true value and triggers this BLoC function. This piece of flutter code does the magic on the view side.

// FLUTTER
IconButton(icon: Icon(Icons.done), onPressed: () => _todoAddEditBloc.addUpdateSink.add(true),),

_addUpdateTodo checks that both the title and description are not empty and changes the value of _todoError BehaviorSubject based on this condition. The _todoError error is responsible for triggering the view error display on input fields if no value is supplied. If everything is fine, it checks whether to create or update the TodoBloc and finally _toDoRepository does the write to FireCloud.

The business logic is here but notice:

  • Only streams and sinks are public in BLoC. _addUpdateTodo is private and can’t be accessed from view.
  • _title.value and _description.value are filled by the user entering the value in the text input. Text input on text change event sends its value to the respective sinks. This way, we have a reactive change of values in the BLoC and the display of them in the view.
  • _toDoRepository is platform dependent and is provided by injection.

Check out the code of the todo_list.dart BLoC _getTodos() method. It listens for a snapshot of the todo collection and streams the collection data to list in its view. The view list is redrawn based on collection stream change.

// BLOC
void _getTodos(){
 showProgress.add(true);
 _toDoRepository.getToDos()
     .listen((todosList) {
       todosSink.add(todosList);
       showProgress.add(false);
       },
     onError: (err) {
       showProgress.add(false);
       error.add(err.toString());
     });
}

The important thing to be aware of when using streams or rx equivalent is that streams must be closed. We do that in the dispose() method of each BLoC. Dispose the BLoC of each view in its dispose/destroy method.

// FLUTTER

@override
void dispose() {
 widget.baseBloc.dispose();
 super.dispose();
}

Or in an AngularDart project:


// ANGULAR DART
@override
void ngOnDestroy() {
 todoListBloc.dispose();
}

Injecting Platform-specific Repositories

Diagram of relationships between the BLoC patterns, todo repository, and more

We said before that everything that comes in a BLoC must be simple Dart and nothing platform-dependent. TodoAddEditBloc needs ToDoRepository to write to Firestore. Firebase has platform-dependent packages, and we must have separate implementations of the ToDoRepository interface. These implementations are injected in apps. For Flutter, I used the flutter_simple_dependency_injection package and it looks like this:


// FLUTTER
class Injection {

 static Firestore _firestore = Firestore.instance;
 static FirebaseAuth _auth = FirebaseAuth.instance;
 static PreferencesInterface _preferencesInterface = PreferencesInterfaceImpl();

 static Injector injector;
 static Future initInjection() async {
   await _preferencesInterface.initPreferences();
   injector = Injector.getInjector();
   //Session
   injector.map<Session>((i) => SessionImpl(_auth, _firestore), isSingleton: true);
   //Repository
   injector.map<ToDoRepository>((i) => ToDoRepositoryImpl(injector.get<Session>()), isSingleton: false);
   //Bloc
   injector.map<LoginBloc>((i) => LoginBloc(_preferencesInterface, injector.get<Session>()), isSingleton: false);
   injector.map<TodoListBloc>((i) => TodoListBloc(injector.get<ToDoRepository>(), injector.get<Session>()), isSingleton: false);
   injector.map<TodoAddEditBloc>((i) => TodoAddEditBloc(injector.get<ToDoRepository>()), isSingleton: false);
 }
}

Use this in a widget like this:

// FLUTTER
TodoAddEditBloc _todoAddEditBloc = Injection.injector.get<TodoAddEditBloc>();

AngularDart has injection built in via providers.

// ANGULAR DART
@GenerateInjector([
 ClassProvider(PreferencesInterface, useClass: PreferencesInterfaceImpl),
 ClassProvider(Session, useClass: SessionImpl),
 ExistingProvider(Endpoints, Session)
])

And in a component:

// ANGULAR DART
providers: [
 overlayBindings,
 ClassProvider(ToDoRepository, useClass: ToDoRepositoryImpl),
 ClassProvider(TodoAddEditBloc),
 ExistingProvider(BaseBloc, TodoAddEditBloc)
],

We can see that Session is global. It provides the sign in/out functionality and endpoints used in ToDoRepository and BLoCs. ToDoRepository needs an endpoints interface which is implemented in SessionImpl and so on. The view should see only its BLoC and nothing more.

Views

Diagram of sinks and streams interacting between the BLoC and view

Views should be as simple as possible. They only display what comes from the BLoC and sends the user’s input to the BLoC. We’ll go over it with TodoAddEdit widget from Flutter and its web equivalent TodoDetailComponent. They display the selected todo title and description and user can add or update a todo.

Flutter:

// FLUTTER
_todoAddEditBloc.todoStream.first.then((todo) {
 _titleController.text = todo.title;
 _descriptionController.text = todo.description;
});

And later in code…

// FLUTTER
StreamBuilder<int>(
 stream: _todoAddEditBloc.todoErrorStream,
 builder: (BuildContext context, AsyncSnapshot errorSnapshot) {
   return TextField(
     onChanged: (text) => _todoAddEditBloc.titleSink.add(text),
     decoration: InputDecoration(hintText: Localization.of(context).title, labelText: Localization.of(context).title, errorText: errorSnapshot.data == 0 ? Localization.of(context).titleEmpty : null),
     controller: _titleController,
   );
 },
),

The StreamBuilder widget rebuilds itself if there is an error (nothing inserted). This happens by listening to _todoAddEditBloc.todoErrorStream . _todoAddEditBloc.titleSink , which is a sink in the BLoC that holds the title and is updated on the user entering text in the text field.

The initial value of this input field (if one todo is selected) is filled by listening to _todoAddEditBloc.todoStream which holds the selected todo or an empty one if we add a new todo.

Assigning value to a text field is done by its controller _titleController.text = todo.title; .

When the user decides to save the todo, it presses the check icon in app bar and it triggers _todoAddEditBloc.addUpdateSink.add(true). That invokes the _addUpdateTodo(bool addUpdate) we talked about in previous BLoC section and does all the business logic of adding, updating, or displaying the error back to the user.

Everything is reactive and there is no need to handle the widget state.

AngularDart code is even more simple. After providing the component its BLoC, using providers, the todo_detail.html file code does the part of displaying the data and sending the user interaction back to the BLoC.

// AngularDart
<material-input
       #title
       label="{{titleStr}}"
       ngModel="{{(todoAddEditBloc.titleStream | async) == null ? '' : (todoAddEditBloc.titleStream | async)}}"
       (inputKeyPress)="todoAddEditBloc.titleSink.add($event)"
       [error]="(todoAddEditBloc.todoErrorStream | async) == 0 ? titleErrString : ''"
       autoFocus floatingLabel style="width:100%"
       type="text"
       useNativeValidation="false"
       autocomplete="off">
</material-input>
<material-input
       #description
       label="{{descriptionStr}}"
       ngModel="{{(todoAddEditBloc.descriptionStream | async) == null ? '' : (todoAddEditBloc.descriptionStream | async)}}"
       (inputKeyPress)="todoAddEditBloc.descriptionSink.add($event)"
       [error]="(todoAddEditBloc.todoErrorStream | async) == 1 ? descriptionErrString : ''"
       autoFocus floatingLabel style="width:100%"
       type="text"
       useNativeValidation="false"
       autocomplete="off">
</material-input>
<material-button
       animated
       raised
       role="button"
       class="blue"
       (trigger)="todoAddEditBloc.addUpdateSink.add(true)">
   {{saveStr}}
</material-button>

<base-bloc></base-bloc>

Similar to Flutter, we are assigning ngModel= the value from the title stream, which is its initial value.

// AngularDart
(inputKeyPress)="todoAddEditBloc.descriptionSink.add($event)"

The inputKeyPress output event sends the characters the user types in the text input back to the BLoC’s description. The material button (trigger)="todoAddEditBloc.addUpdateSink.add(true)" event sends the BLoC add/update event which again triggers the same _addUpdateTodo(bool addUpdate) function in the BLoC. If you take a look at the todo_detail.dart code of the component, you will see that there is almost nothing except the strings that are displayed on the view. I placed them there and not in the HTML because of possible localization that can be done here.

The same goes for every other component—the components and widgets have zero business logic.

One more scenario is worth mentioning. Imagine you have a view with complex data presentation logic or something like a table with values that must be formatted (dates, currencies, etc.). Someone could be tempted to get the values from BLoC and format them in a view. That’s wrong! The values displayed in the view should come to the view already formatted (strings). The reason for that is that the formatting itself is also business logic. One more example is when the formatting of display value depends on some app parameter that can be changed in runtime. By providing that parameter to BLoC and using a reactive approach to view display, the business logic will format the value and redraw only the parts needed. The BLoC model we have in this example, TodoBloc, is very simple. Conversion from a FireCloud model to the BLoC model is done in repository, but if needed, it can be done in BLoC so that the model values are ready for display.

Wrapping Up

This brief article covers the BLoC pattern implementation main concepts. It’s working proof that code sharing between Flutter and AngularDart is possible, allowing for native, and cross-platform development.

Exploring the example, you will see that, when implemented correctly, BLoC shortens the time to create mobile/web apps significantly. An example is ToDoRepository and its implementation. The implementation code is almost identical and even the view composing logic is similar. After a couple of widgets/components, you can quickly start mass production.

I hope this article will give you a glance at the fun and enthusiasm I have making web/mobile apps using Flutter/AngularDart and the BLoC pattern. If you’re looking to build cross-platform desktop applications in JavaScript, read Electron: Cross-platform Desktop Apps Made Easy by fellow Toptaler Stéphane P. Péricat.

Understanding the Basics

What is Flutter?

Flutter is a mobile (Android/iOS) development platform. It's focused on native high-quality user experience and fast development of rich UI apps.

About the author

Marko Perutović, Croatia
member since November 15, 2014
Marko is a skilled software developer with over thirteen years of experience with different challenges and types of technologies. "Spartan: keep it short and simple" is his approach to problems where possible. He is also an excellent communicator, with extensive experience in team leadership and successful interactions with customers. [click to continue...]
Hiring? Meet the Top 10 Freelance Cross-Platform Developers for Hire in December 2018

Comments

Neeraj Kumar
Hi Marko, first of all thank you for the this article. It saved me a lot of hassle. I was trying to implement BLoC pattern in my flutter app and following a few other articles on web. Following those articles, though I was able to set it up and quickly however I was struggling to scale it up. Particularly listening streams in Stateful and Stateless widgets. StreamBuilder is a good option if you want to attach stream to a widget. However many times you want to start listening stream on widget creation and either show snackbar, or navigate to other screen or show bottomsheet. This can not be achieved with StreamBuilder. To pass down bloc to the widget, other articles uses BlocProvider with wrap bloc in a InheritedWidget so that you can access it in child widgets. Problem with this approach is you need context to fetch the bloc and can't get it in initState(). If you do it in build() method, your stream listeners get attached multiple times as flutter rebuild widget many times. Your approach of using DI for passing bloc in widget has great advantage over wrapping bloc in InheritedWidget. Since last 1 week, I was struggling to manage stream listeners in StatefulWidget with BlocProvider approach. Then I saw this article and able to solve the problem within an hour. Thank you for writing it. I wan to ask one question. Why there are two different implementations of `ToDoRepository`? `ToDoRepositoryImpl` in web and flutter project. Also shouldn't be the class `ToDoRepositoryImpl` implements abstract class `ToDoRepository` rather than extending it?
Marko Perutović
Hi Neeraj, thank you! I'm very pleased the article helped you solve the problems and I know exactly what you say. I've been struggling with the same problems myself :) I just followed the MVC pattern on Android and ended up with something like this. ui/base/base_widget.dart in combination with base_bloc.dart solves all the common things like error handling, bottom sheet, dialog, alerts, progress... Remember to extend each BLoC widget from StatefulWidget -> just because of disposing it's BLoC (closing the streams). About your question, the only reason why there are two different implementations of 'ToDoRepository' is because the firebase/firestore package is not the same for flutter and angular-dart. Regarding implementation opposed to extension I wanted to say that endpoints must be injected in every repository implementation. I will mark the endpoints with protected meta. This simple app is intended to be a blueprint for mobile/web development using Flutter/AngularDart and I will expand it over time. Next thing is adding routes handling in web part, localization, ... Feel free to fork it and send pull requests :)
Maynard
Hey Marko, excellent write-up, just what the doctor ordered. I'm having an issue within my app which is highly based off of your app. The issue resides after successful login, although my login is using my own API which I don't believe is the problem. My app has a login page that navigates to the home route after successful login. If I hit the Android back button it takes me to the Android home launcher screen, as to be expected. This is different from your app in that it asks the user to logout. Now if I resume my app it shows the standard Splash screen and navigates to the login page again. Any clue how to handle this? This is the message printed in Android Studio on resume. W/ActivityThread( 9071): handleWindowVisibility: no activity for token [email protected]
Marko Perutović
Hi Maynard, thank you! Not sure I understand you. After login you replace the route with main route? If you do that there is no back route to go to. After resuming the app it starts the app again. Correct?
Maynard
Yes I replace the route like you do. Navigator.of(context).pushReplacementNamed('/home'); I even tried this to no avail. Navigator.of(context).pushNamedAndRemoveUntil('/home', (Route<dynamic> route) => false); Yes, after resuming the app it starts the app again, you get the Splash screen (not custom; Android default Splash screen) and then it navigates to the Login page.
Maynard
FYI, in todo_app_flutter\lib\ui\todo_list\todo_list.dart I removed WillPopScope and your app exhibits the same behavior as mine.
Marko Perutović
If you want to go back to login screen after pressing back from main you have to replace the route again (like I do). Since you replaced the login after successful login, the only widget you have on stack is home widget. You can keep the login screen on stack by just pushing the new home after login (not replacing it). Is that what you want?
Maynard
I want to replace it because it isn't needed anymore so I am good with pushNamedAndRemoveUntil(). The issue I'm trying to figure out is hitting the Android back button on the Home screen of the app. It takes you back to the Android launcher screen (the OS) which is what I would expect, but why does resuming the app show the Splash screen and navigate to the login screen again. And this behavior seems to be across all apps. Take Google Drive for example. I launch the app, then hit the Android back button and it goes to the Android launcher scree. I resume it and Google Drive show the splash screen before displaying the Home screen of the app. They all do it, even Google Photos has the same behavior.
comments powered by Disqus
Subscribe
Free email updates
Get the latest content first.
No spam. Just great articles & insights.
Free email updates
Get the latest content first.
Thank you for subscribing!
Check your inbox to confirm subscription. You'll start receiving posts after you confirm.
Trending articles
Relevant Technologies
About the author
Marko Perutović
C++ Developer
Marko is a skilled software developer with over thirteen years of experience with different challenges and types of technologies. "Spartan: keep it short and simple" is his approach to problems where possible. He is also an excellent communicator, with extensive experience in team leadership and successful interactions with customers.