FlutterFirebaseAutentifikacijaFirestoreCloud FunctionsRazvoj mobilnih aplikacijaVodičTutorial

Flutter + Firebase: Potpuni vodič za 2026. (Auth, Firestore, Functions, Deploy)

Adrijan Omičević··13 min čitanja
Share

# Što ćete izgraditi (i zašto je to važno)#

Ovaj flutter firebase tutorial vodi vas od praznog Flutter projekta do deployane aplikacije s Firebase Authentication, Firestore i Cloud Functions. Na kraju ćete imati aplikaciju koja može prijaviti korisnike, sigurno spremati podatke po korisniku i pokretati serversku logiku kojoj ne možete vjerovati na klijentu.

Firebase je popularan jer uklanja mjesece backend posla, ali produkcijske aplikacije i dalje pucaju na osnovama poput security rulesa, indeksiranja i modeliranja podataka. Ovaj vodič fokusira se na praktične stvari koje su bitne za stvarne deploye, a ne samo na “radi na mom mobitelu”.

Ako procjenjujete cross-platform opcije, usporedite kompromisa u Flutter vs React Native (2026). Ako trebate tim koji isporučuje brže, Samioda gradi produkcijske aplikacije s Flutterom i automation stackovima: mobile & web development.

# Preduvjeti (alati + računi)#

ZahtjevVerzija (preporučeno)Napomene
Flutter SDKStable channel (latest)flutter doctor treba biti “green”
DartDolazi s FlutteromNema potrebe za zasebnom instalacijom
Android Studio / XcodeNajnoviji stableZa emulatore/simulatore
Firebase računGoogle računNapravit ćete projekt + aplikacije
Node.js18+ LTSPotrebno za Firebase CLI + Functions
Firebase CLINajnovijiDeploy Functions, konfiguracija hostinga (opcionalno)

Instalirajte Firebase CLI:

Bash
npm i -g firebase-tools
firebase login

Kreirajte i provjerite svoju Flutter aplikaciju:

Bash
flutter create flutter_firebase_2026
cd flutter_firebase_2026
flutter run

ℹ️ Napomena: Ovaj tutorial koristi moderni Firebase pristup u Flutteru kroz flutterfire CLI za generiranje platformskih konfiguracija. Time se izbjegavaju ručne greške u konfiguraciji i tajne se ne guraju u source control.

# Korak 1: Kreirajte Firebase projekt + registrirajte aplikacije#

1) Kreirajte Firebase projekt#

  1. 1
    Idite na Firebase Console → Add project
  2. 2
    Odaberite naziv projekta (npr. flutter-firebase-2026)
  3. 3
    Uključite Google Analytics samo ako vam treba odmah (možete dodati kasnije)

2) Registrirajte Android i iOS aplikacije#

U Firebase projektu:

  • Dodajte Android app s package nameom poput com.example.flutter_firebase_2026
  • Dodajte iOS app s bundle ID-jem poput com.example.flutterFirebase2026

Za Android, postavite applicationId u android/app/build.gradle (ako želite prilagođeni ID). Za iOS, postavite bundle ID u Xcodeu (Runner target settings) ili u predlošku projekta.

3) Konfigurirajte putem FlutterFire CLI-a#

Instalirajte FlutterFire CLI:

Bash
dart pub global activate flutterfire_cli

Pokrenite konfiguraciju:

Bash
flutterfire configure

Ovo generira lib/firebase_options.dart i dodaje native konfiguracijske datoteke na prava mjesta.

# Korak 2: Dodajte Firebase ovisnosti u Flutter#

Dodajte pakete:

Bash
flutter pub add firebase_core firebase_auth cloud_firestore
flutter pub add flutter_riverpod

Inicijalizirajte Firebase u main.dart:

Dart
import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.dart';
import 'firebase_options.dart';
 
Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform,
  );
  runApp(const MyApp());
}
 
class MyApp extends StatelessWidget {
  const MyApp({super.key});
 
  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      debugShowCheckedModeBanner: false,
      home: Scaffold(body: Center(child: Text('Firebase ready'))),
    );
  }
}

U ovom trenutku pokrenite:

Bash
flutter run

Ako vidite “Firebase ready”, završili ste osnovnu postavu.

💡 Savjet: Dodajte “smoke test” u ovoj fazi (jedan uređaj + jedan emulator). Jeftinije je popraviti native konfiguracijske probleme sada nego nakon što dodate auth, rules i functions.

# Korak 3: Firebase Authentication (Email/Password)#

Autentifikacija je temelj kontrole pristupa. Bez autha Firestore rulesi često postanu previše permisivni “samo za testiranje” i slučajno odu u produkciju.

1) Omogućite metode prijave#

Firebase Console → AuthenticationSign-in method:

  • Omogućite Email/Password

2) Implementirajte minimalni auth servis#

Kreirajte lib/auth/auth_service.dart:

Dart
import 'package:firebase_auth/firebase_auth.dart';
 
class AuthService {
  final FirebaseAuth _auth = FirebaseAuth.instance;
 
  Stream<User?> authStateChanges() => _auth.authStateChanges();
 
  Future<UserCredential> signUp(String email, String password) {
    return _auth.createUserWithEmailAndPassword(
      email: email,
      password: password,
    );
  }
 
  Future<UserCredential> signIn(String email, String password) {
    return _auth.signInWithEmailAndPassword(
      email: email,
      password: password,
    );
  }
 
  Future<void> signOut() => _auth.signOut();
}

3) Izgradite jednostavan UI: sign-in / sign-up#

Kreirajte lib/auth/auth_screen.dart:

Dart
import 'package:flutter/material.dart';
import 'auth_service.dart';
 
class AuthScreen extends StatefulWidget {
  const AuthScreen({super.key});
 
  @override
  State<AuthScreen> createState() => _AuthScreenState();
}
 
class _AuthScreenState extends State<AuthScreen> {
  final _auth = AuthService();
  final _email = TextEditingController();
  final _password = TextEditingController();
  String? _error;
 
  Future<void> _run(Future<void> Function() action) async {
    setState(() => _error = null);
    try {
      await action();
    } catch (e) {
      setState(() => _error = e.toString());
    }
  }
 
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Sign in')),
      body: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          children: [
            TextField(controller: _email, decoration: const InputDecoration(labelText: 'Email')),
            TextField(
              controller: _password,
              decoration: const InputDecoration(labelText: 'Password'),
              obscureText: true,
            ),
            const SizedBox(height: 12),
            if (_error != null) Text(_error!, style: const TextStyle(color: Colors.red)),
            const SizedBox(height: 12),
            Row(
              children: [
                Expanded(
                  child: ElevatedButton(
                    onPressed: () => _run(() async {
                      await _auth.signIn(_email.text.trim(), _password.text.trim());
                    }),
                    child: const Text('Sign in'),
                  ),
                ),
                const SizedBox(width: 12),
                Expanded(
                  child: OutlinedButton(
                    onPressed: () => _run(() async {
                      await _auth.signUp(_email.text.trim(), _password.text.trim());
                    }),
                    child: const Text('Sign up'),
                  ),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

4) Rutirajte korisnike prema auth statusu#

Zamijenite home: u MyApp s auth gateom.

Dart
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import 'auth/auth_screen.dart';
 
class AuthGate extends StatelessWidget {
  const AuthGate({super.key});
 
  @override
  Widget build(BuildContext context) {
    return StreamBuilder<User?>(
      stream: FirebaseAuth.instance.authStateChanges(),
      builder: (context, snapshot) {
        if (snapshot.connectionState == ConnectionState.waiting) {
          return const Scaffold(body: Center(child: CircularProgressIndicator()));
        }
        if (snapshot.data == null) return const AuthScreen();
        return const Scaffold(body: Center(child: Text('Logged in')));
      },
    );
  }
}

I postavite:

Dart
home: const AuthGate(),

⚠️ Upozorenje: Ne prikazujte “logged in” sadržaj prije nego što se authStateChanges() razriješi. Inače ćete dobiti “flicker” i, još gore, nakratko renderirati ekrane koji pretpostavljaju da korisnik postoji.

# Korak 4: Firestore model podataka + sigurnosna pravila#

Firestore je fleksibilan, ali fleksibilnost stvara neuredne sheme. Krenite s čistim, minimalnim modelom.

Model podataka: “todos” po korisniku#

Todos ćemo spremati pod korisnika:

  • users/{uid} (profil)
  • users/{uid}/todos/{todoId} (stavke)

Ova struktura čini security rules jednostavnima: korisnik može pristupiti samo svom podstablu.

Kolekcija/DokumentPutanjaPrimjer polja
Korisnički profilusers/{uid}email, createdAt
Todo stavkausers/{uid}/todos/{todoId}title, done, createdAt, updatedAt

1) Kreirajte Firestore bazu#

Firebase Console → Firestore Database → Create database. Odaberite regiju blizu korisnika (latencija je bitna; čak i 80–150ms po requestu se zbroji).

2) Postavite Firestore security rules (produkcijska osnova)#

Firestore → Rules:

Txt
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
 
    function isSignedIn() {
      return request.auth != null;
    }
 
    match /users/{userId} {
      allow read, write: if isSignedIn() && request.auth.uid == userId;
 
      match /todos/{todoId} {
        allow read, write: if isSignedIn() && request.auth.uid == userId;
      }
    }
  }
}

Ovo zaključava podatke na autentificiranog korisnika. Nije “enterprise RBAC”, ali sprječava najčešći Firebase breach: globalni read pristup.

3) Izgradite Firestore CRUD servis#

Kreirajte lib/data/todo_service.dart:

Dart
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
 
class TodoService {
  final _db = FirebaseFirestore.instance;
 
  String get _uid => FirebaseAuth.instance.currentUser!.uid;
 
  CollectionReference<Map<String, dynamic>> get _todos =>
      _db.collection('users').doc(_uid).collection('todos');
 
  Stream<QuerySnapshot<Map<String, dynamic>>> watchTodos() {
    return _todos.orderBy('createdAt', descending: true).snapshots();
  }
 
  Future<void> addTodo(String title) {
    final now = FieldValue.serverTimestamp();
    return _todos.add({
      'title': title,
      'done': false,
      'createdAt': now,
      'updatedAt': now,
    });
  }
 
  Future<void> toggleDone(String id, bool done) {
    return _todos.doc(id).update({
      'done': done,
      'updatedAt': FieldValue.serverTimestamp(),
    });
  }
 
  Future<void> deleteTodo(String id) => _todos.doc(id).delete();
}

🎯 Ključna poruka: Koristite serverTimestamp() za audit polja. Klijentski timestampi driftaju (i mogu se manipulirati), što kvari sortiranje i može uzrokovati zbunjujuće UI bugove.

4) Izradite ekran za todose#

Kreirajte lib/todos/todos_screen.dart:

Dart
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';
import '../data/todo_service.dart';
import 'package:firebase_auth/firebase_auth.dart';
 
class TodosScreen extends StatefulWidget {
  const TodosScreen({super.key});
 
  @override
  State<TodosScreen> createState() => _TodosScreenState();
}
 
class _TodosScreenState extends State<TodosScreen> {
  final _service = TodoService();
  final _controller = TextEditingController();
 
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('My Todos'),
        actions: [
          IconButton(
            onPressed: () => FirebaseAuth.instance.signOut(),
            icon: const Icon(Icons.logout),
          ),
        ],
      ),
      body: Column(
        children: [
          Padding(
            padding: const EdgeInsets.all(12),
            child: Row(
              children: [
                Expanded(
                  child: TextField(
                    controller: _controller,
                    decoration: const InputDecoration(hintText: 'Add a todo...'),
                  ),
                ),
                const SizedBox(width: 8),
                ElevatedButton(
                  onPressed: () async {
                    final text = _controller.text.trim();
                    if (text.isEmpty) return;
                    _controller.clear();
                    await _service.addTodo(text);
                  },
                  child: const Text('Add'),
                ),
              ],
            ),
          ),
          Expanded(
            child: StreamBuilder<QuerySnapshot<Map<String, dynamic>>>(
              stream: _service.watchTodos(),
              builder: (context, snapshot) {
                if (!snapshot.hasData) {
                  return const Center(child: CircularProgressIndicator());
                }
                final docs = snapshot.data!.docs;
                if (docs.isEmpty) return const Center(child: Text('No todos yet.'));
                return ListView.builder(
                  itemCount: docs.length,
                  itemBuilder: (context, i) {
                    final d = docs[i];
                    final data = d.data();
                    final title = (data['title'] ?? '') as String;
                    final done = (data['done'] ?? false) as bool;
 
                    return Dismissible(
                      key: ValueKey(d.id),
                      background: Container(color: Colors.red),
                      onDismissed: (_) => _service.deleteTodo(d.id),
                      child: CheckboxListTile(
                        value: done,
                        title: Text(title),
                        onChanged: (v) => _service.toggleDone(d.id, v ?? false),
                      ),
                    );
                  },
                );
              },
            ),
          ),
        ],
      ),
    );
  }
}

Zatim ažurirajte auth gate tako da prikazuje TodosScreen kada je korisnik prijavljen.

# Korak 5: Firestore indeksi + osnove performansi#

Problemi s Firestore performansama najčešće dolaze od:

  • neograničenih upita (bez limita),
  • nedostajućih kompozitnih indeksa,
  • “chatty” UI-ja (previše listenera),
  • prevelikih dokumenata.

Dodajte limite upitima#

Ako lista može rasti, dodajte .limit(50) i kasnije uvedite paginaciju.

Dart
return _todos
  .orderBy('createdAt', descending: true)
  .limit(50)
  .snapshots();

Znajte kada vam treba indeks#

Ako radite upit s više where uvjeta + orderBy, Firestore će često zahtijevati kompozitni indeks. U produkciji to postaje runtime error ako niste kreirali indeks.

Pattern upitaObično treba kompozitni indeks?Primjer
orderBy(createdAt)NeNajnovije stavke
where(done == false)NeSamo filtriranje
where(done == false).orderBy(createdAt)Često daFiltriranje + sortiranje
where(status in [...]).orderBy(priority)Često daSložene liste

💡 Savjet: Kada Firestore izbaci index error, uključit će direktan link u konzoli za kreiranje potrebnog indeksa. Kliknite ga, kreirajte indeks i “zaključajte” shape upita u codebaseu kako se ne bi neočekivano mijenjao.

# Korak 6: Cloud Functions (serverska logika kojoj možete vjerovati)#

Firestore rules kontroliraju pristup, ali nisu potpuno programsko okruženje. Cloud Functions je mjesto gdje stavljate privilegiranu logiku poput:

  • dodjele uloga,
  • pisanja audit logova,
  • provođenja ograničenja preko više dokumenata,
  • pozivanja vanjskih API-ja.

1) Inicijalizirajte Functions#

U rootu projekta:

Bash
firebase init functions

Odaberite:

  • Jezik: JavaScript ili TypeScript (TypeScript preporučen)
  • Koristiti ESLint: opcionalno
  • Instalirati dependencies: da

Ako koristite TypeScript, deploy radite iz functions/.

2) Primjer funkcije: kreiranje korisničkog profila pri registraciji#

Kreirajte auth trigger koji zapisuje users/{uid} kada se korisnik registrira.

functions/src/index.ts (TypeScript):

TypeScript
import * as functions from "firebase-functions";
import * as admin from "firebase-admin";
 
admin.initializeApp();
 
export const onUserCreate = functions.auth.user().onCreate(async (user) => {
  const uid = user.uid;
  const email = user.email ?? null;
 
  await admin.firestore().collection("users").doc(uid).set({
    email,
    createdAt: admin.firestore.FieldValue.serverTimestamp(),
  }, { merge: true });
});

Deploy:

Bash
firebase deploy --only functions

3) Primjer callable funkcije: postavljanje “admin” claima (ograničeno)#

Ne dopustite klijentima da dodjeljuju uloge. Ovu funkciju smiju koristiti samo postojeći admini.

TypeScript
export const setAdminClaim = functions.https.onCall(async (data, context) => {
  const callerUid = context.auth?.uid;
  if (!callerUid) throw new functions.https.HttpsError("unauthenticated", "Sign in required.");
 
  const caller = await admin.auth().getUser(callerUid);
  const isAdmin = caller.customClaims?.admin === true;
  if (!isAdmin) throw new functions.https.HttpsError("permission-denied", "Admin only.");
 
  const targetUid = data.uid as string;
  await admin.auth().setCustomUserClaims(targetUid, { admin: true });
 
  return { ok: true };
});

Na Flutter strani, pozvali biste je s paketom cloud_functions kada je potrebno (npr. interna admin aplikacija).

⚠️ Upozorenje: Callable functions nisu automatski “sigurne”. Uvijek validirajte context.auth i autorizirajte pozivatelja. Mnogi Firebase incidenti nastaju zbog funkcija koje pretpostavljaju “UI aplikacije sprječava zloupotrebu”.

# Korak 7: Ojačavanje aplikacije (Rules, validacija, error handling)#

1) Dodajte osnovnu validaciju u Firestore rules#

Spriječite prazne naslove i preduge stringove (trošak i UX).

Txt
match /users/{userId}/todos/{todoId} {
  allow create: if isSignedIn()
    && request.auth.uid == userId
    && request.resource.data.title is string
    && request.resource.data.title.size() > 0
    && request.resource.data.title.size() <= 140;
 
  allow update, delete, read: if isSignedIn() && request.auth.uid == userId;
}

2) Izbjegavajte spremanje tajni u aplikaciju#

Sve što je u klijentu može se izvući. Koristite Functions za:

  • API ključeve koji moraju ostati tajni,
  • billing operacije,
  • provjere dozvola između korisnika.

3) Centralizirajte mapiranje Firebase grešaka#

FirebaseAuth greške nisu user-friendly. Mapirajte ih.

Dart
String authMessage(Object e) {
  final s = e.toString();
  if (s.contains('wrong-password')) return 'Wrong password.';
  if (s.contains('user-not-found')) return 'No user found for that email.';
  if (s.contains('email-already-in-use')) return 'Email already in use.';
  return 'Something went wrong. Please try again.';
}

# Korak 8: Deploy (Functions + produkcijski checklist)#

Flutter aplikacije isporučujete kroz App Store / Play Store, ali i vaš Firebase backend mora biti “deployan” i zaključan.

1) Deploy Cloud Functions#

Već ste koristili:

Bash
firebase deploy --only functions

2) Potvrdite da rules nisu u “test mode”#

U Firestore rules izbjegavajte:

  • allow read, write: if true;

3) Provjerite zahtjeve billing plana#

Neke Firebase značajke (i razine korištenja) zahtijevaju Blaze plan. Nemojte to otkriti u tjednu lansiranja.

ZnačajkaTipično zahtijeva Blaze?Zašto je važno
Firestore osnovnoNe uvijekPostoji free tier, ali limiti vrijede
Cloud Functions (neki triggeri/egress)ČestoVanjski mrežni pozivi i skaliranje
AuthenticationUglavnom neAli SMS/phone ima ograničenja
Scheduled FunctionsČestoUobičajeno za batch poslove

4) Dodajte osnovni monitoring#

  • Firebase Console → Functions logs
  • Razmislite o izvozu logova u BigQuery za veće aplikacije
  • Postavite alerting na skokove grešaka

ℹ️ Napomena: Googleovo DORA istraživanje dosljedno pokazuje da timovi s jakom observability praksom i brzim feedback loopovima isporučuju češće uz nižu stopu promjena koje završavaju greškom. U praksi: logovi i alertovi smanjuju “tihe kvarove” koji znaju pojesti tjedne.

# Česte zamke (i kako ih izbjeći)#

  1. 1
    Ostavljanje Firestorea u test modeu — Zaključajte rules rano i gradite UI oko permission errora.
  2. 2
    Ravne kolekcije s userId poljima — Može funkcionirati, ali je lakše pogriješiti. Preferirajte users/{uid}/... za podatke po korisniku.
  3. 3
    Oslanjanje na klijent za sigurnost — Sve što klijent može napraviti, može i napadač. Koristite rules + functions.
  4. 4
    Bez strategije indeksa — Ako dodajete ekrane s “filterima + sortiranjem”, planirajte i dokumentirajte query oblike da vas indeksi ne iznenade.
  5. 5
    Preveliki dokumenti — Držite dokumente malima; velike blobove spremite u Cloud Storage i referencirajte URL-ove u Firestoreu.

Za timove kojima treba produkcijska pomoć (arhitektura, pregled rulesa, CI/CD, kontrola troškova), pogledajte Samioda mobile & web development. Ako još birate stack, pročitajte Flutter vs React Native (2026).

# Ključne poruke#

  • Koristite FlutterFire CLI za ispravnu konfiguraciju Firebasea kroz platforme i izbjegavanje krhke ručne postave.
  • Prvo izgradite auth, zatim dizajnirajte Firestore oko security rulesa (npr. users/{uid}/...) kako bi kontrola pristupa ostala jednostavna.
  • Preferirajte serverTimestamp() za createdAt/updatedAt kako biste spriječili bugove u sortiranju i manipulaciju.
  • Koristite Cloud Functions za privilegiranu logiku (uloge, integracije, provjere integriteta) i uvijek validirajte context.auth.
  • Rano dodajte limite, indekse i logiranje; većina Firebase “scale” problema su problemi upita + rulesa + observabilityja.

# Zaključak#

Sada imate funkcionalnu Flutter + Firebase aplikaciju s Authentication, Firestore CRUD i Cloud Functions — dovoljno da isporučite MVP i sigurno ga razvijate dalje. Sljedeći korak je ojačati rules, dokumentirati query patterne, dodati monitoring te planirati paginaciju i uloge kako proizvod raste.

Ako želite da Samioda ubrza isporuku ili pregleda vašu Firebase sigurnost i arhitekturu prije lansiranja, kontaktirajte nas putem mobile & web development.

FAQ

Share
A
Adrijan OmičevićSamioda Team
All articles →

Trebate pomoć s projektom?

Gradimo prilagođena rješenja koristeći tehnologije iz ovog članka. Senior tim, fiksne cijene.