Slide 1

Slide 1 text

Developing with Firebase Best practices Google Developer Expert in Firebase Dennis Alund Firebase Dev Day 2023 GDG Bangkok Firebase Thailand Organized by

Slide 2

Slide 2 text

#FirebaseDevDay2023 Backend/ frontend No-backend

Slide 3

Slide 3 text

#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

Slide 4

Slide 4 text

#FirebaseDevDay2023 ● A document describe a screen or view ● Difficult to get data ➡ wrong design ● Never make any joins Rule of thumb

Slide 5

Slide 5 text

#FirebaseDevDay2023

Slide 6

Slide 6 text

@override Widget build(BuildContext context) { final Map obj = { "name": "Dennis Alund" , "email": "[email protected]" , }; // To print out "Dennis Alund " return Text("${obj['name']!} <${obj['email']!}>"); }

Slide 7

Slide 7 text

@override Widget build(BuildContext context) { return StreamBuilder>( 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']!}>"); }, ); }

Slide 8

Slide 8 text

#FirebaseDevDay2023

Slide 9

Slide 9 text

#FirebaseDevDay2023

Slide 10

Slide 10 text

@override Widget build(BuildContext context) { return StreamBuilder?>( 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']!}>"); }, );

Slide 11

Slide 11 text

vs

Slide 12

Slide 12 text

POST https://api.example.com/transactions/ Authorization ... { "from": "", "to": "", "amount": 123, "currency": "USD", } Wait for it to complete… How to check ongoing transactions if reloading page…

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

@override Widget build(BuildContext context) { return TextButton( child: Text("Make transaction" ), onPressed: () => FirebaseFirestore .instance.collection("transactions" ).add({ "status": "requested", "uid": "" , "from": "" , "to": "" , "amount": 123, "currency": "USD", }), ); }

Slide 15

Slide 15 text

@override Widget build(BuildContext context) { return StreamBuilder( stream: FirebaseFirestore .instance .collection("transactions" ) .where("uid", isEqualTo: "") .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

Slide 16

Slide 16 text

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; }

Slide 17

Slide 17 text

#FirebaseDevDay2023

Slide 18

Slide 18 text

#FirebaseDevDay2023

Slide 19

Slide 19 text

#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

Slide 20

Slide 20 text

Trigger a process

Slide 21

Slide 21 text

#FirebaseDevDay2023

Slide 22

Slide 22 text

#FirebaseDevDay2023 Callable cloud function from app API request from external system Report generation, account creation, backups or similar

Slide 23

Slide 23 text

Client App Ext integration Callable Function HTTPS Function Shared logic component

Slide 24

Slide 24 text

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; });

Slide 25

Slide 25 text

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); });

Slide 26

Slide 26 text

What about if it’s only async results?

Slide 27

Slide 27 text

Client App Cloud Tasks Ext integration Enqueue Callable Function HTTPS Function

Slide 28

Slide 28 text

#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

Slide 29

Slide 29 text

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); });

Slide 30

Slide 30 text

export const generateReportFromApp = functions .https.onCall(async (request, context) => { const uid = context.auth?.uid; // Check authorization await getFunctions().taskQueue("generateReport" ) .enqueue({ // JSON data }); });

Slide 31

Slide 31 text

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 })

Slide 32

Slide 32 text

ElevatedButton( child: const Text('Generate report'), onPressed: () => FirebaseFunctions.instance .httpsCallable('generateReport') .call(), ), StreamBuilder( stream: FirebaseFirestore.instance .collection('reports') .where('owner', isEqualTo: FirebaseAuth.instance.currentUser!.uid, ).snapshots(), builder: (context, snapshot) { // Display spinner while loading and then list reports },

Slide 33

Slide 33 text

#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

Slide 34

Slide 34 text

#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

Slide 35

Slide 35 text

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 }}

Slide 36

Slide 36 text

#FirebaseDevDay2023

Slide 37

Slide 37 text

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 }}

Slide 38

Slide 38 text

#FirebaseDevDay2023

Slide 39

Slide 39 text

#FirebaseDevDay2023 Firebase Dev Day 2023 THANK YOU! @DennisAlund