Code Sharing with Bloc
⚠️ This article may be out of date. Please view the updated tutorial at bloclibrary.dev.
One cool aspect of Dart is that it can be used to write applications that run on mobile (iOS and Android), in a browser and on the server.
Today we’re going to take a look at how to share code between a mobile application written with Flutter and a web application written with AngularDart.
Not only are we going to learn how to share code between web and mobile applications, but we’re also going to build a pretty cool Github Search app at the same time.
The application we’re building is inspired by https://github.com/brianegan/github_search_angular_flutter and is going to look like:
Let’s get started!
At a high level, our Github Search codebase is going to be broken up into three subprojects:
- common_github_search
- flutter_github_search
- angular_github_search
Common Github Search
The common_github_search project will contain models, the data provider, the repository, as well as the bloc which will be shared between AngularDart and Flutter.
Setup
We’ll start off by creating a new directory for our application.
mkdir github_search && cd github_search
Next, we’ll create the scaffold for the common_github_search
library.
mkdir common_github_search
We need to create a pubspec.yaml
with the required dependencies.
Lastly, we need to install our dependencies with
pub get
That’s it for the project setup! Now we can get to work on building out the common_github_search
package.
Github Client
The GithubClient
will be providing raw data from the Github API.
You can see a sample of what the data we get back will look like here.
Let’s start by creating github_client.dart
.
Our GithubClient
is simply making a network request to Github's Repository Search API and converting the result into either a SearchResult
or SearchResultError
as a Future
.
Next we need to define our SearchResult
and SearchResultError
models.
Search Result Model
Create search_result.dart
.
Notice that we aren’t including all properties from the Github API. Instead, we only include the properties we are going to use in our application.
The SearchResult
implementation depends on SearchResultItem.fromJson
which we have not yet implemented so we’ll create that next.
Search Result Item Model
Create search_result_item.dart
.
Again, we are only including the properties we care about and SearchResultItem
depends on GithubUser.fromJson
so we’ll implement that next.
Github User Model
Create github_user.dart
.
At this point we have finished implementing SearchResult
and its dependencies so next we'll move onto SearchResultError
.
Search Result Error Model
Create search_result_error.dart
.
At this point, our GithubClient
is finished so next we'll move onto the GithubCache
which will be responsible for memoizing as a performance optimization.
Github Cache
Our GithubCache
will be responsible for remembering all past queries so that we can avoid making unnecessary network requests to the Github API. This will help improve our application's performance and save our users’ data.
Create github_cache.dart
.
Now we’re ready to create our GithubRepository
!
Github Repository
The Github Repository is responsible for creating an abstraction between the data layer (GithubClient
) and the Business Logic Layer (Bloc
). This is also where we're going to put our GithubCache
to use.
Create github_repository.dart
.
Notice that the GithubRepository
has a dependency on the GithubCache
& the GithubClient
and abstracts the underlying implementation. Our application never has to know about how the data is being retrieved or where it’s coming from since it shouldn’t care. We can change how the repository works at any time and as long as we don’t change the interface we shouldn’t need to change any client code.
At this point, we’ve completed the data provider layer and the repository layer so we’re ready to move on to the business logic layer.
At a high level we are going to build a GithubSearchBloc
which converts GithubSearchEvents
into GithubSearchStates
using the bloc package.
Github Search Event
Our bloc will be notified when a user has typed the name of a repository which we will represent as a TextChanged
GithubSearchEvent
.
Create github_search_event.dart
.
We extend Equatable
so that we can compare instances of GithubSearchEvent
; by default, the equality operator returns true if and only if this and other are the same instance. Those are all the events our bloc is going to respond to so we’ll define our states next.
Github Search State
Our presentation layer will need to have several pieces of information in order to properly lay itself out:
SearchStateEmpty
- will tell the presentation layer that no input has been given by the userSearchStateLoading
- will tell the presentation layer it has to display some sort of loading indicatorSearchStateSuccess
- will tell the presentation layer that it has data to presentSearchStateError
- will tell the presentation layer that an error has occurred while fetching repositories
We can now create github_search_state.dart
and implement it like so.
Now that we have our Events and States implemented, we can create our GithubSearchBloc
.
Github Search Bloc
Create github_search_bloc.dart
Our GithubSearchBloc
converts GithubSearchEvent
to GithubSearchState
and has a dependency on the GithubRepository
.
Notice that we override the transform
method to debounce the GithubSearchEvents
so that our application doesn’t spam the Github API while the user is still typing.
In addition, we override onTransition
so that we can log any time a state change occurs.
Awesome! We’re all done with our common_github_search
package. The finished product should look like this.
Next, we’ll work on the Flutter implementation.
Flutter Github Search
Flutter Github Search will be a Flutter application which reuses the models, data providers, repositories, and blocs from common_github_search
to implement Github Search.
Setup
We need to start by creating a new Flutter project in our github_search
directory at the same level as common_github_search
.
flutter create flutter_github_search
Next, we need to update our pubspec.yaml
to include all the necessary dependencies.
Notice how we are including our newly created common_github_search
library as a dependency. This allows us to import it and use it just like any other dart package.
Now we need to install the dependencies.
flutter packages get
That’s it for project setup and since the common_github_search
package contains our data layer as well as our business logic layer all we need to build is the presentation layer.
We’re going to need to create a form with a SearchBar
and SearchBody
widget.
SearchBar
will be responsible for taking user input.SearchBody
will be responsible for displaying search results, loading indicators, and errors.
Search Form
Our SearchForm
will be a StatefulWidget
because it will need to create and dispose of a GithubSearchBloc
.
Let’s create search_form.dart
.
Notice how we are importing our common_github_search
package which gives us access to things like GithubRepository
. In this case, the GithubRepository
is injected into the SearchForm
. In addition, the GithubSearchBloc
is created and disposed by the SearchForm
.
That’s it for the SearchForm
, now let’s move on to the _SearchBar
.
Search Bar
SearchBar
is also going to be a StatefulWidget
because it will need to maintain its own TextController
so that we can keep track of what a user has entered as input.
If we were going to reuse SearchBar
then we could make it a public class and move it to its own file but since it’s only going to be used by the SearchForm
we will keep it in the same file and make it private.
The implementation will look like this.
Note that _SearchBar
has a dependency on GitHubSearchBloc
because it is responsible for notifying the bloc of TextChanged
events on user input.
We’re done with _SearchBar
, now onto _SearchBody
.
Search Body
SearchBody
is a StatelessWidget
which will be responsible for displaying search results, errors, and loading indicators. It will be the consumer of the GithubSearchBloc
.
Again, it will be a private widget in the same file, but in practice you can break it out into its own file and make it publicly accessible.
Notice _SearchBody
has a dependency on GithubSearchBloc
and uses BlocBuilder
in order to rebuild in response to state changes. BlocBuilder
is part of the flutter_bloc package and allows us to rebuild our UI based on state changes in our bloc.
If our state is SearchStateSuccess
we render _SearchResults
which we will implement next.
Search Results
SearchResults
is a StatelessWidget
which takes a List<SearchResultItem>
and displays them as a list of SearchResultItems
.
Just like the other components, it’s private (denoted by the _
prefix) and the implementation is as follows.
All we are doing is using ListView.builder
in order to construct a scrollable list of SearchResultItem
which we will implement next.
Search Result Item
SearchResultItem
is a StatelessWidget
and is responsible for rendering the information for a single search result. It is also responsible for handling user interaction and navigating to the repository url on a user tap.
Again, this widget is private and looks like:
Note that we use the url_launcher package to open external urls if a user taps on an individual result.
Putting it all together
At this point our search_form.dart
should look like
So all that’s left to do is implement our main app in main.dart
.
Note how our GithubRepository
is created in main
and injected into our App
. This makes it possible to inject mock implementations of the repository for testing purposes.
That’s all there is to it! We’ve now successfully implemented a github search app in Flutter using the bloc and flutter_bloc packages and we’ve successfully separated our presentation layer from our business logic.
The full source can be found here.
Finally, we’re going to build our AngularDart Github Search app.
AngularDart Github Search
AngularDart Github Search will be an AngularDart application which reuses the models, data providers, repositories, and blocs from common_github_search
to implement Github Search.
Setup
We need to start by creating a new AngularDart project in our github_search directory at the same level as common_github_search
.
stagehand web-angular
If you haven’t used stagehand, you can activate it by running
pub global activate stagehand
We can then go ahead and replace the contents of pubspec.yaml
with:
Again, we are including common_github_search
so that we can reuse the data layer, repository layer, and bloc layer.
Just like in our Flutter app, we’re going to need to create a SearchForm
with a SearchBar
and SearchBody
component.
Search Form
Our SearchForm
component will implement OnInit
and OnDestroy
because it will need to create and dispose of a GithubSearchBloc
.
Let’s create github_search_form_component.dart
.
Notice how, again, the GithubRepository
is injected into the SearchFormComponent
and the GithubSearchBloc
is created and disposed by the SearchFormComponent
.
Our template (github_search_form_component.html
) will look like:
Next, we’ll implement the SearchBar
component.
Search Bar
Again, SearchBar
will be responsible for taking in user input and notifying the GithubSearchBloc
of text changes.
Create github_search_bar_component.dart
.
Notice how the SearchBarComponent
has a dependency on GitHubSearchBloc
because it is responsible for notifying the bloc of TextChanged
events.
Next, we can create github_search_bar_component.html
.
We’re done with SearchBar
, now onto SearchBody
.
Search Body
Just like in our Flutter app SearchBody
will be responsible for displaying search results, errors, and loading indicators. It will be the consumer of the GithubSearchBloc
.
Create github_search_body_component.dart
Note that SearchBodyComponent
has a dependency on GithubSearchState
which is provided by the GithubSearchBloc
using the angular_bloc
bloc pipe from the angular_bloc package.
Next we need to create github_search_body_component.html
.
If our state isSuccess
we render SearchResults
which we will implement next.
Search Results
SearchResults
is a component which takes a List<SearchResultItem>
and displays them as a list of SearchResultItems
.
Create github_search_results_component.dart
Next up we’ll create github_search_results_component.html
.
We use ngFor
in order to iterate through items and construct a list of SearchResultItem
components.
We only have a few components to go! It’s time to implement SearchResultItem
.
Search Result Item
Similar to the Flutter implementation, SearchResultItem
is a component that is responsible for rendering the information for a single search result. It is also responsible for handling user interaction and navigating to the repository url on a user tap in a new browser tab.
Create github_search_result_item_component.dart
.
and the corresponding template is github_search_result_item_component.html
.
Putting it all together
We have all of our angular components and now it’s time to put them all together in our app_component.dart
.
Again, notice we’re creating the GithubRepository
in the AppComponent
and injecting it into the SearchForm
component so that we can test the SearchForm
with mock implementations of the repository.
That’s all there is to it! We’ve now successfully implemented a github search app in AngularDart using the bloc
and angular_bloc
packages and we’ve successfully separated our presentation layer from our business logic.
The full source can be found here.
Summary
In this tutorial we created a Flutter and AngularDart app while sharing all of the models, data providers, and blocs between the two. 🎉
The only thing we actually had to write twice was the presentation layer (UI) which is awesome in terms of efficiency and development speed. In addition, it’s fairly common for web apps and mobile apps to have different user experiences and styles and this approach really demonstrates how easy it is to build two apps that look totally different but share the same data and business logic layers.
The full source can be found here.
If you enjoyed this exercise as much as I did you can support me by ⭐️the repository, or 👏 for this story.