fluttercouch / fluttercouch

With Fluttercouch is possible to use Couchbase Mobile Native SDK in Flutter Projects.
GNU Lesser General Public License v3.0
106 stars 28 forks source link

sync with couchdb? #8

Open sensiblearts opened 5 years ago

sensiblearts commented 5 years ago

Is your feature request related to a problem? Please describe. I'm looking for a couchdb (not couchbase) client for flutter, but I assume that fluttercouch will not work (because it's using cb-lite v.2)

Describe the solution you'd like Do you think it's feasible to fork this and re-write to use cb-lite 1.4.4, so that it syncs with couchdb?

Describe alternatives you've considered Somehow embed a javascript thread in flutter and use pouchdb.

Additional context Suggestions? Thanks, DA

lchristille commented 5 years ago

Hi David! Thank you for your proposal! I tried to look for 1.4.4 documentation, but having couchbase site undergone a major restyling I can't currently get to it so I have to use my memory. AFAIK the major differences between 1.4.4 and 2.1 are Query and websockets. If these are the only main difference it could be possible to somehow support them directly from fluttercouch. My concern is that with a fork in this moment, since fluttercouch is not fully implemented for 2.1, a double effort has to be done to get in sync the two repositories. I'm looking forward for your opinion and your suggestions. Luca

sensiblearts commented 5 years ago

Hi Luca, Thank you for your reply and I apologize for my delay in responding. I still trying to choose between CouchDb and CouchBase. I don't really need the real-time capability of CouchBase, which comes with significant overhead (e.g., 4GB server, minimum).

I will be back in touch in a week or so and will have made a final decision. David

sensiblearts commented 5 years ago

Hello Luca, I have decided to use your fluttercouch approach and Couchbase-Lite 1.4.4 (which will sync with couchdb). 1.4.4 code is here, and they released an update in October, which is a good sign. My plan is to link in the community (free) edition jars for initial work, as that is the version I plan to use. I assume that the source code is the community edition, but will confirm this later.

I have been using github for a long time, but only by myself, and I have never "forked" a project; actually, I've never used git with a second person. Can you advise me on how to proceed? Should I fork your project and, after some changes, submit a pull request? Or should fluttercouch for 1.4.4 be a separate project? Whatever you think is best.

Thanks, David

lchristille commented 5 years ago

Hello David, I'll create a new repository fluttercouchDB which will contain the 1.4.4 version of CouchBase Lite and we can test it against couchDB server. Sorry for the late answer, I hope I can still help you.

Thanks, Luca

sensiblearts commented 5 years ago

Sure. Just point me in the right direction. I forked the repository. Should I delete that?

On Thu, Dec 13, 2018 at 5:13 PM Luca Christille notifications@github.com wrote:

Hello David, I'll create a new repository fluttercouchDB which will contain the 1.4.4 version of CouchBase Lite and we can test it against couchDB server. Sorry for the late answer, I hope I can still help you.

Thanks, Luca

— You are receiving this because you modified the open/close state. Reply to this email directly, view it on GitHub https://github.com/oltrenuovefrontiere/fluttercouch/issues/8#issuecomment-447139412, or mute the thread https://github.com/notifications/unsubscribe-auth/AAxJws5-2JcigPHxO27zxyv-M3KbirOnks5u4tEMgaJpZM4YcmkR .

-- </> David Alm

lchristille commented 5 years ago

It's your repository, and you can test anything you want in it. It causes no interference with fluttercouchDB, it's up to you if you need that forked repository anymore. If you did some test, let me know so I can take some inspiration!

sensiblearts commented 5 years ago

I haven't done anything since I forked it. I'll watch for the 1.4.4 addition and then start testing too. I'll fork that one, I guess.

lchristille commented 5 years ago

I hope I can do this modification and upload the new repository in two days more or less. Stay tuned!

lchristille commented 5 years ago

Hi David, no problem, I wish I could take a look to fluttercouch at the moment but I'm quite in a hurry with other projects. But I'm glad to help you implementing what you need.

The error you reported is a bit misleading, but I have to ask you a question about the actions you have done so far. You have created a new flutter project called gj_app1 but you added the couchbase.lite dependency to your project instead of changing the dependency of the fluttercouch library, right?

If so, You can find the declaration of couchbase dependency in android/build.gradle and ios/fluttercouch.podspec respectively in your git repository.

For example, in build.gradle for Android you should:

    dependencies {
        //  Change here the version of the dependency from 2.1.0 to 1.4.4 or any other version you are looking for
        implementation 'com.couchbase.lite:couchbase-lite-android-ee:2.1.0'
        testImplementation 'junit:junit:4.12'
        androidTestImplementation 'com.android.support.test:runner:1.0.2'
        androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
    }

Write me if you have any other question or if I can be of any help to you. Thanks for your contribution, Luca

sensiblearts commented 5 years ago

I finally got it to build for cb-lite 1.4.4. Replication is commented out. Will try to save/retrieve docs later. DA

sensiblearts commented 5 years ago

I have saveDocumentWithId and getDocumentWithId working locally on Android for 1.4.4.

Code is rough, but works. Don't hesitate to criticize if you see bad style. https://github.com/sensiblearts/fluttercouch/tree/master/android/src/main/java/it/oltrenuovefrontiere/fluttercouch

Replication is commented out, but I'll try to get it working and clean the code up in the next few days. DA

sensiblearts commented 5 years ago

Well, I sat down this morning to take a look at how to retrieve a subset of documents, and I realize there is a major obstacle to keeping the codebase for fluttercouchDB (for cb 1.4) similar to fluttercouch (for cb 2):

Before I can create a 1.4 query, I have to create a 1.4 view, which is defined by a map function, which is written in Java.

// Create a view and register its map function:
View phoneView = database.getView("phones");
phoneView.setMap(new Mapper() {
    @Override
    public void map(Map<String, Object> document, Emitter emitter) {
        List<String> phones = (List) document.get("phones");
        for (String phone : phones) {
            emitter.emit(phone, document.get("name"));
        }
    }
}, "2");

For discussion purposes, here’s a simple map function in JavaScript:

function(doc) {
    if (doc["type"] == "person")
        emit(doc["name"], doc["phone"]);
}

For me to write a Java function that could anticipate all possible select queries would be reinventing an SQL select parser in Java! (I thought about a design where the Java map function could callback to some Dart code that the user wrote, but I assume that this would perform very slowly, so I ruled it out.)

Consequently, the user of fluttercouchDB will have to write custom business logic in Java. Given this fact, I see no point in trying to make a generic interface for Views and Queries; rather, I think I'm going to just write custom or "canned" view and query logic in Java, with a small Dart interface to return the documents that I need for my app.

For example, in FluttercouchPlugin.dart the onMethodCall switch statement would have an else clause that delegates to e.g., a FluttercouchBusinessLogic.dart or some such, which has its own onMethodCall handler, which would create views, set the map functions, etc.

Your thoughts on this?

(Aside: In a previous pouchdb app, I could retrieve a subset of documents by applying a regular expression to the primary document _id. You can go very far with this approach, by building a large _id with unique parts for different filtering logic. In other words, in pouchdb, you get by with just the _id index, not needing any views or secondary indexes.)

DA

lchristille commented 5 years ago

Hello David, I think the most important difference between 1.4.4 and 2.x is in Queries. I have studied only the 2.x version and is quite complicated to port in Dart, but is feasible because it uses a bunch of nested object and I can simulate the same objects in Dart.

But I get your point and is quite difficult to generate a view in this way. Probably you can get some hints looking in the iOS implementation of View and Query for your version. If you could link me the 1.4.4 documentation, I would help you find a valid approach.

sensiblearts commented 5 years ago

Hi Luca, I have basic CRUD working smoothly. (Also, CB 1.4.4 documentation is here.)

The only Dart file that I modified was fluttercouch.dart, where you will see a (perhaps) reusable function, initView that calls into an app-specific java function to define the View's map function.

Also in fluttercouch.dart, I placed app-specific functions to get query results. For example, at the bottom of fluttercouch you will see getAllEntries(), which returns documents based on app-specific java query code.

So, these newly added fluttercouch.dart methods (like getAllEntries()) that are not handled by your original java onMethodCall(), are handled at the very end of FluttercouchPlugin, where you will see:

            default:
                mCbBusinessLogic.handleCall(call, result);
            }

which calls into a class for app-specific views and queries.

CBManager.java is little changed; however, I changed return types on the Create and Update functions: public Map<String, String> updateDocumentWithId(String _id, Map<String, Object> _map)...

(The return value is ~ {"_id": _id, "_rev": _rev}. I did this because you need _rev in RAM to match database, and this avoids a second call to get rev based on id.)

Lastly, all replicator logic is commented out, because I have not had time to work on that yet.

My strategy is to use the above approach rather than rewrite a new layer of View and Query classes for 1.4.4.

Once I get it working smoothly, with replication, then we can think about making the code more generic / reusable.

So, here is my plan: 1) Implement Views and Queries for another page, to work on the relationship integrity maintenance logic. (Relational equivalent: make sure 2 tables and a join works correctly.) 2) Implement the replication and test it. 3) Check back with you for feedback 4) Finish and ship the app.

Thanks, DA

sensiblearts commented 5 years ago

I have couchbase lite 1.4.4 working with CouchDb 2.3: CRUD works, and push & pull sync to CouchDb works.

It was a "quick and dirty" extension of your code, but just writing Views and Queries in java.

If you want, we can discuss how to build objects as you were doing. But I have some thoughts on why you might want to write more in java rather than dart. DA

lchristille commented 5 years ago

Hi David, Thank you for your work! You have done so much work for fluttercouch and I can only thank you for your impressive work.

I will be glad to discuss with you about your opinions on further development of fluttercouch, perhaps in a videocall? Let me know what is your timezone, so we can arrange a confortable time for both of us. I live in Italy, GMT+01/CET.

Luca

sensiblearts commented 5 years ago

Hi Luca, You're welcome.

I live in US, Michigan, on EST, GMT-5. The best day for me, to do any sort of chat or videocall, would be a Tuesday. I'm very busy teaching (biology) most of the week.

I could probably be available at whatever time was convenient for you on a Tuesday, some time after ~3PM your time (after 9AM my time), if that works for you.

If Tuesdays are not good, let me know, and we could probably do it on a Sat or Sun.

David

sensiblearts commented 5 years ago

Hi again Luca,

I just made a quick change so that the header token (for couchdb replication) was not hard-coded, and I'd thought I would take a minute to walk you through the code sequence:

For example, I have a ScopedModel object type "Label" which is coded in couch with json field "doc" == "lab". (I use "doc" instead of "type" for document types.) So, in couchbase-lite 1.4 I establish a view called "labels".

To do this, in my client code, I call

initView("labels");

which calls into fluttercouch.dart, which delegates the call to CBBusinesslogic.java, which then switches based on the string, to call one of the custom view methods:

public void handleCall(MethodCall call, Result result) {
        switch (call.method) {
            case ("initView"):
                String view = call.arguments();
                switch(view) {
                    case ("entries"):
                        result.success(initEntriesView());
                        break;
                    case ("categories"):
                        result.success(initCategoriesView());
                        break;
                    case ("labels"):
                        result.success(initLabelsView()); // THIS ONE
                        break;
    ...

The initLabelsiew() method is business logic to produce the View. Note that it is indexed on two fields, categoryId and title, because these are the two ways that my app needs this view indexed (for performance):

     private Boolean initLabelsView() {
        if (mCbManager != null) {
            Database db = mCbManager.getDatabase();
            View view = db.getView("labels");
            view.setMap(new Mapper() {
                @Override
                public void map(Map<String, Object> document, Emitter emitter) {
                    if (document.get("doc").equals("lab")) { // is label
                        List<Object> key = new ArrayList<Object>();
                        key.add(document.get("categoryId"));
                        key.add(document.get("title"));
                        emitter.emit(key, null); 
                    }
                }
            }, "2");
            return true;
        } else {
            return false;
        }
    }

Once I have initialized the View, my client code can then do a query to get some labels. In my client code, I do the following to get Labels for a specific category:

List<Label> _labels = [];

// ...

  Future<List<Label>> labelsForCategoryId(String categoryId) async {
    _labels.clear();
    List<dynamic> docs = await getLabels(categoryId); // call fluttercouch
      docs.forEach((map){
        Label l = Label.fromMap(map); // deserialize to dart object
        _labels.add(l);
      }); // each
   return _labels;
  }

And when the above client code calls into fluttercouch with getLabels(categoryId), getLabels is a method that I had to add to the fluttercouch.dart plugin interface:

  Future<List<dynamic>> getLabels(String categoryId) async {
     try {
      final List<dynamic> result =
      await _methodChannel.invokeMethod('getLabels', <String, dynamic>{'categoryId': categoryId});
      return result;
    } on PlatformException {
      throw 'unable to get labels for category: $categoryId';
    }
  }

which then delegates to the DBBusinesslogic.java class:

   private ArrayList<Map<String, Object>> getLabels(String categoryId) {
        ArrayList<Map<String, Object>> results = new ArrayList<Map<String, Object>>();
        if (mCbManager != null) { // needed?
            Database db = mCbManager.getDatabase();
            Query query = db.getView("labels").createQuery();
            query.setPostFilter(new ByCategory(categoryId));
            // query.setDescending(false);
            try {
                QueryEnumerator result = query.run();
                for (Iterator<QueryRow> it = result; it.hasNext(); ) {
                    QueryRow row = it.next();
                    results.add(row.getDocument().getProperties());
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return results;
    }

and you will notice that it uses a filter object to filter the query; which I have implemented like this:

    public class ByCategory implements Predicate<QueryRow> {
        private String uuid;
        public ByCategory(String categoryId) {
            this.uuid = categoryId;
        }
        public boolean apply(QueryRow row) {
            // Weird that this is the way you have to do it if key is array..
            LazyJsonArray<Object> keys = (LazyJsonArray<Object>)row.getKey();
            if (this.uuid.equals((String)keys.get(0))) { // categoryId
                return true;
            } else {
                return false;
            }
        }
    }

In summary:

Next, comes this issue of replication. To start this, my client calls:

 await createReplicatorWithName(DATABASE_NAME);

    await configureReplicator(<String, String>{
      "username" : "ceac7a71-f125-4816-963c-af6a5d027211",
      "token" : "edc54bb202586b3bfe5fe525c46af27611d404c9",
      "url" : "http://192.168.1.11:5984/" + DATABASE_NAME,
      "synctype" : "PUSH_AND_PULL",
      "continuous" : "true"
    });

    await startReplicator();

And these replication methods were added to fluttercouch.dart and implemented in CBManager.java. (I removed the previous replication methods to make it easy to read.)

If any of this is not clear I can clarify when we chat. Cheers, David

sensiblearts commented 5 years ago

Hi Luca, In my cb-lite 1.4 version, I just made a change where I separate the "business logic" out into 1 dart file and 1 java file:

E.g., fluttercouch.dart is now:

abstract class Fluttercouch extends FluttercouchCore {

  ///////////////////////////////////////////////////////////////////////
  ///
  /// App-specific methods to customize this plugin
  ///
  //////////////////////////////////////////////////////////////////////

  Future<List<dynamic>> getPaperJournals() async {
    try {
      final List<dynamic> result =
          await FluttercouchCore.methodChannel.invokeMethod('getPapers');
      return result;
    } on PlatformException {
      throw 'unable to get all papers';
    }
  }
// ... etc..
}

and the old fluttercouch.dart file is now named fluttercouch_core.dart.

Then, in my client code, I do this:

class LabeledEntriesModel extends Model with Fluttercouch, FluttercouchCore {
// ... etc ...
}

Seems to work fine.

Hence, this design could perhaps be merged with what you have already done. For example, if someone is using couchbase-lite 1.4.4, then he has to do the following:

  1. extend his model with both Fluttercouch and FluttercouchCore, as above
  2. write the dispatcher for any custom Query methods, i.e., write code in fluttercouch.dart (which is initially empty of methods)
  3. write the implementation for any Views you need in CBBusinessLogic.java (which is initially empty of methods)
  4. write the implementation for those custom Query methods (in 2 above) in CBBusinessLogic.java.

However, if he is using couchbase-lite 2, then he does not have to implement any of the custom methods, and can simply use it as a plugin (as you had planned); he would then just

  1. extend his model only with FluttercouchCore,
  2. import the dart query files to build your queries, etc., as you had planned.

Thoughts? DA p.s. I notice i get an email when you post here; are you getting an email when I post? Thanks.

lchristille commented 5 years ago

Hey David! Yes I receive an email every time you reply to this thread, but I missed your last post before this last one, sorry!

Tonight I'll read your modifications and I'll make a local copy of your repository in order to deep dive in your code.

I hope we can talk more directly this Tuesday at ~15.30PM your time (~21.30PM my time) if you are not busy, via Chat or Videocall.

Feal free to propose another arrangement based on your disponibility.

See you soon, Luca

sensiblearts commented 5 years ago

No worries -- I just wanted to be sure that you weren't waiting to hear from me.

Tuesday at the time is fine, but no rush; if you get too busy, we can postpone.

We can chat or video, your preference.

To save you some time, the first place to look is in my fluttercouch.dart at method getEntriesForLabel. This is the method that my flutter client calls to get paginated results (beginning with startDate key) for entries with a given labelId. (Before any such queries can be used, I have to call fluttercouch_core.dart's initView method; e.g., initiView("entries").)

Then, for the implementation, see CBBusinesslogic.java, which uses the following to implement the dart calls:

initEntriesView

class ByLabel // used by:

method getEntriesForLabel

Cheers, DA

On Fri, Feb 8, 2019 at 2:42 PM Luca Christille notifications@github.com wrote:

Hey David! Yes I receive an email every time you reply to this thread, but I missed your last post before this last one, sorry!

Tonight I'll read your modifications and I'll make a local copy of your repository in order to deep dive in your code.

I hope we can talk more directly this Tuesday at ~15.30PM your time (~21.30PM my time) if you are not busy, via Chat or Videocall.

Feal free to propose another arrangement based on your disponibility.

See you soon, Luca

— You are receiving this because you modified the open/close state. Reply to this email directly, view it on GitHub https://github.com/oltrenuovefrontiere/fluttercouch/issues/8#issuecomment-461922704, or mute the thread https://github.com/notifications/unsubscribe-auth/AAxJwi3DMsusQNo_kcujbxN2YrIOTgAyks5vLdMdgaJpZM4YcmkR .

-- </> David Alm

lchristille commented 5 years ago

Hi David, I looked at your code and I'm glad that you got working code for replication and query.

I started to code the library with the purpose to replicate as much as possible the couchbase lite API. This way developers are not enforced to learn a full new library but can "easily" translate code from native to Dart. But I have to admit that it would be a lot cleaner to use a map to pass information about a replication configuration instead of using different methods.

I found this wiki page https://github.com/couchbase/couchbase-lite-ios/wiki/JavaScript-Views for iOs and this package https://github.com/couchbase/couchbase-lite-java-javascript for Android that could solve some problems you had in defining views logic in Java instead of Dart. Essentially they make possible to write views logic in a string containing Javascript code.

Of this and other suggestions we can chat tomorrow. If you agree, I can send to your sensiblearts email a link for a videochat session tomorrow around 15.30PM (your time).

Cheers, Luca

sensiblearts commented 5 years ago

Hi Luca, Sure, 15:30PM for chat sounds good. Will I need to install anything to chat?

Also, regarding the JS view approach, I see this on the iOS page under "cons":

Indeed, that is one of the reasons I am considering sticking with the approach of writing views and queries in Java/Obj-C as opposed to Dart. I have noticed that flutter is fast enough (faster performance than, e.g., an Ionic hybrid app); however, it seems a bit sluggish now and then, and I feel like the more that is happening on separate (java) threads, the better.

Anyway, we can discuss it tomorrow. Also, these approaches may not be mutually exclusive: It is possible that a single design could accommodate both approaches.

David

On Mon, Feb 11, 2019 at 12:30 PM Luca Christille notifications@github.com wrote:

Hi David, I looked at your code and I'm glad that you got working code for replication and query.

I started to code the library with the purpose to replicate as much as possible the couchbase lite API. This way developers are not enforced to learn a full new library but can "easily" translate code from native to Dart. But I have to admit that it would be a lot cleaner to use a map to pass information about a replication configuration instead of using different methods.

I found this wiki page https://github.com/couchbase/couchbase-lite-ios/wiki/JavaScript-Views for iOs and this package https://github.com/couchbase/couchbase-lite-java-javascript for Android that could solve some problems you had in defining views logic in Java instead of Dart. Essentially they make possible to write views logic in a string containing Javascript code.

Of this and other suggestions we can chat tomorrow. If you agree, I can send to your sensiblearts email a link for a videochat session tomorrow around 15.30PM (your time).

Cheers, Luca

— You are receiving this because you modified the open/close state. Reply to this email directly, view it on GitHub https://github.com/oltrenuovefrontiere/fluttercouch/issues/8#issuecomment-462418269, or mute the thread https://github.com/notifications/unsubscribe-auth/AAxJwqgMyMW0nq8VBGtz5kE0F7zFGHnLks5vMajIgaJpZM4YcmkR .

-- </> David Alm

lchristille commented 5 years ago

I definitely see your point, and it could be interesting to give developers a choice between ease (JS approach) and performance (native approach).

Usually I host videocalls on zoom.us so you don't have to download any additional software, provided you use Chrome as browser.

See you soon, Luca

sensiblearts commented 5 years ago

Hi Luca, Attachments are now working smoothly. Below is an example of Attachment CRUD, from on the fluttercouch side and an example of my client code (below):

My fluttercouch code currently has two methods for attachments:

First, a generic upsert method to add/update an attachment (in CBManager.java):

 public Map<String, String> upsertNamedAttachmentAsFilepath(String _id, Map<String, Object> _map) throws CouchbaseLiteException, Exception {
       if (        _map.containsKey("mime") 
                    && _map.containsKey("attachName")
                    && _map.containsKey("filePath")) {
   // etc., as we discussed the other day

Second, a use case-specific method that adds the attachment file location just-in-time when a collection is retrieved, in CBBusineslogic.java:

private ArrayList<Map<String, Object>> getEntriesForLabel(int rowsPerPage, String startDate, String labelId) {
        ArrayList<Map<String, Object>> results = new ArrayList<Map<String, Object>>();
        if (mCbManager != null) { // needed?
            Database db = mCbManager.getDatabase();
            Query query = db.getView("entries").createQuery();
            query.setDescending(true);
            List<Object> startKey = new ArrayList<Object>();
            if (startDate != null) {
                startKey.add(startDate);
            } else {
                startKey.add(null);
            }
            if (labelId != null) {
                startKey.add(labelId);
                query.setPostFilter(new ByLabel(labelId));
            }
            if (startDate != null) {
                query.setStartKey(startKey);
            }
           query.setLimit(rowsPerPage);
           try {
                QueryEnumerator result = query.run();
                for (Iterator<QueryRow> it = result; it.hasNext(); ) {
                    QueryRow row = it.next();
                    Document doc = row.getDocument();
                    // ************ Luca:
                    // Couchbase Lite may have changed the client side file and hashed filename
                    // of any named attachment, because it may have sync'd with a doc that was modified
                    // by another client; who e.g., replaced the same attachment name with a 
                    // different file, which would result in a different hash and different local filename. 
                    // Therefore, we load the client-side location of the attachment filename, just-in-time:
                    Revision rev = doc.getCurrentRevision();           
                    Attachment att = rev.getAttachment("entry.jpg"); // use case-specific
                    if (att != null) {
                        URL url = att.getContentURL(); // *** cb-lite file path to attachment on android ***
                        String urlStr = url.toString();
                        String androidPath = urlStr.substring(5, urlStr.length()); // get rid of "file:..."
                        Map<String, Object> props = new HashMap<String, Object>();
                        props.putAll(doc.getProperties());
                        props.put("blobURL", androidPath); // just-in-time
                        props.put("localImagePath", androidPath); // deprecated, perhaps
                        results.add(props);
                    } else {
                        results.add(doc.getProperties());
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return results;
    }

Then, given the above two fluttercouch1.4 methods, my client code for the Entry model looks like this:

void _updateAttachment(String docId, String localImagePath) async {
    if (localImagePath != null && localImagePath != "") {
        Map<String,String> attachMap = <String,String>{"mime": "image/jpeg", 
                    "attachName": "entry.jpg",
                    "filePath": localImagePath};
        upsertNamedAttachmentAsFilepath(docId, attachMap); // fluttercouch
    } else {
      print("Nothing to attach.");
    }
  }

  void addEntry(Entry entry, Label label) async {
    if (entry != null && label != null) {
      entry.labelId = label.uuid;
      entry.labelStr = label.title;
    } 
    entry.when.add(Duration(seconds: DateTime.now().second));
    Document doc = Document(entry.toMap());
    Map<dynamic,dynamic> docm = await saveDocument(doc);
    _updateAttachment(docm["_id"], entry.localImagePath);
    curEntryIsNew = false;
  }

  void updateEntry(Entry entry) async {
    Document doc = Document(entry.toMap());
    Map<dynamic,dynamic> docm = await updateDocumentWithId(entry.uuid, doc);
    _updateAttachment(docm["_id"], entry.localImagePath);
  }

  void deleteEntry(String _id) {
    deleteDocumentWithId(_id);
  }

Future<List<Entry>> entriesForLabel(int rowsPerPage, DateTime startDate, Label label) async {
    List<Entry> result = [];
    String when; // = "";
    if (startDate != null) when = startDate.toIso8601String();
    String labelId;
    if (label != null) labelId = label.uuid;
    //
    // Luca: this call returns entries with just-in-time attachment (entry.blobURL) field:
    //
    List<dynamic> docs = await getEntriesForLabel(rowsPerPage, when, labelId); 
    docs.forEach((map){
      Entry e = Entry.fromMap(map);
      result.add(e);
    }); // each
    return result;      
  }
lchristille commented 5 years ago

Hey David, glad to receive your updates.

I'll get a closer lookup to your implementation so that I can get some hint of how implements Blob attachments in couchbase 2.0. I hope that it wouldn't be so difficult/different so that we could merge the two implementations in the future.

sensiblearts commented 5 years ago

Hi Luca, I could be wrong, but I think I recall reading that cblite v2 inserts a "blob" field in your json and (I think) would keep this field value updated based on the whether the contents of the attachment changed. So, the part where I ("just-in-time") call attachment.getContextURL() (or whatever it was) -- this would not have to be done for version 2. I think. Cheers, DA

On Mon, Feb 18, 2019 at 4:32 PM Luca Christille notifications@github.com wrote:

Hey David, glad to have your updates.

I'll get a close lookup to your implementation so that I can get some hint of how implements Blob attachments in couchbase 2.0. I hope that it wouldn't be so difficult/different so that we could merge the two implementations in the future.

— You are receiving this because you modified the open/close state. Reply to this email directly, view it on GitHub https://github.com/oltrenuovefrontiere/fluttercouch/issues/8#issuecomment-464887169, or mute the thread https://github.com/notifications/unsubscribe-auth/AAxJwtv1BPVd6u0t3Dba-_gfFb-_UVXQks5vOxvUgaJpZM4YcmkR .

-- </> David Alm

sensiblearts commented 5 years ago

Hi again, I made a change to how I had extended your flutter couch. This change should make it easier to eventually merge back.

What I was doing:

abstract class FluttercouchCore { // your original Fluttercouch class
//...
abstract class Fluttercouch extends FluttercouchCore { // where I put my app-specific methods

and in my client code, I was doing this:

class LabeledEntriesModel extends Model with Fluttercouch, FluttercouchCore {

...which was compiling and building, and running as you would expect. However, I recently noticed an error message saying something like, "You cannot use an abstract class as a mixin unless it inherits only from Object"; despite this error message, flutter would build it and it ran ok. But this had to be changed.

What I am now doing:

abstract class Fluttercouch { // essentially the same as your original
// ...
class FluttercouchNative extends Fluttercouch { // notice this is CONCRETE class

...and then, in my client code, instead of a mixin, I use an instance:

class LabeledEntriesModel extends Model {
  FluttercouchNative _cdb = new FluttercouchNative();
//...
await _cdb.initDatabaseWithName(DATABASE_NAME);
// etc..

Hence, when we eventually merge the code, the instructions could be something like, "if you are using couchbase-lite 1.4 and wish to write native Java methods for Views and Queries, then you must create an instance of FluttercouchNative...; however, if you are using couchbase-lite 2.x or greater, you can declare a mixin of Fluttercouch and write exclusively in Dart...; and if you wish to use Javascript Query/View approach, you must create an instance of FluttercouchJS.." (which does not yet exist).

Cheers, DA

lchristille commented 5 years ago

Hi David, I think this last is a prettier approach. I made the decision to use the mix-in approach because I though it would be nice to not overload a Model with other objects references, but studying other libraries I saw that this is not the default approach. IMHO it could be possible to transform my Fluttercouch library to a concrete class to be instantiated.

I'm a bit "worried" about developers extending the native code of fluttercouch to write native code. It has to be done in the android part of the library, if I'm not wrong. I'm wondering if it could be possible to write native code in the android part of the app and make this code talk to the native code of the library. This would be a complication for the library, but a clearer use for developers. What do you think?

Cheers, Luca

sensiblearts commented 5 years ago

Hi Luca, Sorry for the late reply -- very busy teaching biology.

As for your suggestions: "write native code in the android part of the app and make this code talk to the native code of the library" -- I think this is a very good idea. If this works, then the plugin could truly be a plugin, even for couchbase-lite 1.4 users, and there would be no need to make a local copy or fork the code as a developer.

However, I do not know exactly how Flutter manages java files, and where I should put such file(s) so that the developer could be certain that his work was not overwritten by flutter. Any suggestions?

Also, I assume that I would be able to import the java package from the plugin into the developer's java client code; we will see.

Great idea, David

On Tue, Feb 26, 2019 at 6:06 PM Luca Christille notifications@github.com wrote:

Hi David, I think this last is a prettier approach. I made the decision to use the mix-in approach because I though it would be nice to not overload a Model with other objects references, but studying other libraries I saw that this is not the default approach. IMHO it could be possible to transform my Fluttercouch library to a concrete class to be instantiated.

I'm a bit "worried" about developers extending the native code of fluttercouch to write native code. It has to be done in the android part of the library, if I'm not wrong. I'm wondering if it could be possible to write native code in the android part of the app and make this code talk to the native code of the library. This would be a complication for the library, but a clearer use for developers. What do you think?

Cheers, Luca

— You are receiving this because you modified the open/close state. Reply to this email directly, view it on GitHub https://github.com/oltrenuovefrontiere/fluttercouch/issues/8#issuecomment-467652325, or mute the thread https://github.com/notifications/unsubscribe-auth/AAxJwuMgwUoXtBi2tweA6dtQjBglPgdNks5vRb3sgaJpZM4YcmkR .

-- </> David Alm

lchristille commented 5 years ago

Hi David, I’ve done quite of investigation about how we could implement such a feature. Two paths have come to my mind: Dependency Injection / Inversion of Control and Reflection.

Dependency Injection / IoC:

// In the “main” function of the native android code, the developer retrieves a singleton instance of CBManager
CBManager cbManager = CBManager.getInstance();
// Then he injects a self-developed class that implements the interface QueryHandler. (Yet to be implemented)
SimpleQueryHandler simpleQueryHandler = SimpleQueryHandler();
cbManager.registerQueryHandler("simpleName", simpleQueryHandler);

// Here the example code of the class (I wrote in 2.x syntax but the concept is the same for 1.4)
class SimpleQueryHandler implements QueryHandler {

// This method is required by the interface
ResultSet execute(Database database) {
Query query = QueryBuilder
    .select(SelectResult.expression(Meta.id),
        SelectResult.property(“name”),
        SelectResult.property(“type”))
    .from(DataSource.database(database))
    .where(Expression.property(“type”).equalTo(Expression.string(“hotel”)))
    .orderBy(Ordering.expression(Meta.id));
try {
    ResultSet rs = query.execute();
} catch (CouchbaseLiteException e) {
    Log.e(“Sample”, e.getLocalizedMessage());
}
return rs;
}

When the developer in Dart invokes a method executeQuery("simpleName");, the native code invokes the execute method of the right query injecting required arguments (for 2.x is a reference to the database object). The returned ResultSet is then passed back to the Dart code as a map through the platform channel. In this way, it is possible to "store" in the query object also parameters and other things that could be useful to the Query object.

Reflection

The library through reflection looks for classes that implement a precise interface, auto-instantiating and registering those classes. The other parts are the same as above.

Thoughts

First the Android part: It is straightforward to import any of the classes of Fluttercouch (e.g. CBManager) in the native code of the application. The IDE recognizes the package as it would be in the same “project” and it is possible to instantiate an object and do everything as it was in the library native code. Reflection is fully supported but introduces lacks in performance. Perhaps it would be also more transparent for the developer the use of DI/IoC.

The iOs part: I tried but I didn't find a way to instantiate the CBManager from native code of the application. I have to investigate more. But Reflection in Swift is not fully supported and it is slaggy.

Have you any consideration or suggestion? Happy coding! Luca

sensiblearts commented 5 years ago

Hi Luca, Just touching base...

I have not made any changes to my fluttercouch code, but it's working fine.

I need to get this app launched in alpha so I can get some feedback; once that's done, I'll get back to fluttercouch development.

Probably in 4 weeks, by May 1 or so, I'll be back to it. Dave

sensiblearts commented 5 years ago

Hi Luca, I just upgraded flutter yesterday and it broke my version of fluttercouch (1.4).

The problem is the same as this image_picker issue, so I just copied that logic into fluttercouch files:

In FluttercouchPlugin.java, I did this:

import android.os.Handler;
import android.os.Looper;
//...
public class FluttercouchPlugin implements MethodCallHandler {
//...
  // May 9, 2019, copied from image_picker plugin because of
  // new requirement that result response is on the platform thread:
  //
  // MethodChannel.Result wrapper that responds on the platform thread.
  private static class MethodResultWrapper implements MethodChannel.Result {
    private MethodChannel.Result methodResult;
    private Handler handler;

    MethodResultWrapper(MethodChannel.Result result) {
      methodResult = result;
      handler = new Handler(Looper.getMainLooper());
    }

    @Override
    public void success(final Object result) {
      handler.post(
          new Runnable() {
            @Override
            public void run() {
              methodResult.success(result);
            }
          });
    }

    @Override
    public void error(
        final String errorCode, final String errorMessage, final Object errorDetails) {
      handler.post(
          new Runnable() {
            @Override
            public void run() {
              methodResult.error(errorCode, errorMessage, errorDetails);
            }
          });
    }

    @Override
    public void notImplemented() {
      handler.post(
          new Runnable() {
            @Override
            public void run() {
              methodResult.notImplemented();
            }
          });
    }
  }
//...
@Override
    public void onMethodCall(MethodCall call, Result rawResult) {
        String _id;
        MethodChannel.Result result = new MethodResultWrapper(rawResult);
    //...
    // result is now a wrapped object, and the rest of the code is the same.
}

Also, to handle replication events, I had to use the same logic on the event sink.

So, in my ReplicationEventListener_1_4.java, I did this:

import android.os.Handler;
import android.os.Looper;
//...
public class ReplicationEventListener_1_4 implements EventChannel.StreamHandler, ChangeListener {

    private CBManager mCBmanager;
    private ListenerToken mListenerToken;
    private EventChannel.EventSink mEventSink;

    // another wrapper to run responses on the platform thread:
    //
    private static class EventSinkWrapper implements EventChannel.EventSink {
        private EventChannel.EventSink eventSink;
        private Handler handler;

        EventSinkWrapper(EventChannel.EventSink sink) {
        eventSink = sink;
        handler = new Handler(Looper.getMainLooper());
        }

        @Override
        public void success(final Object result) {
        handler.post(
            new Runnable() {
                @Override
                public void run() {
                eventSink.success(result);
                }
            });
        }

        @Override
        public void error(
            final String errorCode, final String errorMessage, final Object errorDetails) {
        handler.post(
            new Runnable() {
                @Override
                public void run() {
                eventSink.error(errorCode, errorMessage, errorDetails);
                }
            });
        }

        @Override
        public void endOfStream() {
            eventSink.endOfStream();
        }
    }
    //...

    @Override
    public void onListen(Object o, final EventChannel.EventSink eventSink) {
        // mEventSink = eventSink;
        mEventSink = new EventSinkWrapper(eventSink);

        mListenerToken = mCBmanager.getReplicator().addChangeListener(this);
    }
   // and then the remainder of the code is the same.

I have not tested this thoroughly, but the errors went away and sink is working again.

(Actually, I added MethodResultWrapper first but the error did not go away until I added EventSinkWrapper; so, EventSinkWrapper is definitely necessary, and MethodResultWrapper may be necessary.)

Hope this helps with version 2. Cheers, David

AakashAgiwal commented 5 years ago

Hi Luca,

Hope your doing great.

In one of our projects, we have a requirement of syncing Couch db in flutter app. I would like to know whether there are any plans on integrating Cb-lite 1.4 support in your project? If yes, would like to know by when it would be available?

Regards, Aakash

lchristille commented 5 years ago

Hi @AakashAgiwal, I'm glad you're interested in Flutter and Couchbase. You can see https://github.com/sensiblearts/fluttercouch for an actual 1.4 implementation with your same aim.

As I can see @sensiblearts's package hasn't an updated documentation yet, but you can try it and ask him any question about his implementation. I'll try to answer too in the case he was currently unreachable.

Regards, Luca