Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Developing with Firebase - Best Practices

Developing with Firebase - Best Practices

This talk will include some best practices of developing fast with firebase and how to get the most out of the platform during development and deployment.

Firebase Thailand

April 01, 2023
Tweet

More Decks by Firebase Thailand

Other Decks in Technology

Transcript

  1. Developing with Firebase Best practices Google Developer Expert in Firebase

    Dennis Alund Firebase Dev Day 2023 GDG Bangkok Firebase Thailand Organized by
  2. #FirebaseDevDay2023 • Start with naive representations of your views, presented

    as JSON data in Firestore. • Whatever you need in your current screen or widget, make a document containing it and see how that works out – try first, fix later • Loop back and adjust data if you need. • Start breaking out data when you find yourself trying to combine screens in documents Data modeling - Top down
  3. #FirebaseDevDay2023 • A document describe a screen or view •

    Difficult to get data ➡ wrong design • Never make any joins Rule of thumb
  4. @override Widget build(BuildContext context) { final Map<String, dynamic> obj =

    { "name": "Dennis Alund" , "email": "[email protected]" , }; // To print out "Dennis Alund <[email protected]>" return Text("${obj['name']!} <${obj['email']!}>"); }
  5. @override Widget build(BuildContext context) { return StreamBuilder<Map<String, dynamic>>( stream: Stream.value({

    "name": "Dennis Alund" , "email" : "[email protected]" , }), builder: (context, snapshot) { if (snapshot.data == null) return Container(); return Text("${snapshot.data['name']!} <${snapshot.data['email']!}>"); }, ); }
  6. @override Widget build(BuildContext context) { return StreamBuilder<Map<String, dynamic>?>( stream: FirebaseFirestore

    .instance .collection("contacts") .doc(id) .snapshots() .map((doc) => doc.data()), builder: (context, snapshot) { if (snapshot.data == null) return Container(); return Text("${snapshot.data['name']!} <${snapshot.data['email']!}>"); }, );
  7. vs

  8. POST https://api.example.com/transactions/ Authorization ... { "from": "<my account>", "to": "<your

    account>", "amount": 123, "currency": "USD", } Wait for it to complete… How to check ongoing transactions if reloading page…
  9. @override Widget build(BuildContext context) { return TextButton( child: Text("Make transaction"

    ), onPressed: () => transactionService .makeTransaction ({ "from": "<my account>" , "to": "<your account>" , "amount": 123, "currency": "USD", }), ); } Wait for it to complete… How to check ongoing transactions if reloading page…
  10. @override Widget build(BuildContext context) { return TextButton( child: Text("Make transaction"

    ), onPressed: () => FirebaseFirestore .instance.collection("transactions" ).add({ "status": "requested", "uid": "<requesting uid>" , "from": "<my account>" , "to": "<your account>" , "amount": 123, "currency": "USD", }), ); }
  11. @override Widget build(BuildContext context) { return StreamBuilder<QuerySnapshot>( stream: FirebaseFirestore .instance

    .collection("transactions" ) .where("uid", isEqualTo: "<my UID>") .where("status", isEqualTo: "processing") // "approved" | "rejected" .snapshots(), builder: (context, snapshot) { return ListView( children: snapshot.data.docs . map((doc) => doc.data()) . map((trx) => TransactionWidget (trx)) . toList(), Bonus: audit log
  12. match /transactions/{id} { allow read: if isLoggedInAs(resource.data.uid) && isMyAccount(resource.data.from); allow

    create: if isLoggedInAs(request.resource.data.uid); } function isLoggedInAs(uid) { return uid == request.auth.uid; } function isMyAccount(accountId) { let accountDoc = get(/databases/$(database)/documents/accounts/$(accountId)); return accountDoc.data.owner == request.auth.uid; }
  13. #FirebaseDevDay2023 Summary • Instant write completion, data sync in background

    • Offline data • Smart cache and partial updates • Internal message bus - writes are locally propagated on app while writing to cloud
  14. #FirebaseDevDay2023 Callable cloud function from app API request from external

    system Report generation, account creation, backups or similar
  15. export const generateReportFromApp = functions .https.onCall(async (request, context) => {

    const uid = context.auth?.uid; // Check authorization // Generate report... can take some time. const reportData = await generateReport (); // After some time, return to client return reportData; });
  16. export const generateReportFromApi = functions .https.onRequest(async (request, response) => {

    const authorization = request.headers.authorization; // Check authorization // Generate report... can take some time. const reportData = await generateReport (); response.json(reportData); });
  17. #FirebaseDevDay2023 Enqueue functions with Cloud Tasks • Schedule a function

    to be triggered after delay • Limit the amount of executions in parallel • Declare the number of retries • Calling external API with rate limit to avoid DDOS • Unifying workflow of processing requests
  18. export const generateReportFromApi = functions .https.onRequest(async (request, response) => {

    const authorization = request.headers.authorization; // Check authorization await getFunctions().taskQueue("generateReport" ) .enqueue({ // JSON data }); response.sendStatus(200); });
  19. export const generateReportFromApp = functions .https.onCall(async (request, context) => {

    const uid = context.auth?.uid; // Check authorization await getFunctions().taskQueue("generateReport" ) .enqueue({ // JSON data }); });
  20. export const generateReport = functions .tasks .onTaskDispatched({ retryConfig: { maxAttempts:

    3, minBackoffSeconds: 5, }, rateLimits: { maxConcurrentDispatches: 10, }, }, async (request) => { const payload = request.data.payload; // Process the data and write to Firestore })
  21. ElevatedButton( child: const Text('Generate report'), onPressed: () => FirebaseFunctions.instance .httpsCallable('generateReport')

    .call(), ), StreamBuilder<QuerySnapshot>( stream: FirebaseFirestore.instance .collection('reports') .where('owner', isEqualTo: FirebaseAuth.instance.currentUser!.uid, ).snapshots(), builder: (context, snapshot) { // Display spinner while loading and then list reports },
  22. #FirebaseDevDay2023 • External access only! • Regular HTTP API endpoint

    • SSR public website • Server to server • Lightweight and easy to maintain API endpoints • Process of data where you do not need to retain the triggering data • Simple invocations to process something • Safe access from your app since it use SDK to verify app credentials • No offline Firestore • Instant write completion, data sync in background • Offline data • Smart cache and partial updates • Internal message bus - writes are locally propagated on app while writing to cloud Callables HTTPS API
  23. #FirebaseDevDay2023 • Use App Distribution for PR reviews or new

    versions to development • Use hosting preview channels and name them according to the PR/branch name • Let’s your PR reviewer test your changes instead of checking out, build and deploy by themselves • Can also be used to avoid having public website permanent on dev/staging environments Integrate the code review process
  24. https://bit.ly/hosting-preview - name: Set up preview channel if: github.event_name ==

    'pull_request' # Deploying to preview channel using the branch name as channel name # String split '/' and use the last part of the branch name run: | echo "CHANNEL_NAME=$(echo ${{ github.head_ref }} | grep -o '[^/]*$')" >> $GITHUB_ENV echo "CHANNEL_LIFESPAN=3d" >> $GITHUB_ENV - name: Deploy preview channel to Firebase if: github.event_name == 'pull_request' run: >- firebase hosting:channel:deploy ${{ env.CHANNEL_NAME }} --expires ${{ env.CHANNEL_LIFESPAN }}
  25. https://bit.ly/hosting-preview - name: Get preview URL if: github.event_name == 'pull_request'

    run: | echo "PREVIEW_URL=$(firebase hosting:channel:list | grep ${{ env.CHANNEL_NAME }} | grep -o 'https.*\.app')" >> $GITHUB_ENV - name: Post preview channel URL as a PR comments uses: mshick/add-pr-comment@v2 with: message-id: ${{ matrix.project }} allow-repeats: false message: | Preview the `${{ matrix.project }}` app for ${{ env.CHANNEL_LIFESPAN }}: ${{ env.PREVIEW_URL }}