<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Jamiu Okanlawon</title>
    <description>The latest articles on DEV Community by Jamiu Okanlawon (@developerjamiu).</description>
    <link>https://dev.to/developerjamiu</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1233428%2F3903dc85-7623-4e2a-a37b-39f6748ed3f0.jpg</url>
      <title>DEV Community: Jamiu Okanlawon</title>
      <link>https://dev.to/developerjamiu</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/developerjamiu"/>
    <language>en</language>
    <item>
      <title>Shipping a Flutter app in 2026</title>
      <dc:creator>Jamiu Okanlawon</dc:creator>
      <pubDate>Wed, 21 Oct 2026 15:43:33 +0000</pubDate>
      <link>https://dev.to/serverpod/shipping-a-flutter-app-in-2026-5e47</link>
      <guid>https://dev.to/serverpod/shipping-a-flutter-app-in-2026-5e47</guid>
      <description>&lt;p&gt;Your Flutter app is complete, and now you want to make it available to people in the App Store, Google Play, or both. If you haven’t published an app before, it’s hard to know what needs to be done and in what order. The instructions exist, but they’re spread across Flutter’s guides, two store consoles, and the documentation for any tools you use.&lt;/p&gt;

&lt;p&gt;This guide connects them. It explains what publishing involves, what help you can get with the work, and how each stage leads to the next, with links to the official instructions for the details.&lt;/p&gt;

&lt;p&gt;If you haven’t built your app yet, the &lt;a href="https://docs.serverpod.dev/quickstart" rel="noopener noreferrer"&gt;Serverpod Quickstart&lt;/a&gt; can help you create a Flutter app with a Serverpod backend and deploy it to &lt;a href="https://serverpod.dev/cloud" rel="noopener noreferrer"&gt;Serverpod Cloud&lt;/a&gt;. Come back here when it’s ready to publish.&lt;/p&gt;

&lt;h2&gt;
  
  
  What publishing an app involves
&lt;/h2&gt;

&lt;p&gt;Publishing an app involves six main steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Create store account and app records:&lt;/strong&gt; Each store needs a developer account, and each app needs a record that holds its store page, builds, and releases.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build and sign your app:&lt;/strong&gt; Each store needs your app built for release and signed to show that it came from you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Create your store page and privacy answers:&lt;/strong&gt; The store page is what people see before they install. Each store also asks about your app’s content and how it handles data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test on real devices:&lt;/strong&gt; Each store’s testing program gets your build to testers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Choose availability and submit for review:&lt;/strong&gt; You decide where and how the app is offered, then send it to the store for review.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Release your app:&lt;/strong&gt; Once approved, your app goes live automatically or when you release it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You can work on some of these at the same time. For example, you can prepare your store page while your app is being built.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ways to handle the publishing steps
&lt;/h2&gt;

&lt;p&gt;You can do the publishing work manually, or use tools to automate much of it. The stores’ requirements and reviews stay the same either way. There are three common approaches, and which one fits depends on what you’re prepared to maintain:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Publishing manually:&lt;/strong&gt; follow Flutter’s &lt;a href="https://docs.flutter.dev/deployment/ios" rel="noopener noreferrer"&gt;iOS&lt;/a&gt; and &lt;a href="https://docs.flutter.dev/deployment/android" rel="noopener noreferrer"&gt;Android&lt;/a&gt; release guides and complete each step in App Store Connect and the Play Console. For every new version, you build, upload, and submit again, and building for iOS requires a Mac. This suits you if you release rarely.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Running your own automation:&lt;/strong&gt; tools such as &lt;a href="https://docs.flutter.dev/deployment/cd#fastlane" rel="noopener noreferrer"&gt;fastlane&lt;/a&gt; automate release steps like building, uploading, and submitting. You can run them on your own machine or in a continuous integration and delivery (CI/CD) service such as GitHub Actions. You maintain the workflow and the store access it uses. This suits you if you want full control over your release workflow or already use a CI/CD service.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Using a hosted service:&lt;/strong&gt; &lt;a href="https://docs.codemagic.io/yaml-quick-start/building-a-flutter-app/" rel="noopener noreferrer"&gt;Codemagic&lt;/a&gt;, &lt;a href="https://docs.bitrise.io/en/bitrise-ci/getting-started/quick-start-guides/getting-started-with-flutter-projects" rel="noopener noreferrer"&gt;Bitrise&lt;/a&gt;, and &lt;a href="https://docs.appcircle.io/build/platform-build-guides/building-flutter-applications" rel="noopener noreferrer"&gt;Appcircle&lt;/a&gt; provide the build machines and run the release workflow for you. You configure the service and give it access to your store accounts. This suits you if you don’t want to maintain that setup yourself, or don’t have a Mac for iOS builds.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Whichever tool you use to automate, you upload your app’s &lt;a href="https://support.google.com/googleplay/android-developer/answer/9859348?hl=en" rel="noopener noreferrer"&gt;first version to Google Play&lt;/a&gt; manually.&lt;/p&gt;

&lt;p&gt;Here’s how these tools can help with each of the six steps:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Step&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;fastlane&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Codemagic&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Bitrise&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Appcircle&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1. Set up store accounts and app records&lt;/td&gt;
&lt;td&gt;✓ App Store app record only&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2. Build and sign your app&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3. Create your store page and privacy answers&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;✓ App Store only&lt;/td&gt;
&lt;td&gt;✓ Enterprise&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4. Test on real devices&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5. Choose availability and submit for review&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6. Release your app&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✓ Google Play only&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;“No” means the service’s documentation doesn’t show a built-in way to do it. Codemagic, Bitrise, and Appcircle can also run fastlane in their workflows.&lt;/p&gt;

&lt;p&gt;:::tip[Working with an agent?]&lt;br&gt;
An agent can help with any of these steps, from uploading a build to updating your app’s store description.&lt;br&gt;
:::&lt;/p&gt;

&lt;h2&gt;
  
  
  The publishing steps
&lt;/h2&gt;

&lt;p&gt;Each step below gives an overview of what to do, with links to the official instructions.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Create your store accounts and app records
&lt;/h3&gt;

&lt;p&gt;Start by setting up a developer account for each store you’re targeting and creating your app’s entry there, called an app record.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;App Store:&lt;/strong&gt; &lt;a href="https://developer.apple.com/programs/enroll/" rel="noopener noreferrer"&gt;enroll in the Apple Developer Program&lt;/a&gt;, then &lt;a href="https://developer.apple.com/help/app-store-connect/create-an-app-record/add-a-new-app" rel="noopener noreferrer"&gt;add your app in App Store Connect&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Google Play:&lt;/strong&gt; &lt;a href="https://support.google.com/googleplay/android-developer/answer/6112435?hl=en" rel="noopener noreferrer"&gt;register for a Play Console developer account&lt;/a&gt;, then &lt;a href="https://support.google.com/googleplay/android-developer/answer/9859152?hl=en" rel="noopener noreferrer"&gt;create your app&lt;/a&gt;. New personal accounts also need to &lt;a href="https://support.google.com/googleplay/android-developer/answer/14316361?hl=en" rel="noopener noreferrer"&gt;verify access to an Android device&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Build and sign your app
&lt;/h3&gt;

&lt;p&gt;Whether you build and sign your app manually or a tool does it for you, each store expects the same thing:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;App Store:&lt;/strong&gt; a build made with Xcode, which runs on macOS, and signed with your Apple distribution certificate and provisioning profile. &lt;a href="https://docs.flutter.dev/deployment/ios" rel="noopener noreferrer"&gt;Flutter’s iOS release guide&lt;/a&gt; covers both, and Apple’s &lt;a href="https://developer.apple.com/news/upcoming-requirements/" rel="noopener noreferrer"&gt;upload requirements&lt;/a&gt; list the Xcode and SDK versions it currently accepts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Google Play:&lt;/strong&gt; an Android App Bundle signed with an upload key. &lt;a href="https://docs.flutter.dev/deployment/android" rel="noopener noreferrer"&gt;Flutter’s Android release guide&lt;/a&gt; covers both, and Google Play’s &lt;a href="https://support.google.com/googleplay/android-developer/answer/11926878?hl=en" rel="noopener noreferrer"&gt;target API requirements&lt;/a&gt; list the Android API level your build must target.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Create your store page and privacy answers
&lt;/h3&gt;

&lt;p&gt;Your store page is what people see before they decide to install. Alongside it, each store asks about your app’s content and how it handles data. Both are part of what the store reviews. Answer the privacy and data questions from what your app actually does, including the third-party SDKs it uses.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;App Store:&lt;/strong&gt; in App Store Connect, add your app’s name, description, and screenshots, following Apple’s &lt;a href="https://developer.apple.com/help/app-store-connect/reference/app-information/screenshot-specifications" rel="noopener noreferrer"&gt;screenshot specifications&lt;/a&gt;, and fill in your &lt;a href="https://developer.apple.com/app-store/app-privacy-details/" rel="noopener noreferrer"&gt;app privacy details&lt;/a&gt;. If your app requires sign-in, provide &lt;a href="https://developer.apple.com/help/app-store-connect/reference/app-information/platform-version-information" rel="noopener noreferrer"&gt;demo account details&lt;/a&gt; for App Review.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Google Play:&lt;/strong&gt; in the Play Console, add your app’s name, description, and screenshots to your store listing, following Google’s &lt;a href="https://support.google.com/googleplay/android-developer/answer/9866151?hl=en" rel="noopener noreferrer"&gt;image requirements&lt;/a&gt;. Then complete the declarations on the &lt;a href="https://support.google.com/googleplay/android-developer/answer/9859455?hl=en" rel="noopener noreferrer"&gt;App content&lt;/a&gt; page, including access instructions if reviewers need to sign in, and fill in the &lt;a href="https://support.google.com/googleplay/android-developer/answer/10787469?hl=en" rel="noopener noreferrer"&gt;Data safety form&lt;/a&gt;. Your store listing and App content answers are part of the setup Google Play requires before you can run a closed test.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Test on real devices
&lt;/h3&gt;

&lt;p&gt;Once you have a signed build, your app is ready for testing on real devices. Testing is recommended before every release and required for some new Google Play accounts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;App Store:&lt;/strong&gt; upload your build to App Store Connect and share it with testers through &lt;a href="https://developer.apple.com/help/app-store-connect/test-a-beta-version/testflight-overview" rel="noopener noreferrer"&gt;TestFlight&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Google Play:&lt;/strong&gt; add your build to a &lt;a href="https://support.google.com/googleplay/android-developer/answer/9845334?hl=en" rel="noopener noreferrer"&gt;testing track&lt;/a&gt; and invite your testers. If your Google Play account is a personal account created after 13 November 2023, &lt;a href="https://support.google.com/googleplay/android-developer/answer/14151465?hl=en" rel="noopener noreferrer"&gt;a closed app test is required&lt;/a&gt; before you can publish your app. At least 12 testers must stay opted in continuously for 14 days, so it helps to line up testers early. You then apply for production access in the Play Console. That’s Google’s approval to publish your app on Google Play. Google reviews your application and may ask you to keep testing.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Choose availability and submit for review
&lt;/h3&gt;

&lt;p&gt;Decide where your app will be available and at what price, then send it to the store for review.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhy828zqcetsjmtu0v1ki.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhy828zqcetsjmtu0v1ki.jpg" alt="app-store-submission-flowchart" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;App Store:&lt;/strong&gt; in App Store Connect, choose the &lt;a href="https://developer.apple.com/help/app-store-connect/manage-your-apps-availability/manage-availability-for-your-app-on-the-app-store" rel="noopener noreferrer"&gt;countries or regions&lt;/a&gt; where your app will be available and set its &lt;a href="https://developer.apple.com/help/app-store-connect/manage-app-pricing/set-a-price" rel="noopener noreferrer"&gt;price&lt;/a&gt;, which can be free. Both are required before you can submit. Choose a &lt;a href="https://developer.apple.com/help/app-store-connect/manage-your-apps-availability/select-an-app-store-version-release-option" rel="noopener noreferrer"&gt;release option&lt;/a&gt; for when an approved version goes live, then &lt;a href="https://developer.apple.com/help/app-store-connect/manage-submissions-to-app-review/submit-an-app" rel="noopener noreferrer"&gt;add your version to a submission and submit it for review&lt;/a&gt;. If App Review finds a problem, check the &lt;a href="https://developer.apple.com/app-store/review/guidelines/" rel="noopener noreferrer"&gt;App Review Guidelines&lt;/a&gt;, &lt;a href="https://developer.apple.com/help/app-store-connect/manage-submissions-to-app-review/reply-to-app-review-messages" rel="noopener noreferrer"&gt;reply in App Store Connect&lt;/a&gt; if needed, and &lt;a href="https://developer.apple.com/help/app-store-connect/manage-submissions-to-app-review/manage-a-submission-with-unresolved-issues" rel="noopener noreferrer"&gt;fix and resubmit&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Google Play:&lt;/strong&gt; in the Play Console, choose the &lt;a href="https://support.google.com/googleplay/android-developer/answer/7550024?hl=en" rel="noopener noreferrer"&gt;countries or regions&lt;/a&gt; for your release and set your app as &lt;a href="https://support.google.com/googleplay/android-developer/answer/6334373?hl=en" rel="noopener noreferrer"&gt;free or paid&lt;/a&gt;. If you needed production access, wait until it’s approved. Then &lt;a href="https://support.google.com/googleplay/android-developer/answer/9859348?hl=en" rel="noopener noreferrer"&gt;create a production release&lt;/a&gt; with your tested build and &lt;a href="https://support.google.com/googleplay/android-developer/answer/9859654?hl=en" rel="noopener noreferrer"&gt;send the changes for review&lt;/a&gt; from the Publishing overview page. If Google finds a problem, &lt;a href="https://support.google.com/googleplay/android-developer/answer/9842754?hl=en" rel="noopener noreferrer"&gt;check your app’s policy status&lt;/a&gt; to see what was found, then fix the issue and resubmit, or appeal.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Release your app
&lt;/h3&gt;

&lt;p&gt;Once the store approves your app, it can go live.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;App Store:&lt;/strong&gt; if you choose automatic release in step 5, the approved version goes live without further action. If you choose manual release, &lt;a href="https://developer.apple.com/help/app-store-connect/manage-your-apps-availability/select-an-app-store-version-release-option" rel="noopener noreferrer"&gt;release the version in App Store Connect&lt;/a&gt; once its status changes to Pending Developer Release.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Google Play:&lt;/strong&gt; with standard publishing, your app goes live once Google approves your release. If you use &lt;a href="https://support.google.com/googleplay/android-developer/answer/9859654?hl=en" rel="noopener noreferrer"&gt;managed publishing&lt;/a&gt; for an update, approved changes wait until you publish them from the Publishing overview page. The &lt;a href="https://support.google.com/googleplay/android-developer/answer/9859751?hl=en" rel="noopener noreferrer"&gt;publishing status&lt;/a&gt; shows when your app is available on Google Play.&lt;/p&gt;

&lt;h2&gt;
  
  
  What carries over to your next release
&lt;/h2&gt;

&lt;p&gt;Most of what you set up the first time stays in place. You reuse your developer accounts and app records. To keep them working, renew your Apple Developer Program membership each year and renew your Apple signing certificates before they expire. Your store page, privacy answers, and availability choices carry over too. Update them when your app or your plans change, and check the stores’ requirements before each release.&lt;/p&gt;

&lt;p&gt;For each new version, you build and sign the update, test it, submit it for review, and release it. A new build and store review are required every time, and testing is recommended. If those repeated steps start to take up your time, that’s where your own automation, a hosted service, or an agent can help.&lt;/p&gt;

&lt;p&gt;Now that you know what it takes to publish a Flutter app, start with step 1 if your app is ready, or use the &lt;a href="https://docs.serverpod.dev/quickstart" rel="noopener noreferrer"&gt;Serverpod Quickstart&lt;/a&gt; to build your next one.&lt;/p&gt;

</description>
      <category>appstore</category>
      <category>playstore</category>
      <category>flutter</category>
      <category>mobile</category>
    </item>
    <item>
      <title>Bringing Flutter’s hot reload experience to the full stack</title>
      <dc:creator>Jamiu Okanlawon</dc:creator>
      <pubDate>Thu, 08 Oct 2026 09:14:21 +0000</pubDate>
      <link>https://dev.to/developerjamiu/bringing-flutters-hot-reload-experience-to-the-full-stack-6j9</link>
      <guid>https://dev.to/developerjamiu/bringing-flutters-hot-reload-experience-to-the-full-stack-6j9</guid>
      <description>&lt;p&gt;You’re testing a form a few screens into your Flutter app, with the fields already filled in. The spacing looks cramped, so you adjust it in the code and trigger a &lt;a href="https://docs.flutter.dev/tools/hot-reload" rel="noopener noreferrer"&gt;hot reload&lt;/a&gt;. The app picks up the change, and the form is still on screen with your values in it.&lt;/p&gt;

&lt;p&gt;That’s the loop Flutter developers rely on. You change the code and see the result right where you were looking, so your attention stays on the feature instead of on getting back to it.&lt;/p&gt;

&lt;h2&gt;
  
  
  When the change is on the server
&lt;/h2&gt;

&lt;p&gt;Now suppose the form books appointments, and you change the booking rule so appointments need at least 24 hours’ notice. That validation lives on the server, so Flutter’s hot reload doesn’t reach it.&lt;/p&gt;

&lt;p&gt;If your server restarts to pick up new code, each edit to the rule looks like this. You save it, restart and wait for the server to come back up, and then try the booking. Do that for every tweak, and your usual Flutter rhythm of save and see becomes save, restart, wait, then see. And the wait might be enough to break your creative flow.&lt;/p&gt;

&lt;p&gt;What if a server edit worked just like a Flutter one? You save it and try the booking again, without waiting for a restart.&lt;/p&gt;

&lt;h2&gt;
  
  
  Full-stack hot reload with Serverpod
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://docs.serverpod.dev/how-it-works" rel="noopener noreferrer"&gt;Serverpod&lt;/a&gt;, an open-source backend written in Dart for Flutter, works that way. During development, &lt;a href="https://docs.serverpod.dev/concepts/server-fundamentals/running-your-server" rel="noopener noreferrer"&gt;&lt;code&gt;serverpod start&lt;/code&gt;&lt;/a&gt; runs your server, Flutter app, and database together in one session.&lt;/p&gt;

&lt;p&gt;Because the server is written in Dart, Serverpod uses Dart’s hot-reload support to update it. When you edit the booking rule and save, the running server picks up the change without restarting. And by using an &lt;a href="https://docs.serverpod.dev/concepts/data-and-the-database/database/embedded-postgres" rel="noopener noreferrer"&gt;embedded PostgreSQL database&lt;/a&gt;, that experience extends to the database layer as well.&lt;/p&gt;

&lt;p&gt;With Serverpod, you go straight from saving the rule to trying it out in the form you already have open. Want 48 hours’ notice instead? Change it, save, and try again. Each edit to the rule now fits the same save-and-see rhythm as a UI change.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/6IwkVQ2Z04Y" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h3&gt;
  
  
  When something goes wrong
&lt;/h3&gt;

&lt;p&gt;Hot reload also helps when something doesn’t behave as expected. Say a booking that should be accepted keeps getting rejected. You add a log line inside the server method that checks availability. Then you save and submit the booking again. The output shows what the method received, so you can make an informed decision about what to change next. Each time you add or adjust a log line in that method, the running session reloads it, so there’s no restart to wait for between tries. You keep debugging without losing your place.&lt;/p&gt;

&lt;p&gt;A coding assistant can work the same way. Through Serverpod’s &lt;a href="https://docs.serverpod.dev/concepts/cli/commands/mcp-server" rel="noopener noreferrer"&gt;Model Context Protocol (MCP) server&lt;/a&gt;, it can read the server and Flutter logs from the running session and use what they show to decide its next step. Its edits to the availability method are hot reloaded too.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it yourself
&lt;/h2&gt;

&lt;p&gt;If you’re choosing a backend for a Flutter app, it’s worth trying this before you decide. Start with the &lt;a href="https://docs.serverpod.dev/quickstart" rel="noopener noreferrer"&gt;official Serverpod Quickstart&lt;/a&gt;. Its agent-assisted guide helps you create a Serverpod project and run it locally with &lt;code&gt;serverpod start&lt;/code&gt;. Once your project is running, change something on the server, save, and check the result from the app you already have open.&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>hotreload</category>
      <category>serverpod</category>
      <category>fullstack</category>
    </item>
    <item>
      <title>Type-safe everything - Eliminate manual API glue code</title>
      <dc:creator>Jamiu Okanlawon</dc:creator>
      <pubDate>Thu, 01 Oct 2026 09:28:14 +0000</pubDate>
      <link>https://dev.to/serverpod/type-safe-everything-eliminate-manual-api-glue-code-mhn</link>
      <guid>https://dev.to/serverpod/type-safe-everything-eliminate-manual-api-glue-code-mhn</guid>
      <description>&lt;p&gt;Every Flutter app that talks to a backend needs some code you never really wanted to write. That includes an endpoint path, an HTTP request, JSON parsing, and a client model that must match the data defined on the server. None of it is the feature, but all of it has to stay correct.&lt;/p&gt;

&lt;p&gt;When you maintain the code that connects to the server by hand, it has to change with the API. Add three fields to a response, and you'll need to update the client model and parsing code before Flutter can use them. Each edit is small, but the server response, client model, and parser all have to stay in sync.&lt;/p&gt;

&lt;p&gt;Serverpod, an open-source backend written in Dart for Flutter, takes a different approach. You define each model once, and Serverpod generates the model classes and serialization code for both your server and Flutter app. It also generates type-safe client methods from your endpoints. Those generated types let the Dart analyzer catch type errors before you run the app.&lt;/p&gt;

&lt;p&gt;In this article, we'll build a ride fare estimator, call it from Flutter, and then modify its API. We'll see which parts of that change we need to write, which parts Serverpod generates, and how the analyzer catches a mismatch.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Serverpod connects the server and Flutter
&lt;/h2&gt;

&lt;p&gt;For our fare estimator, the app sends a pickup location and a destination. The server calculates the price and returns it with a currency code for Flutter to display.&lt;/p&gt;

&lt;p&gt;A model describes the data your app sends or receives. An endpoint contains the Dart methods that your app can call on the server.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj9axz9j0nnuy4wfr98n5.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj9axz9j0nnuy4wfr98n5.jpg" alt="type-safe-serverpod-illustration" width="799" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The serialization code turns Dart objects into a format that can be sent between the app and server. It turns received data back into Dart objects. Flutter calls the endpoint through a generated client method.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before we start
&lt;/h2&gt;

&lt;p&gt;If you haven't installed Serverpod yet, follow the &lt;a href="https://docs.serverpod.dev/installation" rel="noopener noreferrer"&gt;installation guide&lt;/a&gt;. Then create a project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;serverpod create tripquote
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep the recommended defaults during setup.&lt;/p&gt;

&lt;p&gt;The project contains three packages. Here are the parts we'll use, including the fare files we'll add next:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;tripquote/
├── tripquote_server/lib/src/fare/   # Models and endpoint we'll add
│   ├── location.spy.yaml
│   ├── fare_estimate.spy.yaml
│   └── fare_endpoint.dart
├── tripquote_client/               # Generated client code
└── tripquote_flutter/lib/main.dart # Flutter code
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open the &lt;code&gt;tripquote&lt;/code&gt; directory, then start the project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;tripquote
serverpod start
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep it running while you follow the tutorial. Serverpod watches your models and endpoints and regenerates the connecting code when they change.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build the fare estimator
&lt;/h2&gt;

&lt;p&gt;The fare estimator needs a pickup location and a destination. Start by defining a &lt;code&gt;Location&lt;/code&gt; &lt;a href="https://docs.serverpod.dev/concepts/data-and-the-database/models" rel="noopener noreferrer"&gt;model&lt;/a&gt; for both.&lt;/p&gt;

&lt;p&gt;Model definitions go in &lt;code&gt;.spy.yaml&lt;/code&gt; files. Create &lt;code&gt;tripquote_server/lib/src/fare/location.spy.yaml&lt;/code&gt; with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;### A geographic point.&lt;/span&gt;
&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Location&lt;/span&gt;
&lt;span class="na"&gt;fields&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="c1"&gt;### Degrees north of the equator.&lt;/span&gt;
  &lt;span class="na"&gt;latitude&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;double&lt;/span&gt;
  &lt;span class="c1"&gt;### Degrees east of the prime meridian.&lt;/span&gt;
  &lt;span class="na"&gt;longitude&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;double&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, define the estimate that the server will return. Create &lt;code&gt;tripquote_server/lib/src/fare/fare_estimate.spy.yaml&lt;/code&gt; with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;### What a ride is expected to cost.&lt;/span&gt;
&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;FareEstimate&lt;/span&gt;
&lt;span class="na"&gt;fields&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="c1"&gt;### The estimated fare.&lt;/span&gt;
  &lt;span class="na"&gt;fare&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;double&lt;/span&gt;
  &lt;span class="c1"&gt;### ISO currency code, such as USD.&lt;/span&gt;
  &lt;span class="na"&gt;currency&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;String&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With these models in place, let’s create an &lt;a href="https://docs.serverpod.dev/concepts/endpoints-and-apis" rel="noopener noreferrer"&gt;endpoint&lt;/a&gt; to calculate the estimate. We'll use a base fare of USD 2.50 plus USD 1.20 per kilometer.&lt;/p&gt;

&lt;p&gt;Create &lt;code&gt;tripquote_server/lib/src/fare/fare_endpoint.dart&lt;/code&gt; with the following code. The helper functions calculate the distance and round the fare to two decimal places:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'dart:math'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:serverpod/serverpod.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'../generated/protocol.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;/// Estimates what a ride will cost.&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;FareEndpoint&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;Endpoint&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;/// Returns an estimate for a ride from [pickup] to [destination].&lt;/span&gt;
  &lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;FareEstimate&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;estimateFare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;Session&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Location&lt;/span&gt; &lt;span class="n"&gt;pickup&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Location&lt;/span&gt; &lt;span class="n"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;baseFare&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;2.50&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;pricePerKm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;1.20&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;distanceKm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_distanceKm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pickup&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;fare&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;baseFare&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;distanceKm&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;pricePerKm&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;FareEstimate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;fare:&lt;/span&gt; &lt;span class="n"&gt;_round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fare&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nl"&gt;currency:&lt;/span&gt; &lt;span class="s"&gt;'USD'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;/// The great-circle distance between two points, in kilometers.&lt;/span&gt;
&lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="nf"&gt;_distanceKm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Location&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Location&lt;/span&gt; &lt;span class="n"&gt;end&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;earthRadiusKm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;6371.0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;latitudeDelta&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_toRadians&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;end&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;latitude&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;latitude&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;longitudeDelta&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_toRadians&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;end&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;longitude&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;longitude&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
      &lt;span class="n"&gt;pow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;latitudeDelta&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
      &lt;span class="n"&gt;cos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_toRadians&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;latitude&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
          &lt;span class="n"&gt;cos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_toRadians&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;end&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;latitude&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
          &lt;span class="n"&gt;pow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;longitudeDelta&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;earthRadiusKm&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;asin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sqrt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="nf"&gt;_toRadians&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;degrees&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;degrees&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;pi&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;180&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="nf"&gt;_round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;round&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;a href="https://docs.serverpod.dev/concepts/endpoints-and-apis/sessions" rel="noopener noreferrer"&gt;Session&lt;/a&gt; parameter gives the endpoint access to server features such as logging and the database. It stays on the server, so Flutter only needs to send the two locations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Call the endpoint from Flutter
&lt;/h2&gt;

&lt;p&gt;When you save the endpoint from the previous section, Serverpod generates &lt;code&gt;client.fare.estimateFare&lt;/code&gt;. It accepts two &lt;code&gt;Location&lt;/code&gt; objects and returns a &lt;code&gt;Future&amp;lt;FareEstimate&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The template already sets up &lt;code&gt;client&lt;/code&gt; in &lt;code&gt;tripquote_flutter/lib/client.dart&lt;/code&gt;. Keep that file unchanged.&lt;/p&gt;

&lt;p&gt;Create &lt;code&gt;tripquote_flutter/lib/screens/fare_screen.dart&lt;/code&gt; with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:flutter/material.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:tripquote_client/tripquote_client.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'../client.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;FareScreen&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;StatefulWidget&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;FareScreen&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="n"&gt;State&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;FareScreen&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;createState&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_FareScreenState&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;_FareScreenState&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;State&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;FareScreen&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;late&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;FareEstimate&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_estimate&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;initState&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;initState&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="n"&gt;_estimate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fare&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;estimateFare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="n"&gt;Location&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;latitude:&lt;/span&gt; &lt;span class="mf"&gt;6.5244&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;longitude:&lt;/span&gt; &lt;span class="mf"&gt;3.3792&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="n"&gt;Location&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;latitude:&lt;/span&gt; &lt;span class="mf"&gt;6.4281&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;longitude:&lt;/span&gt; &lt;span class="mf"&gt;3.4219&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="n"&gt;Widget&lt;/span&gt; &lt;span class="n"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BuildContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Center&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;FutureBuilder&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;FareEstimate&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;
        &lt;span class="nl"&gt;future:&lt;/span&gt; &lt;span class="n"&gt;_estimate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nl"&gt;builder:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;snapshot&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;snapshot&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;hasError&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'Could not load the fare.'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
          &lt;span class="p"&gt;}&lt;/span&gt;

          &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;estimate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;snapshot&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
          &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;estimate&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;CircularProgressIndicator&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
          &lt;span class="p"&gt;}&lt;/span&gt;

          &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fareLabel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;estimate&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="nf"&gt;fareLabel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;FareEstimate&lt;/span&gt; &lt;span class="n"&gt;estimate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="si"&gt;${estimate.currency}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;${estimate.fare}&lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In &lt;code&gt;tripquote_flutter/lib/main.dart&lt;/code&gt;, replace the greeting-screen import with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'screens/fare_screen.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In &lt;code&gt;MyHomePage&lt;/code&gt;, replace &lt;code&gt;body: const GreetingsScreen(),&lt;/code&gt; with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="nl"&gt;body:&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;FareScreen&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In &lt;code&gt;MyApp&lt;/code&gt;, change the home-page title:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="nl"&gt;home:&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;MyHomePage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;title:&lt;/span&gt; &lt;span class="s"&gt;'Fare estimate'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep the rest of &lt;code&gt;main.dart&lt;/code&gt;, including &lt;code&gt;await initializeClient()&lt;/code&gt;, unchanged.&lt;/p&gt;

&lt;p&gt;The call to &lt;code&gt;client.fare.estimateFare&lt;/code&gt; sends the pickup and destination to the server. The returned &lt;code&gt;FareEstimate&lt;/code&gt; has typed fields, so our &lt;code&gt;fareLabel&lt;/code&gt; function can read &lt;code&gt;currency&lt;/code&gt; and &lt;code&gt;fare&lt;/code&gt; directly.&lt;/p&gt;

&lt;p&gt;Keep the server running and hot restart the Flutter app by pressing &lt;strong&gt;R&lt;/strong&gt; in the &lt;code&gt;serverpod start&lt;/code&gt; terminal. A successful request for these locations displays:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;USD 16.54
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You don't write an HTTP request, specify an endpoint path, or maintain a matching model in Flutter. Serverpod generates the code needed to transfer the data between the app and server.&lt;/p&gt;

&lt;p&gt;The endpoint's documentation also appears in the generated client. Hover over &lt;code&gt;estimateFare&lt;/code&gt; in your editor to read the description from the server code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Add more data to the response
&lt;/h2&gt;

&lt;p&gt;Suppose the app now needs to show a price range and tell the rider when surge pricing applies. It should still show a single fare when surge pricing does not apply.&lt;/p&gt;

&lt;h3&gt;
  
  
  Try replacing the fare
&lt;/h3&gt;

&lt;p&gt;A first approach is to replace &lt;code&gt;fare&lt;/code&gt; with two range fields. Replace the contents of &lt;code&gt;tripquote_server/lib/src/fare/fare_estimate.spy.yaml&lt;/code&gt; with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;### What a ride is expected to cost.&lt;/span&gt;
&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;FareEstimate&lt;/span&gt;
&lt;span class="na"&gt;fields&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;fareLow&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;double&lt;/span&gt;
  &lt;span class="na"&gt;fareHigh&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;double&lt;/span&gt;
  &lt;span class="na"&gt;currency&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;String&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Save the model, leaving the endpoint and Flutter code unchanged. Wait for &lt;code&gt;serverpod start&lt;/code&gt; to process the change.&lt;/p&gt;

&lt;p&gt;The Dart analyzer now reports errors in the server endpoint and the Flutter app:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foqrbl4yvxhg9npidoap2.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foqrbl4yvxhg9npidoap2.jpg" alt="type-safe-analyzer-error" width="800" height="508"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now update the endpoint to match the model. For this example, set the low price 10% below the estimate and the high price 15% above it. In &lt;code&gt;tripquote_server/lib/src/fare/fare_endpoint.dart&lt;/code&gt;, keep the distance and fare calculations inside &lt;code&gt;estimateFare&lt;/code&gt;. Replace only its return statement with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;FareEstimate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;fareLow:&lt;/span&gt; &lt;span class="n"&gt;_round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fare&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.9&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="nl"&gt;fareHigh:&lt;/span&gt; &lt;span class="n"&gt;_round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fare&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;1.15&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="nl"&gt;currency:&lt;/span&gt; &lt;span class="s"&gt;'USD'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Save both files and wait for &lt;code&gt;serverpod start&lt;/code&gt; to finish regenerating. Leave the Flutter code unchanged.&lt;/p&gt;

&lt;p&gt;The Dart analyzer now flags &lt;code&gt;estimate.fare&lt;/code&gt; in &lt;code&gt;fareLabel&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;The getter 'fare' isn't defined for the type 'FareEstimate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The generated &lt;code&gt;FareEstimate&lt;/code&gt; no longer has &lt;code&gt;fare&lt;/code&gt;, but our Flutter code still reads that field. The analyzer reports the mismatch before we run the updated app.&lt;/p&gt;

&lt;p&gt;The first attempt removed a value the app still needs for ordinary pricing. We'll keep &lt;code&gt;fare&lt;/code&gt; and add the extra information alongside it instead.&lt;/p&gt;

&lt;h3&gt;
  
  
  Keep the fare and add optional details
&lt;/h3&gt;

&lt;p&gt;Replace the contents of &lt;code&gt;tripquote_server/lib/src/fare/fare_estimate.spy.yaml&lt;/code&gt; with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;### What a ride is expected to cost.&lt;/span&gt;
&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;FareEstimate&lt;/span&gt;
&lt;span class="na"&gt;fields&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="c1"&gt;### The estimated fare, adjusted when surge applies.&lt;/span&gt;
  &lt;span class="na"&gt;fare&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;double&lt;/span&gt;
  &lt;span class="c1"&gt;### ISO currency code, such as USD.&lt;/span&gt;
  &lt;span class="na"&gt;currency&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;String&lt;/span&gt;
  &lt;span class="c1"&gt;### The low end of the fare range.&lt;/span&gt;
  &lt;span class="na"&gt;fareLow&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;double?&lt;/span&gt;
  &lt;span class="c1"&gt;### The high end of the fare range.&lt;/span&gt;
  &lt;span class="na"&gt;fareHigh&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;double?&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The new fields have values only when surge pricing applies. The &lt;code&gt;?&lt;/code&gt; allows them to be &lt;code&gt;null&lt;/code&gt; otherwise. The response always includes &lt;code&gt;fare&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;We'll use &lt;code&gt;priceSurge&lt;/code&gt; to turn surge pricing on or off in this example. When it is &lt;code&gt;true&lt;/code&gt;, the server doubles the fare before calculating the range.&lt;/p&gt;

&lt;p&gt;In &lt;code&gt;tripquote_server/lib/src/fare/fare_endpoint.dart&lt;/code&gt;, replace the &lt;code&gt;FareEndpoint&lt;/code&gt; class with the code below. Include the &lt;code&gt;priceSurge&lt;/code&gt; line above the class, and keep the imports and helper functions unchanged:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;priceSurge&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;/// Estimates what a ride will cost.&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;FareEndpoint&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;Endpoint&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;/// Returns an estimate for a ride from [pickup] to [destination].&lt;/span&gt;
  &lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;FareEstimate&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;estimateFare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;Session&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Location&lt;/span&gt; &lt;span class="n"&gt;pickup&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Location&lt;/span&gt; &lt;span class="n"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;baseFare&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;2.50&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;pricePerKm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;1.20&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;distanceKm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_distanceKm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pickup&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;fare&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;baseFare&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;distanceKm&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;pricePerKm&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;priceSurge&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;FareEstimate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;fare:&lt;/span&gt; &lt;span class="n"&gt;_round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fare&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nl"&gt;currency:&lt;/span&gt; &lt;span class="s"&gt;'USD'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;surgeMultiplier&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;2.0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;surgeFare&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fare&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;surgeMultiplier&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;FareEstimate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;fare:&lt;/span&gt; &lt;span class="n"&gt;_round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;surgeFare&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="nl"&gt;currency:&lt;/span&gt; &lt;span class="s"&gt;'USD'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;fareLow:&lt;/span&gt; &lt;span class="n"&gt;_round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;surgeFare&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.9&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="nl"&gt;fareHigh:&lt;/span&gt; &lt;span class="n"&gt;_round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;surgeFare&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;1.15&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Save the model and endpoint files. After regeneration, &lt;code&gt;estimate.fare&lt;/code&gt; is valid again. The two new fields are also available in Flutter.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6x4kxt1wfbpnlrbpucx6.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6x4kxt1wfbpnlrbpucx6.jpg" alt="type-safe-auto-fields" width="800" height="508"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Display the new fields in Flutter
&lt;/h3&gt;

&lt;p&gt;In &lt;code&gt;tripquote_flutter/lib/main.dart&lt;/code&gt;, replace only &lt;code&gt;fareLabel&lt;/code&gt;. Keep the client setup, request, and widget unchanged:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="nf"&gt;fareLabel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;FareEstimate&lt;/span&gt; &lt;span class="n"&gt;estimate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;low&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;estimate&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fareLow&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;high&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;estimate&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fareHigh&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;low&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;high&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="si"&gt;${estimate.currency}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;$low&lt;/span&gt;&lt;span class="s"&gt; to &lt;/span&gt;&lt;span class="si"&gt;$high&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;Surge pricing'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="si"&gt;${estimate.currency}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;${estimate.fare}&lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When both range values are present, the display shows the range and indicates that surge pricing applies. Otherwise, it shows the single fare.&lt;/p&gt;

&lt;p&gt;With &lt;code&gt;priceSurge&lt;/code&gt; set to &lt;code&gt;false&lt;/code&gt;, restart the server and Flutter app. The screen shows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;USD 16.54
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Set &lt;code&gt;priceSurge&lt;/code&gt; to &lt;code&gt;true&lt;/code&gt;, then restart both again:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;USD 29.77 to 38.05
Surge pricing
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Restarting the server applies the changed flag. Restarting Flutter makes a new request.&lt;/p&gt;

&lt;p&gt;The request stayed the same. We updated the model definition, pricing logic, and Flutter display. Serverpod generated the corresponding model classes and conversion code on both sides, along with the code that connects the endpoint to Flutter.&lt;/p&gt;

&lt;p&gt;You write the contract. Serverpod generates the plumbing.&lt;/p&gt;

&lt;p&gt;The same process works with more than the numbers and strings used in the example above. Enums, lists, maps, nested models, byte data, and UUIDs are a few of the other supported types. The &lt;a href="https://docs.serverpod.dev/concepts/data-and-the-database/models" rel="noopener noreferrer"&gt;model documentation&lt;/a&gt; covers the full set.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;The code that connects your Flutter app to its backend has to be right. Maintaining it still takes time away from the feature you're building, even with help from a coding agent. Serverpod reduces that maintenance, while the Dart analyzer helps catch type errors before you run the app.&lt;/p&gt;

&lt;p&gt;To apply the same approach to another feature, &lt;a href="https://serverpod.dev/blog/flutter-google-apis-server-side" rel="noopener noreferrer"&gt;learn how to call Google APIs from a Serverpod backend&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Happy coding!&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>backend</category>
      <category>serverpod</category>
    </item>
    <item>
      <title>Accessing Google APIs server-side from a Flutter app</title>
      <dc:creator>Jamiu Okanlawon</dc:creator>
      <pubDate>Thu, 13 Aug 2026 16:51:14 +0000</pubDate>
      <link>https://dev.to/serverpod/accessing-google-apis-server-side-from-a-flutter-app-3ip2</link>
      <guid>https://dev.to/serverpod/accessing-google-apis-server-side-from-a-flutter-app-3ip2</guid>
      <description>&lt;p&gt;Your Flutter app needs data that lives in a user’s Google account. Their calendar, their contacts, their files. Google’s APIs will hand it over, but not to just anyone: the user has to consent in your app, and your backend needs an access token that lets it act on their behalf.&lt;/p&gt;

&lt;p&gt;That second part is where most Flutter developers get stuck, because it means OAuth credentials, token verification, authenticated sessions, and a backend to hold them.&lt;/p&gt;

&lt;p&gt;With Serverpod, that backend comes with Google sign-in built in. When a user signs in, your server gets their verified identity and receives an access token. Reading their calendar is an HTTP call away.&lt;/p&gt;

&lt;p&gt;In this article, we’ll wire it end-to-end: Google sign-in for a Flutter app, and a backend that reads the user’s upcoming calendar events the moment they sign in. We will demonstrate it with the calendar, but the pattern is the same for any Google API that the user can grant you access to.&lt;/p&gt;

&lt;h2&gt;
  
  
  Understanding the flow
&lt;/h2&gt;

&lt;p&gt;Before configuring anything, it’s worth understanding the whole journey.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F28h6a5gbfzayr3pgeue2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F28h6a5gbfzayr3pgeue2.png" alt=" " width="800" height="387"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When a user signs in, your app asks Google for two things: proof of who the user is, and permission to read their calendar. Google shows the consent screen, and what comes back reaches your Serverpod backend.&lt;/p&gt;

&lt;p&gt;The server verifies that the identity token was issued by Google, creates an authenticated session, and receives the access token that carries the user’s consent. That token is what lets your backend call Google’s APIs for this user.&lt;/p&gt;

&lt;p&gt;The user grants access in your app. API access occurs on your server.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before we start
&lt;/h2&gt;

&lt;p&gt;We’ll begin with a new Serverpod project created using:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;serverpod create my_project
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;During project creation, keep the recommended defaults and make sure Authentication remains enabled when prompted.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F00mylv1xt81e5x484pq1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F00mylv1xt81e5x484pq1.png" alt=" " width="800" height="387"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The command gives you a Flutter app (&lt;code&gt;my_project_flutter&lt;/code&gt;), the backend it talks to (&lt;code&gt;my_project_server&lt;/code&gt;), and a generated client package that connects the two, so your app calls the server using plain Dart methods.&lt;/p&gt;

&lt;p&gt;With the project in place, we’re ready to connect it to Google.&lt;/p&gt;

&lt;h2&gt;
  
  
  Configuring Google
&lt;/h2&gt;

&lt;p&gt;Every Google integration begins in Google Cloud, regardless of which backend you’re using.&lt;/p&gt;

&lt;p&gt;You’ll need to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create a Google Cloud project.&lt;/li&gt;
&lt;li&gt;Configure the Google Auth Platform.&lt;/li&gt;
&lt;li&gt;Enable the required APIs: the People API, which Serverpod uses for basic profile data, and the Calendar API, which is used in this article.&lt;/li&gt;
&lt;li&gt;Under Data access, add the &lt;code&gt;userinfo.email&lt;/code&gt;, &lt;code&gt;userinfo.profile&lt;/code&gt;, and &lt;code&gt;calendar.readonly&lt;/code&gt; scopes.&lt;/li&gt;
&lt;li&gt;Create a Web OAuth client for your backend, and any platform-specific clients your app needs.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;The Calendar scope is sensitive, so Google may ask you to verify your app before you publish it. While it’s in testing mode, any account you add as a test user can sign in.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The complete walkthrough, including platform-specific configuration for Android, iOS, and the web, is covered in the Serverpod &lt;a href="https://docs.serverpod.dev/next/concepts/authentication/providers/google/setup#get-your-google-credentials" rel="noopener noreferrer"&gt;documentation&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Once those credentials are ready, it’s time to connect them to Serverpod.&lt;/p&gt;

&lt;h2&gt;
  
  
  Connecting Google to Serverpod
&lt;/h2&gt;

&lt;p&gt;The first step in connecting your backend to Google is making your Google credentials available to Serverpod.&lt;/p&gt;

&lt;p&gt;Copy your Web OAuth credentials into &lt;code&gt;config/passwords.yaml&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;development&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;googleClientSecret&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;{&lt;/span&gt;
      &lt;span class="s"&gt;"web": {&lt;/span&gt;
        &lt;span class="s"&gt;"client_id": "your-client-id.apps.googleusercontent.com",&lt;/span&gt;
        &lt;span class="s"&gt;"client_secret": "your-client-secret",&lt;/span&gt;
        &lt;span class="s"&gt;"redirect_uris": []&lt;/span&gt;
      &lt;span class="s"&gt;}&lt;/span&gt;
    &lt;span class="s"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Serverpod reads these credentials at startup and uses them to verify Google identity tokens.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The credential block is a YAML block scalar, so an indentation error silently produces malformed JSON and surfaces later as an authentication failure rather than an error at startup.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The complete credential structure, along with production configuration options, is covered in the &lt;a href="https://docs.serverpod.dev/next/concepts/authentication/providers/google/setup#store-your-credentials" rel="noopener noreferrer"&gt;documentation&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Register the Google provider
&lt;/h2&gt;

&lt;p&gt;Next, register Google as one of your application’s identity providers.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="n"&gt;pod&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;initializeAuthServices&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nl"&gt;tokenManagerBuilders:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="n"&gt;JwtConfigFromPasswords&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="nl"&gt;identityProviderBuilders:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="n"&gt;GoogleIdpConfigFromPasswords&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;GoogleIdpConfigFromPasswords()&lt;/code&gt; reads the credentials from &lt;code&gt;passwords.yaml&lt;/code&gt; and configures Google as an available authentication provider.&lt;/p&gt;

&lt;p&gt;The final server-side step is exposing that provider through an endpoint.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:serverpod_auth_idp_server/providers/google.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;GoogleIdpEndpoint&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;GoogleIdpBaseEndpoint&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That’s the entire endpoint.&lt;/p&gt;

&lt;p&gt;Serverpod already provides the implementation. Your application exposes it, making the Google authentication flow available to your Flutter client.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reading the calendar from your server
&lt;/h2&gt;

&lt;p&gt;When a user signs in, Serverpod passes your code the access token via a callback in the provider config. Whatever you do with it runs on your server, with the user’s consent.&lt;/p&gt;

&lt;p&gt;Add the &lt;code&gt;http&lt;/code&gt; package to your server, then extend the provider registration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dart pub add http    &lt;span class="c"&gt;# from my_project_server&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="c1"&gt;// my_project_server/lib/server.dart&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'dart:convert'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:http/http.dart'&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;pod&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;initializeAuthServices&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="c1"&gt;// ... tokenManagerBuilders stay as they are ...&lt;/span&gt;
  &lt;span class="nl"&gt;identityProviderBuilders:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="n"&gt;GoogleIdpConfigFromPasswords&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;getExtraGoogleInfoCallback:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="n"&gt;accountDetails&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="n"&gt;accessToken&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="n"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="kt"&gt;Uri&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;https&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'www.googleapis.com'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;'/calendar/v3/calendars/primary/events'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
              &lt;span class="s"&gt;'maxResults'&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;'5'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="s"&gt;'singleEvents'&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;'true'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="s"&gt;'orderBy'&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;'startTime'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="s"&gt;'timeMin'&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;DateTime&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toUtc&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toIso8601String&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="p"&gt;}),&lt;/span&gt;
            &lt;span class="nl"&gt;headers:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;'Authorization'&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;'Bearer &lt;/span&gt;&lt;span class="si"&gt;$accessToken&lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
          &lt;span class="p"&gt;);&lt;/span&gt;
          &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;statusCode&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'Calendar request failed: &lt;/span&gt;&lt;span class="si"&gt;${response.statusCode}&lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="nl"&gt;level:&lt;/span&gt; &lt;span class="n"&gt;LogLevel&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;warning&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
          &lt;span class="p"&gt;}&lt;/span&gt;
          &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jsonDecode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="s"&gt;'items'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kt"&gt;List&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
          &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'Upcoming: &lt;/span&gt;&lt;span class="si"&gt;${event['summary'] ?? '(no title)'}&lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
          &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'Calendar read skipped: &lt;/span&gt;&lt;span class="si"&gt;$e&lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;level:&lt;/span&gt; &lt;span class="n"&gt;LogLevel&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;warning&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That’s a real Google API call, made by your backend, for the user who just signed in. This example logs their next five events. Yours might store them instead.&lt;/p&gt;

&lt;p&gt;There are two things to know about this callback.&lt;/p&gt;

&lt;p&gt;First, it runs as part of the sign-in flow. If the calendar request fails and the error goes uncaught, the entire sign-in fails too. Treat the calendar read as optional: log the error and let sign-in continue.&lt;/p&gt;

&lt;p&gt;Second, the access token passed to the callback is short-lived. It works well for reading calendar data immediately during sign-in. If you need to sync calendar data later, such as in a scheduled or background job, you’ll need to store the credentials and handle token refresh separately.&lt;/p&gt;

&lt;p&gt;The same token works against any Google API covered by the scopes the user granted. The &lt;a href="https://docs.serverpod.dev/next/concepts/authentication/providers/google/customizations#accessing-google-apis" rel="noopener noreferrer"&gt;documentation&lt;/a&gt; covers how to request additional scopes and use the token in your own code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running the application
&lt;/h2&gt;

&lt;p&gt;With the backend configured, it’s time to start the project.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;serverpod start
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That’s the only command it takes.&lt;/p&gt;

&lt;p&gt;Besides launching your backend, Flutter application, and development services, &lt;code&gt;serverpod start&lt;/code&gt; automatically generates any required code before starting the project.&lt;/p&gt;

&lt;p&gt;If your authentication setup introduces new database tables, there’s no need to stop the server. While &lt;code&gt;serverpod start&lt;/code&gt; is running, press &lt;strong&gt;M&lt;/strong&gt; to create and apply the migration. Once it has been applied, development continues without interrupting your workflow.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr6ztdabzff7j91qn413s.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr6ztdabzff7j91qn413s.png" alt=" " width="800" height="387"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;With the backend running, the remaining setup happens in Flutter.&lt;/p&gt;

&lt;h2&gt;
  
  
  Connecting Flutter
&lt;/h2&gt;

&lt;p&gt;The template already initializes authentication when the app starts. Adding Google is one more call in &lt;code&gt;main.dart&lt;/code&gt;, right after &lt;code&gt;client.auth.initialize()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;auth&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;initializeGoogleSignIn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nl"&gt;serverClientId:&lt;/span&gt; &lt;span class="s"&gt;'&amp;lt;web-client-id&amp;gt;.apps.googleusercontent.com'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;serverClientId&lt;/code&gt; is the Web application client ID from earlier, not one of the platform-specific ones. That’s deliberate: the sign-in happens in your app, but the tokens are for your server, and this value tells Google which server that is. On the web, the same value is used as &lt;code&gt;clientId&lt;/code&gt;, along with a redirect URI. The &lt;a href="https://docs.serverpod.dev/next/concepts/authentication/providers/google/setup#initialize-the-google-sign-in-service" rel="noopener noreferrer"&gt;documentation&lt;/a&gt; includes the exact call for each platform.&lt;/p&gt;

&lt;p&gt;Then ask for the calendar scope when the user signs in. The sign-in screen that ships with the template uses &lt;code&gt;SignInWidget&lt;/code&gt;, which accepts a customized Google button:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="n"&gt;SignInWidget&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nl"&gt;client:&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;googleSignInWidget:&lt;/span&gt; &lt;span class="n"&gt;GoogleSignInWidget&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nl"&gt;client:&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;scopes:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="p"&gt;..&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;GoogleAuthController&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;defaultScopes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s"&gt;'https://www.googleapis.com/auth/calendar.readonly'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The scopes here are what Google puts on the consent screen and what your server’s access token is allowed to do.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trying it out
&lt;/h2&gt;

&lt;p&gt;Run the app and tap the Google button.&lt;/p&gt;

&lt;p&gt;The consent screen now asks for calendar access alongside the usual profile permissions.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkw135jjfsfj9nbkxi1lr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkw135jjfsfj9nbkxi1lr.png" alt=" " width="799" height="464"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Approve it, and watch your server’s logs.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvo84g6o298fvkjlpj9gn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvo84g6o298fvkjlpj9gn.png" alt=" " width="800" height="553"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Your backend just called Google on the user’s behalf. Sign-in, verification, session, and a working API call, and the only authentication code you wrote was a callback.&lt;/p&gt;

&lt;h2&gt;
  
  
  A pattern you’ll reuse
&lt;/h2&gt;

&lt;p&gt;The calendar was just an example. Change the scopes you request, and the endpoint you call, and the same flow reads Google Drive files, Contacts, or YouTube data. Add another identity provider, and the same four steps repeat: configure the provider, register it with Serverpod, expose its endpoint, and initialize it in Flutter.&lt;/p&gt;

&lt;p&gt;The provider-specific configuration changes, but the authentication architecture stays the same. That consistency is one of the advantages of Serverpod’s authentication system. As your application grows, you don’t have to rethink your authentication flow. You just plug in another provider, or another API, and keep building.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Accessing Google APIs from a Flutter app has a reputation for being complicated, and most of that reputation stems from the requirements Google imposes, no matter what backend you use: credentials, consent, and verification. Once those are in place, Serverpod’s side is a config entry, an empty endpoint class, and a callback that receives a ready-to-use access token.&lt;/p&gt;

&lt;p&gt;From there, Serverpod handles verifying identity tokens, creating authenticated sessions, and maintaining consistent authentication across your application. What you do with the APIs is up to you.&lt;/p&gt;

&lt;p&gt;Happy coding!&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>dart</category>
      <category>backend</category>
      <category>google</category>
    </item>
  </channel>
</rss>
