Salta el contingut

📘 GUÍA DEL'ALUMNE – SETMANA 5 (18/5 – 24/5)

UD7: API REST II & UD8: Firebase I

Aquesta guia està dissenyada perquè puguis treballar de manera autònoma. Conté explicacions detallades, codi complet en Java, passos pràctics, solució d'errors freqüents i criteris d'autoavaluació. Segueix l'ordre de les sessions i no avancis fins a tenir cada pas verificat.


📦 0. PREPARACIÓ PRÈVIA (Fer abans de començar la Setmana 5)

🔧 Configuració del projecte

  1. Android Studio: Versió 2023.2.1 o superior.
  2. Min SDK: 24 (Android 7.0).
  3. Permisos (app/src/main/AndroidManifest.xml):
    <uses-permission android:name="android.permission.INTERNET" />
    
  4. Dependències (app/build.gradle):
    dependencies {
        // Retrofit + Gson + OkHttp
        implementation 'com.squareup.retrofit2:retrofit:2.11.0'
        implementation 'com.squareup.retrofit2:converter-gson:2.11.0'
        implementation 'com.squareup.okhttp3:logging-interceptor:4.12.0'
    
        // Firebase BoM (gestiona versions automàticament)
        implementation platform('com.google.firebase:firebase-bom:33.1.0')
        implementation 'com.google.firebase:firebase-auth'
        implementation 'com.google.firebase:firebase-analytics'
    }
    
  5. Plugins (build.gradle del projecte, no de l'app):
    buildscript {
        repositories { google(); mavenCentral() }
        dependencies {
            classpath 'com.google.gms:google-services:4.4.2'
        }
    }
    // Al final del mateix fitxer:
    apply plugin: 'com.google.gms.google-services'
    

Verificació: Sincronitza Gradle (Sync Project). Si no hi ha errors en vermell, pots continuar.


📘 SESSIÓ 1 (2h) – Enviament POST amb Retrofit + Gestió de respostes i errors

🎯 Objectius

  • Enviar dades JSON al servidor amb @POST i @Body
  • Gestionar respostes HTTP (2xx, 4xx, 5xx) i errors de xarxa
  • Registrar el tràfic de xarxa per depuració
  • Entendre com Retrofit gestiona fils (threading) a Android

📖 Conceptes clau

Concepte Explicació
@POST("ruta") Indica a Retrofit que ha de fer una petició HTTP POST a la URL base + aquesta ruta.
@Body Object Serialitza automàticament un objecte Java a JSON usant Gson.
Call<T> Representa una petició HTTP que encara no s'ha executat. Per executar-la s'ha d'usar .enqueue() (asíncron) o .execute() (síncron, mai al fil principal).
Callback<T> Interfície amb dos mètodes: onResponse() (èxit HTTP) i onFailure() (error de xarxa/connexió).
Response<T> Conté isSuccessful(), code(), body() i errorBody(). Permet diferenciar errors del servidor de la resposta JSON.
HttpLoggingInterceptor Eina d'OkHttp que imprimeix a Logcat les capçaleres, cos de la petició i resposta. Imprescindible per depurar.

💻 Codi de referència (Java)

1. Models de dades (PostRequest.java, PostResponse.java)

public class PostRequest {
    private String title;
    private String body;
    private int userId;

    public PostRequest(String title, String body, int userId) {
        this.title = title;
        this.body = body;
        this.userId = userId;
    }
    // Getters i setters necessaris per Gson
}

public class PostResponse {
    private int id;
    private String title;
    private String body;
    private int userId;
    // Getters i setters
}

2. Interfície de l'API (ApiService.java)

import retrofit2.Call;
import retrofit2.Response;
import retrofit2.http.Body;
import retrofit2.http.POST;

public interface ApiService {
    @POST("posts")
    Call<PostResponse> createPost(@Body PostRequest post);
}

3. Client Retrofit (RetrofitClient.java)

import okhttp3.OkHttpClient;
import okhttp3.logging.HttpLoggingInterceptor;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;

public class RetrofitClient {
    private static final String BASE_URL = "https://jsonplaceholder.typicode.com/";
    private static ApiService apiService;

    public static ApiService getApiService() {
        if (apiService == null) {
            HttpLoggingInterceptor logging = new HttpLoggingInterceptor();
            logging.setLevel(HttpLoggingInterceptor.Level.BODY);

            OkHttpClient client = new OkHttpClient.Builder()
                    .addInterceptor(logging)
                    .build();

            Retrofit retrofit = new Retrofit.Builder()
                    .baseUrl(BASE_URL)
                    .client(client)
                    .addConverterFactory(GsonConverterFactory.create())
                    .build();

            apiService = retrofit.create(ApiService.class);
        }
        return apiService;
    }
}

4. Ús a l'Activitat (MainActivity.java)

import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;

public class MainActivity extends AppCompatActivity {
    private EditText etTitle, etBody;
    private Button btnSend;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        etTitle = findViewById(R.id.etTitle);
        etBody = findViewById(R.id.etBody);
        btnSend = findViewById(R.id.btnSend);

        btnSend.setOnClickListener(v -> enviarPost());
    }

    private void enviarPost() {
        String title = etTitle.getText().toString().trim();
        String body = etBody.getText().toString().trim();

        if (title.isEmpty() || body.isEmpty()) {
            Toast.makeText(this, "Omple tots els camps", Toast.LENGTH_SHORT).show();
            return;
        }

        PostRequest request = new PostRequest(title, body, 1);
        Call<PostResponse> call = RetrofitClient.getApiService().createPost(request);

        call.enqueue(new Callback<PostResponse>() {
            @Override
            public void onResponse(Call<PostResponse> call, Response<PostResponse> response) {
                if (response.isSuccessful() && response.body() != null) {
                    Toast.makeText(MainActivity.this, "✅ Enviat correctament (ID: " + response.body().getId() + ")", Toast.LENGTH_LONG).show();
                } else {
                    // Error HTTP (4xx, 5xx)
                    Log.e("RETROFIT", "Codi HTTP: " + response.code());
                    Toast.makeText(MainActivity.this, "❌ Error del servidor: " + response.code(), Toast.LENGTH_LONG).show();
                }
            }

            @Override
            public void onFailure(Call<PostResponse> call, Throwable t) {
                // Error de xarxa, DNS, timeout, sense internet
                Log.e("RETROFIT", "Error de xarxa", t);
                Toast.makeText(MainActivity.this, "❌ Error de connexió: " + t.getMessage(), Toast.LENGTH_LONG).show();
            }
        });
    }
}

🛠️ Exercici pràctic

  1. Crea un layout amb dos EditText (títol, cos) i un Button (Enviar).
  2. Copia/adepta les classes Java anteriors.
  3. Executa l'app en un emulador/dispositiu amb internet.
  4. Fes clic a "Enviar" i verifica:
    • Apareix un Toast verd amb l'ID retornat per JSONPlaceholder.
    • A Logcat, filtra per RETROFIT o OkHttp i observa la petició/respuesta en JSON.
    • Desactiva temporalment el WiFi i comprova que salta onFailure.

⚠️ Errors freqüents

Problema Causa Solució
NetworkOnMainThreadException Usar .execute() en lloc de .enqueue() Sempre usa .enqueue() a Android
IllegalArgumentException: No Retrofit annotation found Falta @Body, @POST, o tipus de retorn incorrecte Revisa que el mètode retorna Call<T> i té @POST
Resposta 400 o 422 El servidor espera un camp que no estàs enviant Revisa la documentació de l'API o el HttpLoggingInterceptor
No es mostra el Toast El callback s'executa en un thread secundari (en versions antigues) Retrofit per defecte retorna al main thread a Android. Si fas servir execute(), usa runOnUiThread()

✅ Autoverificació Sessió 1

  • La petició POST s'executa sense bloquejar la UI
  • Es mostren missatges diferents per èxit HTTP vs error de xarxa
  • Logcat mostra el JSON enviat i rebut
  • El codi gestiona camps buits abans d'enviar

📘 SESSIÓ 2 (2h) – Introducció a Firebase + Configuració

🎯 Objectius

  • Entendre què és Firebase i per què s'usa com a Backend-as-a-Service (BaaS)
  • Crear un projecte a la consola Firebase i registrar l'app Android
  • Integrar correctament google-services.json
  • Verificar que Firebase s'inicialitza sense errors

📖 Conceptes clau

Concepte Explicació
BaaS Backend-as-a-Service. Firebase ofereix serveis llests (Auth, Firestore, Storage, Analytics) sense configurar servidors propis.
Consola Firebase Portal web (console.firebase.google.com) on es gestionen projectes, apps, regles de seguretat i mètriques.
google-services.json Fitxer de configuració que conté l'ID del projecte, claus d'API i informació del client Android. Ha d'anar a app/, no a l'arrel del projecte.
Plugin google-services Processa el JSON durant la compilació i genera recursos/manifests automàtics. S'aplica al final de app/build.gradle.
SHA-1 Impressió digital de la clau de signatura. Necessària per a alguns serveis (Google Sign-In, Dynamic Links). Per a develop/debug, Android Studio ja en genera una automàticament.

🛠️ Passos pràctics (Seguir exactament)

  1. Crear projecte:

    • Ves a https://console.firebase.google.com
    • Fes clic a + Afegeix un projecte → Nom: UF5_FirebaseDemo → Accepta termes → Continua → Desactiva Google Analytics (per simplificar) → Crea projecte.
  2. Registrar app Android:

    • Dins del projecte, fes clic a l'icona 🤖 Android.
    • Nom del paquet: El mateix que té el teu app/build.gradle (applicationId "com.elteu.paquet.firebase").
    • Deixa en blanc el nom i SHA-1 (pots afegir-lo després si cal). Fes clic a Registra l'app.
  3. Descarregar configuració:

    • Fes clic a Descarrega google-services.json.
    • Mou el fitxer a: app/google-services.json (dins la carpeta app/, no a l'arrel).
    • Fes clic a SegüentSegüentContinua fins a la consola.
  4. Sincronitzar i verificar:

    • Sincronitza Gradle a Android Studio.
    • Afegeix aquest codi a MainActivity.java dins onCreate():
      import com.google.firebase.FirebaseApp;
      import android.util.Log;
      
      // ...
      FirebaseApp.initializeApp(this);
      Log.d("FIREBASE_CHECK", "Firebase inicialitzat: " + FirebaseApp.getApps(this).size() + " apps");
      
    • Executa l'app i mira Logcat. Ha d'aparèixer: Firebase inicialitzat: 1 apps i cap error vermell.

⚠️ Errors freqüents

Problema Causa Solució
Failed to resolve: com.google.gms:google-services Versió incorrecta o falta repositori google() Assegura't que google() i mavenCentral() estan a buildscript.repositories
google-services.json not found Fitxer a la carpeta equivocada Ha d'estar a app/google-services.json
minSdkVersion must be at least 19 Versió mínima massa baixa Posa minSdk 24 a defaultConfig
Default FirebaseApp is not initialized Plugin no aplicat o JSON corrupte Revisa que apply plugin: 'com.google.gms.google-services' és al final de app/build.gradle

✅ Autoverificació Sessió 2

  • Projecte creat a la consola Firebase
  • google-services.json a app/
  • Gradle sincronitza sense errors
  • Logcat confirma inicialització de Firebase
  • Cap advertència sobre versions o plugins

📘 SESSIÓ 3 (1h) – Firebase Authentication: Conceptes bàsics

🎯 Objectius

  • Entendre el flux d'autenticació amb email i contrasenya
  • Implementar registre i inici de sessió amb FirebaseAuth
  • Gestionar errors específics (contrasenya feble, email duplicat)
  • Detectar automàticament l'estat de l'usuari (AuthStateListener)

📖 Conceptes clau

Concepte Explicació
FirebaseAuth.getInstance() Singleton que gestiona la sessió de l'usuari. Manté la sessió activa encara que tanquis l'app.
createUserWithEmailAndPassword() Crea un compte nou. Retorna un Task<AuthResult> amb callbacks.
signInWithEmailAndPassword() Inicia sessió. Si les credencials són correctes, l'usuari queda autenticat.
AuthStateListener Interfície que s'activa cada vegada que canvia l'estat d'autenticació (login, logout, token renovat). Ideal per redirigir a MainActivity o LoginActivity.
Seguretat per defecte Firebase exigeix contrasenyes de mínim 6 caràcters. No permet emails duplicats al mateix projecte.

💻 Codi de referència (Java)

import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;
import com.google.firebase.auth.AuthResult;
import com.google.firebase.auth.FirebaseAuth;
import com.google.firebase.auth.FirebaseUser;
import com.google.firebase.auth.FirebaseAuthInvalidCredentialsException;
import com.google.firebase.auth.FirebaseAuthUserCollisionException;
import com.google.firebase.auth.FirebaseAuthWeakPasswordException;

public class LoginActivity extends AppCompatActivity {
    private EditText etEmail, etPassword;
    private Button btnRegister, btnLogin;
    private FirebaseAuth mAuth;
    private FirebaseAuth.AuthStateListener authListener;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_login);

        etEmail = findViewById(R.id.etEmail);
        etPassword = findViewById(R.id.etPassword);
        btnRegister = findViewById(R.id.btnRegister);
        btnLogin = findViewById(R.id.btnLogin);

        mAuth = FirebaseAuth.getInstance();

        // Configurar botons
        btnRegister.setOnClickListener(v -> registrar());
        btnLogin.setOnClickListener(v -> iniciarSessio());

        // Listener d'estat d'autenticació
        authListener = firebaseAuth -> {
            FirebaseUser user = firebaseAuth.getCurrentUser();
            if (user != null) {
                Log.d("FIREBASE_AUTH", "Usuari loguejat: " + user.getEmail());
                // Aquí redirigiries a la pantalla principal
                // startActivity(new Intent(LoginActivity.this, HomeActivity.class));
                // finish();
            } else {
                Log.d("FIREBASE_AUTH", "Cap usuari loguejat");
            }
        };
    }

    @Override
    protected void onStart() {
        super.onStart();
        mAuth.addAuthStateListener(authListener);
    }

    @Override
    protected void onStop() {
        super.onStop();
        if (authListener != null) {
            mAuth.removeAuthStateListener(authListener);
        }
    }

    private void registrar() {
        String email = etEmail.getText().toString().trim();
        String password = etPassword.getText().toString().trim();

        if (!validarCamps(email, password)) return;

        mAuth.createUserWithEmailAndPassword(email, password)
                .addOnCompleteListener(task -> {
                    if (task.isSuccessful()) {
                        mostrarMissatge("✅ Registre completat");
                    } else {
                        gestionarErrorAuth(task.getException());
                    }
                });
    }

    private void iniciarSessio() {
        String email = etEmail.getText().toString().trim();
        String password = etPassword.getText().toString().trim();

        if (!validarCamps(email, password)) return;

        mAuth.signInWithEmailAndPassword(email, password)
                .addOnCompleteListener(task -> {
                    if (task.isSuccessful()) {
                        mostrarMissatge("✅ Sessió iniciada");
                    } else {
                        gestionarErrorAuth(task.getException());
                    }
                });
    }

    private boolean validarCamps(String email, String password) {
        if (email.isEmpty() || password.isEmpty()) {
            mostrarMissatge("⚠️ Omple email i contrasenya");
            return false;
        }
        return android.util.Patterns.EMAIL_ADDRESS.matcher(email).matches();
    }

    private void gestionarErrorAuth(Exception e) {
        if (e instanceof FirebaseAuthWeakPasswordException) {
            mostrarMissatge("❌ Contrasenya massa curta (mínim 6 caràcters)");
        } else if (e instanceof FirebaseAuthUserCollisionException) {
            mostrarMissatge("❌ Aquest email ja està registrat");
        } else if (e instanceof FirebaseAuthInvalidCredentialsException) {
            mostrarMissatge("❌ Email o contrasenya incorrectes");
        } else {
            mostrarMissatge("❌ Error inesperat: " + e.getMessage());
        }
    }

    private void mostrarMissatge(String msg) {
        Toast.makeText(this, msg, Toast.LENGTH_LONG).show();
    }
}

🔐 Activació a la consola Firebase

  1. Ves a AuthenticationSign-in method.
  2. Fes clic a Email/Password → Activa'l → Desa.
  3. Important: Sense aquest pas, totes les crides retornaran error intern.

🛠️ Exercici pràctic

  1. Crea activity_login.xml amb 2 EditText (email, password) i 2 Button.
  2. Implementa el codi Java anterior.
  3. Prova els casos:
    • Registre amb email vàlid i contrasenya ≥6 caràcters → ✅ Èxit
    • Registre amb el mateix email → ❌ Col·lisió
    • Login amb credencials incorrectes → ❌ Credencials invàlides
    • Reinicia l'app → Verifica que AuthStateListener detecta l'usuari
  4. Opcional: Afegeix un botó mAuth.signOut() i verifica que el listener detecta logout.

⚠️ Errors freqüents

Problema Causa Solució
An internal error has occurred Mètode Email/Password no activat a la consola Activa'l a Authentication → Sign-in method
Password should be at least 6 characters Firebase imposa límit per defecte Usa contrasenyes ≥6 o canvia polítiques a Consola
FirebaseApp not initialized Falta google-services.json o sincronització Revisa Sessió 2
L'usuari no es manté després de tancar l'app Error de lògica o ús incorrecte de SharedPreferences No cal guardar res manualment. Firebase manté la sessió automàticament

✅ Autoverificació Sessió 3

  • Registre i login funcionen amb èxit
  • Es capturen i mostren errors específics de Firebase
  • AuthStateListener està registrat a onStart() i deregistat a onStop()
  • La sessió persisteix després de tancar/reobrir l'app
  • Cap credencial es desa manualment a SharedPreferences o fitxers

📊 RÚBRICA D'AUTOAVALUACIÓ SETMANA 5

Criteri Comprova-ho
Retrofit POST Faig servir Call<T> + .enqueue(), gestiono onResponse i onFailure per separat
Errors HTTP/Xarxa Diferencio response.isSuccessful() de Throwable en onFailure
Firebase Setup google-services.json a app/, Gradle sense errors, Logcat confirma inicialització
Auth Flow Implemento createUser i signIn amb addOnCompleteListener
Gestió d'estat Uso AuthStateListener i no guardo tokens/emails manualment
Depuració Utilitzo Logcat i HttpLoggingInterceptor per verificar dades i errors

Si compleixes tots els punts, estàs llest per lliurar. Documenta amb captures de Logcat i de la consola Firebase.


🔗 RECURSOS OFICIALS (Java)

  • Retrofit Documentation: https://square.github.io/retrofit/
  • Firebase Android Setup (Java): https://firebase.google.com/docs/android/setup
  • Firebase Auth (Java): https://firebase.google.com/docs/auth/android/start
  • OkHttp Logging: https://github.com/square/okhttp/tree/master/okhttp-logging-interceptor
  • Obtenir SHA-1 debug: keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android -keypass android

💡 Consells finals per a l'alumne 1. No copiïs sense entendre: Cada línia de codi té una funció. Si no saps per què s'usa, pregunta-ho o documenta't. 2. Logcat és el teu millor amic: Filtres útils: RETROFIT, FirebaseAuth, Network, OkHttp. 3. Threading a Android: Mai facis crides de xarxa al fil principal. Retrofit .enqueue() ja ho gestiona. Si fas servir LiveData o ViewModel, els callbacks s'actualitzaran automàticament a la UI. 4. Seguretat: Mai hardcodegis claus, emails de prova o contrasenyes. Usa google-services.json i variables d'entorn si cal. 5. Lliurament: Envia un ZIP amb el projecte Android Studio, captures de Logcat (èxit i error), i captura de la consola Firebase amb Auth activat.

📩 Si trobes un error que no apareix en aquesta guia, indica'm: - Versió d'Android Studio - Logcat complet (primeres 20 línies de l'error) - Fragment de codi on falla I t'ajudaré a resoldre'l pas a pas.