· Workflow · 9 phút đọc

Tạo ảnh chụp màn hình App Store cho Flutter: integration_test, Golden Tests và cách nhanh hơn (2026)

Tạo ảnh chụp màn hình App Store cho Flutter: integration_test, Golden Tests và cách nhanh hơn (2026)
TL;DR. Flutter không có lệnh "tạo ảnh chụp màn hình cửa hàng" chính thức. Các lựa chọn thực tế là: chụp frame thực bằng integration_test + flutter drive, render widget snapshot bằng golden tests (tuyệt vời cho kiểm tra regression, nhưng căng thẳng khi dùng làm tài nguyên cửa hàng), hoặc bọc một trong hai vào Fastlane/Codemagic CI. Không lựa chọn nào trong số đó dựng hình carousel marketing — chú thích, khung, nền, bản sao 50 locale. Đó là một bước riêng biệt, và đó là nơi Mokbi phù hợp: nó dựng hình và bản địa hóa các ảnh chụp màn hình bạn đã chụp. Nó không chụp chúng.

Nếu bạn xây dựng bằng Flutter và đã ra mắt lên App Store hoặc Play Store, bạn đã gặp phải khoảng trống này: không có flutter screenshots. Apple muốn PNG pixel chính xác ở các kích thước như 1320 × 2868 (iPhone 6.9-inch) và 2064 × 2752 (iPad 13-inch); Flutter cung cấp cho bạn một rendering engine và một test harness rồi để phần còn lại của pipeline cửa hàng cho bạn tự lo. Bài viết này là bản đồ trung thực về những gì thực sự hoạt động trong năm 2026, với code bạn có thể dán trực tiếp, và một ranh giới rõ ràng về công cụ nào làm việc gì. Nếu bạn muốn xem tổng quan theo framework trước, hãy xem Mokbi dành cho Flutter developers.

Một điểm cần nói trước, vì nó tránh được nhiều nhầm lẫn: chụp frame (điều khiển ứng dụng đến một trạng thái và lưu PNG của những gì đang hiển thị trên màn hình) và dựng hình ảnh chụp màn hình cửa hàng (frame đó bên trong một mockup thiết bị, với tiêu đề, nền và bản sao đã dịch, xuất ở mọi kích thước yêu cầu) là hai vấn đề hoàn toàn khác nhau. Hầu hết các công cụ Flutter bên dưới giải quyết vấn đề thứ nhất. Không công cụ nào giải quyết vấn đề thứ hai.

Lựa chọn 1: integration_test + flutter drive (frame được chụp thực tế)

Đây là con đường gần nhất Flutter có với một quy trình ảnh chụp màn hình chính thức. Package integration_test khởi chạy ứng dụng thực của bạn trên thiết bị thật hoặc emulator, điều khiển nó bằng cùng API finder/tester như widget test, và cung cấp binding.takeScreenshot(). Điểm bất cập là takeScreenshot trả byte về cho tiến trình test — không tự ghi file — nên bạn cần extended driver để nhận các byte đó trên máy host và lưu chúng.

Đầu tiên là file test. Lưu ý bước riêng cho Android: convertFlutterSurfaceToImage() phải được gọi (và một frame được pump) trước khi chụp, vì Android render Flutter vào một SurfaceView mà framebuffer không thể đọc lại trực tiếp.

// integration_test/screenshot_test.dart
import 'dart:io' show Platform;
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:my_app/main.dart' as app;

void main() {
  final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized();

  testWidgets('home + detail screenshots', (tester) async {
    app.main();
    await tester.pumpAndSettle();

    // Android needs the surface converted to an image before capture.
    if (Platform.isAndroid) {
      await binding.convertFlutterSurfaceToImage();
      await tester.pumpAndSettle();
    }

    await binding.takeScreenshot('01_home');

    await tester.tap(find.byKey(const Key('open-detail')));
    await tester.pumpAndSettle();
    await binding.takeScreenshot('02_detail');
  });
}

Tiếp theo là driver, chạy trên máy host và ghi các byte PNG mà test gửi về. File này nằm trong test_driver/ và dùng integration_test_driver_extended.dart (file integration_test_driver.dart thông thường không có hook onScreenshot):

// test_driver/integration_test.dart
import 'dart:io';
import 'package:integration_test/integration_test_driver_extended.dart';

Future<void> main() async {
  await integrationDriver(
    onScreenshot: (String name, List<int> bytes,
        [Map<String, Object?>? args]) async {
      final file = await File('screenshots/$name.png').create(recursive: true);
      file.writeAsBytesSync(bytes);
      return true; // false fails the test (e.g. on a diff mismatch)
    },
  );
}

Quan trọng là bạn phải chạy lệnh này với flutter drive, không phải flutter test — chỉ đường dẫn driver mới gọi onScreenshot:

flutter drive \
  --driver=test_driver/integration_test.dart \
  --target=integration_test/screenshot_test.dart \
  -d emulator-5554

Bạn nhận được gì và không nhận được gì:

  • Pixel ứng dụng thực. Đây là ảnh chụp màn hình của các widget đang chạy thực sự, không phải render lại — font, theme, dữ liệu động, tất cả đều native.
  • Trạng thái xác định. Bạn điều khiển ứng dụng đến đúng các màn hình muốn và chụp theo yêu cầu.
  • Lớp thiết bị = thiết bị bạn chạy trên. Độ phân giải đầu ra là của emulator/thiết bị đó. Để đạt đúng 1320 × 2868 của Apple bạn phải chạy trên iOS simulator lớp 6.9-inch; với bộ iPad 13-inch bạn chạy simulator đó. Không có cờ "resize về App Store" thần kỳ nào.
  • Locale là việc của bạn. Bạn có thể truyền locale vào ứng dụng khi khởi động và chạy lại theo từng ngôn ngữ, nhưng điều đó chỉ chụp giao diện ứng dụng theo locale đó — không tự viết bản sao tiêu đề marketing của bạn.
  • Chi phí bảo trì là thực tế. Các finder (find.byKey, tap) sẽ hỏng khi giao diện thay đổi, giống như bất kỳ UI test nào.

Lựa chọn 2: golden tests — và tại sao chúng không phải tài nguyên cửa hàng

Golden tests của Flutter (matchesGoldenFile, hoặc testGoldens cấp cao hơn từ các package như golden_toolkit) render cây widget thành ảnh và so sánh byte-for-byte với một reference đã lưu. Chúng rất tuyệt, và được xây dựng cho một mục đích khác với ảnh chụp màn hình cửa hàng: kiểm tra regression pixel. Mục tiêu là phát hiện ngày một thay đổi padding hoặc đổi font âm thầm làm lệch giao diện của bạn. Golden là hợp đồng; diff là test thất bại.

Hai lý do không nên dùng golden tests thô làm pipeline cửa hàng:

  • Golden test render widget trong môi trường test headless, không phải trên bề mặt thiết bị thực. Platform view, một số shader, và bất cứ thứ gì phụ thuộc vào GPU compositor thực có thể render khác (hoặc không render) so với những gì người dùng thấy. Với hợp đồng regression thì ổn — bạn so sánh với chính mình. Với tài nguyên cửa hàng bạn muốn frame thực, và đó là điều Lựa chọn 1 cung cấp.
  • Khớp pixel hoàn hảo sẽ chống lại bạn. Golden thông thường yêu cầu khớp chính xác, nên sự khác biệt antialiasing và font-hinting giữa các máy gây ra các lỗi không ổn định. Đó là độ nghiêm ngặt đúng đắn cho regression và sai cho "tạo một ảnh marketing đẹp".

Tuy nhiên, có một con đường trung gian hợp lý. Các package như golden_screenshot cố ý tái dụng cơ chế golden cho đầu ra cửa hàng: chúng dùng testGoldens, bọc màn hình của bạn trong khung thiết bị thực (phone/tablet/desktop), bật bộ so sánh mờ chấp nhận độ lệch nhỏ (~0.1%, có thể cấu hình) thay vì yêu cầu pixel hoàn hảo, và ghi file theo cấu trúc thư mục kiểu Fastlane (metadata/<locale>/images/...). Nếu màn hình của bạn là pure-Flutter (không có platform view) và bạn muốn có khung quanh render mà không cần chạy emulator, đây là con đường hợp lý và nhanh. Chỉ cần rõ ràng rằng bạn đang render widget, không phải chụp ứng dụng thực, và "marketing" bạn nhận được là khung thiết bị — không có tiêu đề, nền, hay carousel đa bảng.

Lựa chọn 3: Fastlane / Codemagic cho Flutter

Fastlane là công cụ tự động hóa iOS/Android, không phải dành riêng cho Flutter, nhưng nó phù hợp theo hai cách. Các công cụ chụp của Fastlane (snapshot cho iOS qua XCUITest, screengrab cho Android qua harness Espresso/UI Automator) kỳ vọng các native UI test, mà ứng dụng Flutter thường không có — nên hầu hết nhóm Flutter không dùng snapshot để chụp. Nơi Fastlane thực sự hữu ích là phân phối: deliver (iOS) và supply (Android) tải các PNG và metadata đã chụp của bạn lên App Store Connect / Play Console, và chúng đọc một thư mục theo định dạng Fastlane — đúng là cấu trúc mà golden_screenshot ghi và cấu trúc bạn có thể để driver Lựa chọn 1 ghi.

Codemagic là CI native cho Flutter mà hầu hết các nhóm dùng ở đây. Công thức CI thực tế là: chạy integration_test screenshot drive trên các runner macOS/Android theo các lớp thiết bị bạn cần, thu thập các PNG như build artifact, rồi gọi deliver/supply để đẩy chúng. Điều này thực sự hữu ích khi ảnh chụp màn hình bị lệch ở mỗi lần phát hành và bạn ra mắt thường xuyên. Để biết thêm về câu hỏi "tự động hóa hay không cần code" — bao gồm khi nào cấu hình này là quá mức — xem fastlane snapshot vs ảnh chụp màn hình App Store không cần code.

Lựa chọn 4: chụp thủ công (hoàn toàn hợp lệ)

Với nhiều ứng dụng Flutter indie, câu trả lời trung thực là: chỉ cần chụp ảnh màn hình bằng tay một lần mỗi lần phát hành. Chạy ứng dụng trong iOS Simulator ở lớp thiết bị phù hợp (iPhone 6.9-inch, iPad 13-inch), điều hướng đến từng màn hình, và dùng lệnh chụp màn hình của simulator — nó chụp ở đúng độ phân giải thiết bị, là thứ App Store Connect muốn. Trên Android, nút camera của emulator làm tương tự. Tổng thời gian cho ứng dụng năm màn hình: khoảng mười phút.

Chụp thủ công không có chi phí bảo trì, không có finder hỏng bất ngờ, và không có CI phải trông nom. Điểm yếu duy nhất là không có gì tự tạo lại — nếu bạn ra mắt thay đổi giao diện, bạn phải chụp lại. Với nhóm phát hành vài lần một năm, sự đánh đổi đó hoàn toàn đáng. Tự động hóa trong Lựa chọn 1–3 chỉ thực sự có lợi khi "chụp lại bằng tay" không còn là việc mười phút nữa vì bạn ra mắt hàng tuần và có nhiều locale.

Phần mà không cách nào ở trên làm được: dựng hình và bản địa hóa 50 ngôn ngữ

Đây là đoạn văn rõ ràng và trung thực. Mọi lựa chọn ở trên đều tạo ra frame thô — giao diện ứng dụng ở độ phân giải thiết bị. Carousel App Store và Play Store thực sự chuyển đổi không phải là frame thô: đó là frame bên trong một mockup thiết bị, trên nền, dưới một tiêu đề ngắn, thường dưới dạng chuỗi đa bảng, và được lặp lại trên mọi locale bạn nhắm đến. Bước dựng hình đó là việc của Mokbi. Bạn mang các frame đã chụp (từ integration_test, golden_screenshot, simulator, bất cứ đâu), thả vào trình chỉnh sửa trên trình duyệt, thêm khung thiết bị và chú thích và nền, dịch chú thích sang đến 50 ngôn ngữ App Store chỉ một cú nhấp, và xuất hàng loạt ở mọi kích thước yêu cầu. Miễn phí để thiết kế với xem trước có watermark; xuất không giới hạn và xuất bản lên cửa hàng đi kèm gói đăng ký — Solo €29.99/mo (1 ứng dụng) hoặc Studio €49.99/mo (tối đa 5 ứng dụng), không mua một lần. Để nói rõ ranh giới: Mokbi không chụp ảnh chụp màn hình nguồn — nó không chạy ứng dụng hay điều khiển widget của bạn. Việc chụp thuộc về các công cụ Flutter ở trên; Mokbi dựng hình và bản địa hóa những gì các công cụ đó tạo ra.

Quy trình Flutter kết hợp thực tế

  1. Chụp frame nguồn. Viết một integration_test screenshot drive (Lựa chọn 1) cho các màn hình bạn muốn trong carousel, hoặc — nếu bạn ra mắt ít thường xuyên — chụp thủ công từ simulator. Chạy trên thiết bị lớp iPhone 6.9-inch và thiết bị lớp iPad 13-inch để độ phân giải chính xác.
  2. (Tùy chọn) Kết nối vào CI. Nếu lệch lạc là vấn đề thực, chạy drive trên Codemagic và thu thập PNG như artifact (Lựa chọn 3). Nếu không, bỏ qua hoàn toàn.
  3. Dựng hình carousel marketing. Thả frame vào Mokbi, thêm khung/chú thích/nền, xây dựng chuỗi đa bảng. Đây là bước các công cụ Flutter không thể làm.
  4. Bản địa hóa và xuất. Dịch hàng loạt chú thích sang các locale App Store mục tiêu, xuất hàng loạt mọi kích thước yêu cầu, và (nếu muốn) thả đầu ra vào thư mục Fastlane để deliver/supply tải lên.
  5. Với lần thay đổi giao diện tiếp theo, chụp lại và mở lại. Chạy lại drive (hoặc chụp lại), mở lại dự án Mokbi đã lưu, hoán đổi frame, xuất lại. Dựng hình và bản dịch được giữ nguyên.

Khi nào nên bỏ qua tự động hóa hoàn toàn

Nếu bạn ra mắt một đến ba lần một năm, việc xây dựng một integration_test screenshot harness và một pipeline CI sẽ tốn nhiều giờ hơn số giờ nó tiết kiệm được. Chụp thủ công từ simulator, dựng hình và bản địa hóa một lần, rồi tiếp tục. Tự động hóa trong Lựa chọn 1–3 dành cho các nhóm mà lệch lạc ảnh chụp màn hình là khoản thuế thường xuyên — ra mắt thường xuyên, nhiều locale, hoặc nhiều ứng dụng trong danh mục. Hãy chọn công cụ phù hợp với nhịp phát hành, không phải những gì trông có vẻ nghiêm túc.

Trước khi xuất bất cứ thứ gì, hãy kiểm tra lại mục tiêu: lấy kích thước pixel chính xác từ hướng dẫn kích thước ảnh chụp màn hình App Store và các quy tắc định dạng (PNG/JPEG, RGB, không có kênh alpha, 1–10 mỗi lớp thiết bị) từ hướng dẫn yêu cầu ảnh chụp màn hình App Store. Apple từ chối kích thước lệch dù chỉ một pixel, nên việc này đáng bỏ ra ba mươi giây.

Đọc tiếp

Mở trình chỉnh sửa →