Subscription65 views

How to Build a Coconut Water Delivery Home Screen in Flutter (Full Code + Preview)

A subscription app's home screen has to sell the product and start the flow in one scroll. This tutorial builds exactly that for CocoNeer, a fresh-coconut-water delivery app: a forest-green mesh-gradient hero with floating leaf motifs, a two-card product teaser row, a numbered 'How it works' list, and a dark 'farm to door' promo band, all above a shared bottom nav. Every CTA routes to the Shop tab. It's pure Flutter with no images — the leaves and gradient are hand-painted — and by the end you'll have a scrollable, self-contained landing screen.

CocoNeer · Home — Subscription Flutter UI screen
Live preview — CocoNeer · Home, built in pure Flutter.

What you'll build

  • A forest-green hero built from four radial-gradient 'blobs' faded with a ShaderMask, with no image assets
  • Two product teaser cards laid out in an IntrinsicHeight row so both stay equal height
  • A numbered 'How it works' list driven by a small list of step data
  • A dark rounded 'FARM TO DOOR' promo band with a white 'Subscribe now' pill
  • A shared bottom navigation bar and CTAs that all route to the Shop tab

Step-by-step build

1

Create the file

Add a new file at lib/coconeer_home/coconeer_home_screen.dart in your Flutter project.

2

Register the bundled fonts

No external packages — this is pure Flutter. It does bundle its design fonts (Inter, JetBrainsMono), so drop the font files into fonts/ and declare them in pubspec.yaml:

pubspec.yaml
flutter:
  fonts:
    - family: Inter
      fonts:
        - asset: fonts/Inter-Regular.ttf
    - family: JetBrainsMono
      fonts:
        - asset: fonts/JetBrainsMono-Regular.ttf
3

Build it, piece by piece

Here's how the screen goes together. Each block below is a real slice of the code with a plain-English explanation — paste them in order, or grab the whole file from the next section.

Imports, the screen class, and the scrollable scaffold

coconeer_home_screen.dart
import 'package:flutter/material.dart';

import 'widgets/coco_common.dart';

/// CocoNeer — Home. The landing screen of a fresh-coconut-water subscription
/// app: a natural-green mesh-gradient hero with leaf motifs, a product teaser
/// row, a "how it works" list, a dark farm-to-door promo band, and the shared
/// bottom navigation.
///
/// Self-contained, pure Flutter. Bundles its exact design fonts (Inter +
/// JetBrains Mono); uses no images. Renders standalone when pushed as a route.
///
/// [onTabSelected] fires with the tapped bottom-nav index (0 Home · 1 Shop ·
/// 2 My Plan · 3 Orders); every "shop / subscribe" CTA routes to Shop (index 1).
/// It is a no-op by default so the screen works on its own; the preview gallery
/// wires it to switch between the CocoNeer screens.
class CoconeerHomeScreen extends StatelessWidget {
  const CoconeerHomeScreen({super.key, this.onTabSelected});

  final ValueChanged<int>? onTabSelected;

  void _toShop() => onTabSelected?.call(1);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.white,
      body: SafeArea(
        bottom: false,
        child: Column(
          children: <Widget>[
            _header(),
            Expanded(
              child: SingleChildScrollView(
                physics: const BouncingScrollPhysics(),
                padding: const EdgeInsets.only(bottom: 24),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: <Widget>[
                    _Hero(onShop: _toShop),
                    _ourCoconuts(),
                    _howItWorks(),
                    _promoBand(),
                  ],
                ),
              ),
            ),
          ],
        ),
      ),
      bottomNavigationBar: CocoBottomNav(currentIndex: 0, onTabSelected: onTabSelected),
    );
  }

The file imports Flutter's material library plus the local widgets/coco_common.dart, which holds the shared C color palette, the sans()/mono() text helpers, LeafDecor, CocoPillButton, and CocoBottomNav. CoconeerHomeScreen is a StatelessWidget because nothing on it changes state — it just takes an optional onTabSelected callback, and the private _toShop() helper calls onTabSelected(1) so every CTA jumps to the Shop tab. build() returns a white Scaffold whose body is a SafeArea (with bottom: false so content can run under the nav). Inside, a Column pins the _header() at the top and puts everything else in an Expanded SingleChildScrollView with BouncingScrollPhysics, stacking _Hero, _ourCoconuts, _howItWorks, and _promoBand. The bottomNavigationBar is a CocoBottomNav with currentIndex: 0 to highlight Home.

The top app header

coconeer_home_screen.dart
  Widget _header() {
    return Container(
      height: 52,
      padding: const EdgeInsets.symmetric(horizontal: 20),
      decoration: const BoxDecoration(
        color: Color(0xFFFFFFFF),
        border: Border(bottom: BorderSide(color: C.hairline)),
      ),
      child: Row(
        children: <Widget>[
          Expanded(
            child: Text('CocoNeer', style: sans(size: 20, weight: FontWeight.w600, color: C.green, letterSpacing: -1.2)),
          ),
          const Icon(Icons.notifications_none_rounded, size: 20, color: C.body),
        ],
      ),
    );
  }

_header() is a fixed 52px-tall Container with 20px horizontal padding and a single bottom BorderSide in C.hairline (#EBEBEB) for a thin divider. Its Row holds an Expanded 'CocoNeer' wordmark styled with sans() at size 20, weight w600, in C.green (the #1A5C3A forest green) with a tight -1.2 letterSpacing, and a trailing notifications_none_rounded icon at 20px in C.body. Because the header lives outside the scroll view, it stays put while the page scrolls beneath it.

The 'Our Coconuts' product teaser row

coconeer_home_screen.dart
  Widget _ourCoconuts() {
    final List<Map<String, dynamic>> cards = <Map<String, dynamic>>[
      <String, dynamic>{
        'label': 'Natural', 'name': 'Plain', 'desc': 'Pure young coconut water', 'price': '₹149',
        'color': C.greenSoft, 'ac': C.green, 'lc': const Color(0xFF2D6A4F),
      },
      <String, dynamic>{
        'label': 'Premium', 'name': 'Tender + Malai', 'desc': 'With fresh coconut malai', 'price': '₹199',
        'color': C.cream, 'ac': C.amber, 'lc': C.amber,
      },
    ];

    return Padding(
      padding: const EdgeInsets.fromLTRB(20, 28, 20, 0),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: <Widget>[
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceBetween,
            children: <Widget>[
              Flexible(
                child: Text('Our Coconuts',
                    style: sans(size: 17, weight: FontWeight.w600, letterSpacing: -0.4),
                    maxLines: 1, overflow: TextOverflow.ellipsis),
              ),
              const SizedBox(width: 8),
              GestureDetector(
                onTap: _toShop,
                child: Row(
                  mainAxisSize: MainAxisSize.min,
                  children: <Widget>[
                    Text('Shop all', style: sans(size: 13, weight: FontWeight.w500, color: C.green)),
                    const Icon(Icons.chevron_right, size: 16, color: C.green),
                  ],
                ),
              ),
            ],
          ),
          const SizedBox(height: 14),
          IntrinsicHeight(
            child: Row(
              crossAxisAlignment: CrossAxisAlignment.stretch,
              children: <Widget>[
                for (int i = 0; i < cards.length; i++) ...<Widget>[
                  if (i > 0) const SizedBox(width: 12),
                  Expanded(child: _ProductCard(data: cards[i], onTap: _toShop)),
                ],
              ],
            ),
          ),
        ],
      ),
    );
  }

_ourCoconuts() first defines a two-item list of Map card data — a 'Natural / Plain' card at ₹149 tinted C.greenSoft with a green accent, and a 'Premium / Tender + Malai' card at ₹199 tinted C.cream with the amber accent. It renders a section header Row with a 'Our Coconuts' title (sans size 17, w600) and a tappable 'Shop all' link plus a chevron_right, both in C.green and wired to _toShop. Below it, an IntrinsicHeight wraps a Row with crossAxisAlignment.stretch so both cards match the taller one's height; a for-loop builds each card as an Expanded _ProductCard, inserting a 12px SizedBox gap before every card after the first.

The numbered 'How it works' steps

coconeer_home_screen.dart
  Widget _howItWorks() {
    const List<List<String>> steps = <List<String>>[
      <String>['01', 'Choose a plan', 'Pick coconut type and subscription tier.'],
      <String>['02', 'Set your schedule', 'Mon, Wed, Fri delivery. Change anytime.'],
      <String>['03', 'Fresh at your door', 'Cut same morning, delivered within 2 hours.'],
    ];
    return Padding(
      padding: const EdgeInsets.fromLTRB(20, 32, 20, 0),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: <Widget>[
          Text('How it works.', style: sans(size: 17, weight: FontWeight.w600, letterSpacing: -0.4)),
          const SizedBox(height: 18),
          for (final List<String> s in steps)
            Padding(
              padding: const EdgeInsets.only(bottom: 18),
              child: Row(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: <Widget>[
                  Container(
                    width: 34,
                    height: 34,
                    decoration: const BoxDecoration(color: C.greenSoft, shape: BoxShape.circle),
                    alignment: Alignment.center,
                    child: Text(s[0], style: mono(size: 11, weight: FontWeight.w600, color: C.green)),
                  ),
                  const SizedBox(width: 14),
                  Expanded(
                    child: Padding(
                      padding: const EdgeInsets.only(top: 4),
                      child: Column(
                        crossAxisAlignment: CrossAxisAlignment.start,
                        children: <Widget>[
                          Text(s[1], style: sans(size: 14, weight: FontWeight.w600, letterSpacing: -0.3)),
                          const SizedBox(height: 2),
                          Text(s[2], style: sans(size: 13, color: C.muted, height: 18 / 13)),
                        ],
                      ),
                    ),
                  ),
                ],
              ),
            ),
        ],
      ),
    );
  }

_howItWorks() holds a const list of three [number, title, body] string triples — choose a plan, set your schedule, fresh at your door. After a 'How it works.' heading it loops over the steps with a for-in, giving each an 18px bottom Padding. Each step is a Row: a 34x34 circular Container filled with C.greenSoft holding the two-digit number in the mono() (JetBrains Mono) font in C.green, then a 14px gap, then an Expanded Column with the step title (sans 14, w600) and a muted description in C.muted with an 18/13 line-height. Driving the list from data keeps the markup short and easy to extend.

The dark 'farm to door' promo band

coconeer_home_screen.dart
  Widget _promoBand() {
    return Padding(
      padding: const EdgeInsets.fromLTRB(20, 28, 20, 0),
      child: ClipRRect(
        borderRadius: BorderRadius.circular(22),
        child: Stack(
          children: <Widget>[
            const Positioned.fill(child: ColoredBox(color: C.promo)),
            const Positioned(
              top: -38,
              right: -20,
              child: LeafDecor(size: 175, color: Color(0xFF52B788), opacity: 0.10, rotate: 18),
            ),
            Padding(
              padding: const EdgeInsets.fromLTRB(24, 28, 24, 28),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                mainAxisSize: MainAxisSize.min,
                children: <Widget>[
                  Text('FARM TO DOOR', style: mono(size: 11, color: const Color(0xFF74C69D), letterSpacing: 0.66)),
                  const SizedBox(height: 10),
                  Text('Always fresh.\nNever stored.',
                      style: sans(size: 22, weight: FontWeight.w600, color: Colors.white, letterSpacing: -0.8, height: 28 / 22)),
                  const SizedBox(height: 10),
                  Text('Sourced daily within 50 km. Your coconut is cut the same morning it reaches you.',
                      style: sans(size: 13, color: const Color(0xFFA1A1A1), height: 18 / 13)),
                  const SizedBox(height: 22),
                  GestureDetector(
                    onTap: _toShop,
                    child: Container(
                      height: 40,
                      padding: const EdgeInsets.symmetric(horizontal: 20),
                      decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(100)),
                      child: FittedBox(
                        fit: BoxFit.scaleDown,
                        child: Row(
                          mainAxisSize: MainAxisSize.min,
                          children: <Widget>[
                            Text('Subscribe now', style: sans(size: 13, weight: FontWeight.w500, color: C.ink)),
                            const SizedBox(width: 6),
                            const Icon(Icons.chevron_right, size: 16, color: C.ink),
                          ],
                        ),
                      ),
                    ),
                  ),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}

_promoBand() is a ClipRRect with a 22px radius wrapping a Stack. A Positioned.fill ColoredBox paints the deep C.promo (#0D2B1A) background, and a Positioned LeafDecor (size 175, faint 0.10 opacity, rotated 18 degrees) bleeds off the top-right corner as decoration. Over that sits a padded Column: a 'FARM TO DOOR' mono eyebrow in a soft #74C69D green, a two-line 'Always fresh. Never stored.' headline in white (sans 22, w600), a grey supporting line, and a white pill button. That pill is a 40px-tall Container with a fully rounded borderRadius: 100 and a FittedBox holding 'Subscribe now' plus a chevron in C.ink, all wrapped in a GestureDetector calling _toShop.

The mesh-gradient hero and its painted blobs

coconeer_home_screen.dart
class _Hero extends StatelessWidget {
  const _Hero({required this.onShop});

  final VoidCallback onShop;

  @override
  Widget build(BuildContext context) {
    return ClipRect(
      child: Stack(
        children: <Widget>[
          const Positioned.fill(child: _MeshGradient()),
          const Positioned(
            top: -30,
            right: -14,
            child: LeafDecor(size: 170, color: Color(0xFF2D6A4F), opacity: 0.09, rotate: 22),
          ),
          const Positioned(
            bottom: -18,
            left: -28,
            child: LeafDecor(size: 120, color: Color(0xFF2D6A4F), opacity: 0.06, rotate: -32),
          ),
          Padding(
            padding: const EdgeInsets.fromLTRB(20, 38, 20, 46),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              mainAxisSize: MainAxisSize.min,
              children: <Widget>[
                Container(
                  padding: const EdgeInsets.fromLTRB(6, 4, 12, 4),
                  decoration: BoxDecoration(
                    color: Colors.white.withValues(alpha: 0.82),
                    borderRadius: BorderRadius.circular(9999),
                    border: Border.all(color: Colors.black.withValues(alpha: 0.08)),
                  ),
                  child: Row(
                    mainAxisSize: MainAxisSize.min,
                    children: <Widget>[
                      Container(
                        padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
                        decoration: BoxDecoration(color: C.green, borderRadius: BorderRadius.circular(9999)),
                        child: Text('NEW', style: mono(size: 10, weight: FontWeight.w600, color: Colors.white, letterSpacing: 0.4)),
                      ),
                      const SizedBox(width: 8),
                      Flexible(
                        child: Text('Same-morning delivery available',
                            style: sans(size: 12, color: C.body), overflow: TextOverflow.ellipsis),
                      ),
                    ],
                  ),
                ),
                const SizedBox(height: 20),
                Text('Pure coconuts,\ndelivered fresh.',
                    style: sans(size: 38, weight: FontWeight.w600, color: C.ink, letterSpacing: -2, height: 42 / 38)),
                const SizedBox(height: 14),
                SizedBox(
                  width: 280,
                  child: Text("Subscription plans from ₹149/week. Farm to door, the same morning it's cut.",
                      style: sans(size: 15, color: C.body, height: 22 / 15)),
                ),
                const SizedBox(height: 28),
                CocoPillButton(label: 'Start Subscribing', onTap: onShop, height: 48),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

/// Four radial colour blobs faded to transparent toward the bottom — the
/// natural-green replacement for the Vercel cyan/purple mesh.
class _MeshGradient extends StatelessWidget {
  const _MeshGradient();

  @override
  Widget build(BuildContext context) {
    return IgnorePointer(
      child: Opacity(
        opacity: 0.42,
        child: ShaderMask(
          blendMode: BlendMode.dstIn,
          shaderCallback: (Rect r) => const LinearGradient(
            begin: Alignment.topCenter,
            end: Alignment.bottomCenter,
            colors: <Color>[Colors.black, Colors.black, Colors.transparent],
            stops: <double>[0, 0.38, 0.82],
          ).createShader(r),
          child: const Stack(
            children: <Widget>[
              _Blob(Alignment(-0.84, -0.76), Color(0xFF74C69D), 0.62),
              _Blob(Alignment(-0.28, -1.16), Color(0xFFB7E4C7), 0.62),
              _Blob(Alignment(0.32, -0.56), Color(0xFF40916C), 0.64),
              _Blob(Alignment(0.80, -1.00), Color(0xFF95D5B2), 0.58),
            ],
          ),
        ),
      ),
    );
  }
}

class _Blob extends StatelessWidget {
  const _Blob(this.center, this.color, this.radius);

  final Alignment center;
  final Color color;
  final double radius;

  @override
  Widget build(BuildContext context) {
    return Positioned.fill(
      child: DecoratedBox(
        decoration: BoxDecoration(
          gradient: RadialGradient(
            center: center,
            radius: radius,
            colors: <Color>[color, color.withValues(alpha: 0)],
            stops: const <double>[0, 1],
          ),
        ),
      ),
    );
  }
}

_Hero is a ClipRect'd Stack layering the _MeshGradient behind two Positioned LeafDecor motifs and the content Padding. The content is a Column with a white pill 'NEW · Same-morning delivery available' badge (its background uses Colors.white.withValues(alpha: 0.82)), a big 38px w600 headline 'Pure coconuts, delivered fresh.' in C.ink with a -2 letterSpacing, a width-clamped subtitle, and a 48px CocoPillButton 'Start Subscribing'. The gradient itself is faked without images: _MeshGradient wraps four _Blob widgets in a ShaderMask (BlendMode.dstIn) with a top-to-bottom LinearGradient so the whole thing fades out toward the bottom. Each _Blob is a RadialGradient from a green shade to color.withValues(alpha: 0) transparent, positioned at a different Alignment to scatter the color.

Full code

The complete, ready-to-paste source (2 files). Free to use in your projects — one click copies it all.

import 'package:flutter/material.dart';

import 'widgets/coco_common.dart';

/// CocoNeer — Home. The landing screen of a fresh-coconut-water subscription
/// app: a natural-green mesh-gradient hero with leaf motifs, a product teaser
/// row, a "how it works" list, a dark farm-to-door promo band, and the shared
/// bottom navigation.
///
/// Self-contained, pure Flutter. Bundles its exact design fonts (Inter +
/// JetBrains Mono); uses no images. Renders standalone when pushed as a route.
///
/// [onTabSelected] fires with the tapped bottom-nav index (0 Home · 1 Shop ·
/// 2 My Plan · 3 Orders); every "shop / subscribe" CTA routes to Shop (index 1).
/// It is a no-op by default so the screen works on its own; the preview gallery
/// wires it to switch between the CocoNeer screens.
class CoconeerHomeScreen extends StatelessWidget {
  const CoconeerHomeScreen({super.key, this.onTabSelected});

  final ValueChanged<int>? onTabSelected;

  void _toShop() => onTabSelected?.call(1);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.white,
      body: SafeArea(
        bottom: false,
        child: Column(
          children: <Widget>[
            _header(),
            Expanded(
              child: SingleChildScrollView(
                physics: const BouncingScrollPhysics(),
                padding: const EdgeInsets.only(bottom: 24),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: <Widget>[
                    _Hero(onShop: _toShop),
                    _ourCoconuts(),
                    _howItWorks(),
                    _promoBand(),
                  ],
                ),
              ),
            ),
          ],
        ),
      ),
      bottomNavigationBar: CocoBottomNav(currentIndex: 0, onTabSelected: onTabSelected),
    );
  }

  Widget _header() {
    return Container(
      height: 52,
      padding: const EdgeInsets.symmetric(horizontal: 20),
      decoration: const BoxDecoration(
        color: Color(0xFFFFFFFF),
        border: Border(bottom: BorderSide(color: C.hairline)),
      ),
      child: Row(
        children: <Widget>[
          Expanded(
            child: Text('CocoNeer', style: sans(size: 20, weight: FontWeight.w600, color: C.green, letterSpacing: -1.2)),
          ),
          const Icon(Icons.notifications_none_rounded, size: 20, color: C.body),
        ],
      ),
    );
  }

  Widget _ourCoconuts() {
    final List<Map<String, dynamic>> cards = <Map<String, dynamic>>[
      <String, dynamic>{
        'label': 'Natural', 'name': 'Plain', 'desc': 'Pure young coconut water', 'price': '₹149',
        'color': C.greenSoft, 'ac': C.green, 'lc': const Color(0xFF2D6A4F),
      },
      <String, dynamic>{
        'label': 'Premium', 'name': 'Tender + Malai', 'desc': 'With fresh coconut malai', 'price': '₹199',
        'color': C.cream, 'ac': C.amber, 'lc': C.amber,
      },
    ];

    return Padding(
      padding: const EdgeInsets.fromLTRB(20, 28, 20, 0),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: <Widget>[
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceBetween,
            children: <Widget>[
              Flexible(
                child: Text('Our Coconuts',
                    style: sans(size: 17, weight: FontWeight.w600, letterSpacing: -0.4),
                    maxLines: 1, overflow: TextOverflow.ellipsis),
              ),
              const SizedBox(width: 8),
              GestureDetector(
                onTap: _toShop,
                child: Row(
                  mainAxisSize: MainAxisSize.min,
                  children: <Widget>[
                    Text('Shop all', style: sans(size: 13, weight: FontWeight.w500, color: C.green)),
                    const Icon(Icons.chevron_right, size: 16, color: C.green),
                  ],
                ),
              ),
            ],
          ),
          const SizedBox(height: 14),
          IntrinsicHeight(
            child: Row(
              crossAxisAlignment: CrossAxisAlignment.stretch,
              children: <Widget>[
                for (int i = 0; i < cards.length; i++) ...<Widget>[
                  if (i > 0) const SizedBox(width: 12),
                  Expanded(child: _ProductCard(data: cards[i], onTap: _toShop)),
                ],
              ],
            ),
          ),
        ],
      ),
    );
  }

  Widget _howItWorks() {
    const List<List<String>> steps = <List<String>>[
      <String>['01', 'Choose a plan', 'Pick coconut type and subscription tier.'],
      <String>['02', 'Set your schedule', 'Mon, Wed, Fri delivery. Change anytime.'],
      <String>['03', 'Fresh at your door', 'Cut same morning, delivered within 2 hours.'],
    ];
    return Padding(
      padding: const EdgeInsets.fromLTRB(20, 32, 20, 0),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: <Widget>[
          Text('How it works.', style: sans(size: 17, weight: FontWeight.w600, letterSpacing: -0.4)),
          const SizedBox(height: 18),
          for (final List<String> s in steps)
            Padding(
              padding: const EdgeInsets.only(bottom: 18),
              child: Row(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: <Widget>[
                  Container(
                    width: 34,
                    height: 34,
                    decoration: const BoxDecoration(color: C.greenSoft, shape: BoxShape.circle),
                    alignment: Alignment.center,
                    child: Text(s[0], style: mono(size: 11, weight: FontWeight.w600, color: C.green)),
                  ),
                  const SizedBox(width: 14),
                  Expanded(
                    child: Padding(
                      padding: const EdgeInsets.only(top: 4),
                      child: Column(
                        crossAxisAlignment: CrossAxisAlignment.start,
                        children: <Widget>[
                          Text(s[1], style: sans(size: 14, weight: FontWeight.w600, letterSpacing: -0.3)),
                          const SizedBox(height: 2),
                          Text(s[2], style: sans(size: 13, color: C.muted, height: 18 / 13)),
                        ],
                      ),
                    ),
                  ),
                ],
              ),
            ),
        ],
      ),
    );
  }

  Widget _promoBand() {
    return Padding(
      padding: const EdgeInsets.fromLTRB(20, 28, 20, 0),
      child: ClipRRect(
        borderRadius: BorderRadius.circular(22),
        child: Stack(
          children: <Widget>[
            const Positioned.fill(child: ColoredBox(color: C.promo)),
            const Positioned(
              top: -38,
              right: -20,
              child: LeafDecor(size: 175, color: Color(0xFF52B788), opacity: 0.10, rotate: 18),
            ),
            Padding(
              padding: const EdgeInsets.fromLTRB(24, 28, 24, 28),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                mainAxisSize: MainAxisSize.min,
                children: <Widget>[
                  Text('FARM TO DOOR', style: mono(size: 11, color: const Color(0xFF74C69D), letterSpacing: 0.66)),
                  const SizedBox(height: 10),
                  Text('Always fresh.\nNever stored.',
                      style: sans(size: 22, weight: FontWeight.w600, color: Colors.white, letterSpacing: -0.8, height: 28 / 22)),
                  const SizedBox(height: 10),
                  Text('Sourced daily within 50 km. Your coconut is cut the same morning it reaches you.',
                      style: sans(size: 13, color: const Color(0xFFA1A1A1), height: 18 / 13)),
                  const SizedBox(height: 22),
                  GestureDetector(
                    onTap: _toShop,
                    child: Container(
                      height: 40,
                      padding: const EdgeInsets.symmetric(horizontal: 20),
                      decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(100)),
                      child: FittedBox(
                        fit: BoxFit.scaleDown,
                        child: Row(
                          mainAxisSize: MainAxisSize.min,
                          children: <Widget>[
                            Text('Subscribe now', style: sans(size: 13, weight: FontWeight.w500, color: C.ink)),
                            const SizedBox(width: 6),
                            const Icon(Icons.chevron_right, size: 16, color: C.ink),
                          ],
                        ),
                      ),
                    ),
                  ),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}

class _Hero extends StatelessWidget {
  const _Hero({required this.onShop});

  final VoidCallback onShop;

  @override
  Widget build(BuildContext context) {
    return ClipRect(
      child: Stack(
        children: <Widget>[
          const Positioned.fill(child: _MeshGradient()),
          const Positioned(
            top: -30,
            right: -14,
            child: LeafDecor(size: 170, color: Color(0xFF2D6A4F), opacity: 0.09, rotate: 22),
          ),
          const Positioned(
            bottom: -18,
            left: -28,
            child: LeafDecor(size: 120, color: Color(0xFF2D6A4F), opacity: 0.06, rotate: -32),
          ),
          Padding(
            padding: const EdgeInsets.fromLTRB(20, 38, 20, 46),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              mainAxisSize: MainAxisSize.min,
              children: <Widget>[
                Container(
                  padding: const EdgeInsets.fromLTRB(6, 4, 12, 4),
                  decoration: BoxDecoration(
                    color: Colors.white.withValues(alpha: 0.82),
                    borderRadius: BorderRadius.circular(9999),
                    border: Border.all(color: Colors.black.withValues(alpha: 0.08)),
                  ),
                  child: Row(
                    mainAxisSize: MainAxisSize.min,
                    children: <Widget>[
                      Container(
                        padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
                        decoration: BoxDecoration(color: C.green, borderRadius: BorderRadius.circular(9999)),
                        child: Text('NEW', style: mono(size: 10, weight: FontWeight.w600, color: Colors.white, letterSpacing: 0.4)),
                      ),
                      const SizedBox(width: 8),
                      Flexible(
                        child: Text('Same-morning delivery available',
                            style: sans(size: 12, color: C.body), overflow: TextOverflow.ellipsis),
                      ),
                    ],
                  ),
                ),
                const SizedBox(height: 20),
                Text('Pure coconuts,\ndelivered fresh.',
                    style: sans(size: 38, weight: FontWeight.w600, color: C.ink, letterSpacing: -2, height: 42 / 38)),
                const SizedBox(height: 14),
                SizedBox(
                  width: 280,
                  child: Text("Subscription plans from ₹149/week. Farm to door, the same morning it's cut.",
                      style: sans(size: 15, color: C.body, height: 22 / 15)),
                ),
                const SizedBox(height: 28),
                CocoPillButton(label: 'Start Subscribing', onTap: onShop, height: 48),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

/// Four radial colour blobs faded to transparent toward the bottom — the
/// natural-green replacement for the Vercel cyan/purple mesh.
class _MeshGradient extends StatelessWidget {
  const _MeshGradient();

  @override
  Widget build(BuildContext context) {
    return IgnorePointer(
      child: Opacity(
        opacity: 0.42,
        child: ShaderMask(
          blendMode: BlendMode.dstIn,
          shaderCallback: (Rect r) => const LinearGradient(
            begin: Alignment.topCenter,
            end: Alignment.bottomCenter,
            colors: <Color>[Colors.black, Colors.black, Colors.transparent],
            stops: <double>[0, 0.38, 0.82],
          ).createShader(r),
          child: const Stack(
            children: <Widget>[
              _Blob(Alignment(-0.84, -0.76), Color(0xFF74C69D), 0.62),
              _Blob(Alignment(-0.28, -1.16), Color(0xFFB7E4C7), 0.62),
              _Blob(Alignment(0.32, -0.56), Color(0xFF40916C), 0.64),
              _Blob(Alignment(0.80, -1.00), Color(0xFF95D5B2), 0.58),
            ],
          ),
        ),
      ),
    );
  }
}

class _Blob extends StatelessWidget {
  const _Blob(this.center, this.color, this.radius);

  final Alignment center;
  final Color color;
  final double radius;

  @override
  Widget build(BuildContext context) {
    return Positioned.fill(
      child: DecoratedBox(
        decoration: BoxDecoration(
          gradient: RadialGradient(
            center: center,
            radius: radius,
            colors: <Color>[color, color.withValues(alpha: 0)],
            stops: const <double>[0, 1],
          ),
        ),
      ),
    );
  }
}

class _ProductCard extends StatelessWidget {
  const _ProductCard({required this.data, required this.onTap});

  final Map<String, dynamic> data;
  final VoidCallback onTap;

  @override
  Widget build(BuildContext context) {
    final Color ac = data['ac'] as Color;
    return GestureDetector(
      onTap: onTap,
      child: ClipRRect(
        borderRadius: BorderRadius.circular(18),
        child: Stack(
          children: <Widget>[
            Positioned.fill(
              child: DecoratedBox(
                decoration: BoxDecoration(
                  color: data['color'] as Color,
                  border: Border.all(color: Colors.black.withValues(alpha: 0.05)),
                ),
              ),
            ),
            Positioned(
              top: -22,
              right: -14,
              child: LeafDecor(size: 88, color: data['lc'] as Color, opacity: 0.14, rotate: 38),
            ),
            Padding(
              padding: const EdgeInsets.fromLTRB(16, 18, 16, 18),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                mainAxisSize: MainAxisSize.min,
                children: <Widget>[
                  Text((data['label'] as String).toUpperCase(), style: mono(size: 10, color: ac, letterSpacing: 0.4)),
                  const SizedBox(height: 8),
                  Text(data['name'] as String, style: sans(size: 14, weight: FontWeight.w600, letterSpacing: -0.3)),
                  const SizedBox(height: 4),
                  Text(data['desc'] as String, style: sans(size: 12, color: C.body, height: 16 / 12)),
                  const SizedBox(height: 14),
                  Row(
                    crossAxisAlignment: CrossAxisAlignment.baseline,
                    textBaseline: TextBaseline.alphabetic,
                    children: <Widget>[
                      Text(data['price'] as String, style: sans(size: 14, weight: FontWeight.w600, color: ac)),
                      Text('/wk', style: sans(size: 11, color: C.muted)),
                    ],
                  ),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}

Plus bundled 2 binary assets (fonts/images). The CLI and MCP install those for you automatically.

Two faster ways to add it

Copy-paste works, but you can skip it entirely.

1. FlutterKit CLI

One command drops this screen — and its fonts — straight into your project.

$ flutterkit add coconeer-home

2. AI agent (MCP)

Connect FlutterKit's MCP server in Claude or Cursor and just ask your agent to install coconeer-home — it fetches and writes the files for you.

FAQ

Is this coconut delivery home screen free to use?

Yes. The full Dart on this page is free to copy into your own apps, personal or commercial. Paste it directly, install it with the FlutterKit CLI (flutterkit add coconeer-home), or have an AI agent add it for you via MCP.

Does it need any external packages?

No — it's pure Flutter built entirely on the material library, with no image assets (the leaves and mesh gradient are drawn with CustomPaint and gradients). It does use two bundled fonts, Inter and JetBrains Mono, which you register in pubspec.yaml as shown in step 2; the CLI and MCP install those font files for you. The screen also imports a shared coco_common.dart helper file with the color palette, text styles, and bottom nav — install it alongside the screen.

Which Flutter version does it target?

It uses modern APIs like Color.withValues() and super parameters, so it targets Flutter 3.22+ (Dart 3). On an older SDK, swap each withValues(alpha: 0.82) call for withOpacity(0.82) and it will compile.

Related screens