DEV Community

Cover image for How to make a prototype quickly with Flutter and Firebase
pkino
pkino

Posted on • Updated on

How to make a prototype quickly with Flutter and Firebase

Alt Text

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

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

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, 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
  2. Connect Flutter device-unique token to deviceA unique ID
  3. Call cloud functions from deviceA and send a notification from FCM

Code

Flutter

// 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(),
    ),
  );
}

Enter fullscreen mode Exit fullscreen mode

Firebase Cloud Founctions

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,
    })
  })
}
Enter fullscreen mode Exit fullscreen mode

Discussion

Troubles

Top comments (3)

Collapse
 
akashkaintura profile image
AKASH KAINTURA

Can we make the webAPP. using this?

Collapse
 
pkino profile image
pkino • Edited

Thank you for asking the question.
You cannot develop web apps using Flutter. (There is also "Flutter Web", so if you use that, you may be able to develop web apps. However, I am not familiar with that.)

Collapse
 
akashkaintura profile image
AKASH KAINTURA

Thanks for the reply however I have worked on The Flutter Native but somehow not able to cope-up with the developement due to the Health Issues.