You've been there: staring at a blank screen, needing to draw something custom. Maybe it's a real-time chart, a game sprite, or a progress ring that spins just so. The old habit is Custom View + Canvas.onDraw(). The new hotness is Compose Canvas with its declarative `drawBehind` and `drawWithContent`. Both can do the job. But choose wrong, and you'll regret it every time the frame drops below 60 fps.
I've been burned on both sides. Once I picked Compose Canvas for a financial chart that redrew 1000+ data points on every recomposition. The result? Jank city. Another time I stuck with a Custom View for a simple animated icon, only to fight with touch events and lifecycle leaks. This guide is the cheat sheet I wish I'd had — a decision framework based on rendering costs, API maturity, and your actual performance budget.
Who Needs This and What Goes Wrong Without It
The performance hit nobody talks about
You build a custom chart component. Data comes in, you override onDraw, you paint crisp axes. Two hundred data points render fine. Then production data hits — two thousand points, real-time updates, and suddenly the UI thread stutters like a dying fan. That's the moment most developers discover the gap between "it works" and "it works at 60 fps." The Canvas API in Jetpack Compose and the raw View.onDraw path both offer infinite flexibility, but neither warns you when your drawing logic crosses the threshold into jank. I have seen teams ship a polished Compose Canvas implementation only to watch battery drain spike 40% on low-end devices — because every recomposition re-ran a path calculation that should have been cached.
'Drawing is cheap until you force the GPU to redraw what never changed.'
— overheard at a Droidcon debugging session, after three hours of profiling
Real-world pain: chart jank, animation stutter
Consider a fitness app with a live heart-rate graph. The naive approach: redraw the entire bezier curve on every sensor tick. That sounds fine until you notice the animation stutter coincides with garbage collection pauses. The catch is that Canvas.drawPath in Compose doesn't inherently batch — each call is a fresh submission to the hardware renderer. On a Pixel 6 this is invisible. On a budget tablet with a shared GPU? The seam blows out. What usually breaks first is not the drawing itself but the measurement pass: a custom View that recalculates layout metrics every invalidate will trigger parent relayouts, cascading through the hierarchy. I fixed one case where removing a single requestLayout() call inside onDraw cut render time from 34ms to 8ms. That hurts.
The hidden cost nobody measures: state chasing. A Compose Canvas that reacts to three mutable state fields will recompose whenever any of them changes — even if the drawing logic only depends on one. You then pay for snapshot reads, slot table updates, and a full draw pass. Wrong order. Most teams skip this: they jump straight to Canvas API choices without auditing what triggers redraws. The result? A "performant" decision tree that leads to the worst possible hybrid — a custom View that calls invalidate on every frame and a Compose Canvas that recomposes on irrelevant state. Not yet. That's the double-tax nobody budgets for.
Your actual audience: not just 'Android devs'
This is for the senior engineer who has been burned by a custom SurfaceView game loop, the team lead reviewing a PR where someone used Canvas.clipPath inside a LazyColumn item, the consultant who inherits a codebase with three competing drawing paradigms. The stakes are concrete: a financial dashboard that drops frames during market volatility spikes, an onboarding animation that thermal-throttles the device after thirty seconds. A rhetorical question — how many lines of drawing code have you shipped that you never profiled with GPU overdraw or Systrace enabled? The decision between Custom View and Compose Canvas is not aesthetic. It's a binding contract with the rendering pipeline, and breaking that contract costs you user retention. What goes wrong without this choice? You ship a feature that works on your test device, then watch Play Console crash reports spike with Choreographer.doFrame warnings — and you can't explain why. That's the performance hit nobody talks about.
Prerequisites: What You Should Settle First
Know your rendering path: CPU vs GPU
Before you write a single line of drawing code, ask yourself one question: who owns the pixels you’re about to touch? That sounds academic until you watch a custom View’s onDraw method chew through a 60 fps budget because every frame forces a full software rasterization pass. The Android rendering pipeline has two lanes—software (CPU) and hardware-accelerated (GPU)—and they're not interchangeable. Custom Views drawn via Canvas inside a View.onDraw block run on the GPU *only* if your drawing operations map to the hardware-accelerated subset (drawBitmap, drawRect, and friends work; clipPath with a non-rectangular region will silently fall back to CPU.
Compose Canvas, by contrast, always sits atop the RenderNode layer—every Canvas call in Compose gets batched into a display list and shipped to the GPU. The catch: Compose’s recomposition overhead can bury you if your draw lambda captures mutable state that changes every frame. I have seen a perfectly elegant Compose Canvas spiral to 20 fps because a derived Float recomposed 120 times per second. You need to know which lane you’re in before you pick a tool—not after.
Odd bit about development: the dull step fails first.
Measure before you choose: profiling basics
Most teams skip this. They pick a path based on preference or a “feels faster” hunch, then ship. Three weeks later a QA ticket arrives: jank on a Pixel 6 during list scrolling. The fix? Rewrite the drawing approach. Avoid that pain by running two cheap measurements first. Open Profile GPU Rendering on a mid-range device (think Galaxy A series, not your Tensor-bookmark-test device). Draw the same shape—say, a 50‑vertex bezier curve—using both a custom View and a Compose Canvas. Watch the frame time bars.
Custom View might show a consistent 4‑ms draw time per frame; Compose Canvas could spike to 12 ms if that same shape triggers state reads. But flip the test: now animate the shape’s color via a property animation. Custom View redraws the entire Canvas each tick; Compose skips the draw lambda if the color change doesn’t invalidate the layout. Surprised?
‘I spent a week optimizing a custom View’s onDraw loop, only to find Compose would have handled the animation for free.’
— Senior Android engineer, 2024 migration post-mortem
Profilers don’t lie—your gut does. Run the test. The 15 minutes you spend now beats the two-day rewrite later.
API level and Compose version realities
Here’s the part nobody advertises: Canvas in Compose is not the same class as Canvas in View. They share an API surface, but Compose’s DrawScope wraps a Canvas backed by Skia directly—no hardware acceleration negotiation. That sounds great until you need drawTextRun with a custom shader on a device running API 28. The shader path compiles for the GPU fine on API 31; on API 28 it falls back to a software bitmap, and your frame time triples. We fixed this by adding a runtime API check and swapping to a simpler fill for legacy devices. That nuance exists in custom Views too, but the fallback behavior differs: View’s Canvas degrades more gracefully, often dropping from GPU to CPU without a visible hitch.
Another gotcha—Compose BOM version. If you’re on Compose 1.3 or lower, certain Canvas operations (like drawRoundRect with a non-uniform corner radius) produce incorrect clipping on some OEM skins. That bug was squashed in 1.4, but you wouldn’t know unless you read the changelog. I’ve walked into teams blaming “Compose performance” when the real culprit was a version mismatch between their Compose compiler and Kotlin plugin. Version parity is not optional—it’s a prerequisite. Set your BOM to a known-good pair before you benchmark anything.
Core Workflow: The Decision Tree for Custom Drawing
Step 1: Define your draw budget
Before you touch a single pixel, ask how many times your drawing will actually change per second. A weather widget that updates every fifteen minutes? That's a completely different animal from a real-time spectrogram that repaints sixty times a second. The catch here is visceral: if you pick Custom View for a high-frequency nightmare, you're signing up for `onDraw()` calls that the framework can't easily skip, and every bitmap operation cuts into your 16ms frame budget. I have watched teams burn two weeks building a gorgeous animated radar in a standard View, only to discover that the CPU-side canvas allocation eats 8ms every frame—leaving almost nothing for layout or touch handling. Compose Canvas, meanwhile, defers a lot of its rasterization into the slot table, meaning recomposition overhead is not automatically drawing overhead. That said, the Compose Canvas also carries its own tax: every state change triggers a new composition pass, and if your draw logic is not memoized, you pay for lambda creation on top. So start with a hard number: your target frame budget, and the actual redraw rate.
Step 2: Choose based on redraw frequency
Low-frequency drawing—think once per second or slower—rarely punishes either approach. You can prototype in whichever toolchain your team knows best and still ship cleanly. The trap appears around 12–20 redraws per second. Here, the Custom View path starts sweating because Android's invalidate mechanism can coalesce, but once `onDraw` fires, you're re-executing all your commands, even the static background. Compose Canvas handles this differently: `Canvas` in Compose is a single composable that reruns only when its inputs change, and the graphics layer snapshot can skip re-rendering the uncharted parts. However—and this is the trade-off—Compose's recomposition must traverse the entire subtree unless you carve it into fine-grained `remember` blocks. A single large Canvas composable that depends on a mutable float will redraw everything on every recomposition. So the real criterion is not just redraw frequency but which parts stay still. If 80% of your drawing is static decoration and only 20% animates, a layered Custom View with a dirty-region strategy can outperform a monolithic Compose Canvas that keeps recomposing the decorative layer. Most teams skip this analysis: they assume new means faster. Not always.
Step 3: Prototype both and profile
Stop guessing—write a minimal prototype on both paths. The exercise is quick: a hundred lines of imperative `Canvas` drawing versus a hundred lines of Compose `drawBehind` or `Canvas(modifier)`. Run both on a mid-range device, not your Pixel 8 Pro. Hook up `Profile GPU Rendering` or the frame-timeline tool in Android Studio. Watch for dropped frames at your target draw rate. What usually breaks first is memory churn: Custom Views allocate `Paint` and `Path` objects inside `onDraw` if you're not careful, while Compose Canvas can silently allocate lambdas if you inline `drawScope` calls without pulling them into a `remember`. The decisive signal? Launch the prototype, rotate the screen, navigate away and back. If one path consistently spikes a jank, you have your answer. I have done this twice this year—once for a charting component, once for a custom text layout—and both times the Compose Canvas won on redraw rate but lost on composition overhead during configuration changes. We fixed it by hoisting state out of the Canvas call and into a ViewModel, which is a hack, but it worked. So: prototype, profile, then commit. That's the only decision tree that matters.
Tools, Setup, and Environment Realities
Systrace and the GPU Rendering Profiler — Your New Best Friends
You have built a custom view that draws a dozen shimmering particles. Looks great. Then you run it on a Moto G Power from 2021 and the frame rate tanks to 18 fps. What do you reach for first? Systrace (now part of Perfetto) gives you a per-frame breakdown of where the main thread stalled: was it measure/layout, draw, or—more likely with custom drawing—the GPU waiting on buffer swaps? Fire it up with python systrace.py -t 5 -o trace.html from the Android SDK tools. That single file will show you if your onDraw() is chewing up 16 ms because you call Path.op() every frame instead of caching the result. The GPU rendering profiler in Developer Options gives you a simpler bar chart—green means happy, red means you're re-creating a Paint object per frame. I have seen teams over-optimize Compose canvases while ignoring a 20 ms bitmap decode on every recomposition. Wrong order. Profile first, then panic.
Field note: android plans crack at handoff.
The catch: Systrace outputs are verbose. Don't skim them. Look for the “performTraversals” chunk; inside it, the “draw” phase will show you exact method calls. If you see View.draw(Canvas) taking 30 ms, drill into the child stack. A typical pitfall? Hardware layers. Calling setLayerType(View.LAYER_TYPE_HARDWARE, null) on a custom view that redraws often forces a GPU texture re-upload every frame. That hurts. Stick with software rendering for debugging, then flip hardware on only when you confirm the draw commands are batch-friendly.
“I once spent three days optimizing a Compose Canvas ring chart, only to realize the GPU was choking on a 4K bitmap I loaded without downscaling.”
— Hunch from an unreleased PR postmortem, 2024
Compose Layout Inspector for Recomposition — Where Compose Hides Its Cost
Compose is not a silver bullet for custom drawing. The Compose Layout Inspector (Canary, AS Hedgehog+) shows you recomposition counts per composable in real time. Open it, toggle “Show recomposition counts,” and draw a rectangle on your canvas. Watch the numbers. If your Canvas block is inside a Box that recomposes on every ScrollState change, your custom drawing redraws even when no visual data shifted. You fix this by hoisting the draw logic into a remember block or a Modifier.drawBehind { } that doesn't depend on scrolling offset unless you explicitly pass it. Quick reality check—are you using mutableStateOf inside a DrawScope? That triggers recomposition on every state write, not just on the canvas update. Use derivedStateOf or a simple Float parameter instead.
That said, the Layout Inspector also measures “measured” vs “placed” time. A common deception: your custom drawing takes 2 ms, but the parent column recomposes 200 modifier nodes, eating 14 ms. The inspector shows that as “recomposition time” in the parent. You won't see it in Systrace. Use both tools in tandem—Systrace for the GPU/CPU boundary, Layout Inspector for the Compose tree overhead. Most teams skip this: they profile the draw call but never the composition side. That's how you ship a “custom Compose canvas” that stutters on a Pixel 6 because animateFloatAsState fires every 16 ms and your remember-less path builder runs each time. Fix: wrap the path in remember(pathDefinition) { buildPath() }.
Hardware Acceleration Quirks per Device — The Fragment That Breaks Your Build
Hardware acceleration is not uniform. A custom view that uses Canvas.clipPath() with a Region.Op.DIFFERENCE works perfectly on a Samsung Galaxy S23 (Mali GPU) but renders a black rectangle on an older Huawei Kirin chipset. Why? Because some GPU drivers don't implement complex clip regions efficiently—they fall back to software rendering, which can run out of memory for large paths. The fix is ugly but necessary: detect the device model at runtime and fall back to Canvas.clipRect() on known-bad chipsets, or pre-clip your bitmaps before drawing. Another quirk—scaled canvases. On a device with 1.5x display density, your drawText() coordinates might snap to pixel boundaries for HW-accelerated renderers, causing text jitter. Hardware layers again: set layerType = LAYER_TYPE_SOFTWARE only for the text draw call, not the entire canvas. Anecdote: we shipped a watch face custom view that drew second ticks—on a Fossil Gen 6 every third tick disappeared because the GPU driver clipped the path at 44° rotation. We never reproduced it on emulators. That's the reality of hardware acceleration: it's a contract that every chip vendor interprets slightly differently. Always test on three real devices from different OEMs before merging.
Variations for Different Constraints
Legacy codebase: mixing Custom View inside Compose
You inherit a project with 40,000 lines of Custom View drawing—bezier curves, touch slop math, a homegrown chart that nobody fully understands. Ditching it all for a Compose Canvas rewrite would take three sprints and probably break something. The pragmatic move? Wrap the existing Custom View inside a AndroidView composable. That buys you coexistence without a performance tax—the old drawing loop runs on its own Canvas, isolated from Compose’s recomposition scope. I have seen teams waste a week trying to port a particle system pixel-by-pixel. Don’t. The compromise is lifecycle hygiene: you still need to hoist touch events and invalidation calls manually. One invalidate() firing inside a LaunchedEffect and the seam blows out. Quick reality check—if that legacy view redraws every 16ms, you're paying the GPU cost twice: once for the native canvas, once for Compose’s layer compositing. Profile before you merge.
High-frequency updates: when Canvas is the only answer
Consider a spectrogram that renders live audio FFT bins at 60 fps. A Custom View running onDraw() with a stale SurfaceView thread? That works, but you fight double-buffering. A Compose Canvas backed by a LaunchedEffect + withFrameMillis? That can stutter if recomposition triggers on every frame. The trick nobody tells you: use Canvas inside a Modifier.drawBehind { … } with a mutable State that you update outside composition—via Snapshot.noSnapshot { … }. This skips the recomposition pipeline entirely. The catch is that you lose Compose’s layout-aware invalidation. Wrong order and your frame callback fights the UI thread. For anything above 30 Hz updates, I default to a bare SurfaceView with a dedicated render loop and a Compose overlay for controls. That hybrid pattern rarely drops frames. Not yet at least.
Reactive layouts: when Compose’s declarative model wins
Imagine a drag-to-resize overlay that snaps to grid lines—the kind of widget that changes shape as data streams in from a websocket. Here, a Custom View turns into a tangle of invalidate() calls, listener registrations, and manual state diffing. Compose’s declarative model eats this alive because the UI is a function of state: grid definition changes, the Canvas redraws automatically, no leak-prone listeners. What usually breaks first is the animation driver. Too many animateFloatAsState calls inside a single Canvas and the recomposition graph turns into a fire drill. Limit state holders to three, max. Em-dash aside: I have debugged a team’s drag handler where every pixel delta triggered a full recomposition—the fix was hoisting the drag coordinates into a mutableStateListOf and letting only the final snap position trigger a redraw. That cut 80 % of the draw calls. Compose wins here, but only when you starve its reactivity.
“Hybrid is not a dirty word—wrapping a Custom View inside Compose often survives the next three releases without a rewrite.”
— senior Android engineer, after migrating a charting library used by 2M users
Reality check: name the development owner or stop.
Pitfalls, Debugging, and What to Check When It Fails
Overdraw and invalidate loops — the silent thrash
You shipped the custom View, users rotated their phones, and suddenly frame times jumped from 4 ms to 22 ms. I have seen this exact panic three times in the last two years. The usual culprit is not the drawing itself — it's an invalidate() call sitting inside a touch listener that fires on every pixel move. Each invalidation triggers a full onDraw() pass, and if you're also redrawing a large Bitmap or a gradient-heavy path every frame, the GPU queue backs up. Quick reality check—pull up the GPU Overdraw debug overlay on a Pixel device. Red means trouble; pink means you're painting the same pixel eight times per frame. Fix: batch your invalidations, or use postInvalidateOnAnimation() to align with the vsync pulse. One team I worked with cut overdraw by 64 % simply by deferring redraws to a single Choreographer callback.
Allocation in draw — the silent frame killer
The worst bug I ever chased lasted six months. Every scrolling list of custom-drawn charts hit jank after 90 seconds. The root cause? A new Paint() inside onDraw(). Allocation on the drawing thread forces GC collections, and GC pauses are invisible in profiling until they stack into a dropped frame. Same danger exists in Compose Canvas: allocating a Path inside DrawScope.drawPath creates garbage that the Compose snapshot system must track. The fix is boring but indispensable: hoist all Paint, Path, RectF, and Shader objects into class-level fields or remember blocks. Don't allocate inside draw — period. If you must mutate a path per frame, use Path.rewind() instead of new Path(). That single change dropped GC pauses from 14 ms to under 1 ms on a mid-range Xiaomi device.
“We traced a 3‑second UI freeze to a val p = Path() inside a Compose Canvas lambda. Rewound it. Freeze gone.”— Lead engineer on a fitness-tracking app, after the 3.2 release
Compose unstable lambdas and recomposition storms
Compose Canvas looks safe — no invalidate() to mess up, right? Wrong. The trap is lambda stability. If your Canvas block captures a lambda that's marked unstable, every recomposition of the parent invalidates the entire draw scope. That means the drawLine() calls rerun even though nothing visual changed. Most teams skip this: they pass a lambda that reads viewModel.state without wrapping it in derivedStateOf. The fix is twofold: mark your data classes @Stable or @Immutable, and hoist draw-state reading into remember blocks. Also, avoid creating new Color or Brush instances inside the lambda — compose treats them as unstable if they're derived from a live State object. One client's animation-heavy dashboard went from 18 ms frame time to 5 ms just by adding @Stable to a data class that held three float values. That hurts, but it works.
Still hitting jank? Check your Canvas size. A 1080p full-screen Canvas redraw every frame is roughly 2 million pixels of shader work. If you're drawing particle effects or real-time waveforms, consider shrinking the draw area or pre-rendering to a Bitmap cache. The trade-off is memory versus frame budget — but a 500 × 500 pixel offscreen buffer beats a full-phone redraw every time.
FAQ or Checklist in Prose
Is Compose Canvas ready for production?
Short answer: yes—but with a caveat that hinges on your minimum SDK. I have shipped Compose Canvas into a production app with a `minSdk = 24` and it held up fine for dashboards and custom charts. The catch is dropped frames on older devices when you redraw every 16ms without memoization. If your target is API 21 or below, stick to Custom View. Compose Canvas runs a different rendering pipeline—Skia on the GPU vs. the traditional CPU-driven `onDraw`. That shift makes it fast for static or low-frequency drawing, but janky when you hammer it with frequent invalidations. Quick reality check—open the Layout Inspector, check "Show drawing stages," and watch for repeated "Recompose" ticks. If you see more than two per frame, your Canvas is fighting the Compose composition cycle. We fixed this by hoisting state out of the `draw` lambda and using `remember` with stable keys.
— based on a production Dashboard screen that rendered 80+ data points per second
How do I benchmark Canvas vs Custom View?
Stop guessing based on vibes. Use the Frame Rendering API (`Choreographer` on the view side, `FrameMetrics` in Compose) to grab real numbers. I set up a simple test: draw 500 circles with random radii inside a scrollable container. Custom View averaged 14ms per frame; Compose Canvas hit 18ms—noticeable only because I used a naive loop inside `DrawScope` without `Path` batching. The trick is profiling with "Profile HWUI rendering" (developer options) for Custom View and the Compose `benchmark` library with `measureRepeats`. Most teams skip this: they compare a single draw call against a complex composition. That hurts. Benchmark the worst case—rapid scrolls with `snapshotFlow` triggering redraws. One concrete anecdote: a battery gauge view that redrew on every sensor event. Compose Canvas burned through 30% more CPU because each draw triggered a fresh layout pass. The verdict? Benchmark your exact use case, not a synthetic toy.
What's the minimum SDK for Compose Canvas?
Officially API 21—but practically you want API 24 for stable `Canvas` operations without fallback shims. On API 23 and below, I have seen `drawRoundRect` throw cryptic `SkAndroidCodec` errors on certain Samsung devices. Not fun. The Compose Canvas API delegates to `android.graphics.Canvas` underneath, so you inherit the same hardware acceleration quirks as Custom View—minus direct access to `BitmapShader` cleanup, which leaks in older API levels if you forget to recycle. If your user base skews toward cheap phones from 2016, test on a real device running API 22. Emulators lie about GPU performance. The final gotcha: Compose itself requires `minSdk 21`, but if you use `Canvas` inside a `Modifier.drawWithContent`, you're effectively running two rendering systems at once—that doubles the chance of a texture upload stall. Keep it simple: one drawing approach per screen.
What to Do Next: Specific Next Steps
Profile your current custom drawing immediately
Open Android Studio, fire up CPU Profiler, and record a session on your actual device—not the emulator. Most teams skip this step, then wonder why their custom `onDraw()` implementation stutters on a Galaxy A series. I have seen engineers spend three days refactoring a Compose Canvas into a custom View, only to discover the real bottleneck was a forgotten `BitmapFactory.decodeResource()` call inside the draw loop. That hurts. Run the profiler before you change anything. Look for two things: frame timing spikes above 16ms and garbage collection pauses tied to your drawing code. If the measurements are clean, your problem is architectural, not API-specific—don’t switch frameworks yet. The catch is that profiling a single scroll pass rarely tells the full story; record at least ten seconds of typical interaction, including rotation and heavy background thread work. Document the baseline numbers in a shared doc. You will need them when someone—likely you, two months from now—blames the drawing API for a regression that actually came from a layout inflation change.
Apply the decision matrix to one component
Pick the smallest messy UI piece on your screen—a custom progress indicator, a chart sparkline, or a trimmed avatar border—and run it through boomlyx.com’s decision tree. Is it redrawn more than five times per frame? Does it need clipping against irregular shapes? Will you animate less than three properties simultaneously? Wrong order. Most developers start with the API preference (“Compose is new, so I’ll use Canvas”) instead of mapping the constraints first. Write three answers on a sticky note: worst-case draw count, required clipping mode, and animation frequency. Then map those to the article’s matrix. What usually breaks first is the assumption that a Compose Canvas handles bitmap caching the same way as a custom View—it doesn't, and the difference shows when your progress ring starts burning the GPU. Implement the chosen path in a single feature branch. Don't refactor your whole custom View hierarchy at once. I broke a production screen that way; the seam blew out because a nested `drawChild()` call expected an Android `Canvas`, not a Compose `Canvas` proxy. One component. One branch. Merge only after the profiler confirms parity or improvement. That's the rule.
‘The hard part is not writing the code—it's proving you didn't make things worse by writing different code.’
— overheard at a Droidcon panel, 2023, from an Android platform engineer
Share your findings in a team RFC
Write a one-pager. Not a novel. Structure it like this: the component you converted, the before/after profiling numbers, the trade-off you accepted, and one explicit callout for what the team should not extrapolate to other components. Quick reality check—teams often read a single success story as a blanket mandate. “Canvas worked for Sam’s sparkline, so let’s rewrite the map overlay.” That's how you accumulate five thousand lines of unreviewed drawing logic that nobody understands. Your RFC should include a pitfall section: what hand-coded cache strategy you needed, whether touch event hit-testing changed, and exactly where the performance gap widened. If the RFC contains the phrase “it depends,” you're writing honestly. If it promises a universal performance win, delete that sentence. The goal is to start a conversation, not to close it. Tag the person who will maintain the component next year—they deserve to know why you chose what you did. Then ship it. The only thing worse than a late decision is no decision at all.
Comments (0)
Please sign in to post a comment.
Don't have an account? Create one
No comments yet. Be the first to comment!