Amazon.co.uk Widgets

This is the twelth part of a journey to build and publish a Flutter™ based app.

At last lets get back to Flutter and add some code from the Firebase Codelab.

Mock Data

Add the mock data from the codelab, just after the void main() => runApp(MyApp()); in main.dart and modify this code and data to match the Cloud Firestore fields previously set up. Once the app is working with this data we can get it working for real with data from Firebase.


final dummySnapshot = [
  {"name": "The Hand and Glove", "capacity": 15},
  {"name": "The Purple fountain", "capacity": 14},
  {"name": "The royal Acorn", "capacity": 11},
  {"name": "Sparkle", "capacity": 10},
  {"name": "Ration Town", "capacity": 1},
];

Now add the rest of the codelab at the bottom of main.dart, replacing the class that generated our profile page. Rename the route for this to match the pasted code '/Establishments': (context) => MyHomePage(), and rename the onPressed action for the button that navigates to the page Navigator.pushNamed(context, '/Establishments'); and the button that has the Info Icon should load our new mockup data page.


class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() {
    return _MyHomePageState();
  }
}

class _MyHomePageState extends State {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Establishments')),
      body: _buildBody(context),
    );
  }

  Widget _buildBody(BuildContext context) {
    // TODO: get actual snapshot from Cloud Firestore
    return _buildList(context, dummySnapshot);
  }

  Widget _buildList(BuildContext context, List snapshot) {
    return ListView(
      padding: const EdgeInsets.only(top: 20.0),
      children: snapshot.map((data) => _buildListItem(context, data)).toList(),
    );
  }

  Widget _buildListItem(BuildContext context, Map data) {
    final record = Record.fromMap(data);

    return Padding(
      key: ValueKey(record.name),
      padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
      child: Container(
        decoration: BoxDecoration(
          border: Border.all(color: Colors.grey),
          borderRadius: BorderRadius.circular(5.0),
        ),
        child: ListTile(
          title: Text(record.name),
          trailing: Text(record.capacity.toString()),
          onTap: () => print(record),
        ),
      ),
    );
  }
}

class Record {
  final String name;
  final int capacity;
  final DocumentReference reference;

  Record.fromMap(Map map, {this.reference})
      : assert(map['name'] != null),
        assert(map['capacity'] != null),
        name = map['name'],
        capacity = map['capacity'];

  Record.fromSnapshot(DocumentSnapshot snapshot)
      : this.fromMap(snapshot.data, reference: snapshot.reference);

  @override
  String toString() => "Record<$name:$capacity>";
}

What did the codelab code change?

Originally our app opened a second page if the Information action was pressed. Now the app sets up some mock data which will next be replaced by data from our Cloud Firestore database. The _MyHomePageState replaced this second page with a new widget hierarchy for the app which you can see in the Flutter Inspector in Android Studio.

Android Studio Flutter Inspector

The pasted code also has an AppBar, we changed the title to 'Establishments'. The pages body contains a ListView, which displays the mock data. The method _buildListItem describes the structure of the display with names and the capacity data inside rounded rectangles.

According again to the codelab, Record is a convenience class that holds a single record for a name. You don't strictly need this class for a simple app like this to function, but it makes the code a bit cleaner, they say.

Finished mock data page

Mock data page, light   Mock data page, dark

Connect the Flutter app to Cloud Firestore

Once again, following the flutter firebase codelab lets replace the _buildBody method.


Widget _buildBody(BuildContext context) {
 return StreamBuilder(
   stream: Firestore.instance.collection('establishments').snapshots(),
   builder: (context, snapshot) {
     if (!snapshot.hasData) return LinearProgressIndicator();

     return _buildList(context, snapshot.data.documents);
   },
 );
}

This is a StreamBuilder widget. It listens for updates and refreshes the list whenever the data changes.

Make the following changes too:- _buildList

Widget _buildList(BuildContext context, List snapshot) {

_buildListItem

Widget _buildListItem(BuildContext context, DocumentSnapshot data) {
 final record = Record.fromSnapshot(data);

I had to remove int from the line final int capacity; to make the code run. Something about an Int and String type error. Not sure why but removing it made the code run.


class Record {
  final name;
  final capacity;
  final DocumentReference reference;

Cloud Firestore data page, light   Cloud Firestore data page, dark

Android runtime error caused by applicationID not matching Firebase

Unfortunately this code would not run on Android at first (See 2870 --> 2796) This ws with Flutter 1.17.4 and reported itself as a specific issue with the Android build environment settings.gradle.

Plugin project :firebase_core_web not found. Please update settings.gradle. 
Plugin project :firebase_auth_web not found. Please update settings.gradle.

It ran fine on iOS so it looks to be environmental. The Firebase apps had been set up with a more sensible applicationID and Bundle ID and package name than when this project started. The package name had been renamed in the source code. This is perhaps not wise and it is not really supported. The package names and applicationID and Bundle ID's are peppered throughout the project source code and part of the folder structure too. The fix was to find each instance of the applicationID and package name and reconcile them with the one which was set for the iOS and Android apps in the firebase console.

There a dart package called Rename which can help with this, although it may just be easier to create the project anew with the new naming and copy the main source code files over. There were several file changes required, the actual folder names needed to change to represent the new applicationID in src/main/kotlin/com/example/app/ and the MainActivity.kt file needed the correct package name. The Debug, main and profile AndroidManifest.xml files needed to be edited. The app/build.gradle needed the correct applicationID too.

Don't change the name of your project.

It is mentioned here only in case someone else goes down this path. This was a self induced problem caused just by changing the package name. It would be best not to do that. If you were consistent from the start this would not happen.

It 'just works'

The important takeaway is that the data from Cloud Firestore 'just works'. Edit an item in the Firebase Console and you'll see it change on your phone or simulator. Here is the working android version with the corresponding data entry from Firebase.

 Cloud Firestore console data   Cloud Firestore data page, android

So now we have the skeleton of an app with boilerplate Material widgets talking to a database in the cloud. We can move on to make it useful