ANTIGRAVITY LABJP
Articles/App Development
App Development/2026-03-10Intermediate

Building Native iOS and Android Apps with Rork Max × Antigravity

Comprehensive guide to building native mobile applications using Swift and Kotlin with Rork Max and Antigravity Lab

Rork MaxMobile App2iOS27Android27Swift8Kotlin4

Native Swift / Kotlin Development Powered by AI

For a long time, native app development carried an unspoken assumption: you write Swift and Kotlin by hand, or you don't ship. Pairing Rork Max with Antigravity shifts that assumption. You hand the groundwork to the AI and spend your attention on architecture and polish — a realistic way to work across both iOS and Android. Here we lay out the actual development flow and the practices worth keeping.

Understanding Rork Max

Rork Max is an AI-powered native application builder powered by Claude, specifically designed for Swift and Kotlin development. It enables developers to write native mobile applications more efficiently by providing intelligent code generation, real-time assistance, and seamless integration with standard development environments.

Core Capabilities

Intelligent Code Generation Rork Max transforms natural language specifications into production-quality Swift and Kotlin code. From simple UI components to complex business logic, authentication flows to API integration, the system handles the repetitive aspects of mobile development while you focus on architecture and user experience.

Cross-Platform Development With unified support for both iOS and Android, Rork Max lets you maintain consistent architectures across platforms. While platform-specific implementations remain necessary, the framework and common logic can be efficiently shared and managed.

Advanced Error Diagnosis When runtime or compilation errors occur, Rork Max analyzes error messages and proposes targeted fixes, significantly reducing debugging time and improving code quality.

Real-Time IDE Integration Seamless integration with Xcode and Android Studio provides code suggestions and modifications directly within your familiar development environment.

Antigravity and Rork Max: Integrated Workflow

Combining Antigravity's agent capabilities with Rork Max creates a comprehensive development platform that handles every stage of mobile app creation.

Integrated Development Process

Requirements & Planning
    ↓
Antigravity Agents: Architecture & Design
    ↓
Rork Max: Code Generation & Implementation
    ↓
Antigravity: Testing & Validation
    ↓
Deployment & Release

Phase-Based Workflow

Phase 1: Project Initialization

Use Antigravity agents to solidify your project requirements and architecture:

Antigravity Prompt:
"I'm building a native iOS/Android application with:
 - OAuth 2.0 user authentication
 - REST API integration (fetch data, submit forms)
 - SQLite local persistence
 Please create a comprehensive implementation plan
 including module breakdown and dependency analysis."

Antigravity provides detailed project structure recommendations, required libraries, and implementation sequence.

Phase 2: Implementation with Rork Max

Feed your architecture plan to Rork Max for code generation:

Rork Max Prompt:
"Create a Swift UIViewController for user authentication.
 Include:
 - Email and password input fields with validation
 - Real-time input validation feedback
 - Network request to /api/auth/login
 - Secure token storage in Keychain
 - Error handling and user feedback"

Phase 3: Quality Assurance

Pass the generated code through Antigravity's validation agents:

Antigravity Validation Prompt:
"Review this authentication implementation:
 - Check for security vulnerabilities
 - Verify iOS best practices compliance
 - Suggest performance optimizations
 - Identify missing error handling paths"

iOS Development with Swift

Environment Setup

Ensure your development environment is properly configured:

# Verify Xcode installation
xcode-select --install
 
# Check Swift version
swift --version
 
# Install CocoaPods for dependency management
sudo gem install cocoapods
pod setup

Project Structure and Initialization

# Create project directory
mkdir MyMobileApp
cd MyMobileApp
 
# Initialize Swift package (if using SPM)
swift package init --type app --name MyMobileApp

Modern SwiftUI Architecture

Here's a production-ready structure using SwiftUI and MVVM:

// App.swift
import SwiftUI
 
@main
struct MyApp: App {
    @StateObject var authViewModel = AuthViewModel()
 
    var body: some Scene {
        WindowGroup {
            if authViewModel.isLoggedIn {
                DashboardView()
                    .environmentObject(authViewModel)
            } else {
                LoginView()
                    .environmentObject(authViewModel)
            }
        }
    }
}
 
// Views/LoginView.swift
import SwiftUI
 
struct LoginView: View {
    @EnvironmentObject var authViewModel: AuthViewModel
    @State private var email = ""
    @State private var password = ""
    @State private var showError = false
    @State private var errorMessage = ""
 
    var body: some View {
        VStack(spacing: 20) {
            Text("Welcome Back")
                .font(.title)
                .fontWeight(.bold)
 
            TextField("Email Address", text: $email)
                .textFieldStyle(RoundedBorderTextFieldStyle())
                .keyboardType(.emailAddress)
                .autocapitalization(.none)
                .disableAutocorrection(true)
 
            SecureField("Password", text: $password)
                .textFieldStyle(RoundedBorderTextFieldStyle())
 
            if showError {
                Text(errorMessage)
                    .foregroundColor(.red)
                    .font(.caption)
            }
 
            Button(action: handleLogin) {
                if authViewModel.isLoading {
                    ProgressView()
                        .progressViewStyle(CircularProgressViewStyle(tint: .white))
                } else {
                    Text("Sign In")
                        .font(.headline)
                }
            }
            .frame(maxWidth: .infinity)
            .padding()
            .background(Color.blue)
            .foregroundColor(.white)
            .cornerRadius(8)
            .disabled(authViewModel.isLoading || email.isEmpty || password.isEmpty)
 
            Spacer()
        }
        .padding()
        .alert(isPresented: $showError) {
            Alert(title: Text("Login Failed"), message: Text(errorMessage))
        }
    }
 
    private func handleLogin() {
        guard isValidEmail(email) else {
            errorMessage = "Please enter a valid email address"
            showError = true
            return
        }
 
        Task {
            do {
                try await authViewModel.login(email: email, password: password)
            } catch {
                errorMessage = error.localizedDescription
                showError = true
            }
        }
    }
 
    private func isValidEmail(_ email: String) -> Bool {
        let emailPattern = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}"
        return NSPredicate(format: "SELF MATCHES %@", emailPattern).evaluate(with: email)
    }
}

Networking Layer

Implement a robust networking layer with error handling:

// Services/APIClient.swift
import Foundation
 
enum APIError: LocalizedError {
    case invalidURL
    case invalidResponse
    case serverError(statusCode: Int)
    case decodingError
    case networkError(Error)
 
    var errorDescription: String? {
        switch self {
        case .invalidURL:
            return "Invalid URL"
        case .invalidResponse:
            return "Invalid server response"
        case .serverError(let statusCode):
            return "Server error: \(statusCode)"
        case .decodingError:
            return "Failed to decode response"
        case .networkError(let error):
            return error.localizedDescription
        }
    }
}
 
class APIClient {
    static let shared = APIClient()
    private let baseURL = "https://api.example.com"
    private let session = URLSession.shared
 
    func login(email: String, password: String) async throws -> AuthToken {
        let endpoint = "\(baseURL)/auth/login"
        guard let url = URL(string: endpoint) else {
            throw APIError.invalidURL
        }
 
        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
 
        let loginPayload = ["email": email, "password": password]
        request.httpBody = try JSONEncoder().encode(loginPayload)
 
        do {
            let (data, response) = try await session.data(for: request)
 
            guard let httpResponse = response as? HTTPURLResponse else {
                throw APIError.invalidResponse
            }
 
            switch httpResponse.statusCode {
            case 200...299:
                return try JSONDecoder().decode(AuthToken.self, from: data)
            case 401:
                throw APIError.serverError(statusCode: 401)
            default:
                throw APIError.serverError(statusCode: httpResponse.statusCode)
            }
        } catch is DecodingError {
            throw APIError.decodingError
        } catch {
            throw APIError.networkError(error)
        }
    }
}
 
// Models/AuthToken.swift
struct AuthToken: Codable {
    let accessToken: String
    let refreshToken: String
    let expiresIn: Int
    let tokenType: String
 
    enum CodingKeys: String, CodingKey {
        case accessToken = "access_token"
        case refreshToken = "refresh_token"
        case expiresIn = "expires_in"
        case tokenType = "token_type"
    }
}

ViewModel Implementation

// ViewModels/AuthViewModel.swift
import Foundation
 
@MainActor
class AuthViewModel: ObservableObject {
    @Published var isLoggedIn = false
    @Published var isLoading = false
    @Published var currentUser: User?
 
    private let apiClient = APIClient.shared
    private let keychainService = KeychainService.shared
 
    func login(email: String, password: String) async throws {
        isLoading = true
        defer { isLoading = false }
 
        let token = try await apiClient.login(email: email, password: password)
        try keychainService.save(token: token, forKey: "authToken")
 
        // Fetch user details
        let user = try await apiClient.fetchUserProfile()
        currentUser = user
        isLoggedIn = true
    }
 
    func logout() {
        try? keychainService.delete(forKey: "authToken")
        currentUser = nil
        isLoggedIn = false
    }
}

Testing

// Tests/AuthViewModelTests.swift
import XCTest
@testable import MyMobileApp
 
class AuthViewModelTests: XCTestCase {
    var sut: AuthViewModel!
    var mockAPIClient: MockAPIClient!
 
    override func setUp() {
        super.setUp()
        mockAPIClient = MockAPIClient()
        sut = AuthViewModel()
    }
 
    func testSuccessfulLogin() async throws {
        // Given
        let testEmail = "test@example.com"
        let testPassword = "password123"
        mockAPIClient.mockToken = AuthToken(
            accessToken: "test_token",
            refreshToken: "refresh_token",
            expiresIn: 3600,
            tokenType: "Bearer"
        )
 
        // When
        try await sut.login(email: testEmail, password: testPassword)
 
        // Then
        XCTAssertTrue(sut.isLoggedIn)
        XCTAssertNotNil(sut.currentUser)
    }
}

Android Development with Kotlin

Environment Setup

Ensure Android development tools are properly configured:

# Verify Android Studio installation and tools
adb --version
gradle --version

Project Structure with Modern Architecture

Android development using Kotlin with MVVM and Compose:

// MainActivity.kt
package com.example.mymobileapp
 
import android.os.Bundle
import androidx.activity.compose.setContent
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Modifier
import androidx.hilt.navigation.compose.hiltViewModel
import com.example.mymobileapp.ui.screens.LoginScreen
import com.example.mymobileapp.ui.screens.DashboardScreen
 
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MyAppTheme {
                Scaffold(modifier = Modifier.fillMaxSize()) { paddingValues ->
                    AppNavigation(modifier = Modifier.padding(paddingValues))
                }
            }
        }
    }
}
 
// ui/screens/LoginScreen.kt
package com.example.mymobileapp.ui.screens
 
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import com.example.mymobileapp.viewmodel.AuthViewModel
 
@Composable
fun LoginScreen(
    viewModel: AuthViewModel = hiltViewModel(),
    onLoginSuccess: () -> Unit
) {
    var email by remember { mutableStateOf("") }
    var password by remember { mutableStateOf("") }
    val uiState by viewModel.uiState.collectAsState()
 
    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(16.dp),
        verticalArrangement = Arrangement.Center
    ) {
        Text("Welcome Back", style = MaterialTheme.typography.headlineMedium)
 
        Spacer(modifier = Modifier.height(24.dp))
 
        OutlinedTextField(
            value = email,
            onValueChange = { email = it },
            label = { Text("Email") },
            modifier = Modifier.fillMaxWidth(),
            keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Email)
        )
 
        Spacer(modifier = Modifier.height(16.dp))
 
        OutlinedTextField(
            value = password,
            onValueChange = { password = it },
            label = { Text("Password") },
            modifier = Modifier.fillMaxWidth(),
            visualTransformation = PasswordVisualTransformation()
        )
 
        Spacer(modifier = Modifier.height(24.dp))
 
        Button(
            onClick = { viewModel.login(email, password) },
            modifier = Modifier.fillMaxWidth(),
            enabled = email.isNotEmpty() && password.isNotEmpty() && !uiState.isLoading
        ) {
            if (uiState.isLoading) {
                CircularProgressIndicator(modifier = Modifier.size(20.dp))
            } else {
                Text("Sign In")
            }
        }
 
        if (uiState.error != null) {
            Spacer(modifier = Modifier.height(16.dp))
            Text(
                text = uiState.error ?: "",
                color = MaterialTheme.colorScheme.error,
                style = MaterialTheme.typography.bodySmall
            )
        }
 
        LaunchedEffect(uiState.isLoggedIn) {
            if (uiState.isLoggedIn) {
                onLoginSuccess()
            }
        }
    }
}

Networking with Retrofit

// network/APIService.kt
package com.example.mymobileapp.network
 
import retrofit2.http.POST
import retrofit2.http.Body
import com.example.mymobileapp.models.LoginRequest
import com.example.mymobileapp.models.AuthToken
 
interface APIService {
    @POST("auth/login")
    suspend fun login(@Body request: LoginRequest): AuthToken
}
 
// network/RetrofitClient.kt
package com.example.mymobileapp.network
 
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import java.util.concurrent.TimeUnit
 
object RetrofitClient {
    private const val BASE_URL = "https://api.example.com/"
 
    private val loggingInterceptor = HttpLoggingInterceptor().apply {
        level = HttpLoggingInterceptor.Level.BODY
    }
 
    private val httpClient = OkHttpClient.Builder()
        .addInterceptor(loggingInterceptor)
        .connectTimeout(15, TimeUnit.SECONDS)
        .readTimeout(15, TimeUnit.SECONDS)
        .writeTimeout(15, TimeUnit.SECONDS)
        .build()
 
    val apiService: APIService = Retrofit.Builder()
        .baseUrl(BASE_URL)
        .client(httpClient)
        .addConverterFactory(GsonConverterFactory.create())
        .build()
        .create(APIService::class.java)
}
 
// models/AuthToken.kt
package com.example.mymobileapp.models
 
import com.google.gson.annotations.SerializedName
 
data class AuthToken(
    @SerializedName("access_token")
    val accessToken: String,
    @SerializedName("refresh_token")
    val refreshToken: String,
    @SerializedName("expires_in")
    val expiresIn: Int,
    @SerializedName("token_type")
    val tokenType: String
)
 
data class LoginRequest(
    val email: String,
    val password: String
)

ViewModel and State Management

// viewmodel/AuthViewModel.kt
package com.example.mymobileapp.viewmodel
 
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import dagger.hilt.android.lifecycle.HiltViewModel
import com.example.mymobileapp.network.APIService
import com.example.mymobileapp.models.LoginRequest
import javax.inject.Inject
 
data class AuthUiState(
    val isLoading: Boolean = false,
    val isLoggedIn: Boolean = false,
    val error: String? = null
)
 
@HiltViewModel
class AuthViewModel @Inject constructor(
    private val apiService: APIService
) : ViewModel() {
    private val _uiState = MutableStateFlow(AuthUiState())
    val uiState: StateFlow<AuthUiState> = _uiState.asStateFlow()
 
    fun login(email: String, password: String) {
        viewModelScope.launch {
            try {
                _uiState.value = _uiState.value.copy(isLoading = true)
                val token = apiService.login(LoginRequest(email, password))
                _uiState.value = AuthUiState(isLoggedIn = true)
            } catch (e: Exception) {
                _uiState.value = AuthUiState(error = e.message)
            }
        }
    }
}

Unit Testing

// tests/AuthViewModelTest.kt
package com.example.mymobileapp.viewmodel
 
import org.junit.Test
import org.junit.Assert.*
import org.junit.Before
import io.mockk.coEvery
import io.mockk.mockk
import kotlinx.coroutines.test.runTest
import com.example.mymobileapp.network.APIService
import com.example.mymobileapp.models.AuthToken
 
class AuthViewModelTest {
    private lateinit var viewModel: AuthViewModel
    private val mockApiService: APIService = mockk()
 
    @Before
    fun setUp() {
        viewModel = AuthViewModel(mockApiService)
    }
 
    @Test
    fun testSuccessfulLogin() = runTest {
        // Given
        val mockToken = AuthToken(
            accessToken = "test_token",
            refreshToken = "refresh_token",
            expiresIn = 3600,
            tokenType = "Bearer"
        )
        coEvery { mockApiService.login(any()) } returns mockToken
 
        // When
        viewModel.login("test@example.com", "password123")
 
        // Then
        val state = viewModel.uiState.value
        assertTrue(state.isLoggedIn)
        assertNull(state.error)
    }
}

Using Antigravity Agents for Debugging

Antigravity Lab provides sophisticated debugging capabilities integrated into your development workflow.

Error Analysis Workflow

Step 1: Collect Error Information

// iOS crash reporting
import os.log
 
let logger = os.log(subsystem: "com.example.app", category: "Login")
os_log("Login failed: %{public}@", log: logger, type: .error, errorDescription)
// Android crash logging
import android.util.Log
 
class LoginActivity : AppCompatActivity() {
    companion object {
        private const val TAG = "LoginActivity"
    }
 
    private fun handleLoginError(exception: Exception) {
        Log.e(TAG, "Login error", exception)
        // Report to crash analytics service
    }
}

Step 2: Request Antigravity Analysis

Antigravity Prompt:
"Analyze this crash log from our iOS app:

Fatal Exception: Foundation.DecodingError.dataCorrupted
at APIClient.decodeResponse (APIClient.swift:87)

The crash occurs when parsing authentication tokens.
Current JSON structure: {"access_token": "...", "expires": 3600}
Expected structure: {"accessToken": "...", "expiresIn": 3600}

Provide a complete fix that handles both formats."

Step 3: Implement Antigravity's Recommendations

// Improved Codable with flexible parsing
struct AuthToken: Codable {
    let accessToken: String
    let expiresIn: Int
 
    enum CodingKeys: String, CodingKey {
        case accessToken = "access_token"
        case expiresIn = "expires_in"
    }
 
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
 
        // Try modern format first, then legacy
        if let accessToken = try container.decodeIfPresent(String.self, forKey: .accessToken) {
            self.accessToken = accessToken
        } else if let token = try container.decodeIfPresent(String.self, forKey: CodingKeys(stringValue: "token")) {
            self.accessToken = token
        } else {
            throw DecodingError.keyNotFound(CodingKeys.accessToken, .init(codingPath: decoder.codingPath, debugDescription: "Missing access token"))
        }
 
        self.expiresIn = try container.decode(Int.self, forKey: .expiresIn)
    }
}

Best Practices and Project Setup

Recommended Directory Structure

iOS (Swift)

MyMobileApp/
├── App/
│   ├── MyApp.swift
│   ├── AppDelegate.swift
│   └── SceneDelegate.swift
├── Models/
│   ├── User.swift
│   ├── AuthToken.swift
│   └── APIModels.swift
├── Views/
│   ├── LoginView.swift
│   ├── DashboardView.swift
│   └── Components/
├── ViewModels/
│   ├── AuthViewModel.swift
│   └── DashboardViewModel.swift
├── Services/
│   ├── APIClient.swift
│   ├── KeychainService.swift
│   └── StorageService.swift
├── Utils/
│   ├── Extensions.swift
│   └── Constants.swift
└── Tests/

Android (Kotlin)

app/src/main/kotlin/com/example/app/
├── MainActivity.kt
├── models/
│   ├── User.kt
│   ├── AuthToken.kt
│   └── APIModels.kt
├── ui/
│   ├── screens/
│   │   ├── LoginScreen.kt
│   │   └── DashboardScreen.kt
│   ├── components/
│   └── theme/
├── viewmodel/
│   └── AuthViewModel.kt
├── network/
│   ├── APIService.kt
│   └── RetrofitClient.kt
├── repository/
│   └── AuthRepository.kt
└── utils/
    └── Constants.kt

app/src/test/kotlin/
└── viewmodel/
    └── AuthViewModelTest.kt

Naming Conventions

Follow these conventions for consistency:

ElementConventionExample
ClassesPascalCaseLoginViewController, UserAuthService
FunctionscamelCasefetchUserData(), validateEmail()
ConstantsUPPER_SNAKE_CASEMAX_RETRIES, API_BASE_URL
VariablescamelCaseuserName, isLoading
Private membersLeading underscore (Swift) or private keyword (Kotlin)_viewModel, private val repository

Version Control Setup

# Initialize Git repository
git init
 
# Configure .gitignore
cat > .gitignore << EOF
# Build artifacts
build/
dist/
*.o
*.a
 
# IDE
.vscode/
.idea/
*.xcworkspace/
 
# OS
.DS_Store
Thumbs.db
 
# Dependencies
Pods/
.gradle/
node_modules/
 
# Environment
.env
*.local
EOF
 
# Initial commit
git add .
git commit -m "Initial project setup: iOS and Android development environment"

Deployment and Release

iOS App Store Deployment

Prepare for Release

# Update version number in Xcode
# Set Marketing Version: 1.0.0
# Set Build Number: 1
 
# Create App Store Connect Record
# 1. Visit https://appstoreconnect.apple.com
# 2. Create new app bundle ID
# 3. Configure app information
 
# Create Distribution Certificate
# Use Xcode or Apple Developer portal

Build and Upload

# Archive the application
xcodebuild -scheme MyMobileApp \
  -configuration Release \
  -archivePath MyMobileApp.xcarchive archive
 
# Export for app store
xcodebuild -exportArchive \
  -archivePath MyMobileApp.xcarchive \
  -exportOptionsPlist ExportOptions.plist \
  -exportPath ./build
 
# Upload using Transporter app or altool
xcrun altool --upload-app \
  -f MyMobileApp.ipa \
  -t ios \
  -u <apple_id> \
  -p <app_specific_password>

Android Google Play Deployment

Generate Release Key

# Create keystore (if first release)
keytool -genkey -v \
  -keystore release.keystore \
  -keyalg RSA \
  -keysize 2048 \
  -validity 10000 \
  -alias release_key
 
# Build signed APK/AAB
./gradlew bundleRelease \
  -Pandroid.injected.signing.store.file=release.keystore \
  -Pandroid.injected.signing.store.password=<password> \
  -Pandroid.injected.signing.key.alias=release_key \
  -Pandroid.injected.signing.key.password=<key_password>

Upload to Play Store

1. Sign in to Google Play Console
2. Select your app
3. Go to Release → Production
4. Upload the signed AAB file
5. Review store listing and content rating
6. Submit for review

Beta Testing

iOS TestFlight

  • Add testers in App Store Connect
  • Build distribution archive
  • Submit build for review
  • Share TestFlight link with testers

Android Internal Testing

  • Enable internal testing in Play Console
  • Upload signed AAB
  • Add internal testers
  • Gather feedback and crash reports
Share

Thank You for Reading

Antigravity Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

App Dev2026-07-03
When Universal Links Break Silently — Catching Association Drift with an Agent-Run Verification Gate
Universal Links and App Links fall back to the browser with no error when your association files or entitlements drift apart. Here is a design that generates the files from one source of truth and hands weekly and pre-release checks to an agent.
App Dev2026-06-23
The Review Prompt Fired but Nothing Appeared — Designing Around Play In-App Review's Quota and No-Show Guarantee
Play In-App Review's launchReviewFlow can succeed without ever showing a dialog. This walks through the three traps — quota, no display guarantee, and silent testing — and the engagement-based trigger design that fires at the right moment without colliding with ads, with steps to have Antigravity implement it.
App Dev2026-06-23
When StoreKit 2 Users Say They Paid but Can't Access — Field Notes on Subscription Entitlement Drift
StoreKit 2 subscriptions are harder to operate than to implement. This is a record of the drift between currentEntitlements, subscription.status, and server notifications — and the reconcile logic that finally stopped the support tickets.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →