Back to the journal

Flutter's platform channel is where your beautiful cross-platform illusion quietly falls apart

Flutter gives you one codebase for iOS and Android, and for screens full of widgets that stays true. But the moment you need to talk to a native API that Flutter doesn't wrap — biometric prompts with device-specific fallback chains, Bluetooth peripherals, or platform-specific in-app purchase verification — you drop into platform channels. That means writing Swift or Kotlin anyway, maintaining message contracts between Dart and native code, and debugging serialisation mismatches that only surface on one OS. The cross-platform promise doesn't break, but it develops a visible crack right here.

We hit this on a recent project integrating a South African payment SDK that only shipped native Android and iOS libraries. The Flutter side was trivial — a button and a loading state. The platform channel layer underneath needed separate implementations per OS, each with its own error handling, lifecycle awareness, and callback threading model. Our estimate for that single feature doubled, and the extra surface area meant twice the manual QA passes. If you budget your Flutter project as though native code doesn't exist, the first hard integration will blow your timeline.

Before you commit to Flutter for a new app, list every native API you'll need in the first three releases — payments, sensors, OS-level sharing, health data, NFC. If more than two require custom platform channel work with no mature plugin available, price those integrations honestly. Check pub.dev for plugin maintenance activity, not just star counts. A plugin last updated fourteen months ago is a liability, not a shortcut. The useful question isn't whether Flutter works — it's whether Flutter works for the specific native seams your product actually touches.

The scope document your client signed is not the scope they have in their head

You can write the tightest scope document imaginable — numbered deliverables, explicit exclusions, sign-off checkboxes — and your client will still assume the thing they mentioned casually on a Tuesday call is included. This is not dishonesty. It is a genuine gap between what people read and what they remember agreeing to. We have seen it on nearly every fixed-price project we have run: the written scope says three screens, the client's mental model says five, and by week four you are negotiating something nobody planned for.

The fix is not a longer document. It is a shorter, uglier one paired with a conversation. We now send a single-page "not doing" list alongside every proposal. It names the specific things a client might reasonably expect but that fall outside this engagement. "We are not building a dashboard. We are not integrating with your accounting software. We are not handling email notifications." It feels blunt, almost rude, the first time you write one. But clients consistently tell us it is the most useful part of the proposal because it forces the conversation before the deposit clears.

The awkwardness you feel sending that list is exactly the awkwardness you will feel in week six when the client asks why their feature is missing. Move the discomfort forward. If a client pushes back on an exclusion, that is valuable information — it means your scope was already wrong before you started. Better to renegotiate a proposal than renegotiate a relationship halfway through delivery.

Your inventory lives in a Google Sheet and your warehouse manager is the only one who understands column F

We see this pattern constantly: a business runs on a spreadsheet that started as a quick fix three years ago. It now has conditional formatting that acts as business logic, hidden columns that control a VLOOKUP chain, and one person who knows what breaks if you sort by the wrong column. The spreadsheet isn't the problem. The problem is that your business process is encoded in someone's mental model and loosely pinned to a grid that has no validation, no audit trail, and no way to trigger downstream actions.

When we migrate these setups into a proper system — whether that's a lightweight inventory tool, a custom admin panel, or even structured Airtable with automations — the hardest part is never the technology. It's extracting the rules. Column F might mean 'this supplier needs seven days lead time so we reorder earlier' or 'this SKU is actually two products bundled for a specific client.' Every quirky formula is a business rule that was never written down. If you skip the extraction step, you build a system that looks clean but misses half the logic your team actually relies on.

Before you brief anyone on replacing your spreadsheet, do this: sit with the person who maintains it and ask them to walk you through a normal Monday. Record it. Write down every decision they make that isn't obvious from the column headers. That document is your actual requirements spec, and it's worth more than any feature wishlist you'd otherwise hand to a developer.

GPT-4o will happily generate your SQL migration and silently drop a NOT NULL constraint you actually need

We've been using LLM-assisted code generation for database migrations across a few client projects. The speed gain is genuine — describe your schema change in plain English, get a migration file in seconds. But we've hit a pattern worth flagging: when you ask a model like GPT-4o to generate an ALTER TABLE statement, it tends to produce something that runs cleanly rather than something that preserves every constraint from your existing schema. It will add your new column, rename your field, even handle the foreign key — and quietly omit a CHECK constraint or drop a NOT NULL that was on the original column. The migration runs. Your tests pass. Two weeks later you've got empty strings in a field that was never supposed to allow them.

The problem is that LLMs optimise for plausible, complete-looking output. A migration that runs without errors looks more "correct" to the model than one that raises a constraint violation during generation. So it takes the path of least resistance. This is especially dangerous with schema changes on production tables where the constraints exist precisely because someone got burned by bad data years ago. The model has no access to that history and no reason to be conservative.

If you're generating migrations with AI, diff the output against your current schema definition before you run it. Not just a visual scan — use a proper schema comparison tool or at minimum run a \d+ (Postgres) or SHOW CREATE TABLE (MySQL) before and after in staging. Treat AI-generated DDL the way you'd treat a migration written by a new junior developer: assume it works, verify it's complete.

That staging environment you never refresh is testing against a database that no longer exists

Most small teams we work with have a staging environment. Few of them keep it honest. The production database has grown, schemas have drifted, edge-case data has accumulated — but staging still runs on a seed file someone wrote eight months ago with twelve tidy rows and no nulls. You deploy to staging, everything passes, and then production breaks because a query plan that worked fine on twelve rows chokes on twelve thousand, or because a column that staging insists is always populated turns out to be null for 30% of real records.

The fix is not complicated but it requires a habit: regularly restore a sanitised copy of production data into staging. Sanitised is the key word — strip out personal information, hash emails, mask payment details — but keep the shape, volume, and mess of the real dataset. If your production database is too large for a full copy, sample it intelligently: keep the outliers, the long text fields, the rows with unexpected encoding. Those are the rows that find your bugs.

This matters more than most teams admit. A staging environment that lies to you is worse than no staging at all, because it gives you false confidence right before a release. If you're a small team without time to automate the full pipeline, even a monthly manual refresh with a checklist beats a stale seed file. Schedule it, put it in your sprint, and treat it like the infrastructure task it is — not a nice-to-have you'll get to later.

Your Ozow callback silently fails during load-shedding and your order stays stuck on 'pending' forever

Most South African e-commerce builds integrate a payment gateway like Ozow, Yoco, or Payfast using a webhook callback pattern: the customer pays, the gateway posts a result to your server, and you mark the order as paid. This works perfectly until your R99/month VPS on Hetzner or your office-hosted server goes dark during stage 4 load-shedding. The gateway tries your callback URL, gets no response, maybe retries once or twice over a few minutes, and then gives up. The customer paid. Your database never found out. Now you have a support ticket and a trust problem.

The fix is not complicated, but it requires you to stop treating the webhook as the only source of truth. Run a background job — even a simple cron every five minutes — that polls the gateway's transaction status API for any order still marked 'pending' after a reasonable window. Ozow and Payfast both expose lookup endpoints for this. The poll becomes your fallback reconciliation layer. If your server was down for the callback but comes back online ten minutes later, the next poll picks up the missed payment and updates the order. You can run this job on a separate lightweight worker, or even a scheduled Cloud Function that stays outside your load-shedding blast radius.

If you are building for the South African market and your payment flow has no reconciliation loop, you are one four-hour outage away from a backlog of angry customers who can prove they paid. Add the polling job before you launch, not after the first incident. It is a few hours of work that saves you from manually matching bank statements at midnight.

Replacing your 'Get started' button with a specific outcome doubled one client's demo requests

We redesigned a landing page for a B2B client in Joburg last quarter. The product was solid, the copy was decent, the layout was clean. But the primary call-to-action button said 'Get Started' — which told visitors absolutely nothing about what would happen next. Would they hit a pricing wall? A long signup form? A sales call? Ambiguity at the moment of commitment is where conversion quietly dies. We changed the button text to 'Book a 15-min demo' and added a single line beneath it: 'No credit card, no commitment.' Demo requests went from roughly six per week to thirteen.

This is not a grand UX revelation. It's a copywriting fix that took ten minutes. But it works because it removes two specific anxieties at once: what will happen (a short, time-boxed demo) and what won't happen (you won't be charged or locked in). Most landing pages we audit treat the CTA button as an afterthought — a generic label slapped on after the hero section copy gets all the creative energy. The button is the highest-stakes piece of text on the page. It deserves more attention than the subheading.

If your primary CTA says 'Get Started', 'Learn More', or 'Sign Up', test replacing it with the specific next step the user will experience, plus one trust-reducing line underneath. Spell out the action and defuse the objection in the same glance. It is the smallest change on a landing page that tends to produce the largest measurable shift in conversion.

FROM IDEAS TO SOMETHING REAL

Your next chapter.
Let’s build it.

A website, an app, a better way to work. Tell us what you have in mind.

Start a conversation