title: JavaFX Optimized Asynchronous Lazy Loading of TreeItems on TreeView
body: <p>I have an app where i have a <strong>TreeView</strong> which will have <strong>TreeItems</strong> holding a <em>large number</em> of leaf TreeItems. Having a huge number of TreeItems in the treeview hurts the performance of the app noticeably, to avoid that, what i will do, is i will allow only one non-leaf TreeItem to be expanded at a time, and once a TreeItem is folded, i will clear it's children, and load them <strong>asynchronously</strong> once needed (<strong>When the user expands the TreeItem</strong>).</p>
  
  <p>The weird issue is, in this test below, when i first click the expand arrow on the treeitem, the children load fine, and if i fold it (which will clear children) and unfold it again, sometimes it works and others the <code>program hogs and starts consuming 30% of the cpu for a couple of minutes</code> then gets back running. <strong>What's weirder</strong> is that if i double click on the TreeItem to expand it (Not using the arrow) the hog starts right away, even at first program launch.</p>
  
  <p>What could i be possibly doing wrong here?</p>
  
  <p><strong>PS:</strong></p>
  
  <ul>
  <li><p>Some of the code in the LazyTreeItem class is inspired by <strong>James_D's</strong> answer <a href="https://stackoverflow.com/questions/31185441/javafx-treetableview-and-expanding-items-without-children">Here</a></p></li>
  <li><p>I tried running the loadItems task on the fx thread(Not using the ItemLoader), but it didn't make any difference.</p></li>
  <li><p>Same issue occurs using both <strong>JAVA 8</strong> and <strong>JAVA 9</strong></p></li>
  </ul>
  
  <blockquote>
    <p><strong>App.java</strong></p>
  </blockquote>
  
  <pre><code>public class App extends Application {
  
      private TreeView&lt;Item&gt; treeView = new TreeView&lt;&gt;();
  
      @Override
      public void start(Stage primaryStage) throws Exception {
          primaryStage.setTitle("TreeView Lazy Load");
          primaryStage.setScene(new Scene(new StackPane(treeView), 300, 275));
          initTreeView();
          primaryStage.show();
      }
  
      private void initTreeView() {
          treeView.setShowRoot(false);
          treeView.setRoot(new TreeItem&lt;&gt;(null));
  
          List&lt;SingleItem&gt; items = new ArrayList&lt;&gt;();
          for (int i = 0; i &lt; 100000; i++) {
              items.add(new SingleItem(String.valueOf(i)));
          }
          TreeItem&lt;Item&gt; parentItem = new TreeItem&lt;&gt;(new Item());
          parentItem.getChildren().add(new LazyTreeItem(new MultipleItem(items)));
  
          treeView.getRoot().getChildren().add(parentItem);
      }
  
      public static void main(String[] args) {
          launch(args);
      }
  }
  </code></pre>
  
  <blockquote>
    <p><strong>LazyTreeItem.java</strong></p>
  </blockquote>
  
  <pre><code>public class LazyTreeItem extends TreeItem&lt;Item&gt; {
      private boolean childrenLoaded = false;
      private boolean isLoadingItems = false;
  
      public LazyTreeItem(Item value) {
          super(value);
          // Unload data on folding to reduce memory
          expandedProperty().addListener((observable, oldValue, newValue) -&gt; {
              if (!newValue) {
                  flush();
              }
          });
      }
  
      @Override
      public ObservableList&lt;TreeItem&lt;Item&gt;&gt; getChildren() {
          if (childrenLoaded || !isExpanded()) {
              return super.getChildren();
          }
          if (super.getChildren().size() == 0) {
              // Filler node (will translate into loading icon in the
              // TreeCell factory)
              super.getChildren().add(new TreeItem&lt;&gt;(null));
          }
          if (getValue() instanceof MultipleItem) {
              if (!isLoadingItems) {
                  loadItems();
              }
          }
          return super.getChildren();
      }
  
      public void loadItems() {
          Task&lt;List&lt;TreeItem&lt;Item&gt;&gt;&gt; task = new Task&lt;List&lt;TreeItem&lt;Item&gt;&gt;&gt;() {
              @Override
              protected List&lt;TreeItem&lt;Item&gt;&gt; call() {
                  isLoadingItems = true;
                  List&lt;SingleItem&gt; downloadSet = ((MultipleItem) LazyTreeItem.this.getValue()).getEntries();
                  List&lt;TreeItem&lt;Item&gt;&gt; treeNodes = new ArrayList&lt;&gt;();
                  for (SingleItem download : downloadSet) {
                      treeNodes.add(new TreeItem&lt;&gt;(download));
                  }
                  return treeNodes;
              }
          };
          task.setOnSucceeded(e -&gt; {
              Platform.runLater(() -&gt; {
                  super.getChildren().clear();
                  super.getChildren().addAll(task.getValue());
                  childrenLoaded = true;
                  isLoadingItems = false;
              });
          });
          ItemLoader.getSingleton().load(task);
      }
  
      private void flush() {
          childrenLoaded = false;
          super.getChildren().clear();
      }
  
      @Override
      public boolean isLeaf() {
          if (childrenLoaded) {
              return getChildren().isEmpty();
          }
          return false;
      }
  }
  </code></pre>
  
  <blockquote>
    <p><strong>ItemLoader.java</strong></p>
  </blockquote>
  
  <pre><code>public class ItemLoader implements Runnable {
      private static ItemLoader instance;
      private List&lt;Task&gt; queue = new ArrayList&lt;&gt;();
      private Task prevTask = null;
  
      private ItemLoader() {
          Thread runner = new Thread(this);
          runner.setName("ItemLoader thread");
          runner.setDaemon(true);
          runner.start();
      }
  
      public static ItemLoader getSingleton() {
          if (instance == null) {
              instance = new ItemLoader();
          }
          return instance;
      }
  
      public &lt;T&gt; void load(Task task) {
          if (queue.size() &lt; 1) {
              queue.add(task);
          }
      }
  
      @Override
      public void run() {
          while (true) {
              try {
                  Thread.sleep(500);
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
  
              if (!queue.isEmpty()) {
                  Task task = queue.get(0);
                  if (task != prevTask) {
                      prevTask = task;
                      task.run();
                      queue.remove(task);
                  }
              }
          }
      }
  }
  </code></pre>
  
  <blockquote>
    <p>Model (<strong>Item.java</strong>, <strong>SingleItem.java</strong>, <strong>MultipleItem.java</strong>)</p>
  </blockquote>
  
  <pre><code>public class Item {
  
  }
  /****************************************************************
   **********                  SingleItem              ************
   ****************************************************************/
  public class SingleItem extends Item {
      private String id;
  
      public SingleItem(String id) {
          this.id = id;
      }
  
      public void setId(String id) {
          this.id = id;
      }
  }
  /****************************************************************
   **********                  MultipleItem            ************
   ****************************************************************/
  public class MultipleItem extends Item {
  
      private List&lt;SingleItem&gt; entries = new ArrayList&lt;&gt;();
  
      public MultipleItem(List&lt;SingleItem&gt; entries) {
          this.entries = entries;
      }
  
      public List&lt;SingleItem&gt; getEntries() {
          return entries;
      }
  
      public void setEntries(List&lt;SingleItem&gt; entries) {
          this.entries = entries;
      }
  }
  </code></pre>