Skip to content

Fullstack Furniture App with Dart and Flutter Part Three

Posted on:May 10, 2023 at 06:00 PM

image

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:

  1. Restful Dart server using alfred
  2. Client app using Flutter & Riverpod

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

  1. Let’s add riverpod as a dependency by running flutter pub add flutter_riverpod and http package by running flutter pub add http.
  2. Copy and paste all the models we created earlier in the data layer of our server
  3. Create our FutureProvider in home_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();
});
  1. 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 extend ConsumerWidget which is a special widget provided by riverpod 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 of ref.watch(listItems) is the way riverpod binds our FutureProvider state to our widget. Hence this widget will re-build each time FutureProvider 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(),
      ),
    );
  }
}
  1. Finally, let’s clean up a little bit by refactor our code into an ItemCard widget and navigate to its details when in an onTap event and supply FurnitureItemDetailsScreen with itemId 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:

  1. Similar to what we have done in the previous section, we will create our itemDetails provider in furniture_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.

  1. 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:

  1. Integrated getFuntiruresHandler and getFurnitureDetailsHandler with our Flutter application.
  2. Persisted id of selected item by leveraging MaterialApp.router.
  3. Added out-of-the-box conditional render in our ui for loading, error, data states by leveraging riverpod capabilities.

Enhancements

  1. Add add to cart functionality that will submit the select items to a new endpoint on the server.
  2. Add your favorite database to the mix replacing our mock.json file.
  3. Add authentication.
  4. Add in-memory cache on the client.
  5. 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].