I have a confession to make. Most of the time when developing apps, I did not think about supporting tablets. I did not bother thinking about how to structure my app that it also can be used on a larger screen.
Mostly, the reason I did not add tablet support to my apps was because I thought it would take me far more time to implement. I won’t say that supporting multiple screens won’t require more work, but it is also simpler than you think.
In this tutorial, I want to demonstrate how to create a simple responsive master-detail view application in Flutter. The app will contain a list of items, each of them can be clicked to show more detail.
You can also checkout the complete project from github.
Prerequisite
Like in my other tutorials
about creating a splash screen and
using Sembast as your data storage solution,
we start this tutorial by creating a new Flutter project and cleaning up all the comments from pubspec.yaml
and
main.dart
.
We let our main.dart
file be for this moment and start by adding the required dependencies to our pubspec.yaml
.
pubspec.yaml
|
|
This should look familiar to you. The only thing I added here are the dependencies to equatable
and flutter_bloc
.
Equatable
will help us to compare objects by its values rather than by reference, and flutter_bloc
will provide us
with the framework we use for state management.
A simple data class
First, we need a class containing the data we want to show in the app. We keep this to an absolute minimum and create a
class Item
under a new directory data
that has two attributes: name
and detail
.
data/item.dart
|
|
Lines 9 - 15 define a copy constructor that does nothing else than creating a new instance of Item
with the same
attribute values as the instance passed via the parameter.
Line 18 overrides the props
getter that is required since Item
extends Equatable
. All elements of the list
returned as props
are used when comparing instances of Item
.
The Logic using the Bloc Pattern
We are using the BLoC (business logic component) pattern in this example. We already included flutter_bloc in the pubspec.yaml before. To generate the boilerplate code, I use the Bloc Code Generator in Android Studio. This reduces the amount of code I have to write, but it is not required to use this plugin.
First, create a new directory and call it bloc
. In Android Studio, right-click on the directory → New → Bloc
Generator → New Bloc and enter “master_detail” as the name. Check “Do you want to use equatable” and click on ok. This
will generate 4 files: bloc.dart
, master_detail_event.dart
, master_detail_state.dart
, and
master_detail_bloc.dart
.
bloc/bloc.dart
|
|
This file simply allows importing these three files using one import statement: If you import bloc.dart
, these files
listed here are imported.
bloc/master_detail_event.dart
|
|
In this file, we create three Events that can be sent to our bloc to request a state change. All three Events extend our
abstract class MasterDetailEvent
and we again use Equatable for comparison.
The LoadItemsEvent
notifies the bloc to load all existing items. AddItemEvent
requests to add a new item specified
by the event’s attribute element. SelectItemEvent
will be used to notify the bloc that an Item
has been selected in
the list.
bloc/master_detail_state.dart
|
|
Where an event is the input to our bloc, a state is what our bloc will give us in return. For this example, our app will
have 3 different states: LoadingItemsState
, NoItemsState
and LoadedItemsState
.
Depending on the state, we will display different UI elements in our app. Where LoadingItemsState
and NoItemsState
do not contain any additional information, the LoadedItemsState
holds a list of Item
s and the currently selected
Item
.
bloc/master_detail_bloc.dart
|
|
This class is the heart of our business logic. For simplicity, we do not use any kind of database in this example, but we are going to store the list of items and the selected item as attributes of the bloc (lines 9 & 10).
If you want to know how to use a real database storage for your app, have a look at my tutorial “Sembast as local data storage in Flutter”. Let me know in the comments if you would like another tutorial about Sembast, using the bloc pattern instead of Flutter’s default state management.
Let’s have a look at the mapEventToState
function. Here we receive all events that are sent to our block. The aim of
the function is to figure out which state the app should have depending on the received events.
First, we check if the event is an AddItemEvent
. In this case, we add the received item (passed through the event as a
parameter) to our list. Second, we check if the event is a SelectItemEvent
. If this is the case, we assign the
selected element to our variable.
After handling any of the events, we call the _loadItems
function. All it does is to create a new state depending on
if there are no items (NoItemsState
) or there are some items stored in the bloc (LoadedItemsState
).
Note that we do not explicitly handle the LoadItemsEvent
. Since _loadItems
is called regardless of which event we
receive, we do not need any other functionality to be executed when a LoadItemsEvent
is received.
The responsive Master-Detail UI
Finally, we are going to build the UI for our application. For the UI files, create a new directory called ui
. First,
we have a look at the files master.dart
and detail.dart
.
ui/master.dart
|
|
If you have worked with bloc before, this should look familiar. We have a stateful widget Master
here. In its state we
retrieve the MasterDetailBloc
we have created earlier (line 19) and use it in a BlocBuilder
to create the UI
depending on its state.
What is interesting here – from the perspective of building a responsive app – is line 70: Here we check if the screen
is smaller than 768 (which we will then classify as a smartphone). When selecting an item on a small screen size like
this, we need to navigate to a different view to show the detailed information about the item. For a tablet, this is not
necessary, since we have plenty of space to show this information right next to the Master
list.
ui/detail.dart
|
|
The Detail
widget looks very similar to the Master
widget. It also retrieves the MasterDetailBloc
and uses
BlocBuilder
to build a state-dependent UI. Here, we use the LoadedItemsState
’s selectedElement
attribute to get
the detailed information to display.
ui/home_page.dart
|
|
For our main screen (HomePage
) we use a LayoutBuilder
to decide if we want to display the tablet or the mobile
version of our screen. As in the master.dart
file, we set our breakpoint to 768 pixels to decide whether the device is
a tablet.
For the mobile version of the HomePage
(_MobileHomePage
) we only display the Master widget. For the tablet version (
_TabletHomePage
) we show both: Master
and Detail
. We limit the width of the Master
widget to 300px and let the
Detail
take the rest of the space.
main.dart
|
|
Finally, all that is left is to adapt the main.dart file to run our application. We use BlocProvider
to make our
MasterDetailBloc
available throughout the widget tree and create a very basic MaterialApp
showing our HomePage
.
Now when we run the app on a smartphone the app uses two separate views to display the master (overview) widget and the detail widget. On a tablet we are making use of the additional screen space to display both widgets next to each other.
Summary
In this tutorial, we had a look at how to create a responsive master-detail view app in Flutter. By using the bloc pattern, it is easy to have independent widgets reacting on a common state of the app.
As mentioned before, you can find the complete project on github.
I hope this tutorial helps you to make your Flutter apps also available for larger screens without putting too much additional effort in the development process.