1. Was ist reactive?

In der reaktiven Programmierung beschäftigt man sich mit Datenströmen. Bei der reaktiven Programmierung sagen wir an, was passiert, wenn wir etwas in unserem Datenstrom beobachten.

2. Angular

Es gibt mehrere Möglichkeiten, in Angular reaktiv zu arbeiten:

  1. RxJS (Reactive Extensions for JavaScript):: Sie ermöglicht die asynchrone Verarbeitung von Datenströmen und Ereignissen.

  2. Signals: Für synchrones, reaktives State-Management

2.1. RxJS

RxJS ist die standardmäßig integrierte Bibliothek in Angular, die reaktive Programmierung auf Basis von Observables ermöglicht. Hier sind die zentralen Konzepte:

2.1.1. Observable

Ein Observable ist ein Objekt, das asynchrone Datenströme bereitstellt. Es repräsentiert eine Quelle, die im Laufe der Zeit Daten (z. B. Ereignisse, API-Antworten oder Benutzereingaben) senden kann.

Nicht zu verwechseln mit Observer (Subscriber)! Ein Observer ist ein Objekt, das auf die Daten reagiert, die von einem Observable gesendet werden. Es definiert, wie mit den gesendeten Daten umgegangen wird.

observable
Figure 1. Observable

Eigenschaften des Observers:

  • next: Sendet Daten.

  • error: Signalisiert einen Fehler.

  • complete: Signalisiert das Ende des Streams.

2.1.2. Pull- und Push-Verfahren im Zusammenhang mit Observables

  • Pull: Ein Observer (Subscriber) entscheidet, ob er sich für ein Observable registrieren möchte. Dadurch hat der Konsument die Kontrolle darüber, welche Datenquellen ihn interessieren.

  • Push: Sobald ein Observer sich registriert hat, sendet das Observable Daten (Events) aktiv an den Observer, ohne dass der Observer diese explizit anfordern muss.

Beispiel:
import { Observable } from 'rxjs';

const observable = new Observable((subscriber) => {
  subscriber.next(1);
  subscriber.next(2);
  subscriber.next(3);
  setTimeout(() => {
    subscriber.next(4);
    subscriber.complete();
  }, 1000);
});

console.log('just before subscribe');
observable.subscribe({
  next(x) {
    console.log('got value ' + x);
  },
  error(err) {
    console.error('something wrong occurred: ' + err);
  },
  complete() {
    console.log('done');
  },
});
console.log('just after subscribe');
Output
just before subscribe
got value 1
got value 2
got value 3
just after subscribe
got value 4
done
observable exmaple

2.1.3. Subject

Ein Subject in RxJS ist eine besondere Art von Observable, das gleichzeitig als Observer fungiert. Das bedeutet, dass ein Subject Werte empfangen und diese an seine Abonnenten weitergeben kann.

Arten von Subject:

Subject
  • gibt alle Werte nur an die Abonnenten weiter, die zum Zeitpunkt bereits abonniert sind

  • Spätere Abonnenten erhalten keine vorherigen Werte, sondern nur zukünftige Werte

subject
Figure 2. Subject
BehaviorSubject
  • speichert den letzten Wert und gibt ihn sofort an neue Abonnenten weiter.

  • Jeder neue Subscriber bekommt den zuletzt gespeicherten Wert, bevor er weitere Updates erhält.

behavior subject
Figure 3. BehaviorSubject

2.2. Signals

Ein Signal in Angular ist eine reaktive Datenstruktur, die einen bestimmten Zustand hält und automatisch benachrichtigt, wenn sich dieser Zustand ändert. Es stellt eine explizite Abhängigkeit zwischen Daten und der Benutzeroberfläche her, was bedeutet, dass Angular nur die betroffenen Teile der UI aktualisiert.

angular signal
Figure 4. Angular Signals

2.2.1. Probleme, die Signals lösen

  • Komplexität und Ineffizienz in Change Detection:

Angulars ursprüngliches Reaktivitätsmodell (z. B. mit Observables oder Input-Bindings) überprüft oft unnötig viele Komponenten und Templates, was die Performance belastet – besonders in komplexen Anwendungen. Change-Detection-System überprüft standardmäßig alles, auch wenn nur kleine Teile des Zustands geändert wurden.

  • Fehlende explizite Datenabhängigkeiten:

RxJS-basierte Lösungen erfordern häufig Boilerplate-Code und manuelle Abhängigkeitsverkettungen, was die Verständlichkeit erschwert.

  • Komplexität:

Entwickler müssen oft manuell steuern, welche Teile der Anwendung aktualisiert werden, oder komplexe RxJS-Logik implementieren.

Beispiel für ein Signal:
import { signal, effect } from '@angular/core';

// Signal erstellen
const counter = signal(0); // erstellt ein Signal mit einem Anfangswert 0

// Effekt
effect(() => { (1)
  console.log('Counter hat sich geändert:', counter());
});

// Update des Signals
counter.set(1);
1 effect: wird jedes Mal ausgeführt, wenn counter sich ändert.
Output
Counter hat sich geändert: 0
Counter hat sich geändert: 1

2.2.2. Weitere wichtige Arten von Signals

  • Computed Signals: Ein Computed Signal berechnet automatisch einen neuen Wert, wenn sich ein anderes Signal ändert.

Computed Signals
import { signal, computed } from '@angular/core';

const count = signal(2);
const squared = computed(() => count() * count());

console.log(squared());
count.set(3);
console.log(squared());
Output
4
9
  • Effects: Ein Effect reagiert auf Signal-Änderungen und führt eine Aktion aus.

Effect
import { signal, effect } from '@angular/core';

const name = signal('Candice');

effect(() => {
  console.log(`Hallo, ${name()}!`);
});

name.set('Oleg');
Output
Hallo, Candice!
Hallo, Oleg!
  • LinkedSignals: Ein Linked Signal ermöglicht, verschiedene Signale miteinander zu verknüpfen, sodass Änderungen in einem Signal automatisch an ein anderes weitergegeben werden.

Computed Signals
import { signal, linked } from '@angular/core';

const source = signal(10);
const linkedSignal = linked({
  get: () => source(),     // Liest den Wert von `source`
  set: (value) => source.set(value) // Setzt den Wert in `source`
});

console.log(linkedSignal()); // 10
linkedSignal.set(20);        // Ändert auch `source`
console.log(source());       // 20
Output
Hallo, Candice!
Hallo, Oleg!

2.3. RxJS vs. Signals

Eigenschaft RxJS Signals

Synchronität

Asynchron

Synchron

Komplexität

Höher, erfordert mehr Boilerplate

Niedriger, einfache API

Anwendungsfälle

Datenströme, Ereignissteuerung

Reaktives State-Management

Datenfluss

Push-basiert (Observable)

Pull-basiert (Signal)

Abhängigkeitserkennung

Manuell (subscribe)

Automatisch (Tracking)

2.4. Signals mit Single Source of Truth

In diesem Repository befindet sich ein Angular-Projekt, das die Verwendung von Signals. Ziel ist es, wie man mit dem Prinzip der Single Source of Truth (SSOT) den globalen Zustand der Anwendung mittels Signals lösen kann.

2.4.1. Was ist Single Source of Truth?

Das Single Source of Truth-Prinzip bedeutet, dass der gesamte Zustand einer Anwendung an einer einzigen Stelle verwaltet wird. Anstatt dass verschiedene Komponenten ihren eigenen Zustand halten, greifen sie auf eine zentrale Datenquelle ( State Management Service) zu.

Zweck & Vorteile:

  • Konsistenz: Alle Komponenten beziehen ihre Daten aus derselben Quelle → weniger Inkonsistenzen.

  • Vorhersagbarkeit: Änderungen am Zustand sind leicht nachzuvollziehen → weniger Bugs.

  • Effiziente Updates: Änderungen folgen automatisch an alle betroffenen Komponenten → bessere Performance.

2.4.2. Beispiel

angular project
Model
model.ts
import {Todo} from "./todo";
import {signal} from "@angular/core";

export interface Model {
  readonly firstName: string
  readonly lastName: string
  readonly todos: Todo[]
  readonly todosLoaded: boolean;
}

export const initial: Model =
{
  firstName: "Candice",
  lastName: "Oleg",
  todos: [],
  todosLoaded: false
}
export const store = signal<Model>(initial); (1)
1 die Single Source of Truth
Service
store.service.ts
import { Injectable } from '@angular/core';
import { store } from '../model';

@Injectable({
  providedIn: 'root'
})
export class StoreService {

  constructor() { }
  get store () {
    return store
  }
}
app-state.service.ts
import { Injectable, signal, computed, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Model, Todo } from '../model';
import { initial } from '../model/model';
import {StoreService} from "./store.service";

@Injectable({
  providedIn: 'root',
})
export class AppStateService {
  private readonly API_URL = 'https://jsonplaceholder.typicode.com/todos?_limit=20';

  // Single Source of Truth als Signal
  (1)
  private state = inject(StoreService).store

  // Computed Signals für Selektoren
  (2)
  readonly todosSignals = computed(() => this.state().todos);
  readonly firstName = computed(() => this.state().firstName);
  readonly lastName = computed(() => this.state().lastName);

  constructor(private http: HttpClient) {}

  // Zustand aktualisieren
  (3)
  updateState(recipe: (state: Model) => Model) {
    this.state.update(recipe);
  }

  addTodo(todo: Todo) {
    this.updateState((state) => ({
      ...state,
      todos: [...state.todos, todo],
    }));
  }

  toggleTodoStatus(id: number) {
    this.updateState((state) => ({
      ...state,
      todos: state.todos.map((todo) =>
        todo.id === id ? { ...todo, completed: !todo.completed } : todo
      ),
    }));
  }

  removeTodo(id: number) {
    this.updateState((state) => ({
      ...state,
      todos: state.todos.filter((todo) => todo.id !== id),
    }));
  }

  loadTodos() {
    if (!this.state().todosLoaded) {
      this.http
        .get<Todo[]>(this.API_URL)
        .subscribe((todos) => {
          this.updateState((state) => ({
            ...state,
            todos,
            todosLoaded: true,
          }));
        });
    }
  }
}

AppStateService dient zum Verwalten der ganzen State (Zustand) auf Basis der SSOT, z.B. einfügen, löschen etc…​

1 es wird sich der store geholt (SSOT)
2 wird automatisch aktualisiert, wenn sich state ändert.
3 mit recipe → die den Zustand verändert und ein neues Model-Objekt zurückgibt.
Components
signal.component.ts
import {Component, OnInit} from '@angular/core';
import { AppStateService } from '../../services/app-state.service';
import { FormsModule } from '@angular/forms';
import { NgForOf } from '@angular/common';

@Component({
  selector: 'app-signal',
  templateUrl: './signal.component.html',
  styleUrls: ['./signal.component.css'],
  imports: [
    FormsModule,
    NgForOf,
  ],
})
export class SignalComponent implements OnInit{
  newTodoTitle = '';
  todos = this.appState.todosSignals;

  constructor(private appState: AppStateService) {}

  ngOnInit() {
    this.appState.loadTodos();
  }
  addTodo() {
    if (this.newTodoTitle.trim()) {
      this.appState.addTodo({
        userId: 1,
        id: Date.now(),
        title: this.newTodoTitle.trim(),
        completed: false,
      });
      this.newTodoTitle = '';
    }
  }

  toggleTodoStatus(id: number) {
    this.appState.toggleTodoStatus(id);
  }

  deleteTodo(id: number) {
    this.appState.removeTodo(id);
  }
}
  • holt sich vom AppStateService die Methoden für das Einfügen, Löschen, etc…​

Beispiel Vorgang eines Löschens von einem Todo
vorgang delete
  1. Situation: Der Benuter klickt auf den "Delete Button" in der Component (Component)

  2. Es wird eine Action ausgelöst, da er die Methode deleteTodo() aufgerufen hat (Action)

  3. Die AppStateService ruft in der deleteTodo() die updateState() auf. (AppStateService)

  4. Mittels der Methode updateState erzeugt einen neuen Zustand, indem das Todo aus state.todos entfernt wird (Reducer)

  5. der Store wird aktualisiert → alle Komponente, die todosSignals() verwenden, erhalten automatisch den neuen Zustand. (Store)

  6. Der Selector passt die UI an → computed()-Signal erkennt die Änderung und informiert die Component automatisch. (Selector) Das gelöschte Todo verschwindet aus der UI ohne manuelles Refresh. (Component)

Fluss des Löschen in der Grafik:

Component → Action → AppStateService → Reducer → Store → Selector →Component aktualisiert sich automatisch.

Verschiedene RxJS-Operatoren
map
example.component.ts
  demonstrateMap() {
    this.currentMethod = 'map';
    this.currentParameters = 'value => value * 2';
    const source = of(1, 2, 3, 4, 5);
    const results: number[] = [];
    source.pipe(
      map(value => value * 2)
    ).subscribe(value => results.push(value));

    this.before = 'Vorher: [1, 2, 3, 4, 5]';
    this.after = `Nachher: [${results.join(', ')}]`;
  }
map
filter
example.component.ts
  demonstrateFilter() {
    this.currentMethod = 'filter';
    this.currentParameters = 'value => value % 2 === 0';
    const source = of(1, 2, 3, 4, 5);
    const results: number[] = [];
    source.pipe(
      filter(value => value % 2 === 0)
    ).subscribe(value => results.push(value));

    this.before = 'Vorher: [1, 2, 3, 4, 5]';
    this.after = `Nachher: [${results.join(', ')}]`;
  }
filter
take
example.component.ts
  demonstrateTake() {
    this.currentMethod = 'take';
    this.currentParameters = '3';
    const source = of(1, 2, 3, 4, 5);
    const results: number[] = [];
    source.pipe(
      take(3)
    ).subscribe(value => results.push(value));

    this.before = 'Vorher: [1, 2, 3, 4, 5]';
    this.after = `Nachher: [${results.join(', ')}]`;
  }
take
concatMap
example.component.ts
  demonstrateConcatMap() {
    this.currentMethod = 'concatMap';
    this.currentParameters = 'value => of(`${value}1`, `${value}2`)';
    const source = of('A', 'B', 'C');
    const results: string[] = [];
    source.pipe(
      concatMap(value => of(`${value}1`, `${value}2`))
    ).subscribe(value => results.push(value));

    this.before = 'Vorher: [A, B, C]';
    this.after = `Nachher: [${results.join(', ')}]`;
  }
concat
  • concatMap verarbeitet jeden Wert nacheinander.

  • Für jeden Wert von ['A', 'B', 'C'] wird ein neues Observable mit zwei Werten nacheinander erzeugt (A1, A2).

first
example.component.ts
  demonstrateFirst() {
    this.currentMethod = 'first';
    this.currentParameters = 'Kein Parameter';
    const source = of(1, 2, 3, 4, 5);
    const results: number[] = [];
    source.pipe(
      first()
    ).subscribe(value => results.push(value));

    this.before = 'Vorher: [1, 2, 3, 4, 5]';
    this.after = `Nachher: [${results.join(', ')}]`;
  }
first
  • holt sich den ersten Wert der Observable

last
example.component.ts
  demonstrateLast() {
    this.currentMethod = 'last';
    this.currentParameters = 'Kein Parameter';
    const source = of(1, 2, 3, 4, 5);
    const results: number[] = [];
    source.pipe(
      last()
    ).subscribe(value => results.push(value));

    this.before = 'Vorher: [1, 2, 3, 4, 5]';
    this.after = `Nachher: [${results.join(', ')}]`;
  }
last
  • holt sich den letzten Wert der Observable

skip
example.component.ts
  demonstrateSkip() {
    this.currentMethod = 'skip';
    this.currentParameters = '3';
    const source = of('a', 'b', 'c', 'd', 'e')
    const results: string[] = [];
    source.pipe(
      skip(3)
    ).subscribe(value => results.push(value));

    this.before = 'Vorher: [a, b, c, d, e]';
    this.after = `Nachher: [${results.join(', ')}]`;
  }
skip
  • Überspringt die ersten n Werte eines Observables und gibt erst danach die restlichen Werte aus.

distinct
example.component.ts
  demonstrateDistinct() {
    this.currentMethod = 'distinct';
    this.currentParameters = 'Kein Parameter';
    const source = of(1, 1, 2, 2, 3, 3);
    const results: number[] = [];
    source.pipe(
      distinct()
    ).subscribe(value => results.push(value));

    this.before = 'Vorher: [1, 1, 2, 2, 3, 3]';
    this.after = `Nachher: [${results.join(', ')}]`;
  }
distinct
  • Gibt nur einzigartige Werte aus und filtert doppelte Einträge aus dem Observable.

startWith
example.component.ts
  demonstrateStartWith() {
    this.currentMethod = 'startWith';
    this.currentParameters = `'s'`;
    const source = of('a', 'b', 'c');
    const results: string[] = [];
    source.pipe(
      startWith('s')
    ).subscribe(value => results.push(value));

    this.before = 'Vorher: [a, b, c]';
    this.after = `Nachher: [${results.join(', ')}]`;
  }
startWith
  • fügt zuerst einen definierten Startwert, bevor die eigentlichen Werte des Observables ausgegeben werden.

pairwise
example.component.ts
  demonstratePairwise() {
    this.currentMethod = 'pairwise';
    this.currentParameters = 'Kein Parameter';
    const source = of('a', 'b', 'c', 'd', 'e');
    const results: string[] = [];
    source.pipe(
      pairwise()
    ).subscribe(([prev, curr]) => results.push(`[${prev}, ${curr}]`));

    this.before = 'Vorher: [a, b, c, d, e]';
    this.after = `Nachher: [${results.join(', ')}]`;
  }
pairwise
  • Gibt die letzten zwei aufeinanderfolgenden Werte als Paare aus

max
example.component.ts
  demonstrateMax() {
    this.currentMethod = 'max';
    this.currentParameters = 'Kein Parameter';
    const source = of(42, -1, 3);
    const results: number[] = [];
    source.pipe(
      max()
    ).subscribe(value => results.push(value));

    this.before = 'Vorher: [42, -1, 3]';
    this.after = `Nachher: [${results.join(', ')}]`;
  }
max
  • Sucht die maximale Zahl im Observable

find
example.component.ts
  demonstrateFind() {
    this.currentMethod = 'find';
    this.currentParameters = 'value => value % 5 === 0';
    const source = of(3, 9, 15, 20);
    const results: number[] = [];
    source.pipe(
      find(value => value % 5 === 0)
    ).subscribe(value => results.push(value!));

    this.before = 'Vorher: [3, 9, 15, 20]';
    this.after = `Nachher: [${results.join(', ')}]`;
  }
find
  • Gibt das erste Element zurück, das eine bestimmte Bedingung erfüllt.

3. Android

Für das Android-Projekt verwenden wir RxJava. Sie ist von ReactiveX für die JVM und wird häufig in Android-Apps verwendet, um asynchrone Operationen wie Datenbankzugriffe und Benutzerinteraktionen zu verwalten.

3.1. Android-MVVM

android mvvm pattern
Figure 6. Android-MVVM
  • Model:

    • Repräsentiert die Daten und Geschäftslogik der Anwendung

  • ViewModel:

    • verbindet die Model und die View

    • Holt Daten aus dem Model, verarbeitet sie und stellt sie der View bereit.

    • Beobachtet Änderungen im Model und leitet diese an die View weiter.

  • View:

    • Reagiert auf Benutzerinteraktionen (z.B. Klicks) und sendet diese an das ViewModel

    • Beobachtet Datenänderungen aus dem ViewModel und aktualisiert die UI entsprechend

3.2. Drei Prinzipien

  • Single Source of Truth

  • State is Read-Only

  • Changes are Made with Pure Functions

3.2.1. Single Source of Truth

  • Der gesamte Zustand der Anwendung wird in einem einzigen, zentralen Store gespeichert.

3.2.2. State is Read-Only

  • Der Zustand kann nicht direkt geändert werden. Änderungen erfolgen nur durch das Auslösen von Actions.

  • Actions beschreiben, was geändert werden soll, ohne die Details der Implementierung zu enthalten.

3.2.3. Changes are Made with Pure Functions

  • hat keine Seiteneffekte → greift nicht auf externe Ressourcen (z. B. Datenbanken) zu und hat keine unerwarteten Auswirkungen.

  • der Funktionswert bestimmt sich nur durch die Parameter → Sie gibt immer denselben Rückgabewert für dieselben Eingabewerte zurück.

3.3. Projekt

android project
  • Util

  • Model

  • Feature

3.3.1. Util

android project util
mapper
  • dient zur Serialisierung und Deserialisierung von Objekten nach JSON und zurück.

immer
Immer.java
package at.htl.leonding.util.immer;

import android.os.Handler;
import android.os.Looper;
import android.util.Log;

import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer;

import at.htl.leonding.util.mapper.Mapper;
import io.reactivex.rxjava3.subjects.BehaviorSubject;

/** Immer simplifies handling immutable data structures.
 * @param <T> The type of the baseState
 */

public class Immer<T> {
    private static final String TAG = Immer.class.getSimpleName();
    final public Mapper<T> mapper; (1)
    final Handler handler; (2)

    public Immer(Class<? extends T> type) {
        mapper = new Mapper<T>(type);
        handler = new Handler(Looper.getMainLooper());
    }
    /** Create a deep clone of the existing model, apply a recipe to it and finally pass the new state to the consumer.
     * To reduce the load on the main thread we clone the current state in a separate thread.
     * To avoid multithreading issues we call back the recipe and resultConsumer running on the one and only Main thread of the app.
     * We do not call the resultConsumer if the clone equals the currentState,
     * @param pipe the previous readonly single source or truth
     * @param recipe the callback function that modifies parts of the cloned state
     * @param resultConsumer the callback function that uses the cloned & modified model
     */
// Immer.java
    (3)
    public void produce(BehaviorSubject<T> pipe, Consumer<T> recipe, Consumer<T> resultConsumer) {
        handler.post(() -> {
            var t = mapper.clone(pipe.getValue());
            var currentAsJson = mapper.toResource(t);
            recipe.accept(t);
            var nextAsJson = mapper.toResource(t);
            if (!nextAsJson.equals(currentAsJson)) {
                Log.d(TAG, String.format("=== state changed ===\n%s\n=>\n%s---", currentAsJson, nextAsJson));
                resultConsumer.accept(t);
            } else {
                Log.w(TAG, "produce() without change");
            }
        });
    }
}

Immer ist eine JavaScript-Bibliothek, die das Arbeiten mit unveränderlichen (immutable) Zuständen vereinfacht. Intern sorgt Immer dafür, dass der ursprüngliche Zustand unverändert bleibt, indem es eine Kopie erstellt und nur diese aktualisiert.

1 die Objekte werden in JSON umwandelt und zurück
2 führt die Aufgaben im Main-Thread aus
3 die Methode produce(…​) sorgt dafür, dass:
  1. Der aktuelle Zustand aus dem pipe (BehaviorSubject) tief kopiert wird

  2. Die übergebene Änderungsfunktion (recipe) auf diesen Klon angewendet wird, um gezielt Änderungen vorzunehmen.

  3. Der alte und der neue Zustand werden verglichen:

    • Falls sich der Zustand geändert hat, wird der neue Zustand über den resultConsumer (meist pipe.onNext) weitergegeben.

    • Falls keine Änderung erfolgt, wird eine Warnung ("produce() without change") im Log ausgegeben.

produce
Figure 7. produce()
behavior subject
store
StoreBase.java
package at.htl.leonding.util.store;

import android.util.Log;

import java.util.concurrent.CompletionException;
import java.util.function.Consumer;

import at.htl.leonding.util.immer.Immer;
import io.reactivex.rxjava3.subjects.BehaviorSubject;

/** Base class for implementations using a single source of truth approach.
 * @param <T> the class of the ReadOnly Single Source of Truth.
 */
public class StoreBase<T> {
    public final BehaviorSubject<T> pipe;
    protected final Immer<T> immer;

    protected StoreBase(Class<? extends T> type, T initialState) {
        try {
            pipe = BehaviorSubject.createDefault(initialState);
            immer = new Immer<T>(type);
        } catch (Exception e) {
            throw new CompletionException(e);
        }
    }

    public T get() {
        return immer.mapper.clone(pipe.getValue());
    }
    /** clone the current Model, apply the recipe to it and submit it to the pipe as the next Model.
     * @param recipe
     * The function that receives a clone of the current model and applies its changes to it.
     */
    public void apply(Consumer<T> recipe) { (1)
        Consumer<T> onNext = nextState -> pipe.onNext(nextState);
        immer.produce(pipe, recipe, onNext);
    }
}
  • Basisklasse für Store

  • Verwaltet den Zustand

1 produce():
  1. onNext holt sich den neuen Zustand im BehaviorSubject (pipe).

  2. klont sich den aktuellen Zustand und das recipe wird angewendet

  3. Vergleich den alten und neuen Zustand

  4. veröffentlicht den neuen Zustand mit onNext, wenn sich etwas geändert hat

resteasy
  • enthält eine Sammlung von Klassen, die zusammenarbeiten, um REST-API-Clients in einer Android-Anwendung effizient zu erstellen.

  • JAX-RS-Implementierung für Java nutzt Jackson zur JSON-Verarbeitung.

config
  • Konfigurationsverwaltung für die Anwendung bereit

  • Es lädt Konfigurationsdateien (z.B. application.properties)

3.3.2. Model

android project model
Model.java
package at.htl.leonding.model;

/** Our read only <a href="https://redux.js.org/understanding/thinking-in-redux/three-principles">single source of truth</a> model */
public class Model {
    public static class GreetingModel {
        public String greetingText = "Hello, world!";
    }
    public ToDo[] toDos = new ToDo[0];
    public UIState uiState = new UIState();  // sub-model für ui state
    public GreetingModel greetingModel = new GreetingModel();
}
Store.java
package at.htl.leonding.model;

import javax.inject.Inject;
import javax.inject.Singleton;

import at.htl.leonding.util.store.StoreBase;

/** This is our Storage area for our <a href="https://redux.js.org/understanding/thinking-in-redux/three-principles">single source of truth</a> {@link Model}. */
@Singleton
public class Store extends StoreBase<Model> {
    @Inject
    public Store() {
        super(Model.class, new Model());
    }
}

3.3.3. Feature

android project feature

Wir legen unser Fokus auf todo

todo
Details
package at.htl.leonding.feature.todo;

import at.htl.leonding.model.ToDo;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType;

@Path("/todos")
public interface ToDoClient {
    @GET
    @Produces(MediaType.APPLICATION_JSON)
    ToDo[] all(@QueryParam("_start") int start, @QueryParam("_limit") int maxRecords);
}
ToDoService
    public void getAll() { (1)
        Consumer<ToDo[]> setToDos = todos -> store.apply(model -> model.toDos = todos);
        CompletableFuture.supplyAsync(() -> toDoClient.all(0, 40)).thenAccept(setToDos);
    }

    public void addToDo(ToDo toDo) {

        store.apply(model -> {
            ToDo[] newToDos = new ToDo[model.toDos.length + 1];
            System.arraycopy(model.toDos, 0, newToDos, 0, model.toDos.length);
            newToDos[model.toDos.length] = toDo;
            model.toDos = newToDos;
        });
    }
1 getAll()
  • Der Consumer speichert die abgerufenen ToDo-Daten im store.

  • Die apply-Methode wird verwendet, um den toDos-Wert des Models im Store zu setzen.

  • es wird asynchron mit der CompletableFuture die all-Methode im Client aufgerufen

  • wenn er erfolgreich die Daten holen konnte, wird er im store gespeichert

Ablauf eines Erstellen eines Todos
mvvm
  1. in der View wird eine neue Todo erstellt

  2. Dadurch löst er eine Action aus, die dann den Service aufruft

  3. Der Service gibt den Store die aktuelle Version und das jeweilige Rezept mit

  4. der Store erstellt anhand des Rezept das neue Modell, welches dann schließlich im Model gespeichert wird

  5. Modell informiert die Viewmodel, dass sich was geändert hat

  6. Die Viewmodel veröffentlicht die neue Version und die View holt sich dann den Datenstrom ab

4. Glossar

  • ReactiveX (Rx): API für asynchrone und eventbasierte Programmierung mit Observables, das Pull- und Push-Prinzipien kombiniert.

  • Asynchron: Arbeitsweise, bei der Prozesse nicht blockiert werden und parallel laufen können, z. B. bei HTTP-Requests.

  • Observable: Datenquelle, die asynchrone Datenströme bereitstellt und Werte/Ereignisse an Observer sendet.

  • Observer (Subscriber): Konsument eines Observables, der auf gesendete Werte (next), Fehler (error) oder den Abschluss (complete) reagiert.

  • Pull-Verfahren: Observer entscheidet, ob er sich bei einem Observable registriert.

  • Push-Verfahren: Observable sendet Daten aktiv an registrierte Observer.

  • BehaviorSubject: RxJava-Subjekt, das den letzten und zukünftige Werte an Observer sendet.

  • PublishSubject: RxJava-Subjekt, das nur zukünftige Werte an Observer sendet.

  • Signal: Reaktive Quelle von Zustandsänderungen.

    • Computed Signal: Dynamisch berechneter Wert basierend auf anderen Signals.

    • Effect Signal: Signal, das Seiteneffekte bei Änderungen auslöst.

    • Linked Signal: Signal, das von anderen Signals abhängt und sich mit deren Änderungen aktualisiert.

  • MVVM (Model-View-ViewModel): Architektur, die Daten (Model), Logik (ViewModel) und Darstellung (View) trennt.

  • Drei Prinzipien (Redux)

    • Single Source of Truth: Der Zustand der Anwendung wird zentral gespeichert.

    • State is Read-Only: Änderungen am Zustand erfolgen nur durch Actions.

    • Changes are Made with Pure Functions: Änderungen haben keine Seiteneffekte haben und werden durch Parameter bestimmt

  • Immer: Bibliothek für unveränderlichen Zustand

  • Rx-Operatoren:

    • map: Transformiert jedes Element eines Observables basierend auf einer Funktion. Beispiel: Zahlen verdoppeln.

    • filter: Filtert Elemente basierend auf einer Bedingung. Beispiel: Nur gerade Zahlen durchlassen.

    • take: Nimmt nur eine bestimmte Anzahl von Elementen aus dem Stream.

    • zip: Kombiniert mehrere Observables zu einem Stream von Paaren. Beispiel: Zahlen und Buchstaben zu Paaren kombinieren.

    • interval: Erzeugt fortlaufende Werte in einem festen Zeitintervall.

    • concatMap: Führt asynchrone Operationen hintereinander aus.

    • first: Gibt das erste Element eines Streams zurück.

    • last: Gibt das letzte Element eines Streams zurück.

    • skip: Überspringt die ersten n Werte und gibt danach die restlichen Werte aus.

    • distinct: Gibt nur einzigartige Werte aus und filtert doppelte Einträge.

    • startWith: fügt zuerst einen definierten Startwert, bevor die restlichen Werte folgen.

    • pairwise: Gibt die letzten zwei aufeinanderfolgenden Werte als Paare aus.

    • max: Gibt den größten Wert aus einem Stream zurück.

    • find: Gibt das erste Element zurück, das eine bestimmte Bedingung erfüllt.

5. Quelle