Android/ Kotlin - pomoc z warstwą prezentacji w clean architecture

0

Uczę się clean architecture i stworzyłem sobie mini aplikację by załapać temat i później zrobić refactor w mojej większej aplikacji.

Tutaj mini apka: https://github.com/marekski/notes

Mam problem z warstwą prezentacji bo nie wiem jak połączyć w całość te rzeczy co już zrobiłem (w module core stworzyłem pakiety domain, data, interactors, w module app stworzyłem pakiet framework (mam tu klasę odpowiedzialną za zapis notatek do Shared preferences i ich odczyt z sp)).

Teraz pozostało mi skorzystać z tych rzeczy co stworzyłem wcześniej, czyli

  • w klasie NotesFragment pobrać listę notatek
  • w klasie NewNoteFragment zapisać nową notatkę

Co powinieniem dalej zrobić ? W tym projekcie nie korzystam z żadnego DI oraz nie korzystam z żadnego wzorca typu MVVP czy MVP/MVI. W mojej docelowej aplikacji mam MVVM

2

nie korzystam z żadnego DI

Z DI korzystasz, bo przekazujesz rzeczy przez konstruktor.

nie korzystam z żadnego wzorca typu MVVP czy MVP/MVI. W mojej docelowej aplikacji mam MVVM

Nie rozumiem. Chcesz mieć warstwę prezentacji, ale nie chcesz mieć niczego pomiędzy widokiem a logiką? Czy w tej aplikacji też chcesz skorzystać z ViewModelu i nie wiesz jak połączyć kropki?

0

nie korzystam z żadnego wzorca typu MVVP czy MVP/MVI. W mojej docelowej aplikacji mam MVVM

Nie rozumiem. Chcesz mieć warstwę prezentacji, ale nie chcesz mieć niczego pomiędzy widokiem a logiką? Czy w tej aplikacji też chcesz skorzystać z ViewModelu i nie wiesz jak połączyć kropki?

Hmm, trochę się pogubiłem.
Dodałem viewmodel NotesViewModel i przekazuję tutaj interactors i myślę że jest okej, ale możecie spojrzeć czy jest w porządku.

Teraz mam problem jak podejść do listy z view modelu, teraz mam coś takiego w viewmodelu val notes: MutableLiveData<List<Note>> = MutableLiveData() (https://github.com/marekski/notes/blob/main/app/src/main/java/pl/marek/notatnik/presentation/NotesViewModel.kt)

to jest okej?

Do adaptera przekazuję notes.value i na razie mi to nie działa, jutro będę to robił (https://github.com/marekski/notes/blob/main/app/src/main/java/pl/marek/notatnik/NotesFragment.kt)

Bo może powinieniem zrobić coś w stylu live data z shared preferences?

Ogólnie to ten projekt notatnika będę rozwijał, to moja pierwsza styczność z kotlinem, a on już dawno standardem jest w androidze i mam braki z tego plus wstrzykiwania zależności muszę się pouczyć, teraz korzystam z ViewModelFactory, ale to z tutoriala skopiowałem :-D

0

Udało mi się wyświetlić notatki poprzez viewmodel, ale to działa tylko w specyficznym przypadku. Jeśli widok jest tworzony od nowa to korzystam z metody viewModel.loadNotes() i w tym przypadku mam listę, ale nie tak powinna działać LiveData. Jeśli dodam notatkę z widoku listy(poprzez button_insert) to nie wskoczy ona na listę dopóki widok nie zostanie stworzony ponownie.

Pomożecie? tutaj mój viewmodel https://github.com/marekski/notes/blob/main/app/src/main/java/pl/marek/notatnik/presentation/NotesViewModel.kt
fragment z listą notatek https://github.com/marekski/notes/blob/main/app/src/main/java/pl/marek/notatnik/NotesFragment.kt

przycisk button_insert jest tylko do testów abym mógł dodać notatkę z fragmentu, który wyświetla notatki

1
  1. insertNote() we fragmencie nie powinno zapisywać do shared preferences tylko gadac z viewmodelem. W viewmodelu powinieneś mieć metodę vm.addNote(note:Note) i jak klikniesz na button_insert to wywołasz tę metodę. Fragment / activity gada z viewmodelem
  2. We viewmodelu masz interactor - ok powinieneś teraz w tym interactorze dodać metodę interactor.addNote(note:Note) i ten interactor powinen być odpowiedzialny za zapis do shared references. Już nawet coś takiego masz - https://github.com/marekski/notes/blob/c05eb66b53f8db1cb2088c617d28bbaa7785059b/core/src/main/java/pl/marek/core/interactors/AddNote.kt
  3. Jak dodasz Note do repozytorium / shared preferences w viewmodelu wywołujesz loadNotes() i koniec. Nie jest to rozwiązanie idealne bo brakuje cache oraz za każdym razem robisz adapter ale od strony ~clean architecture to już prawie prawie i duży krok naprzód.

W kilku miejscach widziałem że odczytujesz dane bezpośrednio z shared preferences a masz dobrą klasę do tego ~DataSource - wstrzykuj ją tam gdzie tego potrzebujesz.

1

Czy val notes: MutableLiveData<List<note>> = MutableLiveData() w viewmodelu jest ok? I tak i nie - ja osobiście mam inne podejście z livedatą które jest IMO dużo prostsze.

Tak - jest ok, jak chcesz śledzić jedną zmienną to jest ok, problem pojawia się jak masz kilka atrybutów np notes, stan przycisku - enabled / disabled, text w toolbarze, jakieś odświeżanie. Samo użycie livedata jest ok, stosuje się post oraz tą drugą metodę.

Nie - jak pisałem wcześniej dla wielu atrybutów musiałbyś zrobić wiele livedata które ciężko później ogarnąć. Ja pomiędzy VM a View(activity albo fragment) przerzucam całe stany np NotesViewState.Fetched(note:Notes), NotesViewState.Error(error:Error). W viewmodelu mam wtedy dużego while który mi mapuje state na widok.

0

Dzięki.

Jeśli chodzi o te zapisy bezpośrednio do SP a nie poprzez VM to nie przerabiałem tego jeszcze na clean, bo na razie walczyłem z poprawnym odczytem notatek. Zaraz zakodzę poprawnie ;) To na razie chyba wszystko wiem w ramach tej mini apki

0

Jak juz zaktualizujesz kod mozesz tutaj pingnac - przy wolnej godzinie siade i dam kolejne rady.

Co do zapisywania danych gsonem - warto robuc tutaj obiekty domenowe np Notes(notes:List<Note>). Dzieki temu przy obsludze gsona nie musisz sie zastanawiac czy typ bazowy to list czy nie - zawsze odpowiedz bedzie brzmiala nie. Notes vs List<Note>

0

Już zaktualizowałem kod, dodałem viewmodel do klasy NewNoteFragment i przekazuję obiekt Note we fragmencie do VM.

lubie_programowac napisał(a):

Co do zapisywania danych gsonem - warto robuc tutaj obiekty domenowe np Notes(notes:List<Note>). Dzieki temu przy obsludze gsona nie musisz sie zastanawiac czy typ bazowy to list czy nie - zawsze odpowiedz bedzie brzmiala nie. Notes vs List<Note>

Trochę nie rozumiem, teraz mam taki kod(https://github.com/marekski/notes/blob/main/app/src/main/java/pl/marek/notatnik/framework/SharedPreferencesNoteDataSource.kt):

val notesNew = arrayListOf<Note>()
notesNew.addAll(notesOld)
notesNew.add(Note(maxId + 1, title, body))

var jsonString = gson.toJson(notesNew)

Tutaj byś coś zmienił?

1

Przechodząc do sedna:
Wprowadzając data class Notes(notes: List<Note> )
wyrażenie
var notesOld = gson.fromJson(notesJson, Array<Note>::class.java).asList()
można zastąpić na:
var notesOld = gson.fromJson(notesJson, Notes::class.java)

oraz parsowanie

        notesNew.addAll(notesOld)
        notesNew.add(Note(maxId + 1, title, body))

        var jsonString = gson.toJson(notesNew)

na

val notesNew = arrayListOf<Note>()
notesNew.addAll(notesOld)
notesNew.add(Note(maxId + 1, title, body))

var jsonString = gson.toJson(Notes(notesNew))```

Po co ta zmiana? Jak już mówiłem, nie będziesz musiał wiedzieć czy w shared preferences trzymasz listę czy obiekt - zawsze będzie to obiekt. Dzięki temu mógłbyś sprytnie delegować zapisywanie do shared preferences do osobnej klasy dla wielu innych obiektów.
0

Jak mam recycler view i w nim notatki, a obok notatek przyciski do crudowych operacji to jak powinno się to zrobić (np usunięcie notatki)?

Czyli mam notatkę zrobić zakupy i obok przycisk do usuwania pojedyńczej notatki. Chwilowo do konstruktora adaptera dodałem ViewModel i w metodzie onBindViewHolder mam onClickListener i z adaptera wywołuję viewModel.delete(note)

Powinno się to jakoś lepiej robić niż przekazywanie view modelu do adaptera?

1

Przekazać do do adaptera interfejs, który będzie delegował do view modelu. Czyli coś w tym stylu.

class NoteAdapter(
  private val listener: Listener,
): ListAdapter<Note, NoteViewHolder>(DiffCallback) {
  override fun getItemViewType(position: Int) = R.layout.note_view_holder_layout

  override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NoteViewHolder {
    val view = LayoutInflater.from(parent.context).inflate(viewType, parent, false)
    return NoteViewHolder(view, listener)
  }

  override fun onBindViewHolder(holder: NoteViewHolder, position: Int) = holder.bind(getItem(position))

  interface Listener {
    fun onDeleteNote(note: Note)
  }

  private object DiffCallback : ItemCallback<Note>() {
    override fun areItemsTheSame(old: Note, new: Note) = old.id == new.id

    override fun areContentsTheSame(old: Note, new: Note) = old == new

    // Opcjonalnie dla unikania mrugających animacji, jeśli masz z tym problem.
    override fun getChangePayload(old: Note, new: Note) = Unit
  }
}

class NoteViewHolder(
  itemView: View,
  private val listener: NoteAdapter.Listener,
) : ViewHolder(itemView) {
  private var note: Note? = null

  init {
    itemView.setOnClickListener { 
      listener.onDeleteNote(note!!)
    }
  }

  fun bind(note: Note) {
    this.note = note
    // TODO: Bindowanie wartości
  }
}

I gdzieś tam, gdzie to wszystko spinasz.

val adapter = NoteAdapter(object : Listener {
  override fun onDeleteNote(note: Note) = viewModel.deleteNote(note)
})  

Zauważ, że korzystam z !! w listener.onDeleteNote(note!!). Jest to bezpieczne, ponieważ null na tym etapie oznacza albo błąd w Twoim kodzie albo błąd w bibliotece. Możesz opakować w requireNotNull(), jeśli chcesz mieć ładniejszą wiadomość albo zrobić note?.let(listener::onDeleteNote), ale to drugie odradzam, bo można przeoczyć bugi przez tego typu korzystanie z ?.

0

Jeszcze odkopie troche temat.

Ta aplikacja wyżej używa coroutines kotlinowych, które są dostępne bez żadnych zależności.

Jak się ma sprawa jeśli przykładowo mamy rxjave, która wyciąga dane z bazki lokalnej lub zapisuje dane poprzez room?

W pliku Dao (który umieścimy w module androidowym) będziemy zwracać obiekty rxjavy typu Completable czy Flowable<Note>

Plik NoteDataSource.kt mam w module corowym, gdzie założyłem że ma być stricte jvm. Jak to razem połączyć? dodać libki do rxjavy do modułu corowego i w tym interfejsie zwracać Completable/Flowable?

Nie widzę innej opcji, bo skoro później stworzę sobie klasę RoomNotesDataSource(moduł androidowy), który implementuje wcześniejszy interfejs(z modułu core) to tutaj odwołuję się już do DAO(które zwraca obiekty Completable czy Flowable) (np: class RoomNotesDataSource : NoteDataSource i metoda, która zwraca: return noteDao.insertNote() )

Mam nadzieję że czytelnie wyszedł ten post :)

0

Korutyny nie są dostępne bez zależności tak samo jak RxJava. Musisz mieć zazwyczaj org.jetbrains.kotlinx:kotlinx-coroutines-core (albo inny artefakt), żeby w jakiś sensowny sposób wykorzystać suspend.

A odpowiadając na główne pytanie odnośnie RxJavy, to tak. Dodajesz bibliotekę jako api w Gradle.

Inną opcją byłoby posiadanie np. takiej klasy w module core.

interface Dao {
  suspend fun getAll(): List<Note>
}

I potem w module docelowym, albo jakimś pośrednim skorzystać z integracji korutyn z Rx.

class RxDao(private val dao: Dao) {
  fun getAll() = rxSingle { dao.getAll() }
}

1 użytkowników online, w tym zalogowanych: 0, gości: 1