Flutter baked is an open source project that uses Flutter/Dart to build corss-platform applications with various tech stacks.
Table of contents
Open Table of contents
Project Description
furni is a furniture app which consists of the following:
- Restful Dart server using alfred
- Client app using Flutter & Riverpod
Other articles in this series:
Prologue
In the previous episode we kind of reached a point in our server to be able to run it locally and implement our mobile application around it.
In this article we will do exactly that by integrating a Flutter
mobile app with our Dart
server.
Initialize Flutter project
I have already implemented the entire UI for this step so that we are able to focus on integrating our application with what we have on our server so far. However, there are couple of points worth mentioning
Project Structure
I have chose to go with the following structure although not entirely preferable for complex projects, but will suffice for our purpose.
|____core
| |____assets.dart
| |____colors.dart
| |____core.dart
|____screens
| |____furniture_item_details_screen.dart
| |____screens.dart
| |____home_screen.dart
|____main.dart
|____controllers
|____widgets
| |____widgets.dart
| |____price_badge.dart
Routing
Routing is important in the context of this application because in HomeScreen
we will be presented with the list of furniture items and upon clicking on an item, the user should be navigated to FurnitureItemDetailsScreen
of the item of interest. Hence, in the later screen we need to obtain the id of the selected item in order to be able to attempt a request on the server with itemId
. The way I chose do this is leveraging the routing mechanism explained in this article.
This approach is debatable since some people like to consider the persistance of the data between views is a state management problem and not a routing problem. For the sake of this tutorial, this will not make much difference, but alternatively we can leverage the power of riverpod
to store or selected itemId
in memory although we are not going to take this path.
Goal
The goal of this article is simple. Connect our application to the backend to display a list of furniture items on the home page and show the details of a selected item. You may have noticed by now that we will be using riverpod
to achieve this task. In particular, its amazing data fetching API, FutureProvider
.
Code
You can find the starting code for this tutorial here. And the ending code here
Creating our listItems
FutureProvider
- Let’s add
riverpod
as a dependency by runningflutter pub add flutter_riverpod
andhttp
package by runningflutter pub add http
. - Copy and paste all the models we created earlier in the data layer of our server
- Create our
FutureProvider
inhome_screen.dart
that will fetch our data from the server. Make sure the server is running locally!
final listItems = FutureProvider<List<FurnitureListItem>>((ref) async {
final res = await http.get(Uri.parse('http://localhost:4242/furnitures'));
final jsonRes = json.decode(res.body)['furnitures'] as List;
return List<FurnitureListItem>.from(
jsonRes.map((item) => FurnitureListItem.fromJson(item))).toList();
});
- Now to bind our list to the ui, we will refactor our home page to extract our
SliverGrid.builder
into its own widget. This widget will extendConsumerWidget
which is a special widget provided byriverpod
that enables us to access any provider from within. This approach of refactoring only the pieces of UI that rely on a state is a good practice since we are now sure that only the parts that needs updating are affected by changing the state. Notice the use ofref.watch(listItems)
is the wayriverpod
binds ourFutureProvider
state to our widget. Hence this widget will re-build each timeFutureProvider
has a new value. Then we can use.when
method to render different UI based on state.
class HomeScreen extends StatelessWidget {
const HomeScreen({
super.key,
});
static const routeName = '/';
@override
Widget build(BuildContext context) {
return Scaffold(
body: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 23.0,
),
child: CustomScrollView(
slivers: [
SliverAppBar.large(
snap: true,
floating: true,
pinned: false,
stretch: true,
title: Image.asset(
AppAssets.logo,
height: 50,
width: double.infinity,
),
),
const SliverToBoxAdapter(child: FurnitureList()) // moved our grid into separate widget
],
),
),
);
}
}
class FurnitureList extends ConsumerWidget {
const FurnitureList({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final list = ref.watch(listItems);
return list.when(
data: (data) {
// return grid ..
},
error: (error, stackTrace) => const Center(
child: Text('Error'),
),
loading: () => const Center(
child: CircularProgressIndicator.adaptive(),
),
);
}
}
- Finally, let’s clean up a little bit by refactor our code into an
ItemCard
widget and navigate to its details when in anonTap
event and supplyFurnitureItemDetailsScreen
withitemId
it needs to fetch its details.
class ItemCard extends StatelessWidget {
const ItemCard({
super.key,
required this.item,
});
final FurnitureListItem item;
@override
Widget build(BuildContext context) {
return InkWell(
onTap: () => Navigator.pushNamed(
context,
FurnitureItemDetailsScreen.routeName,
arguments: FurnitureItemDetailsScreenArgs(
item.id,
),
),
child: Stack(
children: [
....
],
),
);
}
}
With this we managed to fetch data from our first endpoint to list all items and supply the necessary data for the details screen.
Creating our itemDetails
FutureProvider
:
- Similar to what we have done in the previous section, we will create our
itemDetails
provider infurniture_item_details_screen.dart
:
final itemDetails =
FutureProvider.family<FurnitureItemDetails, int>((ref, id) async {
final res = await http.get(Uri.parse('http://localhost:4242/furnitures/$id'));
final jsonRes = json.decode(res.body);
return FurnitureItemDetails.fromJson(jsonRes);
});
Notice how our new provider has the family
modifier which will enable it to receive a param that it depend on in order to function. In our case the param is the id
of the selected item which is used as a path param in our request. We then parse the response and map it to our data model.
- Bind
itemDetails
provider to state
We will need to the same thing we did before. By refactoring FurnitureItemDetailsScreen
into a ConsumerWidget
, we will have access to our WidgetRef
which we can use to bind the FurnitureItemDetailsScreen
to itemDetails
provider.
class FurnitureItemDetailsScreen extends ConsumerWidget {
const FurnitureItemDetailsScreen({super.key});
static const routeName = '/details';
@override
Widget build(BuildContext context, WidgetRef ref) {
final args = ModalRoute.of(context)!.settings.arguments
as FurnitureItemDetailsScreenArgs;
final item = ref.watch(itemDetails(args.itemId));
return Scaffold(
appBar: AppBar(),
body: item.when(
data: (item) {
// return your body here using item details from your server
},
error: (_, __) => const Center(
child: Text('Error'),
),
loading: () => const CircularProgressIndicator.adaptive(),
),
);
}
}
And that is it, you should be able to select a furniture item from the list and navigate to view its details when you tap on it.
Conclusion
In this article we learned how to:
- Integrated
getFuntiruresHandler
andgetFurnitureDetailsHandler
with ourFlutter
application. - Persisted
id
of selected item by leveragingMaterialApp.router
. - Added out-of-the-box conditional render in our ui for
loading
,error
,data
states by leveragingriverpod
capabilities.
Enhancements
- Add add to cart functionality that will submit the select items to a new endpoint on the server.
- Add your favorite database to the mix replacing our
mock.json
file. - Add authentication.
- Add in-memory cache on the client.
- Gracefully mitigate request failures in the client and display readable messages that describe what went wrong to the user with an optional retry functionality.
Like this series? Consider staring it on Github. You can reach me at
[email protected]
.