How to receive data back to flutter when calling a method using js.context.callMethod()?

2,691

Solution 1

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;
  });
}

Solution 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);
}
Share:
2,691
Abhishek
Author by

Abhishek

Updated on December 22, 2022

Comments

  • Abhishek
    Abhishek over 1 year

    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/