9

I am building a chrome extension using Flutter Web, which is just a simple ListView on the home screen and basic CRUD operations. I am using dart:js package to call methods from a JS file which performs some operations on the Firebase Realtime Database.

Adding a new entry to the database is working through the add() method call. Read operation is also working in the JS file just fine.

My main question is how I am supposed to read the database info as JSON, parse it and display it as ListView in Flutter.


Here goes main.dart and AddAttendee.dart -

import 'dart:js' as js;
import 'dart:convert';

import 'package:flutter/material.dart';
import 'AddAttendee.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Google Flutter Meet Attendance',
      theme: ThemeData.light(),
      darkTheme: ThemeData.dark(),
      home: HomeScreen(),
    );
  }
}

class HomeScreen extends StatefulWidget {
  @override
  _HomeScreenState createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
  List<Map> data;

  getAttendees() async {
    var obj = await js.context.callMethod('read');
    List<Map> users = (jsonDecode(obj) as List<dynamic>).cast<Map>();
    setState(() {
      data = users;
    });
  }

  @override
  void initState() {
    getAttendees();
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Current Attendees"),
      ),
      body: data != null
          ? ListView.builder(
              padding: EdgeInsets.symmetric(horizontal: 10.0, vertical: 20.0),
              itemCount: data.length,
              itemBuilder: (context, index) {
                return ListTile(
                  leading: Icon(Icons.person),
                  title: Text(data.toString()[index]), // I don't know how to read correctly
                );
              },
            )
          : Center(child: CircularProgressIndicator()),
      floatingActionButton: FloatingActionButton(
        tooltip: 'Add to Attendance',
        child: Icon(Icons.person_add),
        onPressed: () => Navigator.push(
          context,
          MaterialPageRoute(
            builder: (_) => AddAttendee(),
          ),
        ),
      ),
    );
  }
}
import 'dart:js' as js;

import 'package:flutter/material.dart';

class AddAttendee extends StatefulWidget {
  @override
  _AddAttendeeState createState() => _AddAttendeeState();
}

class _AddAttendeeState extends State<AddAttendee> {
  final _formKey = GlobalKey<FormState>();
  final TextEditingController _textController = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Add to Attendance List"),
      ),
      body: Form(
        key: _formKey,
        child: SingleChildScrollView(
          padding: EdgeInsets.symmetric(horizontal: 10.0, vertical: 10.0),
          child: TextFormField(
            autocorrect: false,
            autofocus: true,
            controller: _textController,
            onFieldSubmitted: (value) => _textController.text = value,
            decoration: InputDecoration(labelText: "Name"),
            keyboardType: TextInputType.text,
            textInputAction: TextInputAction.next,
            textCapitalization: TextCapitalization.sentences,
            validator: (value) {
              if (value.isEmpty) return 'This field is mandatory';
              return null;
            },
          ),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        tooltip: "Done",
        child: Icon(Icons.done),
        onPressed: () {
          if (_formKey.currentState.validate()) {
            js.context.callMethod('add', [_textController.text]);
            Navigator.pop(context);
          }
        },
      ),
    );
  }
}

Here is the JS code -

const dbRef = firebase.database().ref();

var userList = [];

function read() {
  dbRef.once("value", function (snapshot) {
    userList = [];
    snapshot.forEach(function (childSnapshot) {
      var key = childSnapshot.key;
      var user = childSnapshot.val();
      userList.push({ key, user });
    });
  });
  return userList;
}

function add(user) {
  let newUser = { name: user };

  dbRef.push(newUser, function () {
    console.log("Attendance Record Updated - New Attendee Added");
  });
}

Database structure in Firebase RT DB -

firebase_RTDB

Database structure when parsed -

parsed_DB


It's just so frustrating debugging this code because I can't print the outputs of the main.dart and if any error happens and an exception is thrown then it is displayed in form of transcompiled main.dart.js file which is impossible to read.

I haven't been able to get the data back from the read() method in the JS file to main.dart. How to do that?

Some links for reference -

https://twitter.com/rodydavis/status/1197564669633978368 https://www.reddit.com/r/FlutterDev/comments/dyd8j5/create_chrome_extension_running_flutter/ https://github.com/rodydavis/dart_pad_ext/

Christopher Moore
  • 15,626
  • 10
  • 42
  • 52
Abhishek
  • 412
  • 5
  • 17

2 Answers2

7

When calling callMethod it returns exactly what the javascript function returns. It doesn't return a json encoded String like your code currently implies. Therefore there is no need to decode it. var obj can nearly be treated as a List<Map<String, dynamic>> object. This means you can access say the first object of the list and the key property with js.context.callMethod("read")[0]['key'] in your dart code. You can remove all the json decoding and casting in your code and replace it with a function that uses this method of accessing the returned data to convert it to an actual List<Map<String, dynamic>> yourself.

Example getAttendees modification:

Future<void> getAttendees() async {
  List<Map<String, dynamic>> toReturn = List();
  js.JsArray obj = js.context.callMethod("read");
  for(js.JsObject eachObj in obj) {
    //For every object in the array, convert it to a `Map` and add to toReturn
    toReturn.add({      
      'key': eachObj['key'],
      'user': eachObj['user'],
    });
  }
  setState(() {
    data = toReturn;
  });
}

If you for some reason find the need to use json, you would need to encode on the javascript side, though I don't see a reason to do this as it just adds complexity.

When displaying the data in the build function data.toString()[index] will first convert the javascript output to a String like this: "[{key: key}, {key: key}, ...]" and then find the index of the String, not the List object, which is likely what you intend. For this you use the index first, data[index] and then add the field you're looking for data[index]['key'] as data is a List of Maps.

Unrelated directly to the problem you're having, but some general flutter advice, is to use a FutureBuilder instead of your current method of showing data when the async function completes. Your method nearly does what a FutureBuilder would, just a bit less efficiently and in a less readable way.

EDIT: To handle read as a Promise:

First you have to add js: ^0.6.2 to your pubspec.yaml dependencies.

Then this must be added somewhere in your code:

@JS()
library script.js;

import 'package:js/js.dart';
import 'dart:js_util';

@JS()
external dynamic read();

js.JsArray obj = js.context.callMethod("read"); Should be modified to just be await promiseToFuture(read()). The whole getAttendees should be modified to:

Future<void> getAttendees() async {
  List<Map<String, dynamic>> toReturn = List();
  dynamic obj = await promiseToFuture(read());
  for(dynamic eachObj in obj) {
    //For every object in the array, convert it to a `Map` and add to toReturn
    toReturn.add({      
      'key': eachObj.key,
      'user': eachObj.user,
    });
  }
  setState(() {
    data = toReturn;
  });
}
Christopher Moore
  • 15,626
  • 10
  • 42
  • 52
  • Thanks for putting effort into this question. I got the debugging to work, the error shows `Expected a value of type 'List>', but got one of type 'JsObject'`. Implicit conversion is not taking place, we have to convert it explicitly from `JsObject`. – Abhishek Jul 03 '20 at 07:57
  • 1
    Yes I am aware of this. My answer addresses this already. See my example code. I said it can _nearly_ be treated as that data type. My code does an explicit conversion. – Christopher Moore Jul 03 '20 at 14:39
  • This code doesn't work because of the `async` nature of the JS method `read()`. An empty array is returned from the method first, then the array is updated. I haven't been able to find a workaround for that. (I have modified the `read()` method to be async otherwise nothing works). – Abhishek Jul 03 '20 at 17:24
  • `read` isn't marked as `async` in what you posted. Is that a mistake? There are ways to handle promises still as well so that's not a problem, but it's not marked as async at the moment. – Christopher Moore Jul 03 '20 at 17:26
  • 1
    You can use the [`promiseToFuture`](https://api.flutter.dev/flutter/dart-js_util/promiseToFuture.html) function, but only if `read` is really `async`. – Christopher Moore Jul 03 '20 at 17:28
  • You should modify your `read` function to return a `Promise`. `read` currently wouldn't even work in a pure JS environment. – Christopher Moore Jul 03 '20 at 17:32
  • @Abhishek Using `promiseToFuture` would involve using [package:js annotated JavaScript interop objects](https://pub.dev/packages/js) rather than `callMethod` with `dart:js`. Does that work for you? – Christopher Moore Jul 03 '20 at 18:20
  • Would you please edit your answer to include `promiseToFuture` implementation when the `read()` is `async`. I think lastly it will work as the sole solution to my specific problem. Thanks for investing time in this question. – Abhishek Jul 03 '20 at 18:26
  • yes that would definitely work as I have now refactored the code using js-dart interop – Abhishek Jul 03 '20 at 18:29
  • @Abhishek I modified my answer to accommodate for an `async` `read` method that returns a `Promise`. – Christopher Moore Jul 03 '20 at 18:48
  • 1
    Yes, it works! Thanks for sharing this awesome method. As I modified some code for my exact need I will post it as an answer. – Abhishek Jul 04 '20 at 15:12
2

Since Christopher Moore gave a generalized solution to my problem, I will post an exact implementation I used.

Database Structure -

database

JS Code -

const dbRef = firebase.database().ref();

var userList = [];

async function read() {
  await dbRef.once("value", function (snapshot) {
    userList = [];
    snapshot.forEach(function (childSnapshot) {
      var key = childSnapshot.key;
      var user = childSnapshot.val();
      userList.push({ key, user });
    });
    //console.log(userList);
  });
  return userList;
}

JS-Dart Interop -

@JS()
library js_interop;

import 'package:js/js.dart';
import 'package:js/js_util.dart';

@JS()
external dynamic read();

class FBOps {
  final List<Map<String, dynamic>> toReturn = [];

  Map getUserDetails(objMap) {
    final Map<String, dynamic> temp = {};

    temp['name'] = objMap.user.name;
    temp['status'] = objMap.user.status;

    return temp;
  }

  Future<List> getList() async {
    dynamic obj = await promiseToFuture(read());

    for (dynamic eachObj in obj) {
      toReturn.add({
        'key': eachObj.key,
        'user': getUserDetails(eachObj),
      });
    }

    return toReturn;
  }
}

Getting the Map from interop -

List<Map> data;

Future<void> getAttendees() async {
  await FBOps().getList().then((value) => data = value);
  //print(data);
}
Abhishek
  • 412
  • 5
  • 17