I use dart FFI to retrieve data from native side, and show the data with flutter CustomPaint.

I use ValueNotifier to control CustomPaint repaint.

Code: Poll data at a rate

With a state class, I poll data from native side periodically, and assign it to the ValueNotifier.

class _ColorViewState extends State<ColorView> {
  ValueNotifier<NativeColor> _notifier;
  Timer _pollTimer;

  @override
  void initState() {
    // TODO: implement initState
    super.initState();

    ffiInit();

    // initialize notifier
    _notifier = ValueNotifier<NativeColor>(ffiGetColor().ref);

    _pollTimer = Timer.periodic(Duration(milliseconds: 16), _pollColor);
  }

  _pollColor(Timer t) {

    setState(() {
      print('polling ...');
      _notifier.value = ffiGetColor().ref;
      print('polled: ${_notifier.value.r}, ${_notifier.value.g}, ${_notifier.value.b}');
    });
  }

  ....

}

Note that I poll at the rate of about 60fps.

And I bind the notifier to the CustomPaint repaint

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: EdgeInsets.all(10),
      width: double.infinity,
      height: double.infinity,
      color: widget.clrBackground,
      child: ClipRect(
        child: CustomPaint(
          painter: _ColorViewPainter(
              context: context,
              notifier: _notifier,
              clrBackground: Color.fromARGB(255, 255, 0, 255)
          )
        )
      )
    );
  }

Code: Repaint CustomPaint reactively

Then with the CustomPaint's repaint bound to the ValueNotifier, I paint the screen with the retrieved colour.

class _ColorViewPainter extends CustomPainter {
  ValueNotifier<NativeColor> notifier;
  BuildContext context;
  Color clrBackground;

  _ColorViewPainter({this.context, this.notifier, this.clrBackground})
    : super(repaint: notifier) {
  }

  @override
  bool shouldRepaint(_ColorViewPainter old) {
    print('should repaint');
    return true;
  }

  @override
  void paint(Canvas canvas, Size size) {
    print("paint: start");
    final r = notifier.value.r;
    final g = notifier.value.g;
    final b = notifier.value.b;
    print("color: $r, $g, $b");
    final paint = Paint()
        ..strokeJoin = StrokeJoin.round
        ..strokeWidth = 1.0
        ..color = Color.fromARGB(255, r, g, b)
        ..style = PaintingStyle.fill;

    final width = size.width;
    final height = size.height;
    final content = Offset(0.0, 0.0) & Size(width, height);
    canvas.drawRect(content, paint);
    print("paint: end");
  }

}

Then I noticed that visually the colour updates are at a lower rate than the polling. This is observed by looking at my logging and the phone screen at the same time, although the repaint works.

Question

How should I achieve perceived simultaneous updates?

I should also add that the native backend simulation switches colours between red/green/blue at an 1-second interval.

Since the polling is much more frequent, I expect to see a fairly stable colour changing at about 1-second interval. But right now the colours change at a longer interval. Sometimes repaint is called very rarely, which could be several seconds, all the while the polling returns quite stable data updates.

Update

According to my test I should keep setState otherwise the repaint simply stops. Also by switching the data update to dart land I found that everything works as expected. So it must be something on the native side or in the FFI interface. Here is the modified dart code that works as expected when no FFI is involved.

Basically I use a constant colour collection and iterate through it.


class _ColorViewState extends State<ColorView> {
  ValueNotifier<NativeColor> _notifier;
  Timer _pollTimer;
  var _colors;
  int _step = 0;

  @override
  void initState() {
    // TODO: implement initState
    super.initState();

    ffiInit();

    // constant colour collection
    _colors = [
      [255, 0, 0],
      [0, 255, 0],
      [0, 0, 255]
    ];

    _notifier = ValueNotifier<NativeColor>(ffiGetColor().ref);

    _pollTimer = Timer.periodic(Duration(milliseconds: 1000), _pollColor);
  }

  _pollColor(Timer t) {

    setState(() {
      print('polling ...');

//      _notifier.value = ffiGetColor().ref;

      _notifier.value.r = _colors[_step][0];
      _notifier.value.g = _colors[_step][1];
      _notifier.value.b = _colors[_step][2];
      print('polled: ${_notifier.value.r}, ${_notifier.value.g}, ${_notifier.value.b}');

      if (++_step >= _colors.length) {
        _step = 0;
      }

    });
  }

On the native side, I have a producer/consumer thread model working. The producer is looping through the colour collection at a fixed rate. And the consumer gets it whenever pinged by the producer.

#include <cstdlib>
#include <ctime>
#include <chrono>
#include <condition_variable>
#include <mutex>
#include <thread>

#ifdef __cplusplus
    #define EXTERNC extern "C" __attribute__((visibility("default"))) __attribute__((used))
#else
    #define EXTERNC
#endif  // #ifdef __cplusplus

struct NativeColor {
    int r;
    int g;
    int b;
};

NativeColor* gpColor = nullptr;
NativeColor gWorker = {255, 0, 255};
// producer / consumer thread tools
std::thread gThread;
std::mutex gMutex;
std::condition_variable gConVar;

int gColors[][3] = {
    {255, 0, 0},
    {0, 255, 0},
    {0, 0, 255}
};
int gCounter = 0;
int gCounterPrev = 0;

EXTERNC void ffiinit() {
    if(!gpColor) {
        gpColor = (struct NativeColor*)malloc(sizeof(struct NativeColor));
    }

    if(!gThread.joinable()) {
        gThread = std::thread([&]() {
            while(true) {
                std::this_thread::sleep_for (std::chrono::seconds(1));
                std::unique_lock<std::mutex> lock(gMutex);
                gWorker.r = gColors[gCounter][0];
                gWorker.g = gColors[gCounter][1];
                gWorker.b = gColors[gCounter][2];
                if(++gCounter == 3) {
                    gCounter = 0;
                    gCounterPrev = gCounter;
                }
                lock.unlock();
                gConVar.notify_one();
            }
        });
    }
}

EXTERNC struct NativeColor* ffiproduce() {
    // get yellow
    gpColor->r = 255;
    gpColor->g = 255;
    gpColor->b = 255;

    std::unique_lock<std::mutex> lock(gMutex);
    gConVar.wait(lock, [&]{
        return gCounter > gCounterPrev;
        //return true;
    });
    *gpColor = gWorker;
    gCounterPrev = gCounter;
    lock.unlock();
    return gpColor;
}

ffiproduce() is bound to the dart-side ffiGetColor() function. So I assume this consumer function works in the main thread.

So one idea I have is that maybe the thread coordination on C++ side affected the way flutter renders through CustomPaint.

But I have no idea how to prove that at this point.


Solution 1: kakyo

I made some progress by playing around with the sleep function on the native side.

Here are my findings:

  • I used to use 1-sec interval to churn out data from the native producer thread, and consume it from main thread also on the native side. The consumer must wait for the producer ping. At this rate, flutter rendering thread seems to be on halt.
  • By reducing the sleep all the way to below 20ms, the rendering starts working as expected.
  • With even 20ms sleep, there are hiccups in the rendering.

So I believe my data generation model must adapt to flutter to ensure that the polling should not block or should deliver at the rate around flutter's preferred rate, say, 60fps.