Nessa apresentação demonstro como arquitetar uma aplicação Android utilizando as bibliotecas do Jetpack. O exemplo apresentado utiliza MVVM+Clean:
- Na camada de dados local, Room com Coroutines e Flow;
- View Model, Live Data e Data Binding na camada de apresentação;
- Fragments com a Navigation API na camada de UI.
O app também conta com uma implementação de banco de dados remoto utilizando Firebase.
2. Porque devemos nos
preocupar com arquitetura?
‣ Frameworks forçam o desenvolvedor a seguir o próprio
framework e não os princípios da engenharia de
software.
‣ Sua arquitetura deve deixar claro o propósito do sistema,
não os frameworks utilizados.
‣ A lógica de negócio deve estar claramente separada e
independente de framework.
3. Arquitetura
‣ Regra Principal: não há regras. Mas existem princípios que
devem ser seguidos. Lembra do S.O.L.I.D.?
‣ Promove a organização e o desacoplamento do código.
‣ Deve facilitar a manutenção e a adição de novas
funcionalidades.
‣ Uma arquitetura deve ser testável!
‣ Ela incrementa a complexidade? Sim! Vale à pena? Com
certeza! 😎 Mas deve ser de conhecimento de toda à equipe.
Single Responsibility Principle
Open-Closed Principle
Liskov Substitution Principle
Interface Segregation Principle
Dependency Inversion Principle
20. interface BooksRepository {
fun loadBooks(): Flow<List<Book>>
fun loadBook(bookId: String): Flow<Book>
suspend fun saveBook(book: Book)
suspend fun remove(book: Book)
}
22. Local
‣ Módulo Android
‣ Faz a implementação do
repositório local
‣ Book do módulo data é diferente
do Book deste módulo
23. Room
‣ ORM (Object-Relational Mapping) para
SQLite.
‣ Suporta live updates por meio de
LiveData, RXJava (Observable/Flowable)
e Coroutines (suspend/flow)
Local
24.
25. @Entity
@TypeConverters(MediaTypeConverter::class)
data class Book(
@PrimaryKey
var id: String,
var title: String = "",
var author: String = "",
var coverUrl: String = "",
var pages: Int = 0,
var year: Int = 0,
@Embedded(prefix = "publisher_")
var publisher: Publisher,
var available: Boolean = false,
var mediaType: MediaType = MediaType.PAPER,
var rating: Float = 0f
)
26. @Dao
interface BookDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun save(book: Book)
@Delete
suspend fun delete(vararg book: Book)
@Query("SELECT * FROM Book WHERE title LIKE :title ORDER BY title")
fun bookByTitle(title: String = "%"): Flow<List<Book>>
@Query("SELECT * FROM Book WHERE id = :id")
fun bookById(id: String): Flow<Book>
}
27. @Database(entities = [Book::class], version = 1, exportSchema = false)
abstract class AppDatabase : RoomDatabase() {
abstract fun bookDao(): BookDao
companion object {
private var instance: AppDatabase? = null
fun getDatabase(context: Context): AppDatabase {
if (instance == null) {
instance = Room.databaseBuilder(
context.applicationContext,
AppDatabase::class.java,
"booksDb")
.build()
}
return instance as AppDatabase
}
}
}
28. class RoomRepository(
db: AppDatabase,
private val fileHelper: FileHelper
) : BooksRepository {
private val bookDao = db.bookDao()
override suspend fun saveBook(book: Book) {
if (book.id.isBlank()) {
book.id = UUID.randomUUID().toString()
}
return if (fileHelper.saveCover(book)) {
bookDao.save(BookConverter.fromData(book))
} else {
throw RuntimeException("Error saving book's cover.")
}
}
...
29. class RoomRepository(
db: AppDatabase,
private val fileHelper: FileHelper
) : BooksRepository {
private val bookDao = db.bookDao()
override fun loadBooks(): Flow<List<Book>> {
return bookDao.bookByTitle()
.map { books ->
books.map { book ->
BookConverter.toData(book)
}
}
}
...
35. Data Binding
• Torna fácil a conexão entre a
View e o Presenter/ViewModel.
• Permite extender arquivos de
layout com micro-expressões.
• Muito útil em telas onde há input
de dados.
Presentation
36.
37. @Parcelize
class Book : BaseObservable(), Parcelable {
@Bindable
var id: String = ""
set(value) {
field = value
notifyPropertyChanged(BR.id)
}
@Bindable
var title: String = ""
set(value) {
field = value
notifyPropertyChanged(BR.title)
}
// demais atributos
}
41. LiveData
• LiveData stores observable data
(Observable) and notify the observers
(Observer) when the data changes
allowing the UI to be updated.
• LiveData is lifecycle-aware, which means
that it only notify the observers if the
Activity/Fragment are either in STARTED
or RESUMED state.
Presentation
42. class BookDetailsViewModel(private val useCase: ViewBookDetailsUseCase) : ViewModel() {
private val state: MutableLiveData<ViewState<BookBinding>> = MutableLiveData()
fun getState(): LiveData<ViewState<BookBinding>> = state
fun loadBook(id: String) {
...
viewModelScope.launch {
state.postValue(ViewState(ViewState.Status.LOADING))
try {
useCase.execute(id).collect { book ->
if (book != null) {
val bookBinding = BookConverter.fromData(book)
state.postValue(ViewState(ViewState.Status.SUCCESS, bookBinding))
} else {
state.postValue(
ViewState(ViewState.Status.ERROR, RuntimeException("Book not found"))
)
}
}
} catch (e: Exception) {
state.postValue(ViewState(ViewState.Status.ERROR, error = e))
}
}
}
43. class BookDetailsFragment : BaseFragment() {
private val viewModel: BookDetailsViewModel by viewModels {
BookVmFactory(requireActivity().application)
}
…
private fun init() {
viewModel.getState().observe(viewLifecycleOwner, Observer { viewState ->
when (viewState.status) {
ViewState.Status.SUCCESS -> binding.book = viewState.data
ViewState.Status.LOADING -> {} /* TODO */
ViewState.Status.ERROR -> {}/* TODO */
}
})
val book = arguments?.getParcelable<Book>("book")
book?.let {
viewModel.loadBook(book.id)
}
}
44. Presentation
open class LiveEvent<out T>(private val content: T) {
var hasBeenConsumed = false
private set
fun consumeEvent(): T? {
return if (hasBeenConsumed) {
null
} else {
hasBeenConsumed = true
content
}
}
fun peekContent(): T = content
}
45. class BookFormViewModel(
private val useCase: SaveBookUseCase
) : ViewModel(), LifecycleObserver {
private val state: MutableLiveData<LiveEvent<ViewState<Unit>>> = MutableLiveData()
fun getState(): LiveData<LiveEvent<ViewState<Unit>>> = state
fun saveBook(book: BookBinding) {
state.postValue(LiveEvent(ViewState(ViewState.Status.LOADING)))
viewModelScope.launch {
try {
withContext(Dispatchers.IO) {
useCase.execute(BookConverter.toData(book))
}
state.postValue(LiveEvent(ViewState(ViewState.Status.SUCCESS)))
} catch (e: Exception) {
state.postValue(LiveEvent(ViewState(ViewState.Status.ERROR, error = e)))
}
}
}
46. class BookFormFragment : BaseFragment() {
...
private fun init() {
viewModel.getState().observe(this, Observer { event ->
event?.peekContent()?.let { state ->
when (state.status) {
ViewState.Status.LOADING -> {...}
ViewState.Status.SUCCESS -> {...}
ViewState.Status.ERROR -> {
...
event.consumeEvent()
}
}
}
})
}
47. Lifecycle
‣ Lifecycle is an object which defines a life cycle.
‣ LifecycleOwner is the interface to be implemented by
objects with a life cycle
‣ Activity and Fragment implements LifecycleOwner
and has Lifecycle.
‣ LifecycleObserver is the interface to be implemented
by who wants to observe a LifecycleOwner.
48. class BookListViewModel(
private val loadBooksUseCase: ListBooksUseCase, ...
) : ViewModel(), LifecycleObserver {
@OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
fun loadBooks() {
if (state.value == null) {
viewModelScope.launch {
state.postValue(ViewState(ViewState.Status.LOADING))
try {
loadBooksUseCase.execute()
.collect { books ->
val booksBinding = books.map { book ->
BookConverter.fromData(book)
}
state.postValue(
ViewState(ViewState.Status.SUCCESS, booksBinding)
)
}
} catch (e: Exception) {
state.postValue(ViewState(ViewState.Status.ERROR, error = e))
}
}
}
}
49. class BookListFragment : BaseFragment() {
private val viewModel: BookListViewModel
private fun init() {
...
lifecycle.addObserver(viewModel)
}
...
51. UI (app)
Navigation API
• Introduz o conceito de “Single Activity”.
• Centraliza a lógica de navegação da
aplicação.
• Permite a conexão direta com
componentes de UI, tais como: ActionBar,
Button, Menu, BottomNav, …
• Simplifica a passagem e atribuição de
parâmetros (sem mais método
newInstance).
60. • Você não precisa usar tudo isso na
sua aplicação
• Na verdade, você não precisa usar
nenhum!
• O importante é saber QUANDO USAR
is QUANDO NÃO USAR 😉
• É essencial conhecer esses tópicos,
seus prós e contras e usá-los
adequadamente 💡