Plama
WatermelonDB w React Native (2/2)

Po poprawnym skonfigurowaniu WatermelonDB oraz przygotowaniu modelu i schematu tabeli, możemy przystępować do kolejnych czynności.

Widok aplikacji

Najpierw warto przygotować widok. Zastanówmy się co konkretnie będziemy potrzebować. Każde nowe zadanie powinno być elementem listy, dodatkowo powinniśmy każde zadanie móc usunąć, zmienić nazwę, odznaczyć i zaznaczyć. Na początku utwórzmy nowy katalog o nazwie pages w których będziemy przechowywali nasz widok, a w nim nowy plik TasksPage.js. W pliku tym dodajmy na początku prosta klasę (pamiętając o zaimportowaniu potrzebnych bibliotek), w której będzie nasza lista zadań.

class TasksPage extends Component {
  render = () => {
    return (
      <>
      </>
    );
  };
}

Istotną cechą biblioteki WatermelonDB, jest możliwość wykorzystania komponentów, które będą reagować na zmiany w bazie. W przypadku zmiany elementów listy zadań w bazie danych, będzie ona mogła zostać zaktualizowana na widoku. Aby to zrobić, należy umożliwić zaobserwowanie komponentu:

import withObservables from '@nozbe/with-observables';
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';

...

export default withDatabase(
  withObservables([], ({database}) => ({
    tasks: database.collections.get('tasks').query().observe(),
  }))(TasksPage),
);

Teraz używając props komponentu możemy odwoływać się do elementu tasks, który jest kolekcją rekordów tabeli tasks w bazie danych. Dodajmy Flatlistę w celu wyświetlenia elementów w funkcji render():

render = () => {
  const {tasks} = this.props;

  return (
    <>
      <FlatList
        data={tasks}
        ListHeaderComponent={this.renderHeader}
        renderItem={this.renderTask}
        keyExtractor={(item) => 'item' + item.id}
      />
    </>
  );
};

Aby przygotować renderowanie elementu dodajmy nową bibliotekę dodającą komponent CheckBox. W terminalu w katalogu projektu wykonajmy polecenie:

yarn add react-native-check-box

Teraz dodajmy metodę renderTask():

renderTask = ({item}) => (
  <View style={[styles.itemContainer, styles.insideContainer]}>
    <View style={styles.insideContainer}>
      <CheckBox
        isChecked={item.completed}
        style={styles.checkbox}
        checkBoxColor="#666666"
        uncheckedCheckBoxColor="#666666"
        checkedCheckBoxColor="#38d13b"
        onClick={() => this.onChangeItemCompleted(item)}
      />
      <TextInput
        style={styles.input}
        value={item.name}
        onChangeText={(value) => this.onChangeItemName(item, value)}
      />
    </View>
    <TouchableOpacity onPress={() => this.removeTask(item)}>
      <Image style={styles.icon} source={require('../assets/remove.png')} />
    </TouchableOpacity>
  </View>
);

Funkcje które są używane, zostaną za chwilę wyjaśnione. Przejdźmy jednak do dodania pola, umożliwiającego dodawanie nowego zadania:

renderHeader = () => (
  <View style={[styles.itemContainer, styles.insideContainer]}>
    <View style={styles.insideContainer}>
      <TextInput
        style={[styles.input, styles.headerInput]}
        placeholder="Dodaj nazwę zadania"
        onChangeText={this.onChangeNewTaskName}
        value={this.state.newTaskName}
      />
    </View>
    <TouchableOpacity style={[styles.rightButton, styles.addButton]}>
      <Image style={styles.icon} source={require('../assets/add.png')} />
    </TouchableOpacity>
  </View>
);

Dodajmy też kilka styli, w celu delikatnego poprawienia wyglądu naszej aplikacji:

const styles = StyleSheet.create({
  itemContainer: {
    padding: 15,
  },
  insideContainer: {
    flexDirection: 'row',
    alignItems: 'center',
    justifyContent: 'space-between',
    flex: 1,
  },
  checkbox: {
    width: 30,
    marginRight: 15,
    borderColor: '#666666',
  },
  icon: {
    height: 30,
    width: 30,
  },
  input: {
    flex: 1,
  },
  headerInput: {
    marginLeft: 45,
  },
});

Akcje bazy danych

Po przygotowaniu widoku, należy obsłużyć logikę aplikacji.

Dodawanie nowego zadania

Przy dodawaniu nowego zadania, konieczne jest podanie jego nazwy. w tym celu w klasie dodajmy definicję state oraz metodę umożliwiająca jej zmianę.

state = {
  newTaskName: '',
};

...

onChangeNewTaskName = (value) => {
  this.setState({newTaskName: value});
};

Po kliknięciu w przycisk dodawania nowego elementu, wywołujemy metodę

addTask = async () => {
  const {database} = this.props;
  const {newTaskName} = this.state;
  const tasksCollection = database.collections.get('tasks');

  await database.action(async () => {
    await tasksCollection.create((task) => {
      task.name = newTaskName;
      task.completed = false;
    });
    this.setState({newTaskName: ''});
  });
};

Najpierw musimy wskazać z której kolekcji będziemy pobierać elementy, a jako że w naszym przypadku jej nazwa to tasks, to taką właśnie pobieramy. Następnie na niej przeprowadzamy akcję na bazie danych dodania nowego elementu kolekcji. Po dodaniu elementu do bazy, warto wyczyścić pole tekstowe z nazwy zadania, dlatego w tym celu została użyta funkcja setState().

Edycja zadania

Edycję można podzielić na dwa elementy. Pierwszy z nich to zmiana stanu wykonania zadania, bo kliknięciu na pole. Druga natomiast, to zmiana nazwy zadania. W celu większej przejrzystości podzielimy to na dwie metody, wśród których każda będzie wykonywała inne zadanie.

onChangeItemName = async (item, name) => {
  const {database} = this.props;
  const tasksCollection = database.collections.get('tasks');
  const taskToUpdate = await tasksCollection.find(item.id);

  await database.action(async () => {
    await taskToUpdate.update((task) => {
      task.name = name;
    });

    this.setState({refreshing: !this.state.refreshing});
  });
};

onChangeItemCompleted = async (item) => {
  const {database} = this.props;
  const tasksCollection = database.collections.get('tasks');
  const taskToUpdate = await tasksCollection.find(item.id);

  await database.action(async () => {
    await taskToUpdate.update((task) => {
      task.completed = !item.completed;
    });

    this.setState({refreshing: !this.state.refreshing});
  });
};

Jest to dość proste zadanie. Po wybraniu odpowiedniej kolekcji, znajdujemy element który chcemy edytować, na podstawie jego automatycznie generowanego id.

W tym przypadku należy zastosowac jednak pewną sztuczkę. Flatlist z definicji nie będzie informowana o zmianie w elementach tablicy data. Dlatego należy ją o tym poinformować. W tym celu w state widoku utworzyłem atrybut typu boolean, który zmienia swoją wartość gdy wykonana zostanie jakaś zmiana w bazie. Dodajemy również do flatlisty atrybut extraData, którego wartością będzie wyżej wymieniona wartość state. Dzięki temu po każdej akcji zmiany elementu w bazie, flatlista będzie mogła przeładować zmienione elementy.

Usuwanie zadania

Ostatnią funkcjonalnością będzie usunięcie zadania z listy. Ponownie tworzymy oddzielną metodę za to odpowiedzialną. Znajdujemy kolekcję oraz element który chcemy z niej usunąć i przeprowadzamy akcję usunięcia permanentnego. Możemy skorzystać w zastępstwie z metody markAsDeleted(), która doda do pola flagę informującą o usunięciu, jednak fizycznie rekord taki będzie można dalej odworzyć.

removeTask = async (item) => {
  const {database} = this.props;
  const tasksCollection = database.collections.get('tasks');
  const taskToRemove = await tasksCollection.find(item.id);

  await database.action(async () => {
    await taskToRemove.destroyPermanently();
  });
};

Testy

Przetestujmy zatem działanie naszej aplikacji. 

Test operacji modyfikacji zadań.

Istotne będzie również sprawdzenie, jak zachowa się aplikacja po jej zrestartowaniu (tj. czy baza rzeczywiście działa w trybie offline).

Test działania w trybie offline.

Podsumowanie

Przykład tej aplikacji pokazuje, że stworzenie aplikacji z lokalną bazą danych nie jest trudnym zadaniem. Dzięki gotowym bibliotekom bazodanowym mamy dostęp do znacznie szybszych i bardziej przyjaznych programiście metod. Artykuł ten i poprzedni stanowią jednak tylko wstęp do poznania tematu, który jest znacznie bardziej rozbudowany i zawiera wiele ciekawych niuansów.

Dodatkowe informacje dostępne w dokumentacji: https://nozbe.github.io/WatermelonDB/

Kategorie
Inne posty