In the following tutorial, we're going to build a Github Search app in Flutter and AngularDart to demonstrate how we can share the data and business logic layers between the two projects.
- BlocProvider, a Flutter widget which provides a bloc to its children.
- BlocBuilder, a Flutter widget that handles building the widget in response to new states.
- Using Bloc instead of Cubit. What's the difference?
- Prevent unnecessary rebuilds with Equatable.
- Use a custom
EventTransformer
withbloc_concurrency
. - Making network requests using the
http
package.
The Common Github Search library will contain models, the data provider, the repository, as well as the bloc that will be shared between AngularDart and Flutter.
We'll start off by creating a new directory for our application.
Next, we'll create the scaffold for the common_github_search
library.
We need to create a pubspec.yaml
with the required dependencies.
Lastly, we need to install our dependencies.
That's it for the project setup! Now we can get to work on building out the common_github_search
package.
The
GithubClient
which will be providing raw data from the Github API.
?> Note: You can see a sample of what the data we get back will look like here.
Let's create github_client.dart
.
?> Note: 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
.
?> Note: The GithubClient
implementation depends on SearchResult.fromJson
, which we have not yet implemented.
Next we need to define our SearchResult
and SearchResultError
models.
Create search_result.dart
, which represents a list of SearchResultItems
based on the user's query:
?> Note: The SearchResult
implementation depends on SearchResultItem.fromJson
, which we have not yet implemented.
?> Note: We aren't including properties that aren't going to be used in our model.
Next, we'll create search_result_item.dart
.
?> Note: Again, the SearchResultItem
implementation dependes on GithubUser.fromJson
, which we have not yet implemented.
Next, we'll create github_user.dart
.
At this point, we have finished implementing SearchResult
and its dependencies. Now we'll move onto SearchResultError
.
Create search_result_error.dart
.
Our GithubClient
is finished so next we'll move onto the GithubCache
, which will be responsible for memoizing as a performance optimization.
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 also help improve our application's performance.
Create github_cache.dart
.
Now we're ready to create our GithubRepository
!
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 ourGithubCache
to use.
Create github_repository.dart
.
?> Note: The GithubRepository
has a dependency on the GithubCache
and 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.
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
.
?> Note: 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.
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 user. -
SearchStateLoading
- will tell the presentation layer it has to display some sort of loading indicator. -
SearchStateSuccess
- will tell the presentation layer that it has data to present.items
- will be theList<SearchResultItem>
which will be displayed.
-
SearchStateError
- will tell the presentation layer that an error has occurred while fetching repositories.error
- will be the exact error that occurred.
We can now create github_search_state.dart
and implement it like so.
?> Note: We extend Equatable
so that we can compare instances of GithubSearchState
. By default, the equality operator returns true if and only if this and other are the same instance.
Now that we have our Events and States implemented, we can create our GithubSearchBloc
.
Create github_search_bloc.dart
:
?> Note: Our GithubSearchBloc
converts GithubSearchEvent
to GithubSearchState
and has a dependency on the GithubRepository
.
?> Note: We create a custom EventTransformer
to debounce the GithubSearchEvents
. One of the reasons why we created a Bloc
instead of a Cubit
was to take advantage of stream transformers.
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 will be a Flutter application which reuses the models, data providers, repositories, and blocs from
common_github_search
to implement Github Search.
We need to start by creating a new Flutter project in our github_search
directory at the same level as common_github_search
.
Next, we need to update our pubspec.yaml
to include all the necessary dependencies.
?> Note: We are including our newly created common_github_search
library as a dependency.
Now, we need to install the dependencies.
That's it for project setup. 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.
Let's create search_form.dart
.
Our
SearchForm
will be aStatelessWidget
which renders the_SearchBar
and_SearchBody
widgets.
Next, we'll implement _SearchBar
.
_SearchBar
is also going to be aStatefulWidget
because it will need to maintain its ownTextEditingController
so that we can keep track of what a user has entered as input.
?> Note: _SearchBar
accesses GitHubSearchBloc
via context.read<GithubSearchBloc>()
and notifies the bloc of TextChanged
events.
We're done with _SearchBar
, now onto _SearchBody
.
_SearchBody
is aStatelessWidget
which will be responsible for displaying search results, errors, and loading indicators. It will be the consumer of theGithubSearchBloc
.
?> Note: _SearchBody
uses BlocBuilder
in order to rebuild in response to state changes. Since the bloc parameter of the BlocBuilder
object was omitted, BlocBuilder
will automatically perform a lookup using BlocProvider
and the current BuildContext
. Read more here.
If our state is SearchStateSuccess
, we render _SearchResults
which we will implement next.
_SearchResults
is aStatelessWidget
which takes aList<SearchResultItem>
and displays them as a list of_SearchResultItems
.
?> Note: We use ListView.builder
in order to construct a scrollable list of _SearchResultItem
.
It's time to implement _SearchResultItem
.
_SearchResultItem
is aStatelessWidget
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.
?> Note: We use the url_launcher package to open external urls.
At this point our search_form.dart
should look like
Now all that's left to do is implement our main app in main.dart
.
?> Note: Our GithubRepository
is created in main
and injected into our App
. Our SearchForm
is wrapped in a BlocProvider
which is responsible for initializing, closing, and making the instance of GithubSearchBloc
available to the SearchForm
widget and its children.
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 will be an AngularDart application which reuses the models, data providers, repositories, and blocs from
common_github_search
to implement Github Search.
We need to start by creating a new AngularDart project in our github_search directory at the same level as common_github_search
.
!> Activate stagehand by running pub global activate stagehand
.
We can then go ahead and replace the contents of pubspec.yaml
with:
Just like in our Flutter app, we're going to need to create a SearchForm
with a SearchBar
and SearchBody
component.
Our
SearchForm
component will implementOnInit
andOnDestroy
because it will need to create and close aGithubSearchBloc
.
SearchBar
will be responsible for taking user input.SearchBody
will be responsible for displaying search results, loading indicators, and errors.
Let's create search_form_component.dart.
?> Note: The GithubRepository
is injected into the SearchFormComponent
.
?> Note: The GithubSearchBloc
is created and closed by the SearchFormComponent
.
Our template (search_form_component.html
) will look like:
Next, we'll implement the SearchBar
component.
SearchBar
is a component which will be responsible for taking in user input and notifying theGithubSearchBloc
of text changes.
Create search_bar_component.dart
.
?> Note: SearchBarComponent
has a dependency on GitHubSearchBloc
because it is responsible for notifying the bloc of TextChanged
events.
Next, we can create search_bar_component.html
.
We're done with SearchBar
, now onto SearchBody
.
SearchBody
is a component which will be responsible for displaying search results, errors, and loading indicators. It will be the consumer of theGithubSearchBloc
.
Create search_body_component.dart
.
?> Note: SearchBodyComponent
has a dependency on GithubSearchState
which is provided by the GithubSearchBloc
using the angular_bloc
bloc pipe.
Create search_body_component.html
.
If our state isSuccess
, we render SearchResults
. We will implement it next.
SearchResults
is a component which takes aList<SearchResultItem>
and displays them as a list ofSearchResultItems
.
Create search_results_component.dart
.
Next up we'll create search_results_component.html
.
?> Note: We use ngFor
in order to construct a list of SearchResultItem
components.
It's time to implement SearchResultItem
.
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.
Create search_result_item_component.dart
.
search_result_item_component.dart
and the corresponding template in search_result_item_component.html
.
search_result_item_component.html
We have all of our components and now it's time to put them all together in our app_component.dart
.
?> Note: We're creating the GithubRepository
in the AppComponent
and injecting it into the SearchForm
component.
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.
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.