Best Practice #

Sebuah release yang baik adalah yang tidak terasa seperti event besar — ia terjadi secara rutin, dapat diprediksi, dan mudah dibalik jika ada masalah. Artikel ini merangkum praktik-praktik yang membuat proses build dan release menjadi aman dan berkelanjutan.

1. Versi Otomatis dari Git Tag #

Daripada mengubah versi di pubspec.yaml secara manual, otomasi dari Git tag:

# Alur:
# 1. Developer push tag: git tag v2.3.1 && git push origin v2.3.1
# 2. CI otomatis baca tag dan gunakan sebagai versi
# 3. Build number dari nomor run CI

# Di GitHub Actions
- name: Extract version dari tag
  if: startsWith(github.ref, 'refs/tags/v')
  id: version
  run: |
    # Ambil versi dari tag (v2.3.1 → 2.3.1)
    VERSION=${GITHUB_REF#refs/tags/v}
    # Build number dari GitHub run number (selalu naik)
    BUILD_NUMBER=${{ github.run_number }}
    echo "version=$VERSION" >> $GITHUB_OUTPUT
    echo "build_number=$BUILD_NUMBER" >> $GITHUB_OUTPUT

- name: Update pubspec.yaml version
  run: |
    sed -i "s/^version:.*/version: ${{ steps.version.outputs.version }}+${{ steps.version.outputs.build_number }}/" pubspec.yaml

2. Staged Rollout — Rilis Bertahap #

Jangan langsung rilis ke 100% pengguna. Rilis bertahap memungkinkan deteksi masalah sebelum seluruh pengguna terdampak:

Play Store Staged Rollout:
  5%   → tunggu 24 jam, cek crash rate dan ANR rate
  25%  → tunggu 24 jam
  50%  → tunggu 24 jam
  100% → full release

Batas aman untuk lanjut ke tahap berikutnya:
  ✓ Crash rate < 0.5% (batas Play Store: <1% untuk "good standing")
  ✓ ANR rate < 0.2%
  ✓ Rating rata-rata tidak turun signifikan
  ✓ Tidak ada laporan bug kritis dari tester di tahap sebelumnya

Kapan harus HALT rollout:
  ✗ Crash rate > 2% (naik drastis dari versi sebelumnya)
  ✗ Fitur kritis tidak berfungsi (login gagal, crash saat launch)
  ✗ Data pengguna tidak terbaca atau corrupt

# Di Play Console: Release → Production → Managed publishing
# Ubah rollout percentage kapan saja

3. Monitoring Crash — Sentry atau Firebase Crashlytics #

Jangan rilis tanpa crash monitoring. Kamu tidak akan tahu ada masalah kecuali ada yang melaporkan.

# pubspec.yaml
dependencies:
  # Pilih salah satu:
  firebase_crashlytics: ^4.2.0   # terintegrasi dengan Firebase
  sentry_flutter: ^8.12.0        # standalone, lebih detail

Setup Crashlytics #

// main.dart
void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp();

  // Tangkap semua error Flutter
  FlutterError.onError = FirebaseCrashlytics.instance.recordFlutterFatalError;

  // Tangkap error di luar Flutter (async errors)
  PlatformDispatcher.instance.onError = (error, stack) {
    FirebaseCrashlytics.instance.recordError(error, stack, fatal: true);
    return true;
  };

  runApp(const MyApp());
}

// Tambahkan informasi konteks ke crash report
class AuthNotifier extends Notifier<AuthState> {
  Future<void> login(String email, String password) async {
    try {
      final user = await ref.read(authRepoProvider).login(email, password);

      // Set user identifier di crash report
      await FirebaseCrashlytics.instance.setUserIdentifier(user.id);
      await FirebaseCrashlytics.instance.setCustomKey('email', email);
      await FirebaseCrashlytics.instance.setCustomKey('plan', user.plan);

      state = AuthState.authenticated(user);
    } catch (e, stack) {
      // Log error non-fatal
      await FirebaseCrashlytics.instance.recordError(e, stack, fatal: false);
      state = AuthState.error(e.toString());
    }
  }
}

Setup Sentry #

// main.dart dengan Sentry
void main() async {
  await SentryFlutter.init(
    (options) {
      options.dsn = AppConfig.sentryDsn;
      options.environment = AppConfig.environment.name;  // 'development', 'production'
      options.release = '${AppConfig.appName}@${AppConfig.version}';
      options.tracesSampleRate = AppConfig.isProduction ? 0.1 : 1.0;  // 10% di prod
      options.enableAutoSessionTracking = true;
    },
    appRunner: () => runApp(const MyApp()),
  );
}

// Tangkap error manual
try {
  await riskyOperation();
} catch (exception, stackTrace) {
  await Sentry.captureException(
    exception,
    stackTrace: stackTrace,
    withScope: (scope) {
      scope.setTag('feature', 'checkout');
      scope.setExtra('orderId', orderId);
    },
  );
}

4. Rollback Plan #

Selalu punya rencana rollback sebelum rilis:

Play Store:
  → Rollback dengan rilis versi sebelumnya
  → Play Store tidak punya "rollback" tombol
  → Harus upload build lama sebagai release baru dengan version baru
  → Simpan semua AAB lama (minimal 3 versi terakhir)!

App Store:
  → Bisa "unpublish" versi saat ini dari App Store Connect
  → Tapi versi yang sudah didownload pengguna tetap berjalan
  → Untuk force update: tandai minimum version di backend
  → Pengguna yang buka app di bawah minimum version ditampilkan dialog wajib update

Force Update Strategy:
// Cek minimum version saat app launch
class AppVersionChecker {
  static Future<void> checkMinVersion() async {
    final minVersion = await ApiClient.instance.getMinimumVersion();
    final currentVersion = AppConfig.version;  // dari pubspec

    if (isVersionBelow(currentVersion, minVersion)) {
      // Tampilkan dialog yang tidak bisa di-dismiss
      showForceUpdateDialog();
    }
  }

  static bool isVersionBelow(String current, String minimum) {
    final c = current.split('.').map(int.parse).toList();
    final m = minimum.split('.').map(int.parse).toList();
    for (var i = 0; i < 3; i++) {
      if (c[i] < m[i]) return true;
      if (c[i] > m[i]) return false;
    }
    return false;
  }
}

5. Release Notes yang Berguna #

ANTI-PATTERN: release notes yang tidak informatif
  "Bug fixes and performance improvements"
  "Minor updates"
  "Version 2.3.1"

BENAR: jelaskan apa yang berubah untuk pengguna
  Apa yang baru:
  • Fitur filter produk berdasarkan kategori dan harga
  • Dukungan dark mode yang bisa diatur di Pengaturan

  Perbaikan:
  • Checkout tidak lagi gagal saat menggunakan voucher diskon
  • Scroll di halaman pesanan kini lebih mulus
  • Notifikasi muncul dengan benar setelah restart app

  Tips: Tulis untuk pengguna, bukan developer.
  Pengguna tidak peduli "fixed null pointer exception" --
  mereka peduli "login tidak lagi crash".

Anti-Pattern yang Harus Dihindari #

VERSIONING:
  ✗ Lupa increment build number -- upload ke store akan ditolak
  ✗ Menggunakan tanggal sebagai versi (20250115) -- tidak semantik
  ✗ Skip versi (1.0.0 → 1.0.3) -- membingungkan saat debug

SIGNING:
  ✗ Commit keystore atau certificate ke git
  ✗ Gunakan debug keystore untuk release -- tidak bisa upload ke store
  ✗ Kehilangan keystore tanpa backup -- tidak bisa update app yang ada!
  ✓ Backup keystore di cloud storage yang aman (multiple copy)

TESTING RELEASE BUILD:
  ✗ Hanya test di debug mode, release bisa berbeda perilakunya
  ✗ Tidak test di device target terendah (low-end Android)
  ✗ Upload langsung ke production tanpa staging test

CI/CD:
  ✗ Build lokal lalu upload -- manual dan rawan human error
  ✗ Simpan secret di kode atau environment file yang di-commit
  ✗ Tidak ada notifikasi saat build gagal

RELEASE:
  ✗ Rilis pada hari Jumat sore -- tidak ada yang monitor weekend
  ✓ Rilis Senin-Rabu pagi -- ada waktu untuk monitor dan fix cepat
  ✗ Rilis bersamaan dengan banyak perubahan besar
  ✓ Rilis inkremental, perubahan kecil lebih sering

Checklist Akhir Sebelum Publish #

PRE-RELEASE (1-2 hari sebelum):
  □ Semua test lulus di CI
  □ Build release sudah ditest di device fisik (Android dan iOS)
  □ Flow kritis sudah diverifikasi: login, fitur utama, checkout
  □ Tidak ada crash yang diketahui
  □ Version sudah di-bump di pubspec.yaml
  □ CHANGELOG sudah diperbarui

STORE LISTING:
  □ Screenshot semua ukuran sudah diperbarui jika ada perubahan UI
  □ Release notes sudah ditulis dalam bahasa yang dipahami pengguna
  □ Privacy policy URL masih valid

UPLOAD:
  □ AAB/IPA diupload ke store yang tepat
  □ Staged rollout dikonfigurasi (mulai 5-10%)
  □ Crash monitoring aktif dan terkoneksi ke versi baru

POST-RELEASE (24-48 jam setelah rilis):
  □ Pantau crash rate di Firebase Crashlytics / Sentry
  □ Pantau ANR rate di Play Console
  □ Pantau rating dan review di Play Store / App Store
  □ Lanjutkan rollout jika tidak ada masalah
  □ Siap untuk hotfix jika ada masalah kritis

Ringkasan #

  • Otomasi versioning dari Git tag via CI — tidak ada lagi lupa increment build number atau human error saat update pubspec.yaml.
  • Staged rollout adalah perlindungan terpenting — mulai dari 5%, pantau crash rate 24 jam, baru lanjutkan. Jangan rilis ke 100% sekaligus untuk versi mayor.
  • Crash monitoring wajib sebelum rilis produksi — Crashlytics (gratis, terintegrasi Firebase) atau Sentry (lebih detail, ada free tier). Atur alert untuk crash rate spike.
  • Simpan backup keystore di multiple lokasi yang aman (cloud storage, password manager) — kehilangan keystore berarti tidak bisa update app yang sudah di store.
  • Jangan rilis Jumat sore — tidak ada yang bisa monitor dan fix masalah saat weekend. Rilis Senin-Rabu pagi adalah waktu terbaik.
  • Release notes yang baik menjelaskan perubahan dalam bahasa pengguna, bukan developer — “login tidak lagi crash” bukan “fixed NPE in AuthService”.
  • Selalu punya rollback plan — untuk Play Store simpan AAB lama, untuk App Store implementasi force update di sisi server dengan minimum version check.

← Sebelumnya: CI/CD

Selamat! Kamu telah menyelesaikan seluruh Flutter Tutorial Series.

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact