<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	xmlns:media="http://search.yahoo.com/mrss/" >

<channel>
	<title>Flutter &#8211; GTStudios</title>
	<atom:link href="https://gtstu.com/tag/flutter/feed/" rel="self" type="application/rss+xml" />
	<link>https://gtstu.com</link>
	<description>apps. web. art.</description>
	<lastBuildDate>Sat, 08 Aug 2026 09:21:19 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=7.0.3</generator>

<image>
	<url>https://gtstu.com/wp-content/uploads/2025/09/cropped-gtwebs-icon-300x300-1-100x100.png</url>
	<title>Flutter &#8211; GTStudios</title>
	<link>https://gtstu.com</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>Cross-Platform vs Native App Development: A Founder&#8217;s Guide</title>
		<link>https://gtstu.com/cross-platform-vs-native-app-development-guide-for-founders/</link>
		
		<dc:creator><![CDATA[Gremmy T.]]></dc:creator>
		<pubDate>Fri, 24 Jul 2026 20:22:14 +0000</pubDate>
				<category><![CDATA[App Development]]></category>
		<category><![CDATA[app development for founders]]></category>
		<category><![CDATA[cross-platform development]]></category>
		<category><![CDATA[Flutter]]></category>
		<category><![CDATA[native app development]]></category>
		<category><![CDATA[React Native]]></category>
		<guid isPermaLink="false">https://gtstu.com/?p=5675</guid>

					<description><![CDATA[<p>If you&#8217;re a founder scoping your first app, one of the earliest decisions you&#8217;ll make isn&#8217;t about features — it&#8217;s about how the app gets built. Should you hire one team to write a single codebase that runs on both iPhone and Android, or two separate teams building fully native apps for each platform? The ... </p>
<p class="read-more-container"><a title="Cross-Platform vs Native App Development: A Founder&#8217;s Guide" class="read-more button" href="https://gtstu.com/cross-platform-vs-native-app-development-guide-for-founders/#more-5675" aria-label="Read more about Cross-Platform vs Native App Development: A Founder&#8217;s Guide">Read More</a></p>
<p>The post <a rel="nofollow" href="https://gtstu.com/cross-platform-vs-native-app-development-guide-for-founders/">Cross-Platform vs Native App Development: A Founder&#8217;s Guide</a> appeared first on <a rel="nofollow" href="https://gtstu.com">GTStudios</a>.</p>
]]></description>
										<content:encoded><![CDATA[<p class="wp-block-paragraph">If you&#8217;re a founder scoping your first app, one of the earliest decisions you&#8217;ll make isn&#8217;t about features — it&#8217;s about how the app gets built. Should you hire one team to write a single codebase that runs on both iPhone and Android, or two separate teams building fully native apps for each platform? The answer shapes your budget, your timeline, and how fast you can ship updates for years to come.</p>
<p class="wp-block-paragraph">This guide explains the difference in plain English, walks through how to decide, and points out the mistakes founders commonly make when they get this call wrong.</p>
<figure data-wp-context="{&quot;imageId&quot;:&quot;6a7a08f958aca&quot;}" data-wp-interactive="core/image" data-wp-key="6a7a08f958aca" class="wp-block-image size-large wp-lightbox-container" style="max-width:680px;margin-left:auto;margin-right:auto"><img decoding="async" data-wp-class--hide="state.isContentHidden" data-wp-class--show="state.isContentVisible" data-wp-init="callbacks.setButtonStyles" data-wp-on--click="actions.showLightbox" data-wp-on--load="callbacks.setButtonStyles" data-wp-on--pointerdown="actions.preloadImage" data-wp-on--pointerenter="actions.preloadImageWithDelay" data-wp-on--pointerleave="actions.cancelPreload" data-wp-on-window--resize="callbacks.setButtonStyles" src="https://gtstu.com/wp-content/uploads/2026/07/cross-platform-vs-native-app-development-2.jpg" alt="Cross-platform vs native app development" style="max-height:520px;width:auto;max-width:100%;display:block;margin:0 auto"/><button
			class="lightbox-trigger"
			type="button"
			aria-haspopup="dialog"
			data-wp-bind--aria-label="state.thisImage.triggerButtonAriaLabel"
			data-wp-init="callbacks.initTriggerButton"
			data-wp-on--click="actions.showLightbox"
			data-wp-style--right="state.thisImage.buttonRight"
			data-wp-style--top="state.thisImage.buttonTop"
		>
			<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="none" viewBox="0 0 12 12">
				<path fill="#fff" d="M2 0a2 2 0 0 0-2 2v2h1.5V2a.5.5 0 0 1 .5-.5h2V0H2Zm2 10.5H2a.5.5 0 0 1-.5-.5V8H0v2a2 2 0 0 0 2 2h2v-1.5ZM8 12v-1.5h2a.5.5 0 0 0 .5-.5V8H12v2a2 2 0 0 1-2 2H8Zm2-12a2 2 0 0 1 2 2v2h-1.5V2a.5.5 0 0 0-.5-.5H8V0h2Z" />
			</svg>
		</button><figcaption class="wp-element-caption"><em>Photo: Thamizhpparithi Maari / CC BY-SA 4.0, via Wikimedia Commons</em></figcaption></figure>
<h2 class="wp-block-heading">Quick Answer</h2>
<p class="wp-block-paragraph">Cross-platform development (using tools like Flutter or React Native) lets you build one codebase that runs on both iOS and Android, which is usually faster and cheaper to build and maintain. Native development means writing separate apps in Swift/SwiftUI for iOS and Kotlin/Jetpack Compose for Android, which typically costs more but gives you the best possible performance, the fastest access to new OS features, and the smoothest platform-specific feel. Most early-stage startups start cross-platform and only move to native when a specific, provable need arises.</p>
<h2 class="wp-block-heading">What&#8217;s Actually Different, in Plain English</h2>
<p class="wp-block-paragraph">&#8220;Native&#8221; means the app is written directly in the language and tools each phone maker provides — Swift (and the SwiftUI framework) for Apple devices, and Kotlin (with Jetpack Compose) for Android. Because the code talks directly to the operating system, native apps tend to feel the most polished, launch the fastest, and get first access to new hardware or OS features like camera upgrades or widgets.</p>
<p class="wp-block-paragraph">&#8220;Cross-platform&#8221; means writing the app once in a shared language — Dart for Flutter, or JavaScript/TypeScript for React Native — and having that single codebase compile down to both iOS and Android. Modern cross-platform tools have closed most of the performance gap: Flutter renders its own UI with Google&#8217;s Impeller graphics engine, and React Native&#8217;s newer architecture removed the old performance bottleneck (the &#8220;bridge&#8221;) between JavaScript and native code. For the vast majority of business apps — booking tools, marketplaces, content apps, internal tools — users won&#8217;t notice a meaningful difference in day-to-day performance.</p>
<p class="wp-block-paragraph">There&#8217;s also a middle path worth knowing about: Kotlin Multiplatform (KMP) lets teams share business logic across platforms while still writing native UI on each side, which some teams use to get native feel with less duplicated logic.</p>
<h2 class="wp-block-heading">How to Decide: Key Factors for Founders</h2>
<p class="wp-block-paragraph">Budget and timeline: building two native apps means paying for two specialized teams (an iOS developer and an Android developer) working in parallel, or building one platform first and porting later. Cross-platform lets one team ship to both platforms from a single codebase, which is usually the faster and cheaper route to a testable product — a real consideration if you&#8217;re trying to validate an idea before raising more money.</p>
<p class="wp-block-paragraph">Team and hiring: React Native draws from the huge pool of JavaScript/web developers, which can make hiring or contracting easier and cheaper. Flutter has a smaller but fast-growing developer pool. Native development requires specialists in Swift and Kotlin separately, who tend to command higher rates and are harder to find on a tight timeline.</p>
<p class="wp-block-paragraph">Performance and platform depth: if your app is graphics-intensive (games, AR, heavy animation), does deep hardware integration (camera processing, Bluetooth accessories, background audio), or needs to be first to support brand-new OS features, native gives you more direct control and fewer workarounds. For most CRUD-style, content, or transactional apps, this rarely matters.</p>
<p class="wp-block-paragraph">Long-term plans: if you know you&#8217;ll eventually need best-in-class performance or platform-specific UI on both iOS and Android, some teams choose native from day one to avoid a costly rewrite later. But many successful apps launched cross-platform and never needed to switch — the code quality of your team usually matters more than the framework.</p>
<figure data-wp-context="{&quot;imageId&quot;:&quot;6a7a08f958e61&quot;}" data-wp-interactive="core/image" data-wp-key="6a7a08f958e61" class="wp-block-image size-large wp-lightbox-container" style="max-width:680px;margin-left:auto;margin-right:auto"><img decoding="async" data-wp-class--hide="state.isContentHidden" data-wp-class--show="state.isContentVisible" data-wp-init="callbacks.setButtonStyles" data-wp-on--click="actions.showLightbox" data-wp-on--load="callbacks.setButtonStyles" data-wp-on--pointerdown="actions.preloadImage" data-wp-on--pointerenter="actions.preloadImageWithDelay" data-wp-on--pointerleave="actions.cancelPreload" data-wp-on-window--resize="callbacks.setButtonStyles" src="https://gtstu.com/wp-content/uploads/2026/07/cross-platform-vs-native-app-development-3.jpg" alt="Cross-platform vs native app development" style="max-height:520px;width:auto;max-width:100%;display:block;margin:0 auto"/><button
			class="lightbox-trigger"
			type="button"
			aria-haspopup="dialog"
			data-wp-bind--aria-label="state.thisImage.triggerButtonAriaLabel"
			data-wp-init="callbacks.initTriggerButton"
			data-wp-on--click="actions.showLightbox"
			data-wp-style--right="state.thisImage.buttonRight"
			data-wp-style--top="state.thisImage.buttonTop"
		>
			<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="none" viewBox="0 0 12 12">
				<path fill="#fff" d="M2 0a2 2 0 0 0-2 2v2h1.5V2a.5.5 0 0 1 .5-.5h2V0H2Zm2 10.5H2a.5.5 0 0 1-.5-.5V8H0v2a2 2 0 0 0 2 2h2v-1.5ZM8 12v-1.5h2a.5.5 0 0 0 .5-.5V8H12v2a2 2 0 0 1-2 2H8Zm2-12a2 2 0 0 1 2 2v2h-1.5V2a.5.5 0 0 0-.5-.5H8V0h2Z" />
			</svg>
		</button><figcaption class="wp-element-caption"><em>Photo: Thamizhpparithi Maari / CC BY-SA 4.0, via Wikimedia Commons</em></figcaption></figure>
<h2 class="wp-block-heading">Tips / Common Mistakes</h2>
<p class="wp-block-paragraph">Don&#8217;t choose native &#8220;just in case&#8221; without a concrete reason — it roughly doubles your ongoing engineering cost (two codebases to build, test, and maintain) and slows down every feature release, since every change has to be built twice.</p>
<p class="wp-block-paragraph">Don&#8217;t assume cross-platform means low quality — apps built with Flutter and React Native power many well-known, high-traffic consumer apps today. The framework is rarely the reason an app feels cheap; inconsistent design and rushed UX usually are.</p>
<p class="wp-block-paragraph">Do prototype with your actual use case before committing. If your app leans heavily on animation, camera, or background processing, build a small proof-of-concept in your candidate framework before signing a full development contract.</p>
<p class="wp-block-paragraph">Do ask any agency or freelancer why they&#8217;re recommending a specific framework for your app, not just their default. A good answer references your actual requirements — budget, timeline, platform priority, and technical complexity — not just &#8220;this is what we always use.&#8221;</p>
<p class="wp-block-paragraph">Avoid switching frameworks mid-build. Re-platforming after development has started is one of the most expensive mistakes founders make; it&#8217;s cheaper to spend an extra week evaluating up front.</p>
<p class="wp-block-paragraph">Explore more: <a href="https://gtstu.com/category/app-development/">More app development guides</a>.</p>
<h2 class="wp-block-heading">Cross-platform vs native app development FAQs</h2>
<h3 class="wp-block-heading">Is cross-platform app development good enough for a real business, or just for MVPs?</h3>
<p class="wp-block-paragraph">Cross-platform frameworks like Flutter and React Native are used in production by many established, high-traffic apps, not just prototypes. They&#8217;re a legitimate long-term choice for most business apps, not only a way to test an idea cheaply.</p>
<h3 class="wp-block-heading">Which is cheaper: cross-platform or native app development?</h3>
<p class="wp-block-paragraph">Cross-platform is generally cheaper to build and maintain because one team and one codebase cover both iOS and Android. Native development usually costs more since it requires separate specialists and separate codebases for each platform.</p>
<h3 class="wp-block-heading">Can I start cross-platform and switch to native later?</h3>
<p class="wp-block-paragraph">Yes, some companies do this once they hit a specific, provable performance or feature limitation. It&#8217;s a real rewrite, though, so it&#8217;s worth confirming the limitation is actually caused by the framework — and not by app design or team execution — before committing to the switch.</p>
<h2 class="wp-block-heading">Build It With GTStudios</h2>
<p class="wp-block-paragraph">Need help with your website, app, or small-business tech? GTStudios builds web, apps, and software for small businesses. <a href="https://gtstu.com/services/" target="_blank" rel="noopener">See how GTStudios can help</a>.</p>


<p class="wp-block-paragraph"><em>Photo: Pooja dhamo / CC BY 4.0, via <a href="https://commons.wikimedia.org/wiki/File%3AExpert%20Mobile%20App%20Development%20Consulting%20Company%20in%20Houston.jpg" target="_blank" rel="noopener nofollow">Wikimedia Commons</a>.</em></p><p><a class="a2a_button_facebook" href="https://www.addtoany.com/add_to/facebook?linkurl=https%3A%2F%2Fgtstu.com%2Fcross-platform-vs-native-app-development-guide-for-founders%2F&amp;linkname=Cross-Platform%20vs%20Native%20App%20Development%3A%20A%20Founder%E2%80%99s%20Guide" title="Facebook" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_x" href="https://www.addtoany.com/add_to/x?linkurl=https%3A%2F%2Fgtstu.com%2Fcross-platform-vs-native-app-development-guide-for-founders%2F&amp;linkname=Cross-Platform%20vs%20Native%20App%20Development%3A%20A%20Founder%E2%80%99s%20Guide" title="X" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_linkedin" href="https://www.addtoany.com/add_to/linkedin?linkurl=https%3A%2F%2Fgtstu.com%2Fcross-platform-vs-native-app-development-guide-for-founders%2F&amp;linkname=Cross-Platform%20vs%20Native%20App%20Development%3A%20A%20Founder%E2%80%99s%20Guide" title="LinkedIn" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_sms" href="https://www.addtoany.com/add_to/sms?linkurl=https%3A%2F%2Fgtstu.com%2Fcross-platform-vs-native-app-development-guide-for-founders%2F&amp;linkname=Cross-Platform%20vs%20Native%20App%20Development%3A%20A%20Founder%E2%80%99s%20Guide" title="Message" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_email" href="https://www.addtoany.com/add_to/email?linkurl=https%3A%2F%2Fgtstu.com%2Fcross-platform-vs-native-app-development-guide-for-founders%2F&amp;linkname=Cross-Platform%20vs%20Native%20App%20Development%3A%20A%20Founder%E2%80%99s%20Guide" title="Email" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_copy_link" href="https://www.addtoany.com/add_to/copy_link?linkurl=https%3A%2F%2Fgtstu.com%2Fcross-platform-vs-native-app-development-guide-for-founders%2F&amp;linkname=Cross-Platform%20vs%20Native%20App%20Development%3A%20A%20Founder%E2%80%99s%20Guide" title="Copy Link" rel="nofollow noopener" target="_blank"></a><a class="a2a_dd addtoany_share_save addtoany_share" href="https://www.addtoany.com/share#url=https%3A%2F%2Fgtstu.com%2Fcross-platform-vs-native-app-development-guide-for-founders%2F&#038;title=Cross-Platform%20vs%20Native%20App%20Development%3A%20A%20Founder%E2%80%99s%20Guide" data-a2a-url="https://gtstu.com/cross-platform-vs-native-app-development-guide-for-founders/" data-a2a-title="Cross-Platform vs Native App Development: A Founder’s Guide"></a></p><p>The post <a rel="nofollow" href="https://gtstu.com/cross-platform-vs-native-app-development-guide-for-founders/">Cross-Platform vs Native App Development: A Founder&#8217;s Guide</a> appeared first on <a rel="nofollow" href="https://gtstu.com">GTStudios</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Offline-First Flutter App With SQLite: Complete 2026 Guide</title>
		<link>https://gtstu.com/offline-first-flutter-app-sqlite/</link>
		
		<dc:creator><![CDATA[Gremmy T.]]></dc:creator>
		<pubDate>Wed, 17 Jun 2026 08:29:04 +0000</pubDate>
				<category><![CDATA[App Development]]></category>
		<category><![CDATA[drift]]></category>
		<category><![CDATA[Flutter]]></category>
		<category><![CDATA[Mobile Development]]></category>
		<category><![CDATA[Offline-First]]></category>
		<category><![CDATA[sqflite]]></category>
		<category><![CDATA[SQLite]]></category>
		<guid isPermaLink="false">https://gtstu.com/?p=5109</guid>

					<description><![CDATA[<p>Users expect apps to work in tunnels, on planes, and anywhere signal drops. Building offline-first means making local storage the source of truth — the network is just the mechanism that keeps that truth in sync across devices. Get this right and users never see a blank screen or an error banner just because they ... </p>
<p class="read-more-container"><a title="Offline-First Flutter App With SQLite: Complete 2026 Guide" class="read-more button" href="https://gtstu.com/offline-first-flutter-app-sqlite/#more-5109" aria-label="Read more about Offline-First Flutter App With SQLite: Complete 2026 Guide">Read More</a></p>
<p>The post <a rel="nofollow" href="https://gtstu.com/offline-first-flutter-app-sqlite/">Offline-First Flutter App With SQLite: Complete 2026 Guide</a> appeared first on <a rel="nofollow" href="https://gtstu.com">GTStudios</a>.</p>
]]></description>
										<content:encoded><![CDATA[<p class="wp-block-paragraph">Users expect apps to work in tunnels, on planes, and anywhere signal drops. Building offline-first means making local storage the source of truth — the network is just the mechanism that keeps that truth in sync across devices. Get this right and users never see a blank screen or an error banner just because they stepped into a basement.</p>
<p class="wp-block-paragraph">This guide covers every practical layer of a production-ready offline-first Flutter app: picking the right local database, designing a sync-ready schema (including the primary-key decision most tutorials skip), safe CRUD patterns, a pending-ops sync queue, background sync when the app is closed, conflict resolution, and schema migrations — with package versions and platform behavior verified against current documentation, not guesswork.</p>
<h2 class="wp-block-heading">Quick Answer</h2>
<p class="wp-block-paragraph">Use sqflite (v2.4.3, a Flutter Favorite from Tekartik) or Drift (v2.34.x) with connectivity_plus to build an offline-first Flutter app. Write every change to SQLite first with a synced = 0 flag and a UUID primary key, queue the change in a pending_ops table, then flush that queue to your API whenever connectivity_plus reports an active interface — always confirming real internet access with a lightweight HEAD request first, since a Wi-Fi connection with no upstream route (a hotel captive portal, for example) still reports &#8216;connected.&#8217;</p>
<h2 class="wp-block-heading">Why Offline-First Changes Your Architecture</h2>
<p class="wp-block-paragraph">In an online-first app the network comes first: the UI blocks on a fetch, caches the result, and shows a cached fallback on failure. In an offline-first app the local database comes first — every read and write hits SQLite immediately, and the network becomes a background concern. Users get instant responses regardless of signal strength.</p>
<p class="wp-block-paragraph">The practical implication is that every table needing remote persistence must carry sync metadata: a synced flag, an updated_at timestamp stored in UTC milliseconds, and ideally a version counter. Deletes should be soft (is_deleted = 1) rather than hard, so the sync engine can propagate the removal to the server before the row disappears locally. These design decisions — sync flags, soft deletes, and sync-safe primary keys — are what separate a real offline-first app from one that just caches API responses.</p>
<h2 class="wp-block-heading">Choosing Your Local Database: sqflite vs. Drift vs. Hive/Isar</h2>
<p class="wp-block-paragraph">Flutter does not use expo-sqlite — that package belongs to the React Native/Expo ecosystem and doesn&#8217;t exist on pub.dev. For raw SQL control, sqflite (Tekartik, v2.4.3) is the standard choice: it runs natively on Android, iOS, and macOS and moves database I/O to a background thread automatically. For Linux or Windows, add sqflite_common_ffi and call sqfliteFfiInit() before opening any database; experimental web support comes via sqflite_common_ffi_web, which stores data in browser IndexedDB.</p>
<p class="wp-block-paragraph">Drift (v2.34.x) sits on top of sqlite3 and is now the more common default for new projects: it generates type-safe Dart from your schema, exposes reactive Streams so your UI updates automatically when rows change, handles isolate threading for you, and runs on every platform Flutter targets, web included. If you want compile-time-checked queries and reactive rebuilds without hand-rolling them, start with Drift; if you want direct SQL and the smallest possible dependency footprint, sqflite is still solid.</p>
<p class="wp-block-paragraph">Avoid Hive and Isar for new offline-first projects in 2026. Hive&#8217;s development has stalled, and Isar&#8217;s original repository is unmaintained by its author — a community fork (isar_community on pub.dev) keeps it alive, but adopting it means taking on a smaller, less certain maintenance guarantee than SQL-backed options. sqflite and Drift are both actively maintained and are the safer long-term bet.</p>
<h2 class="wp-block-heading">Design a Sync-Ready Data Model</h2>
<p class="wp-block-paragraph">Bake sync metadata into every table from the start: CREATE TABLE tasks(id TEXT PRIMARY KEY, title TEXT NOT NULL, updated_at INTEGER NOT NULL, synced INTEGER NOT NULL DEFAULT 0, is_deleted INTEGER NOT NULL DEFAULT 0). Store updated_at in UTC milliseconds — never local device time — so timestamps from devices in different time zones compare correctly.</p>
<p class="wp-block-paragraph">Mirror the schema in a Dart model with toMap() and fromMap() helpers that feed directly into db.insert(), db.update(), and query results. Keep the model thin — field mapping only, no business logic. Add an index on synced so your sync query (WHERE synced = 0) scans only unsynced rows: CREATE INDEX idx_tasks_synced ON tasks(synced). On tables with thousands of rows this index is the difference between a sub-millisecond lookup and a full-table scan on every sync cycle.</p>
<h2 class="wp-block-heading">UUID or Auto-Increment: Choosing a Primary Key for Sync</h2>
<p class="wp-block-paragraph">This is the decision most offline-first tutorials skip, and it causes real bugs later. INTEGER PRIMARY KEY AUTOINCREMENT is fine for a single-device, single-source app, but it breaks the moment two offline devices each create a row — both assign id = 1, and your server has no way to tell them apart when both sync up.</p>
<p class="wp-block-paragraph">Generate a UUID on the client at creation time and use it as the primary key, both locally and on the server. The current uuid package on pub.dev supports v4 (fully random) and v7 (time-ordered) generation. Use v4 for a simple random identifier; use v7 when you also want IDs that sort roughly by creation time, which helps with pagination and index locality on the server. Either variant makes every insert globally unique with zero coordination, so sync becomes a pure upsert — the server never needs to remap a client-generated ID to a server-generated one. The tradeoff is a slightly larger primary key (16 bytes vs. 4-8) and non-sequential storage, which is a fair price for eliminating an entire class of sync bugs.</p>
<h2 class="wp-block-heading">Build the Database Helper and CRUD Layer</h2>
<p class="wp-block-paragraph">Open the database once and reuse that single instance across the app — never open a fresh connection per operation, since sqflite serializes writes and repeated open/close calls add latency for no benefit. A typical helper wraps openDatabase() in a singleton, defines onCreate for the initial schema, and defines onUpgrade for migrations (covered below).</p>
<p class="wp-block-paragraph">Every CRUD call should touch two things at once: the actual row, and its sync metadata. On insert, set synced = 0 and updated_at = DateTime.now().toUtc().millisecondsSinceEpoch. On update, bump updated_at again and reset synced to 0 so the row is picked up on the next sync pass. On delete, don&#8217;t call db.delete() directly — set is_deleted = 1 and synced = 0 instead, so the row&#8217;s removal can still be pushed to the server before it&#8217;s purged locally. Wrap multi-row writes in db.transaction() so a partial failure can&#8217;t leave the local database in a half-synced state.</p>
<h2 class="wp-block-heading">Syncing to Your Backend With a Pending-Ops Queue</h2>
<p class="wp-block-paragraph">A synced column alone only tells you a row is dirty — it doesn&#8217;t tell you what happened to it. A dedicated pending_ops table (id, entity_table, entity_id, op_type, payload, created_at) records every insert, update, and delete as a discrete, ordered operation. That matters because a row can be created and then deleted while still offline; a single synced flag can&#8217;t represent that sequence, but two rows in pending_ops can.</p>
<p class="wp-block-paragraph">Flush the queue in created_at order: send each operation, mark it complete (or delete it) only after the server confirms, and leave it in place on failure so the next sync attempt retries it. This also gives you a natural place to add exponential backoff and per-operation retry counts without touching your core tables.</p>
<h2 class="wp-block-heading">Detect Real Connectivity, Not Just a Wi-Fi Icon</h2>
<p class="wp-block-paragraph">connectivity_plus reports which network interface is active — Wi-Fi, mobile, ethernet, or none — but it does not confirm that interface can actually reach the internet. A device on hotel or airport Wi-Fi behind a captive portal, or on a Wi-Fi network with a dead upstream link, will still report a connected interface with zero real connectivity.</p>
<p class="wp-block-paragraph">Treat connectivity_plus as a trigger to attempt sync, not proof that sync will succeed. Before flushing the pending_ops queue, send a lightweight HEAD request to your own API&#8217;s health endpoint (or use a package like internet_connection_checker_plus, which layers real reachability checks on top of connectivity_plus). If that check fails, hold the queue and retry on the next connectivity change event instead of burning a failed sync attempt.</p>
<h2 class="wp-block-heading">Running Sync in the Background When the App Is Closed</h2>
<p class="wp-block-paragraph">For sync to run when the app isn&#8217;t in the foreground, use the workmanager package, which wraps Android&#8217;s WorkManager and iOS&#8217;s BGTaskScheduler behind one Dart API. The two platforms behave very differently, and treating them the same is a common source of &#8216;background sync doesn&#8217;t work&#8217; bugs.</p>
<p class="wp-block-paragraph">On Android, WorkManager enforces a 15-minute minimum interval for periodic work, and the system generally honors it closely on stock Android. On iOS, BGTaskScheduler is best-effort only: you register a preferred interval, but iOS decides the actual run time based on battery level, network conditions, and the user&#8217;s usage patterns, and it can delay or skip a run entirely. iOS also distinguishes BGAppRefreshTask (short work, roughly 30 seconds) from BGProcessingTask (longer jobs the OS runs when the device is idle and charging). Design your sync logic to be idempotent and safe to run late — never assume a background sync fires on a predictable schedule on iOS, and always let a foreground app-resume trigger an immediate sync as a fallback.</p>
<h2 class="wp-block-heading">Conflict Resolution: Choosing a Strategy</h2>
<p class="wp-block-paragraph">The simplest strategy is last-write-wins: compare the updated_at timestamp on the client&#8217;s pending change against the server&#8217;s stored value, and keep whichever is newer. This works well for single-owner data like personal notes or settings, where the odds of a true concurrent edit are low and the stakes of losing a stale write are minor.</p>
<p class="wp-block-paragraph">For records that multiple users or devices genuinely edit concurrently, last-write-wins can silently discard real work. A version counter (bump it on every write, reject a push whose base version doesn&#8217;t match the server&#8217;s current version) turns a silent overwrite into a detectable conflict you can surface to the user or merge field-by-field. For structured records, field-level merging — comparing each column&#8217;s updated_at independently rather than the whole row — preserves more of both edits and is worth the extra complexity for high-collaboration entities like shared task lists or shared documents.</p>
<h2 class="wp-block-heading">Schema Migrations Without Losing Local Data</h2>
<p class="wp-block-paragraph">openDatabase() takes a version number and an onUpgrade(db, oldVersion, newVersion) callback that sqflite calls automatically when it detects the on-disk schema version is behind the version your code declares. Inside onUpgrade, run the specific ALTER TABLE statements needed to move from each old version to the next — modern SQLite supports ALTER TABLE &#8230; ADD COLUMN directly, plus RENAME COLUMN (added in SQLite 3.25.0) and DROP COLUMN (added in SQLite 3.35.0), which are both well within the SQLite versions bundled with current sqflite and Drift releases. DROP COLUMN has limits, though: it only works on a column that isn&#8217;t part of a primary key, a UNIQUE constraint, an index, or referenced by a trigger, view, CHECK constraint, or foreign key. For those cases, or for anything ALTER TABLE can&#8217;t express (like changing a column&#8217;s type), you still need to create a new table, copy the data across, and drop the old one.</p>
<p class="wp-block-paragraph">Write migrations as a sequence of if (oldVersion < N) blocks rather than one big conditional, so a device that's several versions behind runs every intermediate migration in order. Always test the upgrade path from your oldest still-supported schema version, not just from the previous release — a user who hasn't opened the app in eight months will jump several versions at once.</p>
<h2 class="wp-block-heading">Tips and Common Mistakes</h2>
<p class="wp-block-paragraph">Don&#8217;t call setState or rebuild the whole list on every sync tick — query only the rows that changed, or use Drift&#8217;s reactive Streams so the UI updates itself. Don&#8217;t skip the synced index; on a table with more than a few hundred rows, an unindexed WHERE synced = 0 scan on every sync cycle is a measurable performance hit. Don&#8217;t trust connectivity_plus alone to gate a sync attempt — pair it with a real reachability check. And don&#8217;t hard-delete rows the moment a user deletes them locally; a soft delete that&#8217;s purged only after server confirmation is what makes deletes actually sync instead of silently disappearing from one device and reappearing after the next pull.</p>
<h2 class="wp-block-heading">offline-first flutter app with sqlite FAQs</h2>
<h3 class="wp-block-heading">Is expo_sqlite available for Flutter?</h3>
<p class="wp-block-paragraph">No. expo-sqlite is a React Native/Expo-only package and isn&#8217;t published for Flutter. The Flutter equivalent is sqflite (or Drift if you want a type-safe query layer on top of SQLite).</p>
<h3 class="wp-block-heading">Which platforms does sqflite support?</h3>
<p class="wp-block-paragraph">sqflite supports Android, iOS, and macOS natively. Linux, Windows, and the Dart VM are supported via the companion sqflite_common_ffi package, and web support is available experimentally through sqflite_common_ffi_web, which persists data to browser IndexedDB.</p>
<h3 class="wp-block-heading">How do I handle conflicts when two devices edit the same record offline?</h3>
<p class="wp-block-paragraph">For low-collision data, compare updated_at timestamps and keep the newer write (last-write-wins). For records multiple users actively co-edit, use a version counter to detect conflicting writes, or merge field-by-field using each column&#8217;s own updated_at rather than resolving at the whole-row level.</p>
<h3 class="wp-block-heading">How do I migrate a sqflite database schema when I add a new column?</h3>
<p class="wp-block-paragraph">Bump the version number passed to openDatabase() and add a check inside onUpgrade — if (oldVersion < newVersion) db.execute('ALTER TABLE tasks ADD COLUMN priority INTEGER DEFAULT 0'). Modern SQLite also supports ALTER TABLE ... RENAME COLUMN (since 3.25.0) and DROP COLUMN (since 3.35.0) directly. DROP COLUMN can't be used on a column that's part of a primary key, a UNIQUE constraint, an index, or referenced by a trigger, view, or CHECK/foreign-key constraint — those cases still require creating a new table and copying the data over.</p>
<h3 class="wp-block-heading">Does connectivity_plus guarantee that the internet is reachable?</h3>
<p class="wp-block-paragraph">No. It only reports which network interface is active (Wi-Fi, mobile data, none), not whether that interface has real upstream internet access — a captive portal or dead Wi-Fi link still reads as &#8216;connected.&#8217; Confirm reachability separately with a lightweight HEAD request or a package like internet_connection_checker_plus before syncing.</p>
<h3 class="wp-block-heading">Should I use sqflite or Drift for a new Flutter project?</h3>
<p class="wp-block-paragraph">Drift (currently v2.34.x) is the more common default in 2026 because it adds type-safe generated queries, reactive Streams, automatic isolate threading, and full web support on top of SQLite. Choose sqflite instead if you want to write raw SQL directly with the smallest possible dependency surface.</p>
<h3 class="wp-block-heading">How do I safely delete records in an offline-first app?</h3>
<p class="wp-block-paragraph">Use a soft delete: set is_deleted = 1 and synced = 0 instead of calling db.delete(). Your sync engine pushes the deletion to the server, and only after that push is confirmed should the row be purged from the local database and, if applicable, from the server.</p>
<h3 class="wp-block-heading">Why should I use a pending_ops table instead of just a synced column?</h3>
<p class="wp-block-paragraph">A synced flag tells you a row is dirty, but not what happened to it. A pending_ops table records each insert, update, and delete as its own ordered operation, which correctly captures sequences like &#8216;created then deleted while offline&#8217; that a single synced flag on the row can&#8217;t represent.</p>
<h3 class="wp-block-heading">Should I use UUIDs or auto-increment IDs for offline-first primary keys?</h3>
<p class="wp-block-paragraph">Use UUIDs. Auto-increment IDs collide the moment two offline devices each create a row starting from id = 1. A client-generated UUID (v4 for random, v7 if you also want rough time-ordering) makes every row globally unique with no server coordination, turning sync into a simple upsert.</p>
<h3 class="wp-block-heading">How do I sync data in the background when the app is closed?</h3>
<p class="wp-block-paragraph">Use the workmanager package, which wraps Android&#8217;s WorkManager (minimum 15-minute interval, generally honored) and iOS&#8217;s BGTaskScheduler (best-effort only, with no guaranteed run time). Write sync logic that&#8217;s idempotent and safe to run late, and also trigger a sync on app resume as a fallback, since iOS background runs can be delayed or skipped entirely.</p>
<h2 class="wp-block-heading">Build It With GTStudios</h2>
<p class="wp-block-paragraph">Need help with your website, app, or small-business tech? GTStudios builds web, apps, and software for small businesses. <a href="https://gtstu.com/services/" target="_blank" rel="noopener">See how GTStudios can help</a>.</p><p><a class="a2a_button_facebook" href="https://www.addtoany.com/add_to/facebook?linkurl=https%3A%2F%2Fgtstu.com%2Foffline-first-flutter-app-sqlite%2F&amp;linkname=Offline-First%20Flutter%20App%20With%20SQLite%3A%20Complete%202026%20Guide" title="Facebook" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_x" href="https://www.addtoany.com/add_to/x?linkurl=https%3A%2F%2Fgtstu.com%2Foffline-first-flutter-app-sqlite%2F&amp;linkname=Offline-First%20Flutter%20App%20With%20SQLite%3A%20Complete%202026%20Guide" title="X" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_linkedin" href="https://www.addtoany.com/add_to/linkedin?linkurl=https%3A%2F%2Fgtstu.com%2Foffline-first-flutter-app-sqlite%2F&amp;linkname=Offline-First%20Flutter%20App%20With%20SQLite%3A%20Complete%202026%20Guide" title="LinkedIn" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_sms" href="https://www.addtoany.com/add_to/sms?linkurl=https%3A%2F%2Fgtstu.com%2Foffline-first-flutter-app-sqlite%2F&amp;linkname=Offline-First%20Flutter%20App%20With%20SQLite%3A%20Complete%202026%20Guide" title="Message" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_email" href="https://www.addtoany.com/add_to/email?linkurl=https%3A%2F%2Fgtstu.com%2Foffline-first-flutter-app-sqlite%2F&amp;linkname=Offline-First%20Flutter%20App%20With%20SQLite%3A%20Complete%202026%20Guide" title="Email" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_copy_link" href="https://www.addtoany.com/add_to/copy_link?linkurl=https%3A%2F%2Fgtstu.com%2Foffline-first-flutter-app-sqlite%2F&amp;linkname=Offline-First%20Flutter%20App%20With%20SQLite%3A%20Complete%202026%20Guide" title="Copy Link" rel="nofollow noopener" target="_blank"></a><a class="a2a_dd addtoany_share_save addtoany_share" href="https://www.addtoany.com/share#url=https%3A%2F%2Fgtstu.com%2Foffline-first-flutter-app-sqlite%2F&#038;title=Offline-First%20Flutter%20App%20With%20SQLite%3A%20Complete%202026%20Guide" data-a2a-url="https://gtstu.com/offline-first-flutter-app-sqlite/" data-a2a-title="Offline-First Flutter App With SQLite: Complete 2026 Guide"></a></p><p>The post <a rel="nofollow" href="https://gtstu.com/offline-first-flutter-app-sqlite/">Offline-First Flutter App With SQLite: Complete 2026 Guide</a> appeared first on <a rel="nofollow" href="https://gtstu.com">GTStudios</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>subosito/flutter-action@v2: Complete Reference &#038; Setup Guide</title>
		<link>https://gtstu.com/cicd-flutter-github-actions/</link>
		
		<dc:creator><![CDATA[Gremmy T.]]></dc:creator>
		<pubDate>Wed, 17 Jun 2026 08:16:45 +0000</pubDate>
				<category><![CDATA[App Development]]></category>
		<category><![CDATA[devops]]></category>
		<category><![CDATA[Flutter]]></category>
		<category><![CDATA[flutter-cicd]]></category>
		<category><![CDATA[github-actions]]></category>
		<category><![CDATA[mobile-ci]]></category>
		<category><![CDATA[subosito/flutter-action]]></category>
		<guid isPermaLink="false">https://gtstu.com/?p=5103</guid>

					<description><![CDATA[<p>If you&#8217;ve typed `uses: subosito/flutter-action@v2` into a workflow file, you&#8217;ve already found the action almost every Flutter project on GitHub relies on to install the SDK on a runner. What trips people up isn&#8217;t finding it — it&#8217;s the details: which inputs actually exist, why `flutter-version: stable` throws an error, what the outputs are called, ... </p>
<p class="read-more-container"><a title="subosito/flutter-action@v2: Complete Reference &#038; Setup Guide" class="read-more button" href="https://gtstu.com/cicd-flutter-github-actions/#more-5103" aria-label="Read more about subosito/flutter-action@v2: Complete Reference &#038; Setup Guide">Read More</a></p>
<p>The post <a rel="nofollow" href="https://gtstu.com/cicd-flutter-github-actions/">subosito/flutter-action@v2: Complete Reference &#038; Setup Guide</a> appeared first on <a rel="nofollow" href="https://gtstu.com">GTStudios</a>.</p>
]]></description>
										<content:encoded><![CDATA[<p class="wp-block-paragraph">If you&#8217;ve typed `uses: subosito/flutter-action@v2` into a workflow file, you&#8217;ve already found the action almost every Flutter project on GitHub relies on to install the SDK on a runner. What trips people up isn&#8217;t finding it — it&#8217;s the details: which inputs actually exist, why `flutter-version: stable` throws an error, what the outputs are called, and how to fix the SDK failing to resolve in CI when everything works fine locally.</p>
<p class="wp-block-paragraph">This is a complete reference for subosito/flutter-action@v2 — every input and output, the channel vs. flutter-version distinction explained clearly, the errors people hit most (including ones the original docs barely cover, like ARM64 runners and self-hosted git ownership failures), and a full working CI/CD pipeline built around it.</p>
<h2 class="wp-block-heading">Quick Answer</h2>
<p class="wp-block-paragraph">subosito/flutter-action@v2 is the standard GitHub Action for installing the Flutter SDK on a runner (Linux, Windows, and macOS). Add it to a job step with `channel: stable` and `cache: true`, then run `flutter pub get`, `flutter analyze`, `flutter test`, and `flutter build apk` as sequential steps. Set `flutter-version` only to pin an exact SDK build (e.g. `3.44.0`) — never set it to the word &#8220;stable,&#8221; since that&#8217;s a channel name, not a version, and the action will fail to resolve it.</p>
<h2 class="wp-block-heading">What Is subosito/flutter-action@v2?</h2>
<p class="wp-block-paragraph">subosito/flutter-action is a community-maintained GitHub Action, and `@v2` is its current major version tag — the one referenced in essentially every Flutter CI/CD tutorial, template repo, and Stack Overflow answer you&#8217;ll find. Pinning `@v2` (rather than a full tag like `@v2.21.0`) means your workflow automatically picks up bug fixes and new Flutter releases without you editing YAML every time.</p>
<p class="wp-block-paragraph">Under the hood it downloads a Flutter SDK archive that matches your requested channel/version/architecture, extracts it to the runner&#8217;s tool cache, adds it to `PATH`, and (optionally) restores a cached copy on the next run so you&#8217;re not re-downloading a multi-hundred-megabyte SDK on every push. It doesn&#8217;t run your tests or builds for you — that&#8217;s still `flutter test` and `flutter build` steps you write yourself — it just makes sure the right Flutter toolchain exists on the runner before those steps execute.</p>
<h2 class="wp-block-heading">Every Input and Output, Explained</h2>
<p class="wp-block-paragraph">Inputs: `channel` (release track — stable, beta, dev, or master/main), `flutter-version` (an exact SDK version, a partial match like `3.x`, or on the master channel a git ref), `flutter-version-file` (reads the version from `pubspec.yaml`, `.fvmrc`, or `.fvm/fvm_config.json` instead of hardcoding it), `cache` (caches the Flutter SDK itself between runs — off by default), `pub-cache` (caches your pub dependencies separately, inheriting `cache`&#8217;s value if unset), `cache-key`/`cache-path`/`pub-cache-key`/`pub-cache-path` (customize where and how caches are stored, with dynamic tokens like `:os:`, `:channel:`, `:version:`, `:arch:`, and `:hash:`), `architecture` (target CPU architecture), `git-source` (point at a Flutter fork, such as the community Flock mirror, instead of the official repo), and `dry-run` (resolve version info without installing the SDK — useful for debugging cache keys).</p>
<p class="wp-block-paragraph">Outputs, referenced in later steps as `steps.<id>.outputs.<NAME>`: `CACHE-PATH`, `CACHE-KEY`, `CHANNEL`, `VERSION`, `ARCHITECTURE`, `PUB-CACHE-PATH`, `PUB-CACHE-KEY`, `CACHE-HIT`, and `PUB-CACHE-HIT`. These are useful for logging exactly which Flutter build ran a given job, or for gating a downstream step on whether the SDK cache actually hit — for example, skipping a slow `flutter precache` call when `CACHE-HIT` is `true`.</p>
<h2 class="wp-block-heading">channel vs. flutter-version: Which Values Are Valid?</h2>
<p class="wp-block-paragraph">`channel` selects a release track and accepts exactly four values: `stable`, `beta`, `dev`, and `master` (`main` also works as an alias). `flutter-version` expects an actual version number — a full version like `3.44.0`, a partial match like `3.x`, or, only on the master channel, a git tag, commit hash, or branch name. Setting `flutter-version: stable` fails because the action tries to resolve &#8220;stable&#8221; as a version string and can&#8217;t find a matching release — you&#8217;ll see it error out before the SDK ever installs.</p>
<p class="wp-block-paragraph">The fix: either set `channel: stable` and leave `flutter-version` empty to always get the latest stable build, or set both `channel: stable` and `flutter-version: 3.44.0` together to pin an exact release while still validating it against the stable track. If you&#8217;d rather not hardcode a version at all, use `flutter-version-file: pubspec.yaml` so the action reads the exact SDK version from your pubspec&#8217;s `environment` field — that field must specify an exact version (`flutter: 3.44.0`), since version ranges aren&#8217;t resolvable by the action.</p>
<h2 class="wp-block-heading">Fixing &#8220;Unable to Determine Flutter Version&#8221; and Other Common Errors</h2>
<p class="wp-block-paragraph">&#8220;Unable to determine Flutter version for channel: X version: Y architecture: Z&#8221; almost always traces back to one of three causes: an invalid `flutter-version` value (see above), a requested version/architecture combination that Google hasn&#8217;t published a build for (this happens with brand-new hotfix releases on ARM64 before the corresponding artifact ships), or a stale `dry-run` cache masking a typo in your inputs. Start by removing `flutter-version` entirely and letting `channel` alone resolve the latest build — if that works, the problem is in how you specified the version.</p>
<p class="wp-block-paragraph">On self-hosted runners you may also hit `fatal: detected dubious ownership in repository`, a Git security check that trips when the runner user doesn&#8217;t own the checked-out or cached SDK directory. Add a step before flutter-action that runs `git config &#8211;global &#8211;add safe.directory <path>` (matching your workspace or SDK cache path) to resolve it. And if you&#8217;re specifically targeting `ubuntu-24.04-arm` or another ARM64-hosted runner, know that ARM64 support has historically lagged behind x64/macOS in this action — check the project&#8217;s open issues for your exact Flutter version before assuming the action itself is broken.</p>
<h2 class="wp-block-heading">subosito/flutter-action vs. Alternatives</h2>
<p class="wp-block-paragraph">subosito/flutter-action remains the default choice and what most existing templates use, but if you hit a wall the action hasn&#8217;t fixed yet, `flutter-actions/setup-flutter` is a newer, actively maintained alternative worth knowing about. Migration isn&#8217;t a straight copy-paste of input names: subosito/flutter-action&#8217;s `flutter-version` input becomes `version` in setup-flutter, while `channel` carries over directly. Cache behavior is comparable rather than different — setup-flutter exposes separate `cache` (pub) and `cache-sdk` (SDK) booleans that both default to `false`, the same off-by-default posture as subosito/flutter-action&#8217;s single `cache` input; you have to explicitly opt in to caching in either action. For most projects, stick with subosito/flutter-action@v2 unless you&#8217;re specifically blocked on a maintenance-related bug.</p>
<h2 class="wp-block-heading">Step-by-Step: Building the Workflow File</h2>
<p class="wp-block-paragraph">Create `.github/workflows/flutter-ci.yml` in your repo. Trigger it with `on: push` and `on: pull_request` targeting your default branch, choose `runs-on: ubuntu-latest` for Android/web builds (use `macos-latest` for iOS or macOS-desktop), then add the action: `uses: subosito/flutter-action@v2` with `with: channel: stable` and `cache: true`. Follow it with `flutter pub get`, `flutter analyze`, `flutter test`, and `flutter build apk &#8211;release` (or `flutter build ios &#8211;release &#8211;no-codesign` for an unsigned iOS build) as separate `run:` steps so failures are attributed to the right stage in the Actions log.</p>
<p class="wp-block-paragraph">For matrix builds across Android and iOS in one workflow, define a `strategy.matrix` with `os: [ubuntu-latest, macos-latest]` and reference `${{ matrix.os }}` in `runs-on` — each OS gets its own job with the flutter-action step re-run per runner, since the SDK cache doesn&#8217;t share across operating systems.</p>
<h2 class="wp-block-heading">Adding Deployment and Secrets</h2>
<p class="wp-block-paragraph">Store your Android keystore (base64-encoded) and Apple certificates/provisioning profiles as GitHub Encrypted Secrets — never commit them. Decode the keystore in a step before `flutter build appbundle`, reference the secrets via `${{ secrets.YOUR_SECRET_NAME }}`, and hand the signed artifact to a deployment action like `wzieba/Firebase-Distribution-Github-Action` for Firebase App Distribution, or a fastlane lane for TestFlight/Play Store uploads. Gate deployment steps with `if: github.ref == &#8216;refs/heads/main&#8217;` so only merges to your release branch actually ship a build.</p>
<h2 class="wp-block-heading">subosito/flutter-action@v2 FAQs</h2>
<h3 class="wp-block-heading">What is subosito/flutter-action@v2?</h3>
<p class="wp-block-paragraph">It&#8217;s a GitHub Action that installs the Flutter SDK on a runner (Linux, Windows, or macOS) so subsequent workflow steps can run flutter commands. @v2 is its current major version tag, and it&#8217;s the de facto standard used across nearly all Flutter CI/CD workflows on GitHub.</p>
<h3 class="wp-block-heading">Is flutter-version: stable a valid value?</h3>
<p class="wp-block-paragraph">No. &#8220;stable&#8221; is a channel name, not a version number. Use channel: stable instead, and leave flutter-version empty (for the latest build) or set it to an exact version like 3.44.0 (to pin a release on that channel).</p>
<h3 class="wp-block-heading">Which Flutter GitHub Action should I use?</h3>
<p class="wp-block-paragraph">subosito/flutter-action@v2 is the safe default — it&#8217;s what most templates and tutorials use. Consider flutter-actions/setup-flutter only if you need more granular cache control or hit a subosito/flutter-action bug that hasn&#8217;t been patched yet; migrating between the two means renaming flutter-version to version, since input names aren&#8217;t identical.</p>
<h3 class="wp-block-heading">How do I pin an exact Flutter version instead of always using the latest stable?</h3>
<p class="wp-block-paragraph">Set both channel: stable and flutter-version: 3.44.0 (or whatever exact version you need) in the action&#8217;s with: block. Setting flutter-version alone without a channel also works but is less explicit.</p>
<h3 class="wp-block-heading">Can I build for both Android and iOS in the same workflow file?</h3>
<p class="wp-block-paragraph">Yes, using a build matrix with os: [ubuntu-latest, macos-latest] under strategy.matrix, referencing ${{ matrix.os }} in runs-on. Each OS runs the flutter-action step and build commands independently since caches don&#8217;t share across operating systems.</p>
<h3 class="wp-block-heading">How do I make the pipeline run only on pull requests and not on every push?</h3>
<p class="wp-block-paragraph">Remove the push trigger and keep only on: pull_request in your workflow&#8217;s on: block, optionally scoped to specific branches with pull_request: branches: [main].</p>
<h3 class="wp-block-heading">Why is my flutter test step failing in CI but passing locally?</h3>
<p class="wp-block-paragraph">Usually a Flutter/Dart version mismatch — pin flutter-version or flutter-version-file to match your local SDK exactly. Other common causes are missing generated files (run build_runner in CI too) or platform-specific test dependencies not installed on the runner.</p>
<h3 class="wp-block-heading">How do I fix &#8220;Unable to determine Flutter version for channel: stable&#8221;?</h3>
<p class="wp-block-paragraph">Remove flutter-version and rely on channel alone first to confirm the action itself works. If the error is specific to a version/architecture combo, that build may not exist yet for your target architecture (common with brand-new releases on ARM64) — try a slightly older pinned version.</p>
<h3 class="wp-block-heading">How do I fix &#8220;detected dubious ownership in repository&#8221; on a self-hosted runner?</h3>
<p class="wp-block-paragraph">Add a step before flutter-action that runs git config &#8211;global &#8211;add safe.directory <path>, pointing at your workspace or SDK cache directory. This is a Git security check unrelated to Flutter itself.</p>
<h2 class="wp-block-heading">Get More from subosito/flutter-action@v2</h2>
<p class="wp-block-paragraph">Log the coasters, stadiums, and venues you&#8217;ve experienced, rate subosito/flutter-action@v2, and see what your friends thought. <a href="https://app.thrillzing.com" target="_blank" rel="noopener">Get the ThrillZing app</a>.</p><p><a class="a2a_button_facebook" href="https://www.addtoany.com/add_to/facebook?linkurl=https%3A%2F%2Fgtstu.com%2Fcicd-flutter-github-actions%2F&amp;linkname=subosito%2Fflutter-action%40v2%3A%20Complete%20Reference%20%26%20Setup%20Guide" title="Facebook" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_x" href="https://www.addtoany.com/add_to/x?linkurl=https%3A%2F%2Fgtstu.com%2Fcicd-flutter-github-actions%2F&amp;linkname=subosito%2Fflutter-action%40v2%3A%20Complete%20Reference%20%26%20Setup%20Guide" title="X" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_linkedin" href="https://www.addtoany.com/add_to/linkedin?linkurl=https%3A%2F%2Fgtstu.com%2Fcicd-flutter-github-actions%2F&amp;linkname=subosito%2Fflutter-action%40v2%3A%20Complete%20Reference%20%26%20Setup%20Guide" title="LinkedIn" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_sms" href="https://www.addtoany.com/add_to/sms?linkurl=https%3A%2F%2Fgtstu.com%2Fcicd-flutter-github-actions%2F&amp;linkname=subosito%2Fflutter-action%40v2%3A%20Complete%20Reference%20%26%20Setup%20Guide" title="Message" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_email" href="https://www.addtoany.com/add_to/email?linkurl=https%3A%2F%2Fgtstu.com%2Fcicd-flutter-github-actions%2F&amp;linkname=subosito%2Fflutter-action%40v2%3A%20Complete%20Reference%20%26%20Setup%20Guide" title="Email" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_copy_link" href="https://www.addtoany.com/add_to/copy_link?linkurl=https%3A%2F%2Fgtstu.com%2Fcicd-flutter-github-actions%2F&amp;linkname=subosito%2Fflutter-action%40v2%3A%20Complete%20Reference%20%26%20Setup%20Guide" title="Copy Link" rel="nofollow noopener" target="_blank"></a><a class="a2a_dd addtoany_share_save addtoany_share" href="https://www.addtoany.com/share#url=https%3A%2F%2Fgtstu.com%2Fcicd-flutter-github-actions%2F&#038;title=subosito%2Fflutter-action%40v2%3A%20Complete%20Reference%20%26%20Setup%20Guide" data-a2a-url="https://gtstu.com/cicd-flutter-github-actions/" data-a2a-title="subosito/flutter-action@v2: Complete Reference &amp; Setup Guide"></a></p><p>The post <a rel="nofollow" href="https://gtstu.com/cicd-flutter-github-actions/">subosito/flutter-action@v2: Complete Reference &#038; Setup Guide</a> appeared first on <a rel="nofollow" href="https://gtstu.com">GTStudios</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Flutter App Store Checklist: 8 Things to Verify Before You Submit</title>
		<link>https://gtstu.com/flutter-app-store-submission-checklist/</link>
		
		<dc:creator><![CDATA[Gremmy T.]]></dc:creator>
		<pubDate>Wed, 17 Jun 2026 08:09:40 +0000</pubDate>
				<category><![CDATA[App Development]]></category>
		<category><![CDATA[Android Development]]></category>
		<category><![CDATA[App Store Submission]]></category>
		<category><![CDATA[Flutter]]></category>
		<category><![CDATA[Google Play]]></category>
		<category><![CDATA[iOS Development]]></category>
		<guid isPermaLink="false">https://gtstu.com/?p=5097</guid>

					<description><![CDATA[<p>Getting a Flutter app past App Review or Google Play&#8217;s automated checks feels straightforward until your submission bounces back over a missing purpose string or a mismatched bundle ID. Most rejections come down to a handful of predictable oversights — the kind that are easy to miss when you&#8217;ve been heads-down in Dart for months. ... </p>
<p class="read-more-container"><a title="Flutter App Store Checklist: 8 Things to Verify Before You Submit" class="read-more button" href="https://gtstu.com/flutter-app-store-submission-checklist/#more-5097" aria-label="Read more about Flutter App Store Checklist: 8 Things to Verify Before You Submit">Read More</a></p>
<p>The post <a rel="nofollow" href="https://gtstu.com/flutter-app-store-submission-checklist/">Flutter App Store Checklist: 8 Things to Verify Before You Submit</a> appeared first on <a rel="nofollow" href="https://gtstu.com">GTStudios</a>.</p>
]]></description>
										<content:encoded><![CDATA[<p class="wp-block-paragraph">Getting a Flutter app past App Review or Google Play&#8217;s automated checks feels straightforward until your submission bounces back over a missing purpose string or a mismatched bundle ID. Most rejections come down to a handful of predictable oversights — the kind that are easy to miss when you&#8217;ve been heads-down in Dart for months.</p>
<p class="wp-block-paragraph">This checklist covers the eight things most likely to trip you up before you hit submit, for both the iOS App Store and Google Play. Work through each item once and you&#8217;ll avoid the days of back-and-forth that first-time submitters often run into.</p>
<figure class="wp-block-image size-large"><img decoding="async" src="https://gtstu.com/wp-content/uploads/2026/06/flutter-app-store-submission-2.jpg" alt="Flutter App Store Submission"/><figcaption class="wp-element-caption"><em>Photo by Fahim Muntashir on Unsplash</em></figcaption></figure>
<h2 class="wp-block-heading">Quick Answer</h2>
<p class="wp-block-paragraph">Before submitting a Flutter app, verify your pubspec.yaml version numbers, code signing setup, build format (AAB for Android, IPA for iOS), target SDK levels, permissions, privacy policy URL, store assets, and that you&#8217;ve tested a release build on a real device — not just in debug mode.</p>
<h2 class="wp-block-heading">The 8-Item Flutter Submission Checklist</h2>
<p class="wp-block-paragraph">1. Version numbers in pubspec.yaml. Your version follows the format version: 1.0.0+1, where the number before the plus sign is the user-visible version name and the integer after it is the build number. Both stores require the build number to increment with every upload — submitting the same build number twice will cause an immediate rejection. Start new projects at 1.0.0+1 and never reuse a build number.</p>
<p class="wp-block-paragraph">2. Code signing is configured correctly. On iOS, you need a valid Distribution Certificate and an App Store Provisioning Profile generated in Apple&#8217;s Certificates, Identifiers &#038; Profiles portal. After April 28, 2026, Apple requires all submissions to be built with Xcode 26 or later. Open ios/Runner.xcworkspace, set the build target to Any iOS Device (not a simulator), and confirm signing under Product > Archive before running flutter build ipa &#8211;release. On Android, generate your upload keystore with keytool -genkey -v -keystore ~/upload-keystore.jks -keyalg RSA -keysize 2048 -validity 10000 -alias upload, reference it in key.properties, and keep both files out of version control. Because new apps must enroll in Play App Signing, Google holds the actual app signing key — if you ever lose your upload keystore, you can request an upload key reset through the Play Console (Setup > App integrity) and continue publishing without losing your app listing.</p>
<p class="wp-block-paragraph">3. You&#8217;re building in the right format. Android requires an Android App Bundle (.aab), not a fat APK. Run flutter build appbundle &#8211;release to produce the correct artifact. Adding &#8211;obfuscate &#8211;split-debug-info=build/app/outputs/symbols obfuscates your Dart code and keeps debug symbols out of the bundle. For iOS, flutter build ipa &#8211;release produces the signed archive ready for upload via Xcode Organizer or the Transporter app.</p>
<p class="wp-block-paragraph">4. SDK targets meet store requirements. For Android, your build.gradle must set both compileSdk = 35 and targetSdk = 35 — Google Play requires apps to target Android 15 (API level 35) for new submissions and updates. Setting minSdk = 24 covers the large majority of active Android devices. For iOS, your project must be built with Xcode 26 or later for submissions after April 28, 2026. Run flutter doctor -v to confirm your toolchain versions before building.</p>
<p class="wp-block-paragraph">5. Permissions are trimmed and described. Open AndroidManifest.xml and audit every <uses-permission> tag — remove anything your app doesn&#8217;t actively use, since reviewers flag unused permissions and some third-party plugins inject permissions you may not notice. On iOS, every protected API your app accesses needs a corresponding purpose string in Info.plist. Keys like NSCameraUsageDescription must have a specific, user-facing explanation (for example, &#8216;Used to scan QR codes for login&#8217;). Generic descriptions like &#8216;Needed for app functionality&#8217; are a common rejection trigger.</p>
<p class="wp-block-paragraph">6. A hosted privacy policy is in place. Both stores require a privacy policy URL that resolves to a live page — not a PDF, not a Google Docs link, not a staging URL. The policy must describe what data you collect and how it&#8217;s used. On iOS, you enter this URL in App Store Connect under App Information and must also complete the App Privacy labels (the &#8216;nutrition label&#8217; for data collection). On Android, fill out the Data Safety section in Play Console accurately — mismatches between what you declare and what your app actually does can trigger removal after launch.</p>
<p class="wp-block-paragraph">7. Store assets meet spec. On iOS, your app icon must be 1024×1024px with no transparency, and screenshots for the iPhone 6.9-inch display (1320 × 2868px) are mandatory as of 2026 — missing this size blocks submission. On Android, your store icon must be 512×512px PNG with alpha, and a 1024×500px feature graphic is required. Use flutter_launcher_icons to auto-generate the full set of mipmap directories from a single source image, and generate adaptive icons (transparent foreground layer, solid background) to avoid a plain-square fallback on Android 8+. Remove any debug overlays from screenshots — they cause rejection.</p>
<p class="wp-block-paragraph">8. You&#8217;ve tested a release build on a real device. Running flutter run &#8211;release on a physical phone is not optional. Release builds disable debug tooling and apply ProGuard/R8 obfuscation, which can surface crashes and broken deep links that were invisible during development. On iOS, distribute to at least a few testers through TestFlight before submitting — Apple recommends validating on TestFlight as a final gate. On Android, use the Internal Testing track in Play Console (typically approved within 30 minutes) to catch any install or launch issues before pushing to production.</p>
<h2 class="wp-block-heading">Platform-Specific Gotchas Worth Knowing</h2>
<p class="wp-block-paragraph">On iOS, bundle ID consistency is a frequent pain point. The bundle ID must match exactly across your Apple Developer account, App Store Connect record, Xcode&#8217;s Runner target, and any third-party services like Firebase. A single mismatch causes signing failures that are time-consuming to untangle. Also confirm that CocoaPods or Swift Package Manager resolves cleanly on your build machine by running pod install from the ios/ directory before archiving.</p>
<p class="wp-block-paragraph">On Android, bundle size can quietly creep past store limits if you&#8217;re not compressing assets. Convert images to WebP format, enable shrinkResources = true and minifyEnabled = true in your release build type, and verify the final AAB size in Play Console&#8217;s artifact explorer after upload. Also note that Play App Signing is now required for all new apps — Google securely holds the app signing key and you submit with a separate upload key, which means you can recover from a lost upload keystore by requesting a key reset through the Play Console without losing your app listing.</p>
<p class="wp-block-paragraph">For apps with user accounts on either platform, both Apple and Google require you to provide a way for users to delete their account from within the app — not just a mailto link. If your app doesn&#8217;t have this and you request account-creation, expect rejection.</p>
<figure class="wp-block-image size-large"><img decoding="async" src="https://gtstu.com/wp-content/uploads/2026/06/flutter-app-store-submission-3.jpg" alt="Flutter App Store Submission"/><figcaption class="wp-element-caption"><em>Photo by Shubham Dhage on Unsplash</em></figcaption></figure>
<h2 class="wp-block-heading">Common Mistakes That Cause Delays</h2>
<p class="wp-block-paragraph">Submitting without test credentials. If your app requires login and reviewers can&#8217;t access it, Apple will reject immediately. Provide demo credentials in the Notes field of App Store Connect&#8217;s App Review Information section. Google Play has the same requirement under the App Content section. Use a dedicated reviewer account so you&#8217;re not exposing real user data.</p>
<p class="wp-block-paragraph">Skipping TestFlight or Internal Testing. Uploading straight to production review with an untested build risks catching a crash after the clock starts on review. Both platforms allow pre-release testing tracks that give you a chance to catch launch bugs. The time invested is almost always less than the time lost to a rejection and resubmission cycle.</p>
<p class="wp-block-paragraph">Forgetting to increment the build number between submissions. If a build is rejected and you fix the issue, you must bump the build number — even if the version name stays the same — before re-uploading. Submitting the same build number after changes is a common source of &#8216;build already exists&#8217; errors.</p>
<p class="wp-block-paragraph">Leaving debug flags in. Double-check that dart:developer, debugPrint, and any logging packages are not writing sensitive data in release mode. Flutter automatically strips most debug assertions, but third-party logging libraries may need explicit configuration to stay quiet in production.</p>
<p class="wp-block-paragraph">Explore more: <a href="https://gtstu.com/category/app-development/">App Development guides and tutorials</a>.</p>
<h2 class="wp-block-heading">Flutter App Store Submission FAQs</h2>
<h3 class="wp-block-heading">What&#8217;s the difference between flutter build appbundle and flutter build apk?</h3>
<p class="wp-block-paragraph">flutter build appbundle produces an Android App Bundle (.aab), which is required for Google Play submissions. The Play Store uses the AAB to generate optimized APKs for each device configuration, reducing download size. flutter build apk produces a single APK file, which works for direct distribution and testing but cannot be submitted to the Play Store for new apps or updates.</p>
<h3 class="wp-block-heading">What happens if I lose my Android upload keystore?</h3>
<p class="wp-block-paragraph">If your app is enrolled in Google Play App Signing (required for all new apps), losing your upload keystore is not fatal. Google holds the actual app signing key, so you can request an upload key reset through the Play Console under Setup > App integrity. After submitting the request with a new certificate, Google typically processes it within a few business days and you can resume publishing updates without losing your app listing.</p>
<h3 class="wp-block-heading">Do I need a separate Apple Developer account for each Flutter app I publish?</h3>
<p class="wp-block-paragraph">No. One Apple Developer Program membership ($99/year for individuals) lets you publish as many apps as you want under your account. You manage each app as a separate record in App Store Connect. The same applies to Google Play — the $25 one-time registration fee covers unlimited app publications under that developer account.</p>
<h2 class="wp-block-heading">Build It With GTStudios</h2>
<p class="wp-block-paragraph">Need help shipping your app, game, or small-business tech? GTStudios builds web, apps, and games. <a href="https://gtstu.com/services/" target="_blank" rel="noopener">See how GTStudios can help</a>.</p>


<p class="wp-block-paragraph"><em>Photo by Fahim Muntashir on <a href="https://unsplash.com/photos/black-and-silver-laptop-computer-OqOhYRjn_JY" target="_blank" rel="noopener nofollow">Unsplash</a>.</em></p><p><a class="a2a_button_facebook" href="https://www.addtoany.com/add_to/facebook?linkurl=https%3A%2F%2Fgtstu.com%2Fflutter-app-store-submission-checklist%2F&amp;linkname=Flutter%20App%20Store%20Checklist%3A%208%20Things%20to%20Verify%20Before%20You%20Submit" title="Facebook" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_x" href="https://www.addtoany.com/add_to/x?linkurl=https%3A%2F%2Fgtstu.com%2Fflutter-app-store-submission-checklist%2F&amp;linkname=Flutter%20App%20Store%20Checklist%3A%208%20Things%20to%20Verify%20Before%20You%20Submit" title="X" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_linkedin" href="https://www.addtoany.com/add_to/linkedin?linkurl=https%3A%2F%2Fgtstu.com%2Fflutter-app-store-submission-checklist%2F&amp;linkname=Flutter%20App%20Store%20Checklist%3A%208%20Things%20to%20Verify%20Before%20You%20Submit" title="LinkedIn" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_sms" href="https://www.addtoany.com/add_to/sms?linkurl=https%3A%2F%2Fgtstu.com%2Fflutter-app-store-submission-checklist%2F&amp;linkname=Flutter%20App%20Store%20Checklist%3A%208%20Things%20to%20Verify%20Before%20You%20Submit" title="Message" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_email" href="https://www.addtoany.com/add_to/email?linkurl=https%3A%2F%2Fgtstu.com%2Fflutter-app-store-submission-checklist%2F&amp;linkname=Flutter%20App%20Store%20Checklist%3A%208%20Things%20to%20Verify%20Before%20You%20Submit" title="Email" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_copy_link" href="https://www.addtoany.com/add_to/copy_link?linkurl=https%3A%2F%2Fgtstu.com%2Fflutter-app-store-submission-checklist%2F&amp;linkname=Flutter%20App%20Store%20Checklist%3A%208%20Things%20to%20Verify%20Before%20You%20Submit" title="Copy Link" rel="nofollow noopener" target="_blank"></a><a class="a2a_dd addtoany_share_save addtoany_share" href="https://www.addtoany.com/share#url=https%3A%2F%2Fgtstu.com%2Fflutter-app-store-submission-checklist%2F&#038;title=Flutter%20App%20Store%20Checklist%3A%208%20Things%20to%20Verify%20Before%20You%20Submit" data-a2a-url="https://gtstu.com/flutter-app-store-submission-checklist/" data-a2a-title="Flutter App Store Checklist: 8 Things to Verify Before You Submit"></a></p><p>The post <a rel="nofollow" href="https://gtstu.com/flutter-app-store-submission-checklist/">Flutter App Store Checklist: 8 Things to Verify Before You Submit</a> appeared first on <a rel="nofollow" href="https://gtstu.com">GTStudios</a>.</p>
]]></content:encoded>
					
		
		
			</item>
	</channel>
</rss>
