0

Update: Solved, look at the answer below.

I have a simple Text(initial text value is 'Empty') widget and a button at the top of the screen. When I click the button, I am loading a file that takes a long time to complete. While it is working to complete that, I want to change the Text widget to say 'Loading' and then display the content of the file once that task is finished loading the file.

I run the loading task in a Future and in my build function of the State class I am using a FutureBuilder widget to know what the Text widget should show based on if the task is complete yet or not.

The problem, I think, is that because dart is a single-threaded app, changing the text of the Text widget to 'Loading' doesn't get executed only after the task is completed, which beats the purpose (even when calling setState, because it is added to the event loop I believe where the Future is also scheduled to run).

Should I be running the long task in a separate Isolate? Is that the right way to go? I've been on this for a long time, hopefully someone can help, it's something that should be so simple..

This is the code I am using:

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

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'Flutter Demo',
      home: new MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key}) : super(key: key);

  @override
  _MyHomePageState createState() => new _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  String content = 'Empty';
  Future<FileObject> file;

  loadData() async {
     // do the loading of the file here (that takes a LONG time)
file = /*loadfunction for the file (returns a Future, because it takes long)*/;
  }


  loadTask() {
    setState(() {
      content = 'Loading...';
    });
    loadData();
  }

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      backgroundColor: Colors.black,
      body: Column(
        children: <Widget>[
          buildTitle(),
          buildContent(),
        ],
      ),
    );
  }

  FutureBuilder<FileObject> buildContent() {
    return FutureBuilder<FileObject>(
      future: file,
      builder: (BuildContext context, AsyncSnapshot<FileObject> snapshot) {
        if (snapshot.hasData) {
           content = snapshot.data.text; // The FileObject has a text property
        }

        return Expanded(
          child: SingleChildScrollView(
            child: Text(
              content,
              textAlign: TextAlign.center,
              style: TextStyle(
                color: Colors.lightBlue[100],
                fontSize: 25.0,
                fontWeight: FontWeight.normal,
              ),
            ),
          ),
        );
      },
    );
  }

  Padding buildTitle() {
    return Padding(
      padding: const EdgeInsets.only(left: 8.0, top: 50.0, bottom: 30.0),
      child: Row(
        children: <Widget>[
          IconButton(
            icon: Icon(
              Icons.update,
              color: Colors.lightBlue[50],
            ),
            onPressed: loadTask,
          ),
        ],
      ),
    );
  }
}
  • Your `loadContent` function is useless. You have direct access to the file value inside `snapshot.data` :) – Rémi Rousselet Sep 11 '18 at 20:33
  • I actually just changed that now, and moved that loadContent activity to the FutureBuilder section like you suggested (removing the need for setState of course, and using if snapshot.hasData instead of checking the connectionstate), but still it doesn't change anything... so strange – flutterbeginner Sep 11 '18 at 20:38
  • Can you update your code? – Rémi Rousselet Sep 11 '18 at 20:48
  • sure, should be updated now – flutterbeginner Sep 11 '18 at 20:54
  • Why don't you forego the FutureBuilder and just do another call to setState when the file is loaded? – Kirollos Morkos Sep 12 '18 at 05:46
  • The problem is updating the display to show 'Loading' before the long operation starts. It only gets updated to Loading for a split second and then changes to the actual content of the file, meaning, even though I put setState after I update the content variable to be 'Loading' and even though the rest of that function is executing Futures, the build function doesn't get called again until the long operation has ended. – flutterbeginner Sep 12 '18 at 06:03

2 Answers2

0

Try changing your build method slightly

    buildContent() {
    new FutureBuilder<FileObject>(
      future: loadData(),
      builder: (BuildContext context, AsyncSnapshot<FileObject> snapshot) {
      FileObject file = new FileObject();
        if (snapshot.hasData) {
          file = snapshot.data;
          content = file.text; // The FileObject has a text property
          return Expanded(
          child: SingleChildScrollView(
            child: Text(
              content,
              textAlign: TextAlign.center,
              style: TextStyle(
                color: Colors.lightBlue[100],
                fontSize: 25.0,
                fontWeight: FontWeight.normal,
              ),
            ),
          ),
        );
        } else {
          return new Center(
            child: Column(
              children: <Widget>[
                new LoadingIndicator(),                
              ],
            )
          );
        }
      },
    );
  }

In the above code I've changed a few things.

First, the buildContent method isn't a return method so I've removed the Future and return statements.

Secondly, the future now calls the the loadData() method. The snapshot will return your FileObject when it is ready.

Lastly, the Expanded widget is moved inside the IF clause so that only when the Futurebuilder is finished it will create a FileObject for you to display the content string. However, if the snapshot is empty meaning the Futurebuilder hasn't finished or there was an error creating the FileObject then it should just display the loading text and spinner (I've added the spinner because it's good to know the app hasn't frozen).

EDIT:

Here is my loading class, I've edited the code above call the class

import 'package:flutter/material.dart';

class LoadingIndicator extends StatefulWidget {
  @override
  _LoadingIndicatorState createState() => new _LoadingIndicatorState();
}

class _LoadingIndicatorState extends State<LoadingIndicator>
    with TickerProviderStateMixin {
  Animation<int> animation;
  AnimationController controller;

  Widget childBuilder(int value) {
    String text = 'one moment\n';
    for (int i = 0; i < value; i++) {
      text += '.';
    }

    return new Stack(
      alignment: Alignment(0.0, 0.0),
      children: <Widget>[ 
        // SizedBox(  
        // child: new CircularProgressIndicator(),
        // height:  10.0,
        // width: 400.0,
        // ),
       SizedBox(  
        // height:  40.0,
        child: new Padding(
          padding: EdgeInsets.fromLTRB(0.0, 0.0, 0.0, 30.0),
          child: new Text(text,style: new TextStyle(color: Colors.blueGrey, fontSize: 20.0, fontWeight: FontWeight.w500,), textAlign: TextAlign.center,),
        )
       )
      ]
    );


  }

  @override
  void initState() {
    super.initState();
    controller = new AnimationController(
        vsync: this, duration: const Duration(seconds: 2));
    animation = new StepTween(begin: 0, end: 4).animate(controller)
    .. addStatusListener((status){
      if (status == AnimationStatus.completed) {
        controller.reset();
        controller.forward();
      }
    });
    controller.forward();
  }

  @override
  Widget build(BuildContext context) {
    return new AnimatedBuilder(
        animation: animation,
        builder: (BuildContext context, _) {
          return childBuilder(animation.value);
        });
  }

  @override
  void dispose() {
    controller.dispose();
    super.dispose();
  }
}
F-1
  • 2,887
  • 20
  • 28
  • thank you all for the replies! Unfortunately, this still doesn't work. After executing the Future and doing setState (to get the build function to execute again, so FutureBuilder will work and return the loading animation until the Future is actually done), the build function doesn't get called again, until after the Future has ended (so the loading animation never shows up). Again, I think that dart is a one-threaded application, and therefore the actual execution of the Future is put on the event loop before the setState and build only gets called after the Future is complete. – flutterbeginner Sep 12 '18 at 22:24
  • I used this to open an epub file I don't set state at all, the loading text displays for a good 5 seconds or so whilst the epub is being decoded. – F-1 Sep 13 '18 at 08:45
  • It will show the 'Loading' text, but the CircularProgressIndicator's animation will be stuck until the big task finishes. I saw somewhere that for big tasks it's recommended to use the 'compute' function if you don't want the UI to get stuck, but that's not perfect either (you need to report to the main Isolate that the compute function was done and display the content, but apparently you can't use global variables to communicate that because different Isolates get their own copy). Thought this should be an easy use-case (do a big task while animation works in the background). – flutterbeginner Sep 13 '18 at 15:51
  • I'll have to look into the compute function. I'll share with you a loading class I use in my project. – F-1 Sep 14 '18 at 07:52
  • Thank you for the sample code! The widget still doesn't update with time (i.e. adding '.' to the Loading text). I really think it happens because once the long task starts to execute it blocks the UI thread (can't escape it if we are not multi-threaded, unless I use Isolates here, which is what the compute function is doing). I think the framework should support a much easier way to do something like this. – flutterbeginner Sep 20 '18 at 07:25
  • Have you made any progress? I came across this stackoverflow question which might help you https://stackoverflow.com/a/52498902/469335 – F-1 Sep 25 '18 at 13:26
  • 1
    Thank you, that is helpful and confirms what I thought. I also bumped into AsyncLoader while researching the subject and thought it might do the trick but again, there was no progress due to what is written in the article you provided. So I would say the only way to make it work is run it in an isolate and send a message back to the main thread with the data that was loaded upon completion. – flutterbeginner Sep 26 '18 at 18:58
0

I finally solved it, using Isolates as discussed in the thread above.

The process is pretty simple, I followed the example here: Isolate Example

Because the long task now runs in a separate Isolate/thread than the main one, the animation for loading works as expected until the callback from the Isolate gets triggered and shows the loaded content.

Thanks everyone for the help.