Implementing a Custom SliverContainer Widget in Flutter: A Journey of Solving Rendering Issues

Hello! Today, I want to share an interesting rendering issue I encountered while implementing a custom SliverContainer widget in Flutter and the process of solving it. I hope this article can serve as a small guide for those who are facing difficulties while implementing custom Slivers in Flutter.

🎯 The Problem

Why was SliverContainer needed?

When developing a Flutter app, you often need to wrap multiple SliverList widgets inside a card-style container within a CustomScrollView. For example:

CustomScrollView(
  slivers: [
    SliverAppBar(title: Text('My App')),
    // I want to wrap each section in a card like this!
    SliverCard(
      child: SliverList(...),
    ),
    SliverCard(
      child: SliverList(...),
    ),
  ],
)

However, it was difficult to meet this requirement with the default Flutter widgets:

  • Container and Card are not Slivers, so they cannot be used directly in a CustomScrollView.
  • Wrapping them with SliverToBoxAdapter means losing the performance benefits of Slivers.
  • Decorations (background, border, shadow) needed to be applied while maintaining scroll performance.

Setting the Goals

The SliverContainer we wanted had to have the following features:

  1. Maintain Sliver Performance: Preserve the lazy loading and viewport optimization of existing Slivers.
  2. Support Decorations: Allow for various styling options like background color, borders, and shadows.
  3. Support Padding/Margin: Enable setting internal and external spacing.
  4. Support Clipping: Provide a content clipping feature when needed.

πŸ› οΈ Implementation Approach

Basic Structure Design

To create a custom Sliver, I implemented it by inheriting from SingleChildRenderObjectWidget:

class SliverContainer extends SingleChildRenderObjectWidget {
  const SliverContainer({
    super.key,
    required super.child,
    this.decoration,
    this.padding,
    this.margin,
    this.clipBehavior = Clip.none,
  });

  final Decoration? decoration;
  final EdgeInsetsGeometry? padding;
  final EdgeInsetsGeometry? margin;
  final Clip clipBehavior;

  @override
  RenderSliverContainer createRenderObject(BuildContext context) {
    return RenderSliverContainer(
      decoration: decoration,
      padding: padding,
      margin: margin,
      clipBehavior: clipBehavior,
    );
  }
}

Understanding RenderSliver Basics

Before implementing a custom Sliver, let's understand what Flutter's RenderSliver is. It's an advanced concept, but I'll explain it step by step.

What is a RenderSliver?

RenderSliver is a special render object in Flutter's rendering system that efficiently displays content within a scrollable area.

// Regular widgets
Container()  // Based on RenderBox
Text()       // Based on RenderBox
Column()     // Based on RenderBox

// `Sliver` widgets
SliverList()     // Based on RenderSliver
SliverGrid()     // Based on RenderSliver
SliverAppBar()   // Based on RenderSliver

RenderSliver vs RenderBox: What's the difference?

Feature RenderBox RenderSliver
Usage Area General layouts Scrollable areas
Sizing Fixed width, height Dynamic extent
Performance Renders all children at once Lazily renders only necessary parts
Coordinate System Absolute coordinates (x, y) Relative scroll coordinates
// RenderBox example - all children are always in memory
Column(
  children: List.generate(1000, (i) => Text('Item $i')), // 😰 All 1000 are created!
)

// RenderSliver example - only visible parts are created
SliverList(
  delegate: SliverChildBuilderDelegate(
    (context, index) => Text('Item $index'),
    childCount: 1000, // 😊 Only the necessary ones are created!
  ),
)

Core Concepts

1. SliverGeometry - The "Spec" of a Sliver

SliverGeometry(
  scrollExtent: 1000.0,    // The total scrollable length
  paintExtent: 300.0,      // The length currently drawn on the screen
  paintOrigin: -50.0,      // The starting point for painting (negative means the top is clipped)
  layoutExtent: 250.0,     // The length that affects the layout
  visible: true,           // Whether it is currently visible
)

2. Understanding the Coordinate System

Viewport
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” ← 0.0 (Start of the viewport)
β”‚                 β”‚
β”‚   Visible Area  β”‚ ← paintExtent: length visible on screen
β”‚                 β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚                 β”‚
β”‚  Scrolled Area  β”‚ ← scrollExtent: total scroll length
β”‚                 β”‚

3. The Meaning of paintOrigin

// paintOrigin = 0: Content starts at the top of the viewport
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” ← Viewport start
β”‚ Content Start ←───── ← paintOrigin = 0
β”‚                 β”‚

// paintOrigin = -100: Top 100px of content is scrolled out of view
     ↑ Hidden part (100px)
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” ← Viewport start
β”‚ Content Middle ←──── ← paintOrigin = -100
β”‚                 β”‚

Why is a custom Sliver needed?

You can't implement these advanced scrolling behaviors with regular widgets:

// ❌ This causes performance issues
CustomScrollView(
  slivers: [
    SliverToBoxAdapter(  // Loses the performance benefits of `Sliver`
      child: Container(  // Always exists in memory
        decoration: BoxDecoration(...),
        child: Column(children: heavyWidgets), // 😰 All heavy widgets are loaded
      ),
    ),
  ],
)

// βœ… Solved with a custom `Sliver`
CustomScrollView(
  slivers: [
    SliverContainer(    // Maintains `Sliver` performance
      decoration: BoxDecoration(...),
      child: SliverList(...), // 😊 Only necessary parts are lazily loaded
    ),
  ],
)

Now, let's look at the actual implementation based on these fundamental concepts!

Understanding SingleChildRenderObjectWidget and Custom Render Objects

To create a custom Sliver, you need to use SingleChildRenderObjectWidget. Let's explore what it is and why it's necessary.

What is SingleChildRenderObjectWidget?

SingleChildRenderObjectWidget is a special widget that acts as a bridge connecting Flutter's widget layer and render layer. As its name suggests, it's for widgets that have exactly one child. Generally, widgets that take a child are of this type, while widgets that take children (like Column or Row) inherit from MultiChildRenderObjectWidget.

// Flutter's basic structure
Widget (Declarative UI) β†’ Element (Manages the widget tree) β†’ RenderObject (Actual rendering)

// Role of SingleChildRenderObjectWidget
class MyCustomWidget extends SingleChildRenderObjectWidget {
  // Widget Layer: The API developers see

  @override
  RenderObject createRenderObject(BuildContext context) {
    // Render Layer: The actual drawing logic
    return MyCustomRenderObject();
  }
}

Flutter's 3-Layer Architecture

Flutter is composed of three layers:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   Widget Layer  β”‚ ← Declarative UI written by developers
β”‚  (Immutable objects) β”‚   Container(), Text(), Column(), etc.
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  Element Layer  β”‚ ← Manages the widget tree and lifecycle
β”‚  (State management) β”‚   BuildContext is actually an Element
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚   Render Layer  β”‚ ← Actual layout, painting, hit testing
β”‚ (Performance optimized) β”‚   RenderBox, RenderSliver, etc.
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

A Widget only holds simple configuration information, making it immutable. During an update, it's simply replaced in the widget tree. This is not an expensive operation! However, the rendering logic is expensive, so it's reused whenever possible. That's why the Element is not replaced but updated by internal logic, allowing the RenderObject to be reused.

In most cases, existing widgets are sufficient, but when advanced rendering logic is needed, you have to create a RenderObject yourself.

When should you use SingleChildRenderObjectWidget?

A custom render object is needed in the following cases:

// ❌ Cases not possible with existing widgets

// 1. Complex layout logic
class CustomLayoutWidget extends SingleChildRenderObjectWidget {
  // A complex layout where the parent dynamically changes based on the child's size
}

// 2. Special painting logic
class CustomPaintWidget extends SingleChildRenderObjectWidget {
  // Drawing complex graphics over a child or special clipping
}

// 3. High-performance scrolling behavior
class SliverContainer extends SingleChildRenderObjectWidget {
  // Applying decorations while maintaining `Sliver` performance
}

Why was this approach chosen for SliverContainer?

I tried other approaches, but they all had limitations:

❌ Approach 1: SliverToBoxAdapter + Container

SliverToBoxAdapter(
  child: Container(
    decoration: BoxDecoration(...),
    child: Column(
      children: [/* Many items */], // 😰 All items are created at once
    ),
  ),
)

Problem: Completely loses the lazy-loading benefit of Sliver.

❌ Approach 2: Decorating individual items

SliverList(
  delegate: SliverChildBuilderDelegate(
    (context, index) => Container(
      decoration: BoxDecoration(...), // 😰 Separate decoration for each item
      child: ListTile(...),
    ),
  ),
)

Problem: Impossible to achieve a unified card style that wraps the whole list.

βœ… Approach 3: SingleChildRenderObjectWidget

SliverContainer(
  decoration: BoxDecoration(...), // 😊 A single decoration that wraps everything
  child: SliverList(...),         // 😊 Maintains `Sliver` performance
)

Advantage: Sliver performance + unified decoration + flexible customization.

Code Structure Pattern

Here's the typical pattern when using SingleChildRenderObjectWidget:

class SliverContainer extends SingleChildRenderObjectWidget {
  // 1. Define widget properties
  const SliverContainer({
    super.key,
    required super.child,
    this.decoration,
    this.padding,
    // ... other properties
  });

  final Decoration? decoration;
  final EdgeInsetsGeometry? padding;

  // 2. Create Render Object - called when the widget is first created
  @override
  RenderSliverContainer createRenderObject(BuildContext context) {
    return RenderSliverContainer(
      decoration: decoration,
      padding: padding,
    );
  }

  // 3. Update Render Object - called when the widget is rebuilt
  @override
  void updateRenderObject(BuildContext context, RenderSliverContainer renderObject) {
    renderObject
      ..decoration = decoration
      ..padding = padding;
  }
}

The key to this pattern is:

  • createRenderObject: For creating a new render object (expensive operation).
  • updateRenderObject: For updating only the properties of an existing render object (efficient).

Now let's see how the actual render object works!

Render Object Implementation

The core is the RenderSliverContainer class, which inherits from RenderSliver:

class RenderSliverContainer extends RenderSliver
    with RenderObjectWithChildMixin<RenderSliver> {
  
  @override
  void performLayout() {
    // 1. Calculate the layout of the child `Sliver`
    // 2. Calculate geometry considering padding/margin
    // 3. Handle paintOrigin
  }

  @override
  void paint(PaintingContext context, Offset offset) {
    // 1. Paint the decoration
    // 2. Paint the child `Sliver` (with clipping)
  }
}

πŸ› Challenges Encountered

Discovering Rendering Issues

After completing the implementation and testing, a serious problem emerged:

  • Child content disappears on scroll: When the content was partially scrolled out of the viewport, it would vanish completely.
  • Clipping errors: The child content was being clipped at the wrong position.
  • Decoration position errors: The background decoration did not align with the content.

Initial Analysis

At first, I thought it was a problem with handling paintOrigin. In Flutter's Sliver system:

  • paintOrigin = 0: The content starts at the viewport boundary.
  • paintOrigin < 0: The content is partially scrolled up and clipped at the top.
  • paintOrigin > 0: The content starts below the viewport (a rare case).

The problematic code:

// The problematic paint method
@override
void paint(PaintingContext context, Offset offset) {
  // Clamping paintOrigin to 0 - this was the problem!
  final decorationTop = offset.dy + _resolvedMargin!.top + math.max(0.0, paintOrigin);
  final childOffset = offset.translate(
    _resolvedMargin!.left + _resolvedPadding!.left,
    _resolvedMargin!.top + _resolvedPadding!.top + math.max(0.0, paintOrigin),
  );
  
  // Apply clipping
  context.pushClipRect(
    needsCompositing,
    offset,  // This was the real problem!
    clipRect,
    (context, offset) => context.paintChild(child!, childOffset),
    clipBehavior: clipBehavior,
  );
}

πŸ” Solution Process

The Debugging Breakthrough

The key to solving the problem came from a simple test:

// Debugging test: set paintExtent to 0
final paintExtent = 0; // geometry!.paintExtent;

Surprisingly, doing this resulted in:

  • βœ… The white rounded-rectangle decoration completely disappeared.
  • βœ… The child content appeared in the correct position.

This was a crucial clue:

  1. The child content position calculation was actually correct.
  2. The decoration itself was not the problem.
  3. There was an issue with the clipping logic.

Discovering the Real Cause

The problem was a coordinate system mismatch in context.pushClipRect:

// Problem analysis
final clipRect = Rect.fromLTWH(
  decorationRect.left + _resolvedPadding!.left,   // Absolute coordinate, already includes offset
  decorationRect.top + _resolvedPadding!.top,     // Absolute coordinate, already includes offset
  contentWidth,
  contentHeight,
);

context.pushClipRect(
  needsCompositing,
  offset,     // ❌ Applying offset again causes a double transformation!
  clipRect,   // This is already an absolute coordinate...
  (context, offset) => context.paintChild(child!, childOffset),
  clipBehavior: clipBehavior,
);

The Solution: A Single Line of Magic

The solution was surprisingly simple:

context.pushClipRect(
  needsCompositing,
  Offset.zero,  // βœ… Prevents double offset application!
  clipRect,
  (context, offset) => context.paintChild(child!, childOffset),
  clipBehavior: clipBehavior,
);

Why does this solution work?

  1. Coordinate System Consistency: clipRect is already calculated in absolute coordinates based on decorationRect.
  2. Prevents Double Transformation: Using Offset.zero prevents an additional coordinate transformation.
  3. Correct Clipping: The child is painted at childOffset and clipped by the correctly positioned clipRect.

πŸ’‘ Usage Example

The completed SliverContainer can be used like this:

// Example implementation of SliverCard
class SliverCard extends StatelessWidget {
  const SliverCard({
    super.key,
    required this.sliver,
    this.backgroundColor,
    this.borderRadius,
    this.elevation = 2.0,
    this.padding,
    this.margin,
  });

  final Widget sliver;
  final Color? backgroundColor;
  final BorderRadius? borderRadius;
  final double elevation;
  final EdgeInsetsGeometry? padding;
  final EdgeInsetsGeometry? margin;

  @override
  Widget build(BuildContext context) {
    return SliverContainer(
      decoration: BoxDecoration(
        color: backgroundColor ?? Colors.white,
        borderRadius: borderRadius ?? BorderRadius.circular(12),
        boxShadow: [
          BoxShadow(
            color: Colors.black.withOpacity(0.1),
            blurRadius: elevation * 2,
            offset: Offset(0, elevation),
          ),
        ],
      ),
      padding: padding ?? const EdgeInsets.all(16),
      margin: margin ?? const EdgeInsets.all(8),
      clipBehavior: Clip.antiAlias,
      child: sliver,
    );
  }
}

// Actual usage
CustomScrollView(
  slivers: [
    SliverAppBar(title: Text('My App')),
    SliverCard(
      child: SliverList(
        delegate: SliverChildBuilderDelegate(
          (context, index) => ListTile(title: Text('Item $index')),
          childCount: 20,
        ),
      ),
    ),
  ],
)

πŸŽ“ Lessons Learned

1. The Importance of Coordinate Systems

I realized how crucial it is to understand coordinate transformations in Flutter's rendering system.

2. Debugging Techniques

Testing with extreme values (e.g., paintExtent = 0) can reveal the true nature of a problem.

3. The Minimalist Solution

Sometimes, a seemingly complex problem can be solved with a single-line change.

4. Understanding the Rendering Pipeline

I came to understand the difference between clipping and position calculation and how they interact.

Actual Implementation

🎯 Implementation Summary

To summarize the problems we've solved:

  • βœ… Maintained Sliver performance: Preserved lazy loading and viewport optimization of existing Slivers.
  • βœ… Supported decorations: Enabled various styling options like background color, borders, and shadows.
  • βœ… Supported padding/margin: Enabled setting internal and external spacing.
  • βœ… Solved clipping issue: Resolved the coordinate system mismatch using Offset.zero.
  • βœ… Flexible customization: Compatible with various Sliver child widgets.

πŸ’» Full SliverContainer Code

Below is the complete SliverContainer implementation with all features included:

import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';

class SliverContainer extends SingleChildRenderObjectWidget {
  const SliverContainer({
    super.key,
    required super.child,
    this.decoration,
    this.padding,
    this.margin,
    this.clipBehavior = Clip.none,
  });

  final Decoration? decoration;
  final EdgeInsetsGeometry? padding;
  final EdgeInsetsGeometry? margin;
  final Clip clipBehavior;

  @override
  RenderSliverContainer createRenderObject(BuildContext context) {
    return RenderSliverContainer(
      decoration: decoration,
      padding: padding,
      margin: margin,
      clipBehavior: clipBehavior,
    );
  }

  @override
  void updateRenderObject(
    BuildContext context,
    RenderSliverContainer renderObject,
  ) {
    renderObject
      ..decoration = decoration
      ..padding = padding
      ..margin = margin
      ..clipBehavior = clipBehavior;
  }
}

class RenderSliverContainer extends RenderSliver
    with RenderObjectWithChildMixin<RenderSliver> {
  RenderSliverContainer({
    Decoration? decoration,
    EdgeInsetsGeometry? padding,
    EdgeInsetsGeometry? margin,
    Clip clipBehavior = Clip.none,
  })  : _decoration = decoration,
        _padding = padding,
        _margin = margin,
        _clipBehavior = clipBehavior;

  Decoration? _decoration;
  Decoration? get decoration => _decoration;
  set decoration(Decoration? value) {
    if (_decoration == value) return;
    _decoration = value;
    markNeedsPaint();
  }

  EdgeInsetsGeometry? _padding;
  EdgeInsetsGeometry? get padding => _padding;
  set padding(EdgeInsetsGeometry? value) {
    if (_padding == value) return;
    _padding = value;
    markNeedsLayout();
  }

  EdgeInsetsGeometry? _margin;
  EdgeInsetsGeometry? get margin => _margin;
  set margin(EdgeInsetsGeometry? value) {
    if (_margin == value) return;
    _margin = value;
    markNeedsLayout();
  }

  Clip _clipBehavior;
  Clip get clipBehavior => _clipBehavior;
  set clipBehavior(Clip value) {
    if (_clipBehavior == value) return;
    _clipBehavior = value;
    markNeedsPaint();
  }

  EdgeInsets? _resolvedPadding;
  EdgeInsets? _resolvedMargin;
  BoxPainter? _painter;

  void _updatePainter() {
    _painter?.dispose();
    _painter = _decoration?.createBoxPainter(markNeedsPaint);
  }

  @override
  void attach(PipelineOwner owner) {
    super.attach(owner);
    _updatePainter();
  }

  @override
  void detach() {
    _painter?.dispose();
    _painter = null;
    super.detach();
  }

  @override
  void performLayout() {
    if (child == null) {
      geometry = SliverGeometry.zero;
      return;
    }

    _resolvedPadding = padding?.resolve(TextDirection.ltr) ?? EdgeInsets.zero;
    _resolvedMargin = margin?.resolve(TextDirection.ltr) ?? EdgeInsets.zero;

    final adjustedConstraints = constraints.copyWith(
      crossAxisExtent: math.max(
        0.0,
        constraints.crossAxisExtent -
            _resolvedPadding!.horizontal -
            _resolvedMargin!.horizontal,
      ),
    );

    child!.layout(adjustedConstraints, parentUsesSize: true);

    final childGeometry = child!.geometry!;
    final totalVerticalPadding =
        _resolvedPadding!.vertical + _resolvedMargin!.vertical;

    final maxAllowedPaintExtent = constraints.remainingPaintExtent;
    final requestedPaintExtent =
        childGeometry.paintExtent + totalVerticalPadding;
    final actualPaintExtent = math.min(
      requestedPaintExtent,
      maxAllowedPaintExtent,
    );

    final childPaintOrigin = childGeometry.paintOrigin;
    final marginAdjustment = _resolvedMargin!.top;

    final actualPaintOrigin = childPaintOrigin - marginAdjustment;

    final adjustedPaintExtent = math.min(
      actualPaintExtent,
      maxAllowedPaintExtent + actualPaintOrigin,
    );

    final finalPaintExtent = math.max(0.0, adjustedPaintExtent);

    final requestedLayoutExtent =
        childGeometry.layoutExtent + totalVerticalPadding;
    final finalLayoutExtent = math.min(
      requestedLayoutExtent,
      finalPaintExtent,
    );

    geometry = SliverGeometry(
      scrollExtent: childGeometry.scrollExtent + totalVerticalPadding,
      paintExtent: finalPaintExtent,
      paintOrigin: actualPaintOrigin,
      layoutExtent: finalLayoutExtent,
      maxPaintExtent: childGeometry.maxPaintExtent + totalVerticalPadding,
      maxScrollObstructionExtent: childGeometry.maxScrollObstructionExtent,
      hitTestExtent: finalPaintExtent,
      visible: childGeometry.visible && finalPaintExtent > 0,
      hasVisualOverflow:
          childGeometry.hasVisualOverflow ||
          requestedPaintExtent > actualPaintExtent,
      scrollOffsetCorrection: childGeometry.scrollOffsetCorrection,
      cacheExtent: math.min(
        childGeometry.cacheExtent + totalVerticalPadding,
        constraints.remainingCacheExtent,
      ),
    );
  }

  @override
  void paint(PaintingContext context, Offset offset) {
    if (child == null || !geometry!.visible) return;

    _updatePainter();

    _resolvedPadding ??= padding?.resolve(TextDirection.ltr) ?? EdgeInsets.zero;
    _resolvedMargin ??= margin?.resolve(TextDirection.ltr) ?? EdgeInsets.zero;

    final paintExtent = geometry!.paintExtent;
    final paintOrigin = geometry!.paintOrigin;

    final decorationTop =
        offset.dy + _resolvedMargin!.top + math.max(0.0, paintOrigin);
    final decorationHeight = math.max(
      0.0,
      paintExtent - _resolvedMargin!.vertical,
    );

    final decorationRect = Rect.fromLTWH(
      offset.dx + _resolvedMargin!.left,
      decorationTop,
      constraints.crossAxisExtent - _resolvedMargin!.horizontal,
      decorationHeight,
    );

    if (_painter != null) {
      _painter!.paint(
        context.canvas,
        decorationRect.topLeft,
        ImageConfiguration(
          size: decorationRect.size,
          textDirection: TextDirection.ltr,
        ),
      );
    }

    final childOffset = offset.translate(
      _resolvedMargin!.left + _resolvedPadding!.left,
      _resolvedMargin!.top + _resolvedPadding!.top + math.max(0.0, paintOrigin),
    );

    if (clipBehavior != Clip.none &&
        _decoration != null &&
        decorationHeight > 0) {
      final contentLeft = decorationRect.left + _resolvedPadding!.left;
      final contentTop = decorationRect.top + _resolvedPadding!.top;
      final contentWidth = math.max(
        0.0,
        decorationRect.width - _resolvedPadding!.horizontal,
      );
      final contentHeight = math.max(
        0.0,
        decorationRect.height - _resolvedPadding!.vertical,
      );

      final clipRect = Rect.fromLTWH(
        contentLeft,
        contentTop,
        contentWidth,
        contentHeight,
      );

      context.pushClipRect(
        needsCompositing,
        Offset.zero,
        clipRect,
        (context, offset) => context.paintChild(child!, childOffset),
        clipBehavior: clipBehavior,
      );
    } else {
      context.paintChild(child!, childOffset);
    }
  }

  @override
  bool hitTestChildren(
    SliverHitTestResult result, {
    required double mainAxisPosition,
    required double crossAxisPosition,
  }) {
    if (child == null) return false;

    _resolvedPadding ??= padding?.resolve(TextDirection.ltr) ?? EdgeInsets.zero;
    _resolvedMargin ??= margin?.resolve(TextDirection.ltr) ?? EdgeInsets.zero;

    final adjustedMainAxisPosition =
        mainAxisPosition - _resolvedMargin!.top - _resolvedPadding!.top;
    final adjustedCrossAxisPosition =
        crossAxisPosition - _resolvedMargin!.left - _resolvedPadding!.left;

    return child!.hitTest(
      result,
      mainAxisPosition: adjustedMainAxisPosition,
      crossAxisPosition: adjustedCrossAxisPosition,
    );
  }

  @override
  double childMainAxisPosition(RenderSliver child) {
    return (_resolvedMargin?.top ?? 0.0) + (_resolvedPadding?.top ?? 0.0);
  }

  @override
  double childCrossAxisPosition(RenderSliver child) {
    return (_resolvedMargin?.left ?? 0.0) + (_resolvedPadding?.left ?? 0.0);
  }
}

πŸš€ Usage Examples

Basic Usage

CustomScrollView(
  slivers: [
    SliverAppBar(
      title: Text('SliverContainer Example'),
      floating: true,
    ),

    // Basic card-style SliverContainer
    SliverContainer(
      decoration: BoxDecoration(
        color: Colors.white,
        borderRadius: BorderRadius.circular(12),
        boxShadow: [
          BoxShadow(
            color: Colors.black.withOpacity(0.1),
            blurRadius: 8,
            offset: Offset(0, 2),
          ),
        ],
      ),
      margin: EdgeInsets.all(16),
      padding: EdgeInsets.all(16),
      clipBehavior: Clip.antiAlias,
      child: SliverList(
        delegate: SliverChildBuilderDelegate(
          (context, index) => ListTile(
            leading: CircleAvatar(child: Text('${index + 1}')),
            title: Text('Item ${index + 1}'),
            subtitle: Text('List item inside SliverContainer'),
          ),
          childCount: 10,
        ),
      ),
    ),

    // SliverContainer with a different style
    SliverContainer(
      decoration: BoxDecoration(
        gradient: LinearGradient(
          colors: [Colors.blue.shade100, Colors.blue.shade50],
          begin: Alignment.topLeft,
          end: Alignment.bottomRight,
        ),
        borderRadius: BorderRadius.circular(16),
        border: Border.all(color: Colors.blue.shade200),
      ),
      margin: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
      padding: EdgeInsets.all(20),
      child: SliverGrid(
        gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
          crossAxisCount: 2,
          mainAxisSpacing: 8,
          crossAxisSpacing: 8,
        ),
        delegate: SliverChildBuilderDelegate(
          (context, index) => Container(
            decoration: BoxDecoration(
              color: Colors.white,
              borderRadius: BorderRadius.circular(8),
            ),
            child: Center(child: Text('Grid ${index + 1}')),
          ),
          childCount: 6,
        ),
      ),
    ),
  ],
)

SliverCard Helper Widget

You can also create a helper widget for more convenient use:

class SliverCard extends StatelessWidget {
  const SliverCard({
    super.key,
    required this.child,
    this.backgroundColor,
    this.borderRadius,
    this.elevation = 2.0,
    this.padding,
    this.margin,
  });

  final Widget child;
  final Color? backgroundColor;
  final BorderRadius? borderRadius;
  final double elevation;
  final EdgeInsetsGeometry? padding;
  final EdgeInsetsGeometry? margin;

  @override
  Widget build(BuildContext context) {
    return SliverContainer(
      decoration: BoxDecoration(
        color: backgroundColor ?? Theme.of(context).cardColor,
        borderRadius: borderRadius ?? BorderRadius.circular(12),
        boxShadow: [
          BoxShadow(
            color: Colors.black.withOpacity(0.1),
            blurRadius: elevation * 2,
            offset: Offset(0, elevation),
          ),
        ],
      ),
      padding: padding ?? const EdgeInsets.all(16),
      margin: margin ?? const EdgeInsets.all(8),
      clipBehavior: Clip.antiAlias,
      child: child,
    );
  }
}

// Usage Example
SliverCard(
  elevation: 4,
  child: SliverList(
    delegate: SliverChildListDelegate([
      ListTile(title: Text('Simple Usage')),
      ListTile(title: Text('Wrapped in a SliverCard')),
    ]),
  ),
)

πŸš€ Conclusion

Through this experience, I gained a deep understanding of Flutter's low-level rendering system. When implementing a custom Sliver:

  1. Always keep the coordinate system in mind.
  2. Be careful of double transformations.
  3. Isolate problems with simple tests.
  4. Understand Flutter's rendering pipeline.

πŸ’‘ Additional Usage Tips

  • Performance Optimization: SliverContainer maintains Sliver performance, so it can be used safely with large lists.
  • Theming: Use Theme.of(context) to maintain consistency with your app's overall theme.
  • Animation: Animation effects can be added in a way similar to AnimatedContainer.
  • Accessibility: Improve accessibility by using it with the Semantics widget.

If you are facing similar problems, I hope this article helps. Feel free to leave comments with any questions or feedback!

Happy coding! πŸŽ‰