How to make a prototype quickly with Flutter and Firebase
Tags: flutter, firebase, dart, prototype
Published: 2020-11-30 05:46:29

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/cvkdrx40nt5bb6uuvgi0.png)

# Introduction
In our team, we develop new products/services every day. In a proof of concept (POC), we had to check the useability of a physical device (I will call it deviceA). Therefore, we decided to develop a native app that gets notifications trigger by deviceA. 
In making a prototype, the most important thing is to implement fastly. Therefore, I decided to use Flutter and Firebase because they are easy to implement.
In fact, I developed this for 7days from the begining including learning the Dart language.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/7upcwxdpsq3qiozo5vpl.png)


# Methods
### Requirements
1. Connect the native app and the unique number of deviceA
2. Send notifications to the native app when deviceA send a signal to a server

### Solutions
1. Register deviceA's unique number from the native app using Cloud Firestore
2. Call a Cloud Function from deviceA and send a notification  
from Cloud Messaging to the native app using the data in Cloud Firestore

![Flutter App Structure](https://dev-to-uploads.s3.amazonaws.com/i/xvnaoyfel9ubi9wi224x.png)


### Technology Selection
#### Flutter
In making the prototype, the requirements above were relatively easy. In addition, we needed to execute the app both on Android and iOS. Moreover, as an R&D department, we need to try new emerging technologies to apply products at any time. Therefore, I decided to choose Flutter. There was [a sample project of Firebase Cloud Messaging](https://pub.dev/packages/firebase_messaging), so I used it.

#### Firestore
The requirements above were relatively easy and the prototype was just for POC. Therefore, I decided to choose Firestore because the implementation was easy.

### Implementation way
1. Use [Firebase Cloud Messaging (FCM) example package](https://pub.dev/packages/firebase_messaging/example)
2. Connect Flutter device-unique token to deviceA unique ID
3. Call cloud functions from deviceA and send a notification from FCM


### Code
#### Flutter
```dart
// Copyright 2017 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:async';

import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/material.dart';
import 'package:cloud_firestore/cloud_firestore.dart';

final Map<String, Item> _items = <String, Item>{};
Item _itemForMessage(Map<String, dynamic> message) {
  final dynamic data = message['data'] ?? message;
  final String itemId = data['id'];
  final Item item = _items.putIfAbsent(itemId, () => Item(itemId: itemId))
    ..status = data['status'];
  return item;
}

class Item {
  Item({this.itemId});
  final String itemId;

  StreamController<Item> _controller = StreamController<Item>.broadcast();
  Stream<Item> get onChanged => _controller.stream;

  String _status;
  String get status => _status;
  set status(String value) {
    _status = value;
    _controller.add(this);
  }

  static final Map<String, Route<void>> routes = <String, Route<void>>{};
  Route<void> get route {
    final String routeName = '/detail/$itemId';
    return routes.putIfAbsent(
      routeName,
      () => MaterialPageRoute<void>(
        settings: RouteSettings(name: routeName),
        builder: (BuildContext context) => DetailPage(itemId),
      ),
    );
  }
}

class DetailPage extends StatefulWidget {
  DetailPage(this.itemId);
  final String itemId;
  @override
  _DetailPageState createState() => _DetailPageState();
}

class _DetailPageState extends State<DetailPage> {
  Item _item;
  StreamSubscription<Item> _subscription;

  @override
  void initState() {
    super.initState();
    _item = _items[widget.itemId];
    _subscription = _item.onChanged.listen((Item item) {
      if (!mounted) {
        _subscription.cancel();
      } else {
        setState(() {
          _item = item;
        });
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("${_item.itemId}"),
      ),
      body: Material(
        child: Center(child: Text("${_item.status}")),
      ),
    );
  }
}

class PushMessagingExample extends StatefulWidget {
  @override
  _PushMessagingExampleState createState() => _PushMessagingExampleState();
}

class _PushMessagingExampleState extends State<PushMessagingExample> {
  String _homeScreenText = "Waiting for token...";
  bool _topicButtonsDisabled = false;

  final FirebaseMessaging _firebaseMessaging = FirebaseMessaging();
  final Firestore _firestore = Firestore();
  final TextEditingController _topicController = TextEditingController();

  final String collectionName = 'test';

  CollectionReference get messages => _firestore.collection(collectionName);
  Future<void> _addMessage(String deviceA) async {
    String deviceId;
    await _firebaseMessaging.getToken().then((String token) {
      assert(token != null);
      deviceId = token;
    });
    await messages
        .document(deviceA)
        .collection('deviceId')
        .document(deviceId)
        .setData(<String, dynamic>{
      'created_at': FieldValue.serverTimestamp(),
    });
  }

  Widget _buildDialog(BuildContext context, Item item) {
    return AlertDialog(
      content: Text("${item.itemId}"),
      actions: <Widget>[
        FlatButton(
          child: const Text('CLOSE'),
          onPressed: () {
            Navigator.pop(context, false);
          },
        ),
        FlatButton(
          child: const Text('SHOW'),
          onPressed: () {
            Navigator.pop(context, true);
          },
        ),
      ],
    );
  }

  void _showItemDialog(Map<String, dynamic> message) {
    showDialog<bool>(
      context: context,
      builder: (_) => _buildDialog(context, _itemForMessage(message)),
    ).then((bool shouldNavigate) {
      if (shouldNavigate == true) {
        _navigateToItemDetail(message);
      }
    });
  }

  void _navigateToItemDetail(Map<String, dynamic> message) {
    final Item item = _itemForMessage(message);
    // Clear away dialogs
    Navigator.popUntil(context, (Route<dynamic> route) => route is PageRoute);
    if (!item.route.isCurrent) {
      Navigator.push(context, item.route);
    }
  }

  @override
  void initState() {
    super.initState();
    _firebaseMessaging.configure(
      onMessage: (Map<String, dynamic> message) async {
        print("onMessage: $message");
        _showItemDialog(message);
      },
      onLaunch: (Map<String, dynamic> message) async {
        print("onLaunch: $message");
        _navigateToItemDetail(message);
      },
      onResume: (Map<String, dynamic> message) async {
        print("onResume: $message");
        _navigateToItemDetail(message);
      },
    );
    _firebaseMessaging.requestNotificationPermissions(
        const IosNotificationSettings(sound: true, badge: true, alert: true));
    _firebaseMessaging.onIosSettingsRegistered
        .listen((IosNotificationSettings settings) {
      print("Settings registered: $settings");
    });
    _firebaseMessaging.getToken().then((String token) {
      assert(token != null);
      setState(() {
        _homeScreenText = "Push Messaging token: $token";
      });
      print(_homeScreenText);
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: const Text('Prototype'),
        ),
        // For testing -- simulate a message being received
        body: Material(
          child: Column(
            children: <Widget>[
              Container(
                child: Padding(
                  padding: EdgeInsets.only(
                      top: 40.0, right: 20.0, bottom: 10.0, left: 20.0),
                  child: Text(
                    'Register deviceA unique ID',
                    textAlign: TextAlign.left,
                  ),
                ),
              ),
              Row(children: <Widget>[
                Expanded(
                    child: Padding(
                  padding: EdgeInsets.only(left: 10.0),
                  child: TextField(
                      controller: _topicController,
                      decoration: InputDecoration(hintText: 'deviceA unique ID'),
                      onChanged: (String v) {
                        setState(() {
                          _topicButtonsDisabled = v.isEmpty;
                        });
                      }),
                )),
                FlatButton(
                  child: const Text('send'),
                  onPressed: _topicButtonsDisabled
                      ? null
                      : () {
                          _addMessage(_topicController.text);
                          _clearTopicText();
                        },
                ),
              ]),
              Expanded(
                  flex: 2,
                  child: Padding(
                      padding: EdgeInsets.only(
                          left: 20.0, right: 20.0, bottom: 30.0),
                      child: Center(
                        child: Align(
                            alignment: Alignment.bottomCenter,
                            child: Text(
                              _homeScreenText,
                            )),
                      )))
            ],
          ),
        ));
  }

  void _clearTopicText() {
    setState(() {
      _topicController.text = "";
      _topicButtonsDisabled = true;
    });
  }
}

void main() {
  runApp(
    MaterialApp(
      home: PushMessagingExample(),
    ),
  );
}

```

#### Firebase Cloud Founctions
```ts
import * as functions from 'firebase-functions'
import * as admin from 'firebase-admin'

admin.initializeApp(functions.config().firebase)
export const notify = functions.https.onRequest(async (request, response) => {
  await sendNotification(request.query.deviceA)

  response.send("Notified")
})

async function sendNotification(deviceA: string): Promise<void> {
  const db = admin.firestore()
  const collectionName: string = 'test'
  const collectionName2nd: string = 'deviceId'
  const deviceTokenList: Array<string> = []

  await db.collection(collectionName).doc(deviceA).collection(collectionName2nd).get()
    .then((snapshot) => {
      snapshot.forEach((doc) => {
        deviceTokenList.push(doc.id)
      })
    })
    .catch((err) => {
      console.log('Error getting documents', err);
    })

  deviceTokenList.forEach(async (deviceToken) => {
    await admin.messaging().send({
      android: {
        priority: 'high',
      },
      data: {
        click_action: 'FLUTTER_NOTIFICATION_CLICK',
        id: 'Hello!!!',
        status: 'Hello from ZOZO.',
      },
      notification: {
        title: 'Notification from deviceA',
        body: 'Here is a notification',
      },
      token: deviceToken,
    })
  })
}
```

# Discussion
### Troubles
- [It seems to be hard to run FCM locally.](https://github.com/firebase/quickstart-nodejs/tree/master/messaging)
- When I added Firestore to `pubspec.yml`, there is a problem.
[Therefore, I modified `minsdk` from 16 to 21.](https://developer.android.com/studio/build/multidex?hl=JA)