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);
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)