Cloud Functions for Firebase ile tam yığınlı Dart uygulaması oluşturma

1. Giriş

Bu codelab'de çok oyunculu bir sayaç uygulaması oluşturacaksınız. Hem Flutter ön ucu hem de Firebase arka ucu için Dart'ı nasıl kullanacağınızı öğreneceksiniz.

Ayrıca, veri modellerini uygulamanız ve sunucunuz arasında nasıl paylaşacağınızı da öğrenerek mantığı kopyalama ihtiyacını ortadan kaldırabilirsiniz.

Bu codelab'in kaynak kodunun tamamını Cloud Functions for Firebase örnekleri deposunda bulabilirsiniz.

Neler öğreneceksiniz?

  • Paylaşılan iş mantığını bağımsız bir Dart paketine ayırın.
  • Cloud Functions for Firebase işlevlerini Dart'ta yerel olarak yazıp dağıtın.
  • Sunucusuz baştan başlatma işlemlerini azaltmak için Dart'ın önceden derleme (AOT) özelliğinden yararlanın.
  • Firebase Emulator Suite'i kullanarak yığınınızı yerel olarak test edin.

2. Ön koşullar

  • Flutter SDK (en son kararlı sürüm).
  • Firebase CLI (v15.15.0 veya daha yeni bir sürüm gerekir).
  • Dart ve Flutter eklentilerinin yüklü olduğu Antigravity, Visual Studio Code, IntelliJ veya Android Studio gibi bir kod düzenleyici.
  • Flutter ve Firebase hakkında temel bilgi sahibi olmanız gerekir.

3. Arka uçta neden Dart kullanılmalıdır?

Birçok bulut uygulaması, ön uç kullanıcı arayüzü için Dart'ı, arka uç için ise TypeScript, Python veya Go gibi başka bir dili kullanır. Bu işlem için iki ayrı veri modeli grubunun korunması gerekir. Veritabanı şeması değiştiğinde her iki kod tabanını da güncellemeniz gerekir.

Not: Arka uçta Dart kullanmak, kodu kopyalamadan istemcide Flutter'ın duyarlı kullanıcı deneyimini sunucu tarafında güvenli doğrulama ile birleştirmenize olanak tanır.

4. Flutter uygulamasını oluşturma

Standart bir Flutter uygulaması oluşturun:

flutter create my_counter
cd my_counter
# Run the app to see the default counter example
flutter run

Standart bir Flutter uygulamasında lib/main.dart, sayaç durumunu yerel olarak işler:

int _counter = 0;

void _incrementCounter() {
  setState(() {
    _counter++;
  });
}

Bu yaklaşım yerel durum için işe yarar ancak sunucunun doğruluk kaynağı olarak hareket etmesi gereken çok oyunculu bir uygulamada ölçeklenmez. Birden fazla oyuncuyu desteklemek için bu mantığı aşağıdaki adımlarda arka uca taşıyacağız.

5. Paylaşılan paketi oluşturma

Ön uçta ve arka uçta modellerin yinelenmesini önlemek için proje deponuzda paylaşılan bir Dart paketi oluşturun. Hem Flutter uygulaması hem de Firebase için işlevler bu pakete bağlıdır.

my_counter projesinin kökünden aşağıdaki komutları çalıştırın:

mkdir -p packages
cd packages
dart create -t package shared

Bağımlılık ekleme

packages/shared/pubspec.yaml bölümüne JSON serileştirme araçlarını ekleyin:

dependencies:
  json_annotation: ^4.11.0

dev_dependencies:
  build_runner: ^2.13.1
  json_serializable: ^6.13.1

Paylaşılan modellerinizi tanımlama

Oluştur packages/shared/lib/src/models.dart. Bu dosya, hem uygulama hem de sunucu tarafından kullanılan veri yapısını tanımlar.

import 'package:json_annotation/json_annotation.dart';

part 'models.g.dart';

@JsonSerializable()
class IncrementResponse {
  final bool success;
  final String? message;
  final int? newCount;

  const IncrementResponse({required this.success, this.message, this.newCount});

  factory IncrementResponse.fromJson(Map<String, dynamic> json) =>
      _$IncrementResponseFromJson(json);

  Map<String, dynamic> toJson() => _$IncrementResponseToJson(this);
}

// Store the function name as a constant to ensure consistency between client and server.
const incrementCallable = 'increment';

packages/shared/lib/shared.dart'da modellerinizi dışa aktarın:

library shared;

export 'src/models.dart';

packages/shared dizininde, JSON serileştirme kodunu oluşturmak için derleme çalıştırıcıyı çalıştırın:

dart run build_runner build

6. Cloud Functions for Firebase'i ayarlama

Firebase için Cloud Functions, kendi sunucularınızı yönetip ölçeklendirmeniz gerekmeden arka uç kodunu otomatik olarak çalıştırmanıza olanak tanıyan sunucusuz bir çerçevedir. Dart, Ahead-of-Time (AOT) derlemesiyle ikiliye derlendiği ve Node.js veya Java gibi ağır bir çalışma zamanı ortamı gerektirmediği için bu iş için idealdir. Bu, işlevlerinizin baştan başlatma sürelerini önemli ölçüde azaltır.

Projenizin kök dizinine gidin ve Cloud Functions for Firebase'i başlatın:

cd ../..
firebase experiments:enable dartfunctions
firebase init functions
dart pub add google_cloud_firestore
  • Dil istendiğinde Dart'ı seçin.

functions/pubspec.yaml içinde, paylaşılan pakete göreli bir yol ekleyin:

dependencies:
  firebase_admin_sdk: ^0.5.6
  firebase_functions: ^0.8.0
  google_cloud_firestore: ^0.5.5
  shared:
    path: ../packages/shared

7. İşlevi yazma

Arka uç mantığını yazmak için functions/bin/server.dart dosyasını açın ve içeriği aşağıdaki kodla değiştirin:

import 'dart:convert';
import 'package:firebase_functions/firebase_functions.dart';
import 'package:google_cloud_firestore/google_cloud_firestore.dart'
    show FieldValue;
import 'package:shared/shared.dart';

void main() {
  runFunctions((firebase) {

    // Listen for calls to the http request and name defined in the shared package.
    firebase.https.onRequest(name: incrementCallable, (request) async {

      // In a production app, verify the user with request.auth?.uid here.
      print('Incrementing counter on the server...');

      // Get firestore database instance
      final firestore = firebase.adminApp.firestore();

      // Get a reference to the counter document
      final counterDoc = firestore.collection('counters').doc('global');

      // Get the current snapshot for the count data
      final snapshot = await counterDoc.get();

      // Increment response we will send back
      IncrementResponse incrementResponse;

      // Check for the current count and if the snapshot exists
      if (snapshot.data() case {'count': int value} when snapshot.exists) {
        if (request.method == 'GET') {
          // Get the current result
          incrementResponse = IncrementResponse(
            success: true,
            message: 'Read-only sync complete',
            newCount: value,
          );
        } else if (request.method == 'POST') {
          // Increment count by one
          final step =
              int.tryParse(request.url.queryParameters['step'] ?? '') ?? 1;
          await counterDoc.update({'count': FieldValue.increment(step)});
          incrementResponse = IncrementResponse(
            success: true,
            message: 'Atomic increment complete',
            newCount: value + step,
          );
        } else {
          throw HttpResponseException.badRequest(
            message: 'only GET and POST requests are allowed',
          );
        }
      } else {
        // Create a new document with a count of 1
        await counterDoc.set({'count': 1});
        incrementResponse = const IncrementResponse(
          success: true,
          message: 'Cloud-sync complete',
          newCount: 1,
        );
      }

      // Return the response as JSON
      return Response(
        200,
        body: jsonEncode(incrementResponse.toJson()),
        headers: {'Content-Type': 'application/json'},
      );
    });

  });
}

8. Firebase Emulator Suite ile yerel olarak test etme

Hem ön ucu hem de arka ucu dağıtmadan yerel olarak çalıştırabilirsiniz.

Projenizin kökünden Firebase Emulator Suite'i başlatın:

# Enable functions and firestore for the emulators
firebase init emulators
# Start the emulators and optionally open up the Admin UI
firebase emulators:start

pubspec.yaml içinde, paylaşılan pakete göreli bir yol ekleyin ve http paketini ekleyin:

dependencies:
  http: ^1.6.0
  shared:
    path: packages/shared

Flutter projenizde lib/main.dart dosyasını açın ve içeriğini aşağıdaki kodla değiştirin. Bu ön uç kodu, arka uçla aynı IncrementResponse sınıfını kullanır.

import 'dart:convert';

import 'package:http/http.dart' as http;
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:shared/shared.dart';

/// Get from emulator output when running or when deploying:
/// ✔ functions[us-central1-increment]: http function initialized
///  (http://127.0.0.1:5001/demo-no-project/us-central1/increment).
const incrementUrl = 'FIREBASE_FUNCTIONS_URL_HERE';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});
  @override
  Widget build(BuildContext context) => MaterialApp(
    debugShowCheckedModeBanner: false,
    theme: ThemeData(useMaterial3: true, colorSchemeSeed: Colors.blue),
    home: const CounterPage(),
  );
}

class CounterPage extends StatefulWidget {
  const CounterPage({super.key});
  @override
  State<CounterPage> createState() => _CounterPageState();
}

class _CounterPageState extends State<CounterPage> {
  int _count = 0;
  bool _loading = false;

  @override
  void initState() {
    super.initState();
    // Fetch the current count
    _increment(readOnly: true).ignore();
  }

  Future<void> _increment({bool readOnly = false}) async {
    setState(() => _loading = true);
    try {
       // Call the Dart function.
      final uri = Uri.parse(incrementUrl);
      final response = readOnly ? await http.get(uri) : await http.post(uri);

      // Parse the response back into the shared Dart object.
      final responseData = jsonDecode(response.body);
      final incrementResponse = IncrementResponse.fromJson(responseData);

      if (incrementResponse.success) {
        setState(() => _count = incrementResponse.newCount ?? _count);
      }
    } catch (e) {
      print("Error calling function: $e");
    } finally {
      setState(() => _loading = false);
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Multiplayer Counter')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Text('You have pushed the button this many times:'),
            Text(
              '$_count',
              style: Theme.of(context).textTheme.headlineMedium,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _loading ? null : _increment,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}

Flutter uygulamasını çalıştırın. Kayan işlem düğmesini tıkladığınızda uygulama, yerel Dart arka ucunu çağırır, yeni sayıyı alır ve kullanıcı arayüzünü günceller.

9. Firebase'e dağıtma

Bu codelab'de, Firebase Local Emulator Suite'i kullanarak Firebase projesi veya faturalandırma hesabı olmadan işlevleri deneyebildiniz. İşlevlerinizi gerçek bir ortamda (ör. üretim ortamı) kullanmak istiyorsanız Firebase projesi ve faturalandırma ayarlamanız gerekir.

Firebase projesi oluşturma

  1. Google Hesabınızı kullanarak Firebase konsolunda oturum açın.
  2. Yeni bir proje oluşturmak için düğmeyi tıklayın ve ardından bir proje adı girin.
  3. Devam'ı tıklayın.
  4. İstenirse Firebase şartlarını inceleyip kabul edin ve Devam'ı tıklayın.
  5. (İsteğe bağlı) Firebase konsolunda yapay zeka yardımını etkinleştirin ("Firebase'de Gemini" olarak adlandırılır).
  6. Bu codelab için Google Analytics'e ihtiyacınız yoktur. Bu nedenle Google Analytics seçeneğini devre dışı bırakın.
  7. Proje oluştur'u tıklayın, projenizin hazırlanmasını bekleyin ve ardından Devam'ı tıklayın.

Firebase fiyatlandırma planınızı yükseltme

Bu codelab'deki Firebase hizmetlerini kullanmak için Firebase projenizin kullandıkça öde (Blaze) fiyatlandırma planında olması gerekir. Bu da projenin bir Cloud Faturalandırma hesabına bağlı olduğu anlamına gelir.

  • Cloud Faturalandırma Hesabı için kredi kartı gibi bir ödeme yöntemi gerekir.
  • Özel promosyonlar sırasında veya bu codelab'i bir etkinliğin parçası olarak yapıyorsanız Google Cloud kredileri kullanılabilir.
  • Firebase ve Google Cloud'u kullanmaya yeni başladıysanız 300 ABD doları değerinde kredi ve ücretsiz deneme Cloud Faturalandırma hesabı için uygun olup olmadığınızı kontrol edin.

Projenizi Blaze planına yükseltmek için aşağıdaki adımları uygulayın:

  1. Firebase konsolunda planınızı yükseltmeyi seçin.
  2. Blaze planını seçin. Cloud Faturalandırma Hesabı'nı projenize bağlamak için ekrandaki talimatları uygulayın.
    • Bu codelab için Google Cloud kredileri kullanıyorsanız faturalandırma hesabının adı büyük olasılıkla Google Cloud Platform Trial Billing Account veya My Billing Account'dır.
    • Bu yükseltme kapsamında bir Cloud Faturalandırma hesabı oluşturmanız gerekiyorsa yükseltmeyi tamamlamak için Firebase konsolundaki yükseltme akışına geri dönmeniz gerekebilir.

Firebase projenize dağıtma

Dart arka ucunuzu dağıtmak için Firebase CLI'yı kullanarak aşağıdaki komutu çalıştırın:

firebase use <PROJECT_ID>
firebase deploy --only functions

Komut çalıştıktan sonra URL'yi kopyalayın ve daha önce eklediğimiz Flutter uygulaması kaynak kodundaki FIREBASE_FUNCTIONS_URL_HERE yerine yapıştırın.

10. Sorun giderme

firebase: command not found

Firebase CLI'nın yüklendiğinden ve PATH sürümünüzün güncellendiğinden emin olun. npm kullanarak yükleyebilirsiniz: npm install -g firebase-tools.

Dart, init işlevleri şablonlarında eksik

Dart'ın, firebase init functions çalıştırıldığında dağıtılacak seçenekler listesi olarak gösterilmesi ve şablon kodunun oluşturulması için firebase experiments:enable dartfunctions çalıştırılarak bir deneme işareti ayarlanması gerekir.

İşlevler emülatörü bağlanmıyor

localhost ve bağlantı noktası 5001 kullandığınızı doğrulayın. Android emülatöründe test yapıyorsanız cihaz, localhost öğesini ana makinenize çözmez. main.dart içinde emülatör yapılandırmasını güncelleyerek 10.0.2.2 kullanın.

Paylaşılan paket bulunamadı

functions/pubspec.yaml içindeki göreli yolu doğrulayın. Klasör yapınız codelab'den farklıysa path: ../packages/shared değerini doğru dizini gösterecek şekilde ayarlayın.

json_serializable kullanmam gerekir mi?

Kesinlikle gerekli olmasa da json_serializable kullanmak, fromJson ve toJson yöntemlerinin manuel olarak yazılmasından kaynaklanan hataları önler. Bu, ön uç ve arka ucunuzun tam olarak aynı veri biçimini beklediğinden emin olmanızı sağlar.

11. Tebrikler

Full-stack Dart uygulaması oluşturmayı başarıyla tamamladınız. Veri modellerinizi paylaşılan bir pakette tutarak API yanıtlarınızın ve istemci kullanıcı arayüzünüzün senkronize kalmasını sağlarsınız. Ayrıca, tüm yığın boyunca tek bir programlama dili kullanırsınız.

Bu projenin tamamlanmış kodunun tamamını görüntülemek için firebase/functions-samples deposundaki codelab örneğine bakın.