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.

Neler öğreneceksiniz?

  • Paylaşılan iş mantığını bağımsız bir Dart paketine ayırın.
  • Cloud Functions for Firebase'i 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 sonraki 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 düzeyde 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 kümesi tutmanız gerekir. Bir veritabanı şeması değiştiğinde her iki kod tabanını da güncellemeniz gerekir.

Not: Arka uçta Dart kullanmak, kodu kopyalamadan istemcideki Flutter'ın duyarlı kullanıcı deneyimini sunucudaki 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ünde JSON serileştirme araçlarını ekleyin:

dependencies:
  json_annotation: ^4.9.0

dev_dependencies:
  build_runner: ^2.4.9
  json_serializable: ^6.8.0

Paylaşılan modellerinizi tanımlama

Oluşturma 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 içinde 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

Cloud Functions for Firebase, kendi sunucularınızı yönetip ölçeklendirmeniz gerekmeden arka uç kodunu otomatik olarak çalıştırmanızı sağlayan sunucusuz bir çerçevedir. Dart, Ahead-of-Time (AOT) derlemesiyle ikili programa 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 sayede işlevlerinizin sıfırdan başlatma süreleri önemli ölçüde azalı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_functions:
  google_cloud_firestore:
  shared:
    path: ../packages/shared

7. İşlevi yazma

Arka uç mantığını yazmak için functions/bin/server.dart bağlantısı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(List<String> args) async {
  await fireUp(args, (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 = request.url.queryParameters['step'] as int? ?? 1;
          await counterDoc.update({'count': FieldValue.increment(step)});
          incrementResponse = IncrementResponse(
            success: true,
            message: 'Atomic increment complete',
            newCount: value + step,
          );
        } else {
          throw FailedPreconditionError(
            '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

Dağıtım yapmadan hem ön ucu hem de arka ucu 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 proje adını 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 geçirmek 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 Console'daki yükseltme akışına geri dönmeniz gerekebilir.

Firebase projenize dağıtma

Dart arka uçunuzu 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 kodunda FIREBASE_FUNCTIONS_URL_HERE yerine yapıştırın.

10. Sorun giderme

firebase: command not found

Firebase CLI'nin yüklendiğinden ve PATH'nizin güncellendiğinden emin olun. npm kullanarak yükleyebilirsiniz: npm install -g firebase-tools.

Başlatma işlevleri şablonlarında Dart eksik

Dart'ın, firebase init functions çalıştırılırken 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 5001 bağlantı noktasını kullandığınızı doğrulayın. Android emülatöründe test yapıyorsanız cihaz, localhost öğesini ana makinenize çözmez. main.dart içindeki emülatör yapılandırmasını 10.0.2.2 kullanacak şekilde güncelleyin.

Paylaşılan paket bulunamadı

functions/pubspec.yaml'daki 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 gerekiyor mu?

Kesinlikle zorunlu 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ını başarıyla oluşturdunuz. 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.