Android Kotlin Fundamentals 06.2: Coroutines and Room

Ten moduł Codelab jest częścią kursu Android Kotlin Fundamentals. Najwięcej korzyści przyniesie Ci ukończenie wszystkich ćwiczeń w kolejności. Wszystkie ćwiczenia z tego kursu znajdziesz na stronie docelowej kursu Android Kotlin Fundamentals.

Wprowadzenie

Jednym z najważniejszych priorytetów podczas tworzenia aplikacji jest zapewnienie użytkownikom jak najlepszych wrażeń. W tym celu należy zadbać o to, aby interfejs użytkownika zawsze działał płynnie i szybko reagował na działania użytkownika. Jednym ze sposobów na poprawę wydajności interfejsu jest przeniesienie długotrwałych zadań, takich jak operacje na bazie danych, w tle.

W tym laboratorium kodowania zaimplementujesz część aplikacji TrackMySleepQuality widoczną dla użytkownika. Do wykonywania operacji na bazie danych poza wątkiem głównym użyjesz współprogramów Kotlin.

Co warto wiedzieć

Musisz znać:

  • Tworzenie podstawowego interfejsu użytkownika za pomocą aktywności, fragmentów, widoków i procedur obsługi kliknięć.
  • Przechodzenie między fragmentami i używanie safeArgs do przekazywania prostych danych między fragmentami.
  • Wyświetl modele, fabryki modeli, przekształcenia i LiveData.
  • Jak utworzyć bazę danych Room, utworzyć DAO i zdefiniować encje.
  • Warto znać koncepcje wątków i wieloprocesowości.

Czego się nauczysz

  • Jak działają wątki na Androidzie.
  • Jak używać korutyn w Kotlinie, aby przenieść operacje na bazie danych poza wątek główny.
  • Jak wyświetlać sformatowane dane w TextView.

Jakie zadania wykonasz

  • Rozszerz aplikację TrackMySleepQuality, aby zbierać, przechowywać i wyświetlać dane w bazie danych i z niej.
  • Używaj korutyn do uruchamiania długotrwałych operacji na bazie danych w tle.
  • Użyj LiveData, aby wywołać nawigację i wyświetlić pasek informacyjny.
  • Aby włączyć i wyłączyć przyciski, użyj LiveData.

W tym ćwiczeniu utworzysz model widoku, współprogramy i części aplikacji TrackMySleepQuality odpowiedzialne za wyświetlanie danych.

Aplikacja ma 2 ekrany reprezentowane przez fragmenty, jak pokazano na ilustracji poniżej.

Na pierwszym ekranie (po lewej) znajdują się przyciski rozpoczynania i zatrzymywania śledzenia. Na ekranie wyświetlają się wszystkie dane snu użytkownika. Przycisk Wyczyść trwale usuwa wszystkie dane zebrane przez aplikację na temat użytkownika.

Drugi ekran, widoczny po prawej stronie, służy do wybierania oceny jakości snu. W aplikacji ocena jest przedstawiana w formie liczbowej. Na potrzeby programowania aplikacja wyświetla zarówno ikony twarzy, jak i ich odpowiedniki liczbowe.

Proces wygląda następująco:

  • Użytkownik otwiera aplikację i widzi ekran śledzenia snu.
  • Użytkownik klika przycisk Rozpocznij. Zapisuje to czas rozpoczęcia i wyświetla go. Przycisk Start jest wyłączony, a przycisk Stop jest włączony.
  • Użytkownik klika przycisk Zatrzymaj. Zapisze to czas zakończenia i otworzy ekran jakości snu.
  • Użytkownik wybiera ikonę jakości snu. Ekran zamyka się, a na ekranie śledzenia wyświetla się godzina zakończenia snu i jego jakość. Przycisk Stop jest wyłączony, a przycisk Start jest włączony. Aplikacja jest gotowa na kolejną noc.
  • Przycisk Wyczyść jest włączony, gdy w bazie danych znajdują się dane. Gdy użytkownik kliknie przycisk Wyczyść, wszystkie jego dane zostaną bezpowrotnie usunięte – nie pojawi się komunikat „Czy na pewno?”.

Ta aplikacja korzysta z uproszczonej architektury, jak pokazano poniżej w kontekście pełnej architektury. Aplikacja korzysta tylko z tych komponentów:

  • kontroler interfejsu,
  • Wyświetl model i LiveData
  • baza danych Room,

W tym zadaniu użyjesz TextView, aby wyświetlić sformatowane dane śledzenia snu. (To nie jest ostateczny interfejs. Lepszy sposób poznasz w innym codelabie).

Możesz kontynuować pracę nad aplikacją TrackMySleepQuality, którą utworzono w poprzednim przewodniku, lub pobrać aplikację początkową do tego przewodnika.

Krok 1. Pobierz i uruchom aplikację startową

  1. Pobierz aplikację TrackMySleepQuality-Coroutines-Starter z GitHuba.
  2. Skompiluj i uruchom aplikację. Wyświetli ona interfejs fragmentu SleepTrackerFragment, ale bez danych. Przyciski nie reagują na dotknięcia.

Krok 2. Sprawdź kod

Kod początkowy do tych ćwiczeń z programowania jest taki sam jak kod rozwiązania do ćwiczeń 6.1. Tworzenie bazy danych Room.

  1. Otwórz plik res/layout/activity_main.xml. Ten układ zawiera fragment nav_host_fragment. Zwróć też uwagę na tag <merge>.

    Tag merge może być używany do eliminowania zbędnych układów podczas ich uwzględniania. Warto go używać. Przykładem zbędnego układu może być ConstraintLayout > LinearLayout > TextView, w którym system może wyeliminować LinearLayout. Tego rodzaju optymalizacja może uprościć hierarchię widoków i poprawić wydajność aplikacji.
  2. W folderze navigation otwórz plik navigation.xml. Widoczne będą 2 fragmenty i działania nawigacyjne, które je łączą.
  3. W folderze layout kliknij dwukrotnie fragment śledzenia snu, aby wyświetlić jego układ XML. Zwróć uwagę na te kwestie:
  • Dane układu są zawarte w elemencie <layout>, aby umożliwić wiązanie danych.
  • ConstraintLayout i pozostałe widoki są umieszczone w elemencie <layout>.
  • Plik zawiera tag zastępczy <data>.

Aplikacja startowa zawiera też wymiary, kolory i style interfejsu. Aplikacja zawiera bazę danych Room, DAO i SleepNight. Jeśli nie udało Ci się ukończyć poprzedniego laboratorium, zapoznaj się z tymi aspektami kodu.

Teraz, gdy masz już bazę danych i interfejs, musisz zbierać dane, dodawać je do bazy danych i wyświetlać. Wszystkie te działania są wykonywane w modelu widoku. Model widoku trackera snu będzie obsługiwać kliknięcia przycisków, wchodzić w interakcje z bazą danych za pomocą obiektu DAO i dostarczać dane do interfejsu za pomocą LiveData. Wszystkie operacje na bazie danych będą musiały być wykonywane poza głównym wątkiem interfejsu, a do tego celu będziesz używać korutyn.

Krok 1. Dodaj SleepTrackerViewModel

  1. W pakiecie sleeptracker otwórz plik SleepTrackerViewModel.kt.
  2. Sprawdź klasę SleepTrackerViewModel, która jest dostępna w aplikacji początkowej i jest też pokazana poniżej. Zwróć uwagę, że klasa rozszerza AndroidViewModel(). Ta klasa jest taka sama jak ViewModel, ale przyjmuje kontekst aplikacji jako parametr i udostępnia go jako właściwość. Będzie Ci potrzebny później.
class SleepTrackerViewModel(
       val database: SleepDatabaseDao,
       application: Application) : AndroidViewModel(application) {
}

Krok 2. Dodaj SleepTrackerViewModelFactory

  1. W pakiecie sleeptracker otwórz plik SleepTrackerViewModelFactory.kt.
  2. Sprawdź kod fabryki, który jest podany poniżej:
class SleepTrackerViewModelFactory(
       private val dataSource: SleepDatabaseDao,
       private val application: Application) : ViewModelProvider.Factory {
   @Suppress("unchecked_cast")
   override fun <T : ViewModel?> create(modelClass: Class<T>): T {
       if (modelClass.isAssignableFrom(SleepTrackerViewModel::class.java)) {
           return SleepTrackerViewModel(dataSource, application) as T
       }
       throw IllegalArgumentException("Unknown ViewModel class")
   }
}

Zwróć uwagę na te kwestie:

  • Podana funkcja SleepTrackerViewModelFactory przyjmuje ten sam argument co funkcja ViewModel i rozszerza ViewModelProvider.Factory.
  • W fabryce kod zastępuje create(), który przyjmuje jako argument dowolny typ klasy i zwraca ViewModel.
  • W treści funkcji create() kod sprawdza, czy dostępna jest klasa SleepTrackerViewModel, a jeśli tak, zwraca jej instancję. W przeciwnym razie kod zgłosi wyjątek.

Krok 3. Zaktualizuj SleepTrackerFragment

  1. SleepTrackerFragment uzyskaj odniesienie do kontekstu aplikacji. Umieść odwołanie w onCreateView(), poniżej binding. Aby przekazać do dostawcy fabryki modelu widoku, musisz mieć odwołanie do aplikacji, do której jest dołączony ten fragment.

    Funkcja requireNotNull w Kotlinie zgłasza wyjątek IllegalArgumentException, jeśli wartość to null.
val application = requireNotNull(this.activity).application
  1. Musisz mieć odniesienie do źródła danych za pomocą odniesienia do obiektu DAO. W polu onCreateView() przed znakiem return określ dataSource. Aby uzyskać odniesienie do obiektu DAO bazy danych, użyj SleepDatabase.getInstance(application).sleepDatabaseDao.
val dataSource = SleepDatabase.getInstance(application).sleepDatabaseDao
  1. W onCreateView() przed return utwórz instancję viewModelFactory. Musisz przekazać dataSourceapplication.
val viewModelFactory = SleepTrackerViewModelFactory(dataSource, application)
  1. Teraz, gdy masz już fabrykę, uzyskaj odwołanie do SleepTrackerViewModel. Parametr SleepTrackerViewModel::class.java odnosi się do klasy Java w czasie działania tego obiektu.
val sleepTrackerViewModel =
       ViewModelProviders.of(
               this, viewModelFactory).get(SleepTrackerViewModel::class.java)
  1. Gotowy kod powinien wyglądać tak:
// Create an instance of the ViewModel Factory.
val dataSource = SleepDatabase.getInstance(application).sleepDatabaseDao
val viewModelFactory = SleepTrackerViewModelFactory(dataSource, application)

// Get a reference to the ViewModel associated with this fragment.
val sleepTrackerViewModel =
       ViewModelProviders.of(
               this, viewModelFactory).get(SleepTrackerViewModel::class.java)

Oto dotychczasowe wyniki onCreateView():

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                              savedInstanceState: Bundle?): View? {

        // Get a reference to the binding object and inflate the fragment views.
        val binding: FragmentSleepTrackerBinding = DataBindingUtil.inflate(
                inflater, R.layout.fragment_sleep_tracker, container, false)

        val application = requireNotNull(this.activity).application

        val dataSource = SleepDatabase.getInstance(application).sleepDatabaseDao

        val viewModelFactory = SleepTrackerViewModelFactory(dataSource, application)

        val sleepTrackerViewModel =
                ViewModelProviders.of(
                        this, viewModelFactory).get(SleepTrackerViewModel::class.java)

        return binding.root
    }

Krok 4. Dodaj wiązanie danych do modelu widoku

Po utworzeniu podstawowego ViewModel musisz dokończyć konfigurowanie powiązania danych w SleepTrackerFragment, aby połączyć ViewModel z interfejsem.


W pliku układu fragment_sleep_tracker.xml:

  1. W bloku <data> utwórz element <variable>, który odwołuje się do klasy SleepTrackerViewModel.
<data>
   <variable
       name="sleepTrackerViewModel"
       type="com.example.android.trackmysleepquality.sleeptracker.SleepTrackerViewModel" />
</data>

SleepTrackerFragment:

  1. Ustaw bieżącą aktywność jako właściciela cyklu życia powiązania. Dodaj ten kod w metodzie onCreateView() przed instrukcją return:
binding.setLifecycleOwner(this)
  1. Przypisz zmienną wiążącą sleepTrackerViewModel do sleepTrackerViewModel. Umieść ten kod w sekcji onCreateView(), pod kodem, który tworzy SleepTrackerViewModel:
binding.sleepTrackerViewModel = sleepTrackerViewModel
  1. Prawdopodobnie zobaczysz błąd, ponieważ musisz ponownie utworzyć obiekt powiązania. Aby pozbyć się błędu, wyczyść i ponownie skompiluj projekt.
  2. Na koniec, jak zawsze, upewnij się, że kod kompiluje się i działa bez błędów.

W Kotlinie korutyny to sposób na eleganckie i wydajne obsługiwanie długotrwałych zadań. Korutyny Kotlin umożliwiają przekształcenie kodu opartego na wywołaniach zwrotnych w kod sekwencyjny. Kod napisany sekwencyjnie jest zwykle łatwiejszy do odczytania i może nawet korzystać z funkcji języka, takich jak wyjątki. Ostatecznie korutyny i wywołania zwrotne robią to samo: czekają, aż wynik długotrwałego zadania będzie dostępny, i kontynuują wykonywanie.

Korutyny mają te właściwości:

  • Korutyny są asynchroniczne i nieblokujące.
  • Korutyny używają funkcji suspend, aby kod asynchroniczny był wykonywany sekwencyjnie.

Korutyny są asynchroniczne.

Korutyna działa niezależnie od głównych kroków wykonywania programu. Może to być przetwarzanie równoległe lub na osobnym procesorze. Może też się zdarzyć, że gdy reszta aplikacji czeka na dane wejściowe, Ty wykonujesz część przetwarzania. Jednym z ważnych aspektów programowania asynchronicznego jest to, że nie możesz oczekiwać, że wynik będzie dostępny, dopóki na niego nie poczekasz.

Załóżmy na przykład, że masz pytanie, które wymaga przeprowadzenia badań, i prosisz współpracownika o znalezienie odpowiedzi. Zajmują się tym, co jest podobne do pracy „asynchronicznej” i „w osobnym wątku”. Możesz kontynuować inne zadania, które nie zależą od odpowiedzi, dopóki kolega nie wróci i nie powie Ci, jaka jest odpowiedź.

Korutyny nie blokują wątków.

Nieblokowanie oznacza, że korutyna nie blokuje wątku głównego ani wątku interfejsu. Dzięki korutynom użytkownicy zawsze mają zapewnioną jak największą wygodę, ponieważ interakcja z interfejsem użytkownika ma zawsze priorytet.

Korutyny używają funkcji zawieszających, aby kod asynchroniczny był wykonywany sekwencyjnie.

Słowo kluczowe suspend w języku Kotlin oznacza, że funkcja lub typ funkcji jest dostępny dla współprogramów. Gdy korutyna wywołuje funkcję oznaczoną symbolem suspend, zamiast blokować się do momentu zwrócenia wyniku przez funkcję, jak w przypadku normalnego wywołania funkcji, korutyna wstrzymuje wykonanie do momentu, aż wynik będzie gotowy. Następnie korutyna wznawia działanie w miejscu, w którym zostało ono przerwane, i zwraca wynik.

Gdy korutyna jest zawieszona i oczekuje na wynik, odblokowuje wątek, w którym jest uruchomiona. Dzięki temu mogą działać inne funkcje lub współprogramy.

Słowo kluczowe suspend nie określa wątku, w którym jest uruchamiany kod. Funkcja zawieszania może być uruchamiana w wątku w tle lub w wątku głównym.

Aby używać w Kotlinie korutyn, potrzebujesz 3 rzeczy:

  • Zadanie
  • dyspozytor,
  • zakres,

Zadanie: zadanie to w zasadzie wszystko, co można anulować. Każda korutyna ma zadanie, za pomocą którego możesz ją anulować. Zadania można układać w hierarchie nadrzędne i podrzędne. Anulowanie zadania nadrzędnego powoduje natychmiastowe anulowanie wszystkich jego zadań podrzędnych, co jest znacznie wygodniejsze niż ręczne anulowanie każdej korutyny.

Dispatcher : wysyła korutyny do uruchomienia w różnych wątkach. Na przykład Dispatcher.Main wykonuje zadania w wątku głównym, a Dispatcher.IO przenosi blokujące zadania wejścia-wyjścia do wspólnej puli wątków.

Zakres: zakres korutyny określa kontekst, w którym jest ona wykonywana. Zakres łączy informacje o zadaniu i dispatcherze korutyny. Zakresy śledzą coroutines. Gdy uruchamiasz korutynę, jest ona „w zakresie”, co oznacza, że wskazujesz, który zakres będzie śledzić korutynę.

Chcesz, aby użytkownik mógł korzystać z danych o śnie w ten sposób:

  • Gdy użytkownik naciśnie przycisk Rozpocznij, aplikacja utworzy nowy nocny sen i zapisze go w bazie danych.
  • Gdy użytkownik naciśnie przycisk Stop, aplikacja zaktualizuje noc, dodając godzinę zakończenia.
  • Gdy użytkownik kliknie przycisk Wyczyść, aplikacja usunie dane z bazy danych.

Te operacje na bazie danych mogą trwać długo, więc powinny być wykonywane w osobnym wątku.

Krok 1. Skonfiguruj współprogramy do operacji na bazie danych

Gdy użytkownik kliknie przycisk Start w aplikacji Sleep Tracker, chcesz wywołać funkcję w SleepTrackerViewModel, aby utworzyć nową instancję SleepNight i zapisać ją w bazie danych.

Kliknięcie dowolnego przycisku powoduje wykonanie operacji na bazie danych, np. utworzenie lub zaktualizowanie SleepNight. Z tego i innych powodów do implementowania procedur obsługi kliknięć przycisków aplikacji używasz korutyn.

  1. Otwórz plik build.gradle na poziomie aplikacji i znajdź zależności dla współprogramów. Aby używać współprogramów, musisz mieć te zależności, które zostały dodane za Ciebie.

    $coroutine_version jest zdefiniowany w pliku build.gradle projektu jako coroutine_version = '1.0.0'.
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutine_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutine_version"
  1. Otwórz plik SleepTrackerViewModel.
  2. W treści klasy zdefiniuj viewModelJob i przypisz do niego instancję Job. Ten viewModelJob umożliwia anulowanie wszystkich współprogramów uruchomionych przez ten model widoku, gdy model widoku nie jest już używany i zostaje zniszczony. Dzięki temu nie będziesz mieć korutyn, które nie mają dokąd wrócić.
private var viewModelJob = Job()
  1. Na końcu treści klasy zastąp onCleared() i anuluj wszystkie korutyny. Gdy ViewModel zostanie zniszczony, wywoływana jest funkcja onCleared().
override fun onCleared() {
   super.onCleared()
   viewModelJob.cancel()
}
  1. Bezpośrednio pod definicją viewModelJob zdefiniuj uiScope dla korutyn. Zakres określa, w którym wątku będzie działać korutyna, i musi też znać zadanie. Aby uzyskać zakres, poproś o instancję CoroutineScope i przekaż dyspozytora oraz zadanie.

Użycie Dispatchers.Main oznacza, że korutyny uruchomione w uiScope będą działać w wątku głównym. Jest to rozsądne w przypadku wielu coroutines uruchamianych przez ViewModel, ponieważ po wykonaniu pewnych operacji powodują one aktualizację interfejsu.

private val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)
  1. Pod definicją uiScope zdefiniuj zmienną o nazwie tonight, która będzie przechowywać bieżącą noc. Ustaw zmienną na MutableLiveData, ponieważ musisz mieć możliwość obserwowania danych i ich zmiany.
private var tonight = MutableLiveData<SleepNight?>()
  1. Aby zainicjować zmienną tonight jak najszybciej, utwórz blok init poniżej definicji zmiennej tonight i wywołaj funkcję initializeTonight(). W następnym kroku zdefiniujesz initializeTonight().
init {
   initializeTonight()
}
  1. Pod blokiem init wdróż initializeTonight(). W uiScope uruchom korutynę. Wewnątrz pobierz wartość tonight z bazy danych, wywołując getTonightFromDatabase(), i przypisz ją do tonight.value. W następnym kroku zdefiniujesz getTonightFromDatabase().
private fun initializeTonight() {
   uiScope.launch {
       tonight.value = getTonightFromDatabase()
   }
}
  1. Użyj mechanizmu getTonightFromDatabase(). Zdefiniuj ją jako funkcję private suspend, która zwraca wartość dopuszczającą wartość null SleepNight, jeśli nie ma obecnie uruchomionego SleepNight. W takim przypadku wystąpi błąd, ponieważ funkcja musi coś zwrócić.
private suspend fun getTonightFromDatabase(): SleepNight? { }
  1. W treści funkcji getTonightFromDatabase() zwróć wynik z korutyny, która działa w kontekście Dispatchers.IO. Użyj dyspozytora wejścia/wyjścia, ponieważ pobieranie danych z bazy danych to operacja wejścia/wyjścia, która nie ma nic wspólnego z interfejsem.
  return withContext(Dispatchers.IO) {}
  1. W bloku zwracania pozwól korutynie pobrać z bazy danych dzisiejszą noc (najnowszą). Jeśli godziny rozpoczęcia i zakończenia nie są takie same, co oznacza, że noc już się skończyła, zwróć wartość null. W przeciwnym razie zwróć noc.
       var night = database.getTonight()
       if (night?.endTimeMilli != night?.startTimeMilli) {
           night = null
       }
       night

Ukończona funkcja getTonightFromDatabase() zawieszania powinna wyglądać tak. Nie powinno być już żadnych błędów.

private suspend fun getTonightFromDatabase(): SleepNight? {
   return withContext(Dispatchers.IO) {
       var night = database.getTonight()
       if (night?.endTimeMilli != night?.startTimeMilli) {
           night = null
       }
       night
   }
}

Krok 2. Dodaj obsługę kliknięcia przycisku Start

Teraz możesz zaimplementować onStartTracking(), czyli procedurę obsługi kliknięć przycisku Start. Musisz utworzyć nowy SleepNight, wstawić go do bazy danych i przypisać do tonight. Struktura onStartTracking() będzie bardzo podobna do struktury initializeTonight().

  1. Zacznij od definicji funkcji onStartTracking(). Moduły obsługi kliknięć możesz umieścić nad onCleared() w pliku SleepTrackerViewModel.
fun onStartTracking() {}
  1. W funkcji onStartTracking() uruchom korutynę w funkcji uiScope, ponieważ potrzebujesz tego wyniku, aby kontynuować i zaktualizować interfejs.
uiScope.launch {}
  1. W ramach uruchomienia korutyny utwórz nowy obiekt SleepNight, który rejestruje bieżący czas jako czas rozpoczęcia.
        val newNight = SleepNight()
  1. W ramach uruchomienia korutyny wywołaj funkcję insert(), aby wstawić do bazy danych wartość newNight. Pojawi się błąd, ponieważ nie zdefiniowano jeszcze funkcji insert() suspend. (Nie jest to funkcja DAO o tej samej nazwie).
       insert(newNight)
  1. W ramach uruchomienia korutyny zaktualizuj też tonight.
       tonight.value = getTonightFromDatabase()
  1. Poniżej onStartTracking() zdefiniuj insert() jako funkcję private suspend, która przyjmuje argument SleepNight.
private suspend fun insert(night: SleepNight) {}
  1. W treści funkcji insert() uruchom korutynę w kontekście wejścia/wyjścia i wstaw noc do bazy danych, wywołując funkcję insert() z obiektu DAO.
   withContext(Dispatchers.IO) {
       database.insert(night)
   }
  1. W pliku układu fragment_sleep_tracker.xml dodaj moduł obsługi kliknięć elementu onStartTracking() do elementu start_button, korzystając z powiązywania danych skonfigurowanego wcześniej. Notacja funkcji @{() -> tworzy funkcję lambda, która nie przyjmuje argumentów i wywołuje moduł obsługi kliknięć w sleepTrackerViewModel.
android:onClick="@{() -> sleepTrackerViewModel.onStartTracking()}"
  1. Utwórz i uruchom aplikację. Kliknij przycisk Start. To działanie tworzy dane, ale na razie nic nie widać. Zrobisz to w następnym kroku.
fun someWorkNeedsToBeDone {
   uiScope.launch {

        suspendFunction()

   }
}

suspend fun suspendFunction() {
   withContext(Dispatchers.IO) {
       longrunningWork()
   }
}

Krok 3. Wyświetl dane

SleepTrackerViewModel zmienna nights odwołuje się do LiveData, ponieważ getAllNights() w obiekcie dostępu do danych zwraca LiveData.

Jest to funkcja Room, która powoduje, że za każdym razem, gdy dane w bazie ulegają zmianie, LiveData nights jest aktualizowany, aby wyświetlać najnowsze dane. Nie musisz jawnie ustawiać ani aktualizować LiveData. Room aktualizuje dane, aby pasowały do bazy danych.

Jeśli jednak wyświetlisz nights w widoku tekstowym, pojawi się odwołanie do obiektu. Aby zobaczyć zawartość obiektu, przekształć dane w sformatowany ciąg znaków. Użyj mapy Transformation, która jest wykonywana za każdym razem, gdy nights otrzymuje nowe dane z bazy danych.

  1. Otwórz plik Util.kt i usuń komentarz z kodu definicji formatNights() oraz powiązanych instrukcji import. Aby odkomentować kod w Android Studio, zaznacz cały kod oznaczony symbolem // i naciśnij klawisz Cmd+/ lub Control+/.
  2. Zwraca typ Spanned, czyli ciąg znaków w formacie HTML.formatNights()
  3. Otwórz plik strings.xml. Zwróć uwagę na użycie CDATA do sformatowania zasobów ciągów znaków w celu wyświetlania danych o śnie.
  4. Otwórz plik SleepTrackerViewModel. W klasie SleepTrackerViewModel, poniżej definicji uiScope, zdefiniuj zmienną o nazwie nights. Pobierz wszystkie noce z bazy danych i przypisz je do zmiennej nights.
private val nights = database.getAllNights()
  1. Bezpośrednio pod definicją zmiennej nights dodaj kod, który przekształci zmienną nights w zmienną nightsString. Użyj funkcji formatNights() z klasy Util.kt.

    Przekaż wartość nights do funkcji map() z klasy Transformations. Aby uzyskać dostęp do zasobów ciągów, zdefiniuj funkcję mapowania jako wywołującą formatNights(). Podaj nights i obiekt Resources.
val nightsString = Transformations.map(nights) { nights ->
   formatNights(nights, application.resources)
}
  1. Otwórz plik układu fragment_sleep_tracker.xml. W TextView we właściwości android:text możesz teraz zastąpić ciąg znaków zasobu odwołaniem do nightsString.
"@{sleepTrackerViewModel.nightsString}"
  1. Ponownie skompiluj kod i uruchom aplikację. Powinny się teraz wyświetlać wszystkie dane o śnie, w tym godziny rozpoczęcia.
  2. Kliknij przycisk Rozpocznij jeszcze kilka razy, aby zobaczyć więcej danych.

W następnym kroku włączysz funkcję przycisku Stop.

Krok 4. Dodaj obsługę kliknięcia przycisku Stop

Korzystając z tego samego wzorca co w poprzednim kroku, zaimplementuj procedurę obsługi kliknięcia przycisku StopSleepTrackerViewModel.

  1. Dodaj onStopTracking() do ViewModel. Uruchomienie korutyny w uiScope. Jeśli czas zakończenia nie został jeszcze ustawiony, ustaw endTimeMilli na bieżący czas systemowy i wywołaj update() z danymi nocnymi.

    W języku Kotlin składnia return@label określa funkcję, z której ten komunikat jest zwracany, spośród kilku zagnieżdżonych funkcji.
fun onStopTracking() {
   uiScope.launch {
       val oldNight = tonight.value ?: return@launch
       oldNight.endTimeMilli = System.currentTimeMillis()
       update(oldNight)
   }
}
  1. Zaimplementuj update(), używając tego samego wzorca co w przypadku insert().
private suspend fun update(night: SleepNight) {
   withContext(Dispatchers.IO) {
       database.update(night)
   }
}
  1. Aby połączyć moduł obsługi kliknięć z interfejsem, otwórz plik układu fragment_sleep_tracker.xml i dodaj moduł obsługi kliknięć do elementu stop_button.
android:onClick="@{() -> sleepTrackerViewModel.onStopTracking()}"
  1. Skompiluj i uruchom aplikację.
  2. Kliknij Start, a potem Stop. Wyświetli się godzina rozpoczęcia i zakończenia snu, jakość snu bez wartości oraz czas snu.

Krok 5. Dodaj procedurę obsługi kliknięcia przycisku Wyczyść

  1. Podobnie wdróż atrybuty onClear() i clear().
fun onClear() {
   uiScope.launch {
       clear()
       tonight.value = null
   }
}

suspend fun clear() {
   withContext(Dispatchers.IO) {
       database.clear()
   }
}
  1. Aby połączyć moduł obsługi kliknięć z interfejsem, otwórz fragment_sleep_tracker.xml i dodaj moduł obsługi kliknięć do clear_button.
android:onClick="@{() -> sleepTrackerViewModel.onClear()}"
  1. Skompiluj i uruchom aplikację.
  2. Kliknij Wyczyść, aby usunąć wszystkie dane. Następnie kliknij StartStop, aby utworzyć nowe dane.

Projekt Android Studio: TrackMySleepQualityCoroutines

  • Użyj ViewModel, ViewModelFactory i powiązania danych, aby skonfigurować architekturę interfejsu aplikacji.
  • Aby interfejs działał płynnie, używaj w przypadku długotrwałych zadań, takich jak wszystkie operacje na bazie danych, współprogramów.
  • Korutyny są asynchroniczne i nieblokujące. Używają funkcji suspend, aby kod asynchroniczny był wykonywany sekwencyjnie.
  • Gdy korutyna wywołuje funkcję oznaczoną symbolem suspend, zamiast blokować się do momentu zwrócenia przez nią wartości (jak w przypadku zwykłego wywołania funkcji), zawiesza wykonanie do czasu, aż wynik będzie gotowy. Następnie wznawia działanie od miejsca, w którym zostało przerwane, i wyświetla wynik.
  • Różnica między blokowaniemzawieszaniem polega na tym, że jeśli wątek jest zablokowany, nie są wykonywane żadne inne działania. Jeśli wątek jest zawieszony, inne zadania są wykonywane do momentu uzyskania wyniku.

Aby uruchomić korutynę, potrzebujesz zadania, dyspozytora i zakresu:

  • Zadanie to w zasadzie wszystko, co można anulować. Każda korutyna ma zadanie, którego możesz użyć do jej anulowania.
  • Dispatcher wysyła coroutines do uruchomienia w różnych wątkach. Dispatcher.Main wykonuje zadania w wątku głównym, a Dispartcher.IO służy do przenoszenia blokujących zadań wejścia-wyjścia do wspólnej puli wątków.
  • Zakres łączy informacje, w tym zadanie i dyspozytora, aby zdefiniować kontekst, w którym działa korutyna. Zakresy śledzą coroutines.

Aby zaimplementować procedury obsługi kliknięć, które wywołują operacje na bazie danych, postępuj zgodnie z tym wzorcem:

  1. Uruchomienie korutyny, która działa w głównym wątku lub wątku interfejsu, ponieważ wynik wpływa na interfejs.
  2. Wywołaj funkcję zawieszającą, aby wykonać długotrwałą pracę, dzięki czemu nie zablokujesz wątku interfejsu podczas oczekiwania na wynik.
  3. Długotrwałe zadanie nie ma nic wspólnego z interfejsem, więc przełącz się na kontekst wejścia/wyjścia. Dzięki temu zadanie może być wykonywane w puli wątków zoptymalizowanej i zarezerwowanej dla tego typu operacji.
  4. Następnie wywołaj funkcję bazy danych, aby wykonać zadanie.

Użyj mapowania Transformations, aby utworzyć ciąg znaków z obiektu LiveData za każdym razem, gdy obiekt się zmieni.

Kurs Udacity:

Dokumentacja dla deweloperów Androida:

Inne dokumenty i artykuły:

W tej sekcji znajdziesz listę możliwych zadań domowych dla uczniów, którzy wykonują ten moduł w ramach kursu prowadzonego przez instruktora. Nauczyciel musi:

  • W razie potrzeby przypisz pracę domową.
  • Poinformuj uczniów, jak przesyłać projekty.
  • Oceń zadania domowe.

Instruktorzy mogą korzystać z tych sugestii w dowolnym zakresie i mogą zadawać inne zadania domowe, które uznają za odpowiednie.

Jeśli wykonujesz ten kurs samodzielnie, możesz użyć tych zadań domowych, aby sprawdzić swoją wiedzę.

Odpowiedz na te pytania

Pytanie 1

Które z tych stwierdzeń opisują zalety korutyn:

  • Nie blokują one działania aplikacji.
  • Działają asynchronicznie.
  • Mogą być uruchamiane w wątku innym niż główny.
  • Zawsze przyspieszają działanie aplikacji.
  • Może użyć wyjątków.
  • Można je zapisywać i odczytywać jako kod liniowy.

Pytanie 2

Co to jest funkcja zawieszania?

  • Zwykła funkcja oznaczona słowem kluczowym suspend.
  • Funkcja, którą można wywoływać w korutynach.
  • Gdy funkcja zawieszania jest uruchomiona, wątek wywołujący jest zawieszony.
  • Funkcje zawieszania muszą zawsze działać w tle.

Pytanie 3

Jaka jest różnica między blokowaniem a zawieszaniem wątku? Zaznacz wszystkie prawdziwe stwierdzenia.

  • Gdy wykonanie jest zablokowane, w zablokowanym wątku nie można wykonywać żadnych innych działań.
  • Gdy wykonanie jest zawieszone, wątek może wykonywać inne zadania, czekając na zakończenie przeniesionej pracy.
  • Zawieszanie jest bardziej wydajne, ponieważ wątki mogą nie czekać i nic nie robić.
  • Niezależnie od tego, czy jest zablokowana, czy zawieszona, funkcja oczekuje na wynik korutyny, zanim będzie mogła kontynuować działanie.

Przejdź do następnej lekcji: 6.3. Używanie LiveData do kontrolowania stanu przycisków

Linki do innych ćwiczeń z tego kursu znajdziesz na stronie docelowej ćwiczeń z podstaw języka Kotlin na Androidzie.