Nessa apresentação demonstro como arquitetar uma aplicação Android utilizando MVVM+Clean Architecture no Android utilizando as bibliotecas do Jetpack (View Model, Room, LiveData, ...)
2. Porque devemos nos
preocupar com arquitetura?
‣ Diversos tipos de negócios estão sendo feitos pelo
celular. Muitos deles são mobile first.
‣ A complexidade dos aplicativos está cada dia maior.
‣ Por isso o código tem que ser robusto, testável e
deve facilitar a manutenção e a adição de
funcionalidades.
‣ Atualmente existem diversos frameworks para ajudar
nessas tarefas, mas eles são suficientes?
3. 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.
‣ É preciso ter uma arquitetura que deixe 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.
4. 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 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)
}
21. interface BooksRepository {
fun loadBooks(): Flow<List<Book>>
fun loadBook(bookId: String): Flow<Book>
suspend fun saveBook(book: Book)
suspend fun remove(book: Book)
}
22. interface BooksRepository {
fun loadBooks(): Flow<List<Book>>
fun loadBook(bookId: String): Flow<Book>
suspend fun saveBook(book: Book)
suspend fun remove(book: Book)
}
24. Data - Local
• Módulo Android
• Faz a implementação do
repositório local.
• A classe Book deste
módulo é diferente da
classe Book do módulo
data.
Local
25. Room
• ORM (Object-Relational
Mapping) para SQLite.
• Suporta live updates por
meio de:
• LiveData
• RXJava
(Observable/Flowable)
• Coroutines
(flow)
Local
26.
27. @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
)
28. @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
)
29. @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>
}
30. @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
}
}
}
31. @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
}
}
}
32. @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
}
}
}
33. @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
}
}
}
34. 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.")
}
}
...
35. 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.")
}
}
...
36. 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.")
}
}
...
37. 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)
}
}
}
...
46. Data Binding
• Torna fácil a ligação
entre a View e o
ViewModel (ou
Presenter).
• Permite estender
arquivos de layout com
micro-expressões.
• Particularmente útil em
telas onde há input de
dados.
Presentation
47.
48. @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
}
49. @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
}
50. @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
}
54. class BookFormFragment : BaseFragment() {
private lateinit var binding: FragmentBookFormBinding
override fun onCreateView ... {
binding = DataBindingUtil.inflate(
inflater,
R.layout.fragment_book_form,
container,
false
)
return binding.root
}
override fun onViewCreated ... {
binding.book = viewModel.book
binding.formView = this
}
...
55. class BookFormFragment : BaseFragment() {
private lateinit var binding: FragmentBookFormBinding
override fun onCreateView ... {
binding = DataBindingUtil.inflate(
inflater,
R.layout.fragment_book_form,
container,
false
)
return binding.root
}
override fun onViewCreated ... {
binding.book = viewModel.book
binding.formView = this
}
...
56. class BookFormFragment : BaseFragment() {
private lateinit var binding: FragmentBookFormBinding
override fun onCreateView ... {
binding = DataBindingUtil.inflate(
inflater,
R.layout.fragment_book_form,
container,
false
)
return binding.root
}
override fun onViewCreated ... {
binding.book = viewModel.book
binding.formView = this
}
...
57. class BookFormFragment : BaseFragment() {
private lateinit var binding: FragmentBookFormBinding
override fun onCreateView ... {
binding = DataBindingUtil.inflate(
inflater,
R.layout.fragment_book_form,
container,
false
)
return binding.root
}
override fun onViewCreated ... {
binding.book = viewModel.book
binding.formView = this
}
...
58. ViewModel Presentation
• Mantém a lógica de
apresentação.
• Recebe as ações da
view.
• Fornece dados
observáveis para view.
• Sobrevive às mudanças
de configuração.
60. LiveData
• Armazena dados observáveis
(Observable) e notifica os
observadores (Observer)
quando esses dados são
modificados permitindo que
UI seja atualizada.
• LiveData é lifecycle-aware,
o que significa que os
observadores só serão
notificados se a Activity/
Fragment estiver no estado
de STARTED ou RESUMED.
Presentation
61. 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))
}
}
}
62. 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))
}
}
}
63. 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))
}
}
}
64. 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))
}
}
}
65. 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 -> {} /* Exibir Loading */
ViewState.Status.ERROR -> {} /* Exibir Erro */
}
})
val book = arguments?.getParcelable<Book>("book")
book?.let {
viewModel.loadBook(book.id)
}
}
66. 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 -> {} /* Exibir Loading */
ViewState.Status.ERROR -> {} /* Exibir Erro */
}
})
val book = arguments?.getParcelable<Book>("book")
book?.let {
viewModel.loadBook(book.id)
}
}
67. 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 -> {} /* Exibir Loading */
ViewState.Status.ERROR -> {} /* Exibir Erro */
}
})
val book = arguments?.getParcelable<Book>("book")
book?.let {
viewModel.loadBook(book.id)
}
}
68. 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 -> {} /* Exibir Loading */
ViewState.Status.ERROR -> {} /* Exibir Erro */
}
})
val book = arguments?.getParcelable<Book>("book")
book?.let {
viewModel.loadBook(book.id)
}
}
69. 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
}
70. 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)))
}
}
}
71. 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)))
}
}
}
72. 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)))
}
}
}
73. 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)))
}
}
}
74. 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()
}
}
}
})
}
75. 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()
}
}
}
})
}
76. Lifecycle
• Lifecycle é um objeto que define um ciclo de vida.
• LifecycleOwner é a interface a ser implementada por
objetos que que possuem um ciclo de vida.
• Activity e Fragment implementam LifecycleOwner e
têm Lifecycle.
• LifecycleObserver é a interface a ser implementada
pela classe que deseja observar o ciclo de vida de
um LifecycleOwner.
77. class BookListViewModel(
private val loadBooksUseCase: ListBooksUseCase, ...
) : ViewModel(), LifecycleObserver {
@OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
fun loadBooks() {
...
}
78. class BookListFragment : BaseFragment() {
private val viewModel: BookListViewModel
private fun init() {
...
lifecycle.addObserver(viewModel)
}
...
80. Navigation API
• Introduz o paradigma de Single
Activity.
• Representação visual do fluxo de
telas.
• Centraliza a lógica de
navegação da aplicação.
• Permite a conexão direta com
componentes de UI como:
ActionBar, Button, Menu,
BottomNav, …
• Simplifica a passagem e
atribuição de parâmetros.
UI (app)
87. class BookActivity : AppCompatActivity() {
private val navController: NavController by lazy {
Navigation.findNavController(this, R.id.navHost)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_book)
NavigationUI.setupActionBarWithNavController(this, navController)
}
override fun onSupportNavigateUp(): Boolean {
return navController.navigateUp()
}
}
88. class BookActivity : AppCompatActivity() {
private val navController: NavController by lazy {
Navigation.findNavController(this, R.id.navHost)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_book)
NavigationUI.setupActionBarWithNavController(this, navController)
}
override fun onSupportNavigateUp(): Boolean {
return navController.navigateUp()
}
}
89. class BookActivity : AppCompatActivity() {
private val navController: NavController by lazy {
Navigation.findNavController(this, R.id.navHost)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_book)
NavigationUI.setupActionBarWithNavController(this, navController)
}
override fun onSupportNavigateUp(): Boolean {
return navController.navigateUp()
}
}
90. class BookActivity : AppCompatActivity() {
private val navController: NavController by lazy {
Navigation.findNavController(this, R.id.navHost)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_book)
NavigationUI.setupActionBarWithNavController(this, navController)
}
override fun onSupportNavigateUp(): Boolean {
return navController.navigateUp()
}
}
91. // BookListFragment
val args = Bundle().apply {
putParcelable("book", book)
}
navController.navigate(R.id.action_list_to_details, args)
// BookDetailsFragment
val book = arguments?.getParcelable<Book>("book")
binding.book = book
92. WorkManager
• WorkManager facilita o agendamento de tarefas assíncronas e que
devem ser executadas mesmo que o aplicativo não esteja em
execução ou o dispositivo seja reiniciado.
• O agendamento pode ser único ou periódico (15 min. de intervalo)
• Uso de parâmetros de entrada e saída.
• Suporte a execuções em cadeia (chaining) (beginWith & then)
• Observar o estado do work via LiveData.
• Permite o uso de constraints (nível de bateria, tipo de rede, storage, …)
• Possui políticas de retries & backoff.
94. Paging
• Carregar dados de forma paginada e sob demanda
reduz o uso da largura de banda da rede e dos
recursos do sistema.
• A Paging Library permite carregar os dados
gradualmente para listagens finitas ou “infinitas”.
• Integração com RecyclerView e LiveData
97. Bonus Topics
• Organização do projeto: por pacote ou por feature?
• Injeção de Dependência: Dagger x Koin (ou Kodein)?
• Testes (JUnit, Robolectric, Espresso, MockK,
MockWebServer, …)
• Lint & KtLint
• Git Hooks
• Como está o mercado?
99. • Você não precisa usar tudo isso
na sua aplicação
• Na verdade, você não precisa
usar nenhum!
• O importante é saber QUANDO
USAR é QUANDO NÃO USAR
😉
• É essencial conhecer esses
tópicos, seus prós e contras e
usá-los adequadamente 💡