DEV Community

shaojie gong
shaojie gong

Posted on

GrowBook: turning your phone into a light meter, one EventChannel at a time

Every plant tag says the same thing: "bright indirect light." Nobody knows what that means in lux, and nobody's going to buy a real light meter for a fern. But your phone already has an ambient light sensor — it's what dims your screen. So I wanted GrowBook to turn that sensor into a light meter: point your phone at where the plant sits, get a lux number, get told whether that spot is "low / medium / bright / direct."

Simple idea. It's the feature that made me write native Android for the first time.

Here's what I assumed: Flutter has a package for everything. There's sensors_plus, which gives you the accelerometer, gyroscope, magnetometer — all the motion sensors. I added it, went looking for the light sensor, and… it's not there. The ambient light sensor isn't a motion sensor, so it falls outside what the popular cross-platform package exposes. I checked a few others. Same story, or abandoned, or Android-only in a way that didn't build.

So I had a choice: find some half-maintained plugin and pray, or just talk to the Android sensor myself. I went with myself.

This is where Flutter's platform channels come in, and honestly it's the moment Flutter clicked for me as more than a UI toolkit. There are two kinds of channel: MethodChannel for one-off calls ("take a photo, give me the result"), and EventChannel for a continuous stream of values over time. A light sensor is a firehose of readings, so EventChannel is the right tool.

The shape ended up being tiny. On the Dart side, I open a named channel and listen:

static const _channel = EventChannel('com.yuntai.growbook/light_sensor');
Stream get lightSensorStream =>
    _channel.receiveBroadcastStream().map((e) => e as int);
Enter fullscreen mode Exit fullscreen mode

That's the entire Dart service. Six lines. The other end lives in the native Android code: register a SensorEventListener on TYPE_LIGHT, and every time the OS hands you a new reading, push the lux value into the channel's event sink. The stream just… flows. My Flutter UI subscribes to a Stream and never knows or cares that the numbers are coming from Kotlin.

The lux value maps onto plant-friendly bands, which took some reading to get right:

  • under 1,000 lux — low light (snake plant, pothos, ZZ, peace lily)
  • 1,000–5,000 — medium (philodendron, ferns, spider plant)
  • 5,000–20,000 — bright (fiddle leaf fig, monstera, rubber plant)
  • over 20,000 — direct (succulents, cacti, herbs)

Then the part I didn't plan for: not every phone has this sensor. Cheaper devices and some tablets skip it entirely to save a few cents. If you subscribe to a sensor that isn't there, you don't get a clean "not supported" — you get silence, or an error buried in the stream. So the whole thing has to degrade honestly: catch the error, show the user "your device doesn't have a light sensor" instead of a spinner that spins forever, and never pretend a reading of 0 is a real measurement of a dark room.

What I took away: "Flutter has a plugin for everything" is true right up until it isn't, and the gap is not a wall — it's a twenty-line bridge you can build yourself. I'd been treating native code as this scary other country. It turned out to be a short walk across one channel. The scariest part of the feature was the sentence "I guess I have to write Kotlin now," and that sentence was wrong.

If you build cross-platform: what finally forced you to drop down to native, and was it as bad as you feared? Mine was an ambient light sensor and it absolutely wasn't.

— building GrowBook in public, #3

Top comments (0)