Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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/

like image 765
Abhishek Avatar asked Jun 27 '20 09:06

Abhishek


People also ask

Can flutter web call methods from a JS file?

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.

What is callback or voidcallback in flutter?

Callback is basically a function or a method that we pass as an argument into another function or a method to perform an action. In the simplest words, we can say that Callback or VoidCallback are used while sending data from one method to another and vice-versa. It is very important to maintain a continuous flow of data throughout the flutter app.

What is a method channel in flutter?

In Flutter, method channels are two-way communication tools and their APIs are uniform in all platforms. By storing a reference to a channel in any language, we can call methods with invokeMethod () and accept calls with setMethodCallHandler ().

How to pass and receive data in flutter?

A Guide for Flutter beginners. What is the simplest method by which we can pass and receive data in Flutter? Certainly, through the widget class constructors we can easily pass and receive data. However, we also need to understand another concept named route, which involves another Flutter widget, such as Navigator.


2 Answers

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;
  });
}
like image 79
Christopher Moore Avatar answered Sep 30 '22 12:09

Christopher Moore


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);
}
like image 27
Abhishek Avatar answered Sep 30 '22 13:09

Abhishek