Skip to content

Refactor, Modernize & Enhance Android Camera Samples with new features#636

Open
madebymozart wants to merge 41 commits into
mainfrom
madebymozart/camera-samples-overhaul
Open

Refactor, Modernize & Enhance Android Camera Samples with new features#636
madebymozart wants to merge 41 commits into
mainfrom
madebymozart/camera-samples-overhaul

Conversation

@madebymozart

@madebymozart madebymozart commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Overview

The legacy camera-samples repo was a loose collection of ~12 independent Gradle projects
(CameraXBasic, Camera2Basic, CameraXAdvanced, …), each with its own build, largely View-based
UI, and copy-pasted boilerplate. This PR replaces all of them with a single, modern, Compose-first
Camera Samples Catalog app
— one cohesive application that showcases Camera2 and CameraX
through small, self-contained samples that share a common architecture and design system.

909 files changed · +24.4k / −32.7k

Screenshots

Home catalog — all samples     Home catalog — the ML category

Highlights

  • One app, one build. A single Gradle project replaces the dozen standalone sample projects; a
    filterable catalog on the home screen routes to each sample.
  • Modular architecture. Shared code lives in :core-theme, :core-camera, and :core-ui, with
    one thin library module per sample under :samples/{api}-{feature}.
  • Layered & unidirectional. Every sample follows the same UiState → ViewModel → Controller → Screen pattern (documented in android_architecture.md) — a
    sealed interface Ui state, a @HiltViewModel exposing one StateFlow, a @Stable controller
    that owns the camera lifecycle, and a Screen that renders with when(state).
  • "Console" design system. A violet accent, Space Grotesk + Space Mono typography, a dense
    2-column catalog, a viewfinder HUD (accent focus reticle, rule-of-thirds, torch glow), and
    console-styled in-app settings menus.
  • createSample generator. ./gradlew createSample -PsampleName=… -PscreenName=… scaffolds a
    working, preview-only module and wires it into the build + catalog automatically.
  • Modern toolchain. compileSdk 37 (Android 17), JDK 17, Jetpack Compose + Material 3, Hilt,
    Navigation-Compose. Formatting is enforced with Spotless (ktlint + Apache license headers).
  • Modernized CI. The old per-project job matrix is replaced by a single root build that runs
    spotlessCheck + assembleDebug.

Samples

28 samples across six categories (the home screen filters by these; within a category, CameraX is
listed first, then Camera2):

  • Images — Take a Photo (CameraX · Camera2) · Ultra HDR (CameraX) · RAW/DNG (Camera2)
  • Video — Take a Video (CameraX · Camera2) · Slow Motion (Camera2) · Pause/Resume (CameraX) ·
    HDR Video (CameraX · Camera2) · Video Stabilization (CameraX) · Flip While Recording (CameraX)
  • ML — QR Scanner (CameraX · Camera2) · Image Labeling (CameraX) · Green Screen (CameraX)
  • Graphics — Viewfinder Effects (Camera2) · Luminosity (CameraX) · Concurrent Camera (CameraX) ·
    Effects (CameraX — featured)
  • Extensions — Extensions (CameraX · Camera2)
  • Controls — Zoom & Torch (CameraX · Camera2) · Exposure (CameraX) · Manual Controls (Camera2) ·
    Low-Light Boost (CameraX) · Feature Combination (CameraX)

Samples that depend on optional hardware (extensions, high-speed/HDR recording, manual sensor) detect
support at runtime and show a friendly "not supported on this device" state instead of crashing.

Module layout

app/                       catalog UI + Navigation-Compose NavHost
core-theme/                design system: color scheme, typography, AISampleCatalogTheme
core-camera/               Camera2/CameraX plumbing: controllers, previews, permissions, MediaStore
core-ui/                   shared Compose chrome: scaffold, controls, viewfinder HUD, capture review
samples/{api}-{feature}/   one library module per sample

Breaking changes

Removes the legacy standalone projects — Camera2Basic, Camera2Extensions, Camera2Video,
Camera2SlowMotion, CameraXBasic, CameraXAdvanced, CameraXExtensions, CameraXVideo,
CameraX-MLKit, HdrViewfinder, CameraUtils, and Presentations. Their functionality is
re-implemented and consolidated in the new catalog app.

Testing

  • ./gradlew assembleDebug and ./gradlew spotlessCheck pass from the project root.
  • Installed and smoke-tested on a Pixel 10 Pro (Android 17) and the Pixel 9 Pro XL emulator.

…iewfinder` for camera preview and remove unused MLKit dependencies.
…0x1920 and set viewfinder scale type to `FIT_CENTER`.
…introduce `createSample.gradle.kts` for sample generation, and remove `GeminiDataSource.kt`.
…age structure and update build configurations and the `camera2-takeaphoto` sample's package.
…ave recorded videos to the public Movies directory.
@madebymozart madebymozart requested a review from donovanfm June 24, 2026 00:39
@madebymozart madebymozart self-assigned this Jun 24, 2026
@madebymozart madebymozart added the enhancement New feature or request label Jun 24, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request performs a major cleanup of the repository by deleting several camera-related sample applications, including Camera2Basic, Camera2Extensions, Camera2SlowMotion, Camera2Video, CameraX-MLKit, and CameraX-TFLite, along with their associated source code, resources, and utility modules. Additionally, it updates the .editorconfig file with ktlint naming rules for Composable functions and modifies the CODEOWNERS file to assign new owners. There are no review comments to assess, and I have no additional feedback to provide.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

@madebymozart madebymozart marked this pull request as ready for review June 24, 2026 14:04
@donovanfm

Copy link
Copy Markdown
Contributor

This looks great, Mozart! Really nice overhaul! I did an AI code review and some manual testing.

Manual testing comments:

  1. The labels at the top of each demo are inconsistent. I like that they tell you what demo you're in, but in some of the demos the label is missing (for example, "Take a video"). Also, some of them have the demo name label lower on the screen than others (for example, Zoom & Torch has the label a bit low, and the "x" button and "torch" button are on a different y-value as well, making the buttons feel a little off). This was tested on a Pixel 9 Pro.
  2. There are some Camera2 examples that are easier to do CameraX, but there is no CameraX sample. For example, slow motion video capture and Raw DNG photo capture. Both of these CameraX features are explained in this blog post: https://developer.android.com/blog/posts/introducing-camera-x-powerful-video-recording-and-pro-level-image-capture. And Viewfinder Effects are possible in CameraX with the CameraX-Media3-effects bridge, which is exaplined in this blog post: https://android-developers.googleblog.com/2024/12/whats-new-in-camerax-140-and-jetpack-compose-support.html
  3. It would be nice to have a toast message when media is saved in all of the demos.
  4. Consistent controls. For example, Take a Photo has "Retake" and "Done" buttons after capture, but Take a Video only has a back button in the upper left-hand corner after capture.
  5. The CameraX Extensions demo is bugging for me. When I click the capture button, it flashes what I think the the post capture screen, but then it takes me back to the preview screen and no photo is saved.
  6. Concurrent camera has a round rect over the front facing feed. Is that meant to be a mask, so that the front feed has a sleek rounded look?
  7. The UIs for the CameraX and Camera2 effects demos are different. Not a major issue, but I found it odd. Also, the "Invert" effects aren't the same. I think the Camera2 one applies a B&W filter and then inverts it, and the CameraX one actually inverts the colors.

AI review comments:

🏗️ 1. Code Pattern & Architecture Review
Status: EXCELLENT
The architectural overhaul is highly successful, introducing significant improvements in maintainability and readability.

  • Architectural Consistency: The 26 new sample modules demonstrate strict adherence to a modern MVVM/MVI architecture. Each sample is cleanly divided into a Screen (UI), ViewModel (state orchestration),
    UiState (MVI pattern via StateFlow), and a Controller (Camera API plumbing).
  • DRY Principles: The extraction of core-camera and core-ui was highly effective. Complex Camera2 boilerplate (background threads, lifecycle, tap-to-focus) is abstracted cleanly into BaseCamera2Controller.
    Reusable Compose UI elements (CameraSampleScaffold, FocusIndicator, etc.) ensure a uniform aesthetic and eliminate redundant code.

🛡️ 2. Security Review
Status: CRITICAL ISSUE FOUND

  • 🔴 Improper Scoped Storage Access (MediaStoreSaver.kt): There is a critical flaw in how new video files are created for Camera2 samples. The newVideoFile() function uses
    Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM) and writes directly to java.io.File. Starting in Android 10 (API 29), apps are restricted from directly writing to shared storage
    without Scoped Storage compliant APIs. This will result in a SecurityException or IOException (Permission denied) crash on modern Android versions.
    • Fix: Refactor to use the MediaStore API (inserting a pending MediaStore.Video.Media.EXTERNAL_CONTENT_URI record) or write to scoped storage (Context.getExternalFilesDir()).
  • 🟢 Manifest & Intents: MainActivity is correctly exported for Android 12+. The FileProvider is safely configured without path traversal vulnerabilities.
  • 🟡 Permissions: Use of @SuppressLint("MissingPermission") in controllers is deemed acceptable here because permissions are explicitly verified at the UI layer by CameraSampleScaffold.kt before
    initialization.

🏎️ 3. Performance & Memory Review
Status: CRITICAL ISSUES FOUND

  • 🔴 Leaked ImageProxy Instances: In controllers like CameraXTakeAPhotoController, the ImageProxy received in onCaptureSuccess is handed off to a Compose-tied coroutine. If the Composable is disposed and the
    coroutine cancels, image.close() is never called, exhausting the ImageReader buffer and permanently stalling the camera pipeline.
    • Fix: Convert the ImageProxy to a Bitmap (or extract the buffer) synchronously inside onCaptureSuccess, close it via finally, and then launch the coroutine to pass the result.
  • 🔴 Activity Context Leaks: Controllers are injected with LocalContext.current (an Activity Context) and passed directly to ProcessCameraProvider.getInstance(context).
    • Fix: Always use context.applicationContext to prevent leaking the Activity singleton.
  • 🟡 Thread Pool Leaks: Controllers define Executors.newSingleThreadExecutor() and are managed by Compose's remember. If not properly disposed in a DisposableEffect, threads will leak.
  • 🟡 Unstable UI State Parameters: UI State classes use standard Kotlin Lists (e.g., val availableModes: List). Compose treats List as unstable, forcing unnecessary recompositions of the camera UI tree.
    • Fix: Use @immutable data classes or ImmutableList from kotlinx.collections.immutable.
  • 🟡 Unbounded Coroutines: ViewModels process images via indiscriminate viewModelScope.launch { withContext(Dispatchers.IO) }. Spamming capture can spike memory allocations and lead to an OutOfMemoryError.

📱 4. Modern Android Development (MAD) Review
Status: GOOD WITH MINOR DEVIATIONS
The codebase makes excellent use of Jetpack Compose, Coroutines, StateFlow, and Dagger Hilt. However, there are a few deviations from idiomatic practices:

  • 🟡 Imperative Future Handling: In controllers, ProcessCameraProvider.getInstance(context) is resolved using .addListener(...).
    • Idiomatic Approach: Use the androidx.concurrent:concurrent-futures-ktx artifact and await the result cleanly using Coroutines: ProcessCameraProvider.getInstance(context).await().
  • 🟡 Verbose hiltViewModel(): hiltViewModel(checkNotNull(LocalViewModelStoreOwner.current)) is used in UI screens.
    • Idiomatic Approach: Just call hiltViewModel() without arguments; it automatically resolves the current LocalViewModelStoreOwner.
  • 🟡 Callback Stability in remember: Passing callbacks like viewModel::onCameraReady as a remember key in screens can cause unintended controller recreations.
    • Idiomatic Approach: Use rememberUpdatedState for callbacks inside remembered components to ensure they invoke the latest reference without recreation.
  • 🟡 Internal ViewModel State: Standalone mutable variables (like var isFrontCamera) require imperative methods to reconstruct and push the entire UI state.
    • Idiomatic Approach: Fold these properties into a unified state data class or use flow combine operators for a true single source of truth.

@donovanfm donovanfm left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See my comment on the PR for the requested changes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants